feat: deepseek ai, досье, парсер .txt, ролеплей roast
This commit is contained in:
parent
80863a9592
commit
2990f01958
17 changed files with 868 additions and 10 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
163
src/main/java/creator/loki/feature/roast/AgrCommand.java
Normal file
163
src/main/java/creator/loki/feature/roast/AgrCommand.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/main/java/creator/loki/feature/roast/AgrSlashCommands.java
Normal file
108
src/main/java/creator/loki/feature/roast/AgrSlashCommands.java
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/main/java/creator/loki/feature/roast/AndrertListener.java
Normal file
113
src/main/java/creator/loki/feature/roast/AndrertListener.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/main/java/creator/loki/feature/roast/DossierCommand.java
Normal file
121
src/main/java/creator/loki/feature/roast/DossierCommand.java
Normal 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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/java/creator/loki/feature/roast/RoastModule.java
Normal file
12
src/main/java/creator/loki/feature/roast/RoastModule.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
300
src/main/java/creator/loki/feature/roast/RoastService.java
Normal file
300
src/main/java/creator/loki/feature/roast/RoastService.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue