feat: deepseek ai, досье, парсер .txt, ролеплей roast

This commit is contained in:
loki5512344 2026-07-06 22:29:06 +02:00
parent 80863a9592
commit 2990f01958
17 changed files with 868 additions and 10 deletions

View file

@ -10,6 +10,9 @@ HEALTH_HTTP_PORT=8712
SUPER_ADMIN_ID=your_discord_user_id SUPER_ADMIN_ID=your_discord_user_id
DB_PATH=data/bot.db DB_PATH=data/bot.db
# DeepSeek AI API (для roast)
DEEPSEEK_API_KEY=your_deepseek_key
# Lavalink # Lavalink
LAVALINK_HOST=127.0.0.1 LAVALINK_HOST=127.0.0.1
LAVALINK_PORT=2333 LAVALINK_PORT=2333

View file

@ -36,6 +36,7 @@ services:
LAVALINK_PASSWORD: Hch4HiVCxkKs9JPiSnrt4Q LAVALINK_PASSWORD: Hch4HiVCxkKs9JPiSnrt4Q
SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID} SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID}
SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET}
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
volumes: volumes:
- bot-data:/app/data - bot-data:/app/data

View file

@ -9,6 +9,7 @@ public class BotConfig {
private final int healthHttpPort; private final int healthHttpPort;
private final String superAdminId; private final String superAdminId;
private final String dbPath; private final String dbPath;
private final String deepseekApiKey;
private final LavalinkConfig lavalinkConfig; private final LavalinkConfig lavalinkConfig;
public BotConfig( public BotConfig(
@ -20,6 +21,7 @@ public class BotConfig {
int healthHttpPort, int healthHttpPort,
String superAdminId, String superAdminId,
String dbPath, String dbPath,
String deepseekApiKey,
LavalinkConfig lavalinkConfig) { LavalinkConfig lavalinkConfig) {
this.mainToken = mainToken; this.mainToken = mainToken;
this.musicToken = musicToken; this.musicToken = musicToken;
@ -29,6 +31,7 @@ public class BotConfig {
this.healthHttpPort = healthHttpPort; this.healthHttpPort = healthHttpPort;
this.superAdminId = superAdminId; this.superAdminId = superAdminId;
this.dbPath = dbPath; this.dbPath = dbPath;
this.deepseekApiKey = deepseekApiKey;
this.lavalinkConfig = lavalinkConfig; this.lavalinkConfig = lavalinkConfig;
} }
@ -68,6 +71,10 @@ public class BotConfig {
return dbPath; return dbPath;
} }
public String getDeepseekApiKey() {
return deepseekApiKey;
}
public LavalinkConfig getLavalinkConfig() { public LavalinkConfig getLavalinkConfig() {
return lavalinkConfig; return lavalinkConfig;
} }

View file

@ -73,6 +73,8 @@ public final class ConfigLoader {
dbPath = "data/bot.db"; dbPath = "data/bot.db";
} }
String deepseekApiKey = dotenv.get("DEEPSEEK_API_KEY");
LavalinkConfig lavalinkConfig = getLavalinkConfig(); LavalinkConfig lavalinkConfig = getLavalinkConfig();
LoggerDecorator.success(logger, "Конфигурация загружена успешно"); LoggerDecorator.success(logger, "Конфигурация загружена успешно");
return new BotConfig( return new BotConfig(
@ -84,6 +86,7 @@ public final class ConfigLoader {
healthHttpPort, healthHttpPort,
superAdminId, superAdminId,
dbPath, dbPath,
deepseekApiKey,
lavalinkConfig); lavalinkConfig);
} }
} }

View file

@ -19,6 +19,8 @@ import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.cache.CacheFlag;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -66,7 +68,10 @@ public class BotInitializer {
LoggerDecorator.startup(logger, "Инициализация main бота..."); LoggerDecorator.startup(logger, "Инициализация main бота...");
registrar.registerAll(); registrar.registerAll();
JDA mainJda = buildMainJDA(); JDA mainJda = buildMainJDA();
mainJda.updateCommands().queue(); mainJda.upsertCommand(
Commands.slash("agr", "Наслать AI на пользователя (только для админов)")
.addOption(OptionType.USER, "user", "Цель (по умолчанию Andrert_)", false)
).queue();
muteScheduler.restorePending(mainJda); muteScheduler.restorePending(mainJda);
mainJda.getGuilds().forEach(g -> memberCounterListener.startScheduler(g)); mainJda.getGuilds().forEach(g -> memberCounterListener.startScheduler(g));
healthHttpServer.start(); healthHttpServer.start();

View file

@ -6,6 +6,8 @@ import creator.loki.bootstrap.ReadyListener;
import creator.loki.feature.counter.service.MemberCounterListener; import creator.loki.feature.counter.service.MemberCounterListener;
import creator.loki.feature.economy.FishMessageListener; import creator.loki.feature.economy.FishMessageListener;
import creator.loki.feature.help.HelpMenuHandler; 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.leaderboard.LeaderboardButtonListener;
import creator.loki.feature.level.listener.MessageListener; import creator.loki.feature.level.listener.MessageListener;
import creator.loki.feature.level.listener.ResetLevelsButtonHandler; import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
@ -39,7 +41,8 @@ public class ListenerAggregator {
LeaderboardButtonListener leaderboardButtonListener, LeaderboardButtonListener leaderboardButtonListener,
MessageCache messageCache, MessageCache messageCache,
MusicSlashCommands musicSlashCommands, MusicSlashCommands musicSlashCommands,
FishMessageListener fishMessageListener) { FishMessageListener fishMessageListener,
AndrertListener andrertListener) {
this.listeners = this.listeners =
new Object[] { new Object[] {
readyListener, readyListener,
@ -57,7 +60,8 @@ public class ListenerAggregator {
leaderboardButtonListener, leaderboardButtonListener,
messageCache, messageCache,
musicSlashCommands, musicSlashCommands,
fishMessageListener fishMessageListener,
andrertListener
}; };
} }

View file

@ -32,6 +32,7 @@ import creator.loki.feature.music.MusicModule;
import creator.loki.feature.profile.ProfileModule; import creator.loki.feature.profile.ProfileModule;
import creator.loki.feature.radio.RadioCommandRegistrar; import creator.loki.feature.radio.RadioCommandRegistrar;
import creator.loki.feature.radio.RadioModule; import creator.loki.feature.radio.RadioModule;
import creator.loki.feature.roast.RoastModule;
import creator.loki.feature.role.RoleModule; import creator.loki.feature.role.RoleModule;
import creator.loki.feature.utility.UtilityCommandRegistrar; import creator.loki.feature.utility.UtilityCommandRegistrar;
import creator.loki.feature.voice.VoiceModule; import creator.loki.feature.voice.VoiceModule;
@ -62,6 +63,7 @@ public class BotModule extends AbstractModule {
install(new WelcomeModule()); install(new WelcomeModule());
install(new EconomyModule()); install(new EconomyModule());
install(new RadioModule()); install(new RadioModule());
install(new RoastModule());
Multibinder<CommandRegistrar> commandBinder = Multibinder<CommandRegistrar> commandBinder =
Multibinder.newSetBinder(binder(), CommandRegistrar.class); Multibinder.newSetBinder(binder(), CommandRegistrar.class);

View file

@ -0,0 +1,163 @@
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;
public AgrCommand(BotConfig config, RoastService roastService, AndrertListener autoResponder) {
this.config = config;
this.roastService = roastService;
this.autoResponder = autoResponder;
}
@Override
public String getName() {
return "agr";
}
@Override
public String getDescription() {
return "Наслать AI на пользователя (по умолчанию Andrert_). Только для админов";
}
@Override
public String getUsage() {
return "agr [@user]";
}
@Override
public String getCategory() {
return "Супер-Админ";
}
@Override
public void execute(CommandContext context) {
if (!config.getSuperAdminId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Только супер-администратор")).queue();
return;
}
if (context.getGuild() == null) {
context.getChannel().sendMessage("Команда работает только на сервере").queue();
return;
}
if (context.getArgs().isEmpty()) {
autoResponder.trackUser(ANDRERT_USER_ID);
doScan(context, ANDRERT_USER_ID, "Andrert_");
} else {
String arg = context.getArgs().get(0);
String targetId = arg.replaceAll("<@!?(\\d+)>", "$1");
if (targetId.equals(arg) && !arg.matches("\\d+")) {
context.getChannel().sendMessage("Укажи @упоминание или ID пользователя").queue();
return;
}
String finalId = targetId;
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();
}
}

View file

@ -0,0 +1,108 @@
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;
@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;
@Inject
public AgrSlashCommands(BotConfig config, RoastService roastService) {
this.config = config;
this.roastService = roastService;
}
@Override
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
if (!event.getName().equals("agr")) return;
if (!config.getSuperAdminId().equals(event.getUser().getId())) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
.setEphemeral(true).queue();
return;
}
if (event.getGuild() == null) {
event.reply("Команда работает только на сервере").setEphemeral(true).queue();
return;
}
event.deferReply().queue();
String targetId;
String targetName;
var userOption = event.getOption("user");
if (userOption != null) {
var user = userOption.getAsUser();
targetId = user.getId();
targetName = user.getName();
} else {
targetId = ANDRERT_USER_ID;
targetName = "Andrert_";
}
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));
}
}
}
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;
});
}
}

View file

@ -0,0 +1,113 @@
package creator.loki.feature.roast;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
@Singleton
public class AndrertListener extends ListenerAdapter {
private static final String ANDRERT_USER_ID = "1405129454165954630";
private static final int CHANNEL_CONTEXT = 15;
private static final long TRACK_TTL = TimeUnit.HOURS.toMillis(1);
private final RoastService roastService;
private final ConcurrentHashMap<String, Long> trackedUsers = new ConcurrentHashMap<>();
@Inject
public AndrertListener(RoastService roastService) {
this.roastService = roastService;
trackedUsers.put(ANDRERT_USER_ID, System.currentTimeMillis() + TRACK_TTL);
}
public void trackUser(String userId) {
trackedUsers.put(userId, System.currentTimeMillis() + TRACK_TTL);
}
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
if (event.getAuthor().isBot()) return;
String userId = event.getAuthor().getId();
Long expiresAt = trackedUsers.get(userId);
if (expiresAt == null) return;
long now = System.currentTimeMillis();
if (now > expiresAt) {
trackedUsers.remove(userId);
return;
}
String content = event.getMessage().getContentRaw().strip();
if (content.length() < 2 && event.getMessage().getAttachments().isEmpty()) return;
trackedUsers.put(userId, now + TRACK_TTL);
respond(event, userId);
}
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();
StringBuilder line = new StringBuilder();
line.append(author).append(": ").append(msg.getContentRaw());
line.append(replyInfo(msg));
line.append(attachmentInfo(msg));
context.add(new RoastService.CachedMessage(
line.toString(),
event.getChannel().getName(),
msg.getTimeCreated().toEpochSecond() * 1000));
}
java.util.Collections.reverse(context);
String response = roastService.generateResponse(context, targetName, userId);
if (response != null && !response.isBlank()) {
event.getMessage().reply(response).queue();
}
});
}
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();
}
}

View file

@ -0,0 +1,121 @@
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;
public DossierCommand(BotConfig config, RoastService roastService) {
this.config = config;
this.roastService = roastService;
}
@Override
public String getName() {
return "dossier";
}
@Override
public String getDescription() {
return "Составить AI-досье на пользователя по его сообщениям";
}
@Override
public String getUsage() {
return "dossier @user";
}
@Override
public String getCategory() {
return "Супер-Админ";
}
@Override
public void execute(CommandContext context) {
if (!config.getSuperAdminId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Только супер-администратор")).queue();
return;
}
if (context.getGuild() == null) {
context.getChannel().sendMessage("Команда работает только на сервере").queue();
return;
}
if (context.getArgs().isEmpty()) {
context.getChannel().sendMessage("Укажи @упоминание пользователя").queue();
return;
}
String arg = context.getArgs().get(0);
String targetId = arg.replaceAll("<@!?(\\d+)>", "$1");
if (targetId.equals(arg) && !arg.matches("\\d+")) {
context.getChannel().sendMessage("Укажи @упоминание или ID пользователя").queue();
return;
}
String finalId = targetId;
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 + ":**\n" + dossier).queue();
} else {
reply.editMessage("AI не смог составить досье").queue();
}
})
.exceptionally(e -> {
reply.editMessage("Ошибка: " + e.getMessage()).queue();
return null;
});
});
}
}

View file

@ -0,0 +1,12 @@
package creator.loki.feature.roast;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
public class RoastModule extends AbstractModule {
@Override
protected void configure() {
bind(RoastService.class).in(Singleton.class);
bind(AndrertListener.class).in(Singleton.class);
}
}

View file

@ -0,0 +1,300 @@
package creator.loki.feature.roast;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
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 int MAX_DOSSIER_MSGS = 30;
private final OkHttpClient httpClient;
private final String apiKey;
private final Map<String, DossierData> dossiers = new ConcurrentHashMap<>();
@Inject
public RoastService(BotConfig config) {
this.apiKey = config.getDeepseekApiKey();
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
loadDossiers();
}
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;
}
public String generateResponse(List<CachedMessage> context, String targetName) {
return generateResponse(context, targetName, null);
}
public String generateResponse(List<CachedMessage> context, String targetName, String userId) {
if (apiKey == null || apiKey.isBlank()) return null;
String systemPrompt = SYSTEM_PROMPT.formatted(targetName, targetName, targetName);
if (userId != null) {
DossierData dd = dossiers.get(userId);
if (dd != null && !dd.text.isBlank()) {
systemPrompt += "\n\nДОСЬЕ НА ПОЛЬЗОВАТЕЛЯ " + targetName + ":\n" + dd.text;
}
}
StringBuilder userContent = new StringBuilder();
for (CachedMessage m : context) {
userContent.append("[").append(m.channelName).append("] ").append(m.content).append("\n");
}
String text = userContent.toString().strip();
return callModel(MODEL, systemPrompt, text);
}
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(), MAX_DOSSIER_MSGS);
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");
}
String prompt = DOSSIER_GENERATE_PROMPT.formatted(targetName, 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;
}
}
public String getDossier(String userId) {
DossierData dd = dossiers.get(userId);
return dd != null ? dd.text : null;
}
private void loadDossiers() {
try {
Files.createDirectories(DOSSIER_DIR);
Files.list(DOSSIER_DIR).filter(p -> p.toString().endsWith(".txt")).forEach(p -> {
try {
String userId = p.getFileName().toString().replace(".txt", "");
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() + ".txt");
String header = entry.getValue().userName + "\n" + "-".repeat(20) + "\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;
}
}
private static final String DOSSIER_GENERATE_PROMPT = """
Проанализируй сообщения пользователя Discord "%s" и составь короткое досье (3-5 предложений) на русском.
Досье должно содержать:
- Типичные темы и интересы
- Особенности речи (ошибки, любимые слова, стиль)
- Слабые места для троллинга и насмешек
- Противоречия в поведении
Сообщения:
%s
Только факты, без воды. Кратко и по делу.
""";
private static final String SYSTEM_PROMPT = """
Твоя цель уничтожать в чате пользователя "%s". Отвечай только ему, на других похуй.
Цель только "%s". На остальных насри, они не существуют.
ТЫ ДОЛБОЁБ И ТОКСИЧНЫЙ МУДАК:
- ты не локи не бог не ии ты просто ебанат который хочет выбесить
- никаких умных слов никаких метафор никаких спектаклей
- просто пизди человека как в дс чате в 2024
- без тире без двоеточий без скобочек без кавычек
- строчными буквами коротко 1-2 предложения
- без приветов без обращений без "о, %s" сразу нахуй
- используй мат это нормально
- въебывай за его сообщения за ошибки за гифки за всё
ЧТО ДЕЛАТЬ:
- высмеивай всё что он написал
- передёргивай его слова
- пиши что он нихуя не сказал по факту
- если он матерится ответь что у него убогий словарный запас
- если кинул гифку напиши что он немой и текст написать не может
- если скинул тхт файл высмеивай содержимое
ПРИМЕРЫ:
- "ты сам то понял что сказал или нет"
- "нахуй ты это написал я это читать должен"
- "снова гифку кинул потому что буквы страшные"
- "у тебя кроме мата есть что то или это всё"
- "написал пиздец а по факту пустота"
- "ты звучишь как школьник который только мат выучил"
- "столько букв а смысла ноль"
- "классный аргумент конечно прямо пиздец"
- "ты бы ещё воды налил а то сухо совсем"
- "братан ты вообще о чём"
- "иди нахуй со своими гифками"
Пиши на русском коротко токсично и без предисловий.
""";
public record CachedMessage(String content, String channelName, long timestamp) {}
private static class DossierData {
String userName;
String text;
long updatedAt;
DossierData() {}
DossierData(String userName, String text, long updatedAt) {
this.userName = userName;
this.text = text;
this.updatedAt = updatedAt;
}
}
}

View file

@ -8,6 +8,10 @@ import creator.loki.core.security.CooldownManager;
import creator.loki.feature.admin.commands.GenerateRatingsCommand; import creator.loki.feature.admin.commands.GenerateRatingsCommand;
import creator.loki.feature.config.repository.ConfigRepository; import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.counter.service.CounterManager; 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.service.CounterSetupPrefixCommand;
import creator.loki.feature.counter.repository.CounterStatsRepository; import creator.loki.feature.counter.repository.CounterStatsRepository;
import creator.loki.feature.help.commands.HelpCommand; import creator.loki.feature.help.commands.HelpCommand;
@ -39,6 +43,8 @@ public class UtilityCommandRegistrar implements CommandRegistrar {
private final ReputationRepository reputationRepository; private final ReputationRepository reputationRepository;
private final CooldownManager cooldownManager; private final CooldownManager cooldownManager;
private final ServerStatsService serverStatsService; private final ServerStatsService serverStatsService;
private final RoastService roastService;
private final AndrertListener andrertListener;
@Inject @Inject
public UtilityCommandRegistrar( public UtilityCommandRegistrar(
@ -51,7 +57,9 @@ public class UtilityCommandRegistrar implements CommandRegistrar {
RatingsRepository ratingsRepository, RatingsRepository ratingsRepository,
ReputationRepository reputationRepository, ReputationRepository reputationRepository,
CooldownManager cooldownManager, CooldownManager cooldownManager,
CounterStatsRepository counterStatsRepository) { CounterStatsRepository counterStatsRepository,
RoastService roastService,
AndrertListener andrertListener) {
this.commandRegistry = commandRegistry; this.commandRegistry = commandRegistry;
this.config = config; this.config = config;
this.metrics = metrics; this.metrics = metrics;
@ -62,6 +70,8 @@ public class UtilityCommandRegistrar implements CommandRegistrar {
this.reputationRepository = reputationRepository; this.reputationRepository = reputationRepository;
this.cooldownManager = cooldownManager; this.cooldownManager = cooldownManager;
this.serverStatsService = new ServerStatsService(counterStatsRepository); this.serverStatsService = new ServerStatsService(counterStatsRepository);
this.roastService = roastService;
this.andrertListener = andrertListener;
} }
@Override @Override
@ -79,6 +89,8 @@ public class UtilityCommandRegistrar implements CommandRegistrar {
registry.register(new CounterSetupPrefixCommand(counterManager, configRepository)); registry.register(new CounterSetupPrefixCommand(counterManager, configRepository));
registry.register(new GenerateRatingsCommand(ratingsRepository, config)); registry.register(new GenerateRatingsCommand(ratingsRepository, config));
registry.register(new AgrCommand(config, roastService, andrertListener));
registry.register(new DossierCommand(config, roastService));
registry.register(new RepCommand(reputationRepository, cooldownManager)); registry.register(new RepCommand(reputationRepository, cooldownManager));
registry.register(new MinusRepCommand(reputationRepository, cooldownManager)); registry.register(new MinusRepCommand(reputationRepository, cooldownManager));

View file

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

View file

@ -32,6 +32,8 @@ import creator.loki.feature.music.core.SearchManager;
import creator.loki.feature.profile.service.ProfileService; import creator.loki.feature.profile.service.ProfileService;
import creator.loki.feature.rating.repository.RatingsRepository; import creator.loki.feature.rating.repository.RatingsRepository;
import creator.loki.feature.reputation.repository.ReputationRepository; import creator.loki.feature.reputation.repository.ReputationRepository;
import creator.loki.feature.roast.AndrertListener;
import creator.loki.feature.roast.RoastService;
import creator.loki.feature.role.CustomRoleManager; import creator.loki.feature.role.CustomRoleManager;
import creator.loki.feature.ship.repository.ShipPairRepository; import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.utility.UtilityCommandRegistrar; import creator.loki.feature.utility.UtilityCommandRegistrar;
@ -119,7 +121,9 @@ class SmokeCommandRegistrationTest {
mock(RatingsRepository.class), mock(RatingsRepository.class),
mock(ReputationRepository.class), mock(ReputationRepository.class),
mock(CooldownManager.class), mock(CooldownManager.class),
mock(CounterStatsRepository.class)); mock(CounterStatsRepository.class),
mock(RoastService.class),
mock(AndrertListener.class));
utilityRegistrar.register(registry); utilityRegistrar.register(registry);
var musicRegistrar = var musicRegistrar =

View file

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