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) { saveConfig(guildId, tempSession.config); activeSetupSessions.delete(guildId); await interaction.update({ content: "✅ **Ustawienia zostały pomyślnie zapisane!**", embeds: [], components: [] }); } } if (interaction.customId === "setup_cancel") { activeSetupSessions.delete(guildId); await interaction.update({ content: "❌ **Konfiguracja została anulowana.**", embeds: [], components: [] }); } if (interaction.customId === "reportError") { await interaction.reply({ 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łędną klasyfikację.`); 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); } } if (interaction.customId === "modelCorrect") { await interaction.reply({ 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);