DetectMeBotBackend / index.js
Gradii's picture
frontend selecting model
7365bef
Raw
History Blame
17.7 kB
import dotenv from "dotenv";
dotenv.config();
import {
Client,
GatewayIntentBits,
Events,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder,
MessageFlags,
ApplicationCommandType,
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
PermissionFlagsBits,
ChannelSelectMenuBuilder,
StringSelectMenuBuilder,
ChannelType
} from "discord.js";
import { loadConfig, saveConfig } from "./configManager.js";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
const API_URL = process.env.API_URL || "http://127.0.0.1:8000";
const activeSetupSessions = new Map();
client.once(Events.ClientReady, async () => {
console.log(`Bot ready: ${client.user.tag}`);
try {
await client.application.commands.set([
{
name: "detect",
description: "Otwiera okienko do wklejenia linku lub tekstu do analizy",
type: ApplicationCommandType.ChatInput
},
{
name: "setup",
description: "Ustawienia kanału logów i modeli analizy (Wymaga Administratora)",
default_member_permissions: PermissionFlagsBits.Administrator.toString(),
type: ApplicationCommandType.ChatInput
},
{
name: "Wykryj deepfake",
type: ApplicationCommandType.Message
}
]);
console.log("Pomyślnie zarejestrowano komendy (/detect, /setup oraz menu kontekstowe)");
} catch (error) {
console.error("Błąd podczas rejestracji komend:", error);
}
});
// Pobieranie modeli bezpośrednio z FastAPI
async function fetchAvailableModels() {
try {
const response = await fetch(API_URL);
if (response.ok) {
const data = await response.json();
if (data.available_models) {
return data.available_models;
}
}
} catch (err) {
console.error("Błąd połączenia z FastAPI:", err.message);
}
return null;
}
function preparePayload(input, explicitContentType = null) {
const trimmed = input.trim();
if (explicitContentType) {
if (explicitContentType.startsWith("image/")) {
return { type: "image", payload: { image_url: trimmed, content_type: "image" } };
} else if (explicitContentType.startsWith("video/")) {
return { type: "video", payload: { video_url: trimmed, content_type: "video" } };
} else {
return { type: "file", payload: { file_url: trimmed, content_type: "file" } };
}
}
const isUrl = trimmed.startsWith("http://") || trimmed.startsWith("https://");
if (isUrl) {
const cleanUrl = trimmed.toLowerCase().split('?')[0];
if (cleanUrl.match(/\.(png|jpg|jpeg|webp|gif)$/)) {
return { type: "image", payload: { image_url: trimmed, content_type: "image" } };
} else if (cleanUrl.match(/\.(mp4|webm|mov|avi)$/)) {
return { type: "video", payload: { video_url: trimmed, content_type: "video" } };
} else {
return { type: "file", payload: { file_url: trimmed, content_type: "file" } };
}
}
return {
type: "text",
payload: { text: trimmed, content_type: "text" }
};
}
function getProgressBar(confidence, isDeepfake) {
const totalBlocks = 10;
const filledBlocks = Math.min(totalBlocks, Math.max(0, Math.round(confidence * totalBlocks)));
const emptyBlocks = totalBlocks - filledBlocks;
const blockEmoji = isDeepfake ? "🟥" : "🟩";
return blockEmoji.repeat(filledBlocks) + "⬛".repeat(emptyBlocks);
}
// CAŁKOWICIE DYNAMICZNY GENERATOR WIDOKU SETUPU
function generateSetupView(tempConfig, availableModels) {
const embed = new EmbedBuilder()
.setColor(0x5865F2)
.setTitle("⚙️ Konfiguracja Systemu Detekcji")
.setDescription("Wybierz kanał do wysyłania logów oraz aktywne modele dla poszczególnych formatów danych.")
.setTimestamp()
.setFooter({ text: "Wybierz opcje i kliknij Zapisz ustawienia" });
embed.addFields({
name: "📂 Kanał logów (Raporty)",
value: tempConfig.logChannelId ? `<#${tempConfig.logChannelId}>` : "*Wysyłanie tylko do konsoli*",
inline: false
});
// Dynamicznie dodajemy pola dla każdego formatu zwróconego przez FastAPI
for (const [contentType, models] of Object.entries(availableModels)) {
const currentSelected = tempConfig.models[contentType] || models[0] || "Brak";
embed.addFields({
name: `⚙️ Model dla formatu: ${contentType.toUpperCase()}`,
value: `\`${currentSelected}\``,
inline: true
});
}
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId("setup_log_channel")
.setPlaceholder("Wybierz kanał dla raportów")
.addChannelTypes(ChannelType.GuildText);
const components = [
new ActionRowBuilder().addComponents(channelSelect)
];
// Dynamicznie generujemy menu rozwijane dla każdego formatu danych (tekst, obraz, wideo itp.)
for (const [contentType, models] of Object.entries(availableModels)) {
if (components.length >= 4) break; // Limit Discorda (max 5 rzędów komponentów na wiadomość)
const currentSelected = tempConfig.models[contentType] || models[0];
const selectOptions = models.map(model => ({
label: model,
value: model,
default: currentSelected === model
}));
const modelSelect = new StringSelectMenuBuilder()
.setCustomId(`setup_model_${contentType}`)
.setPlaceholder(`Wybierz model dla ${contentType}`)
.addOptions(selectOptions);
components.push(new ActionRowBuilder().addComponents(modelSelect));
}
const buttonsRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("setup_save")
.setLabel("Zapisz ustawienia")
.setStyle(ButtonStyle.Success)
.setEmoji("💾"),
new ButtonBuilder()
.setCustomId("setup_cancel")
.setLabel("Anuluj")
.setStyle(ButtonStyle.Danger)
.setEmoji("❌")
);
components.push(buttonsRow);
return {
embeds: [embed],
components: components
};
}
async function sendLogToDiscord(guild, embedToSend) {
const config = loadConfig(guild.id);
if (!config.logChannelId) return;
try {
const channel = await guild.channels.fetch(config.logChannelId);
if (channel) {
await channel.send({ embeds: [embedToSend] });
}
} catch (err) {
console.warn(`Nie można wysłać logu na kanał ${config.logChannelId}:`, err.message);
}
}
async function handleAnalysis(interaction, userContent, targetMessage = null, explicitContentType = null) {
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
const serverConfig = loadConfig(interaction.guildId);
try {
const { type, payload } = preparePayload(userContent, explicitContentType);
// DYNAMICZNE POBIERANIE MODELU Z PLIKU KONFIGURACYJNEGO DLA DANEGO FORMATU (np. text, image, video)
const chosenModel = serverConfig.models[type];
if (chosenModel) {
payload.model = chosenModel;
}
console.log(`Wysyłanie zapytania typu: ${type} do API z modelem: ${payload.model || "domyślny"}...`);
const response = await fetch(`${API_URL}/analyze`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error("Szczegóły błędu z FastAPI:", JSON.stringify(errorData, null, 2));
let errorMsg = `Błąd serwera API (Status ${response.status})`;
if (errorData.detail) {
if (Array.isArray(errorData.detail)) {
errorMsg = errorData.detail
.map(err => `• Pole \`${err.loc.join(".")}\`: ${err.msg}`)
.join("\n");
} else {
errorMsg = errorData.detail;
}
}
throw new Error(errorMsg);
}
const data = await response.json();
if (targetMessage) {
try {
if (data.is_deepfake) {
await targetMessage.react('⚠️');
} else {
await targetMessage.react('✅');
}
} catch (reactError) {
console.warn("Nie udało się dodać reakcji do wiadomości:", reactError.message);
}
}
const embedColor = data.is_deepfake ? 0xFF0000 : 0x00FF00;
const verdictText = data.is_deepfake ? "⚠️ Wykryto potencjalny Deepfake!" : "✅ Zawartość wydaje się oryginalna";
const progressBar = getProgressBar(data.confidence, data.is_deepfake);
const confidencePercent = (data.confidence * 100).toFixed(2);
const embed = new EmbedBuilder()
.setColor(embedColor)
.setTitle("🛡️ Wynik Analizy Treści")
.setDescription(`**Werdykt:** ${verdictText}`)
.addFields(
{ name: "Pewność modelu", value: `\`${confidencePercent}%\` \n${progressBar}` },
{ name: "Czas przetwarzania", value: `\`${data.analysis_time.toFixed(3)}s\``, inline: true },
{ name: "Użyty model", value: `\`${data.used_model}\``, inline: true },
{ name: "Format danych", value: `\`${data.content_type.toUpperCase()}\``, inline: true }
)
.setTimestamp()
.setFooter({ text: "Deepfake Detection Service", iconURL: client.user.displayAvatarURL() });
const buttonRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("modelCorrect")
.setLabel("Model odpowiedział poprawnie")
.setStyle(ButtonStyle.Success)
.setEmoji("✅"),
new ButtonBuilder()
.setCustomId("reportError")
.setLabel("Zgłoś błąd analizy")
.setStyle(ButtonStyle.Danger)
.setEmoji("⚠️")
);
await interaction.editReply({
embeds: [embed],
components: [buttonRow]
});
} catch (error) {
console.error("Błąd podczas analizy:", error);
await interaction.editReply({
content: `❌ Nie udało się przeprowadzić analizy.\n\n**Szczegóły błędu:**\n${error.message}`,
});
}
}
client.on(Events.InteractionCreate, async (interaction) => {
if (interaction.isChatInputCommand()) {
if (interaction.commandName === "detect") {
const modal = new ModalBuilder()
.setCustomId("detectModal")
.setTitle("Detektor Deepfake");
const textInput = new TextInputBuilder()
.setCustomId("detectInput")
.setLabel("Wklej tutaj tekst lub link (obraz/wideo):")
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder("Wklej zawartość...")
.setRequired(true);
const actionRow = new ActionRowBuilder().addComponents(textInput);
modal.addComponents(actionRow);
await interaction.showModal(modal);
}
if (interaction.commandName === "setup") {
const guildId = interaction.guildId;
const currentConfig = loadConfig(guildId);
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
// Pobieramy aktywne modele bezpośrednio z FastAPI
const availableModels = await fetchAvailableModels();
// Jeśli backend nie działa, natychmiast przerywamy i wyświetlamy błąd
if (!availableModels || Object.keys(availableModels).length === 0) {
return interaction.editReply({
content: "❌ **Błąd konfiguracji:** Nie udało się nawiązać połączenia z backendem (FastAPI). Uruchom swój backend w Pythonie i spróbuj ponownie!"
});
}
// Inicjalizujemy domyślne modele w konfiguracji, jeśli nie były wcześniej ustawione
for (const [contentType, models] of Object.entries(availableModels)) {
if (!currentConfig.models[contentType] && models.length > 0) {
currentConfig.models[contentType] = models[0];
}
}
// Zapisujemy sesję z konfiguracją oraz pobranymi modelami
activeSetupSessions.set(guildId, {
config: { ...currentConfig },
availableModels
});
const setupView = generateSetupView(currentConfig, availableModels);
await interaction.editReply(setupView);
}
}
if (interaction.isChannelSelectMenu()) {
if (interaction.customId === "setup_log_channel") {
const guildId = interaction.guildId;
const tempSession = activeSetupSessions.get(guildId);
if (tempSession) {
tempSession.config.logChannelId = interaction.values[0];
await interaction.update(generateSetupView(tempSession.config, tempSession.availableModels));
}
}
}
// OBSŁUGA DYNAMICZNYCH MENU ROZWIJANYCH DLA MODELI
if (interaction.isStringSelectMenu()) {
const guildId = interaction.guildId;
const tempSession = activeSetupSessions.get(guildId);
if (tempSession) {
// Sprawdzamy czy zmieniany jest model (szukamy przedrostka setup_model_)
if (interaction.customId.startsWith("setup_model_")) {
const contentType = interaction.customId.replace("setup_model_", "");
tempSession.config.models[contentType] = interaction.values[0];
await interaction.update(generateSetupView(tempSession.config, tempSession.availableModels));
}
}
}
if (interaction.isMessageContextMenuCommand()) {
if (interaction.commandName === "Wykryj deepfake") {
const targetMessage = interaction.targetMessage;
let contentToAnalyze = targetMessage.content;
let explicitContentType = null;
const attachment = targetMessage.attachments.first();
if (attachment) {
contentToAnalyze = attachment.url;
explicitContentType = attachment.contentType;
}
if (!contentToAnalyze || contentToAnalyze.trim().length === 0) {
return interaction.reply({
content: "❌ Ta wiadomość nie zawiera tekstu ani załączników do analizy.",
flags: [MessageFlags.Ephemeral]
});
}
await handleAnalysis(interaction, contentToAnalyze, targetMessage, explicitContentType);
}
}
if (interaction.isModalSubmit()) {
if (interaction.customId === "detectModal") {
const userContent = interaction.fields.getTextInputValue("detectInput");
await handleAnalysis(interaction, userContent);
}
}
if (interaction.isButton()) {
const guildId = interaction.guildId;
if (interaction.customId === "setup_save") {
const tempSession = activeSetupSessions.get(guildId);
if (tempSession) {
try {
const response = await fetch(`http://backend-api-url/guilds/${guildId}/setup`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
active_text_model: tempSession.config.active_text_model || "none",
log_channel_id: tempSession.config.log_channel_id || null
})
});
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.detail || "Błąd zapisu na backendzie");
}
activeSetupSessions.delete(guildId);
await interaction.update({
content: "✅ **Ustawienia zostały pomyślnie zapisane na backendzie!**",
embeds: [],
components: []
});
} catch (error) {
console.error("[SETUP ERROR]", error);
await interaction.update({
content: `❌ **Wystąpił błąd podczas zapisywania konfiguracji:** ${error.message}`,
embeds: [],
components: []
});
}
}
}
if (interaction.customId === "setup_cancel") {
activeSetupSessions.delete(guildId);
await interaction.update({
content: "❌ **Konfiguracja została anulowana.**",
embeds: [],
components: []
});
}
if (interaction.customId === "reportError") {
const disabledRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("modelCorrect")
.setLabel("Model odpowiedział poprawnie")
.setStyle(ButtonStyle.Success)
.setEmoji("✅")
.setDisabled(true),
new ButtonBuilder()
.setCustomId("reportError")
.setLabel("Zgłoś błąd analizy")
.setStyle(ButtonStyle.Danger)
.setEmoji("⚠️")
.setDisabled(true)
);
// 2 UPDATE BUTTONS
await interaction.update({
embeds: [interaction.message.embeds[0]],
components: [disabledRow]
});
// 3 CONFIRMATION
await interaction.followUp({
content: "✅ **Dziękujemy!** Twoje zgłoszenie błędu zostało zarejestrowane.",
flags: [MessageFlags.Ephemeral]
});
console.log(`[RAPORT BŁĘDU] Użytkownik ${interaction.user.tag} (ID: ${interaction.user.id}) zgłosił błąd klasyfikacji.`);
const originalEmbed = interaction.message.embeds[0];
if (originalEmbed) {
const logEmbed = EmbedBuilder.from(originalEmbed)
.setColor(0xFFAA00)
.setTitle("⚠️ Zgłoszenie błędu analizy")
.setDescription(`Użytkownik **${interaction.user.tag}** (ID: \`${interaction.user.id}\`) zgłosił błąd analizy w poniższym raporcie.`);
await sendLogToDiscord(interaction.guild, logEmbed);
}
}
// CORRECT CLICK
if (interaction.customId === "modelCorrect") {
const disabledRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("modelCorrect")
.setLabel("Model odpowiedział poprawnie")
.setStyle(ButtonStyle.Success)
.setEmoji("✅")
.setDisabled(true),
new ButtonBuilder()
.setCustomId("reportError")
.setLabel("Zgłoś błąd analizy")
.setStyle(ButtonStyle.Danger)
.setEmoji("⚠️")
.setDisabled(true)
);
// 2 DISABLE BUTTONS
await interaction.update({
embeds: [interaction.message.embeds[0]],
components: [disabledRow]
});
// 3 CONFIRMATION
await interaction.followUp({
content: "✅ **Dziękujemy!** Twoje potwierdzenie zostało pomyślnie zapisane.",
flags: [MessageFlags.Ephemeral]
});
console.log(`[POTWIERDZENIE] Użytkownik ${interaction.user.tag} (ID: ${interaction.user.id}) potwierdził poprawną klasyfikację.`);
const originalEmbed = interaction.message.embeds[0];
if (originalEmbed) {
const logEmbed = EmbedBuilder.from(originalEmbed)
.setColor(0x00AAFF)
.setTitle("✅ Potwierdzona poprawność analizy")
.setDescription(`Użytkownik **${interaction.user.tag}** (ID: \`${interaction.user.id}\`) potwierdził poprawność raportu.`);
await sendLogToDiscord(interaction.guild, logEmbed);
}
}
}
});
client.on(Events.MessageCreate, (message) => {
if (message.author.bot) return;
console.log(`Message from ${message.author.tag}: ${message.content}`);
});
client.login(process.env.DISCORD_TOKEN);