Gradii commited on
Commit
2d188a5
·
2 Parent(s): da164cc7b59e80

Merge branch 'main' into backend-setup

Browse files
backend/app/api/factcheck_router.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from app.models.factcheck_schemas import FactCheckRequest, FactCheckResponse, FactCheckSource
3
+ from app.services.factcheck_service import analyze_with_gemini_grounding
4
+
5
+ router = APIRouter()
6
+
7
+ @router.post(
8
+ "/factcheck",
9
+ response_model=FactCheckResponse,
10
+ tags=["Fact-checking"],
11
+ summary="Zweryfikuj prawdziwość stwierdzenia"
12
+ )
13
+ async def fact_check_endpoint(payload: FactCheckRequest):
14
+ statement = payload.statement.strip()
15
+ if len(statement) < 10:
16
+ raise HTTPException(status_code=400, detail="Tekst do weryfikacji musi mieć co najmniej 10 znaków.")
17
+
18
+ # Wywołujemy usługę integrującą Google Search i Gemini
19
+ analysis = await analyze_with_gemini_grounding(statement)
20
+
21
+ # Konwersja słowników na obiekty Pydantic
22
+ formatted_sources = []
23
+ for s in analysis.get("sources", []):
24
+ formatted_sources.append(FactCheckSource(
25
+ title=s["title"],
26
+ url=s["url"],
27
+ snippet=s["snippet"]
28
+ ))
29
+
30
+ return FactCheckResponse(
31
+ verdict=analysis.get("verdict", "SPORNE"),
32
+ explanation=analysis.get("explanation", "Brak szczegółowego uzasadnienia."),
33
+ confidence=analysis.get("confidence", 0.5),
34
+ sources=formatted_sources
35
+ )
backend/app/api/routes.py CHANGED
@@ -224,4 +224,7 @@ async def analyze(request: Request, payload: AnalysisRequest) -> AnalysisRespons
224
  used_model=used_model,
225
  content_type=content_type,
226
  details=analysis_result.get("details"),
227
- )
 
 
 
 
224
  used_model=used_model,
225
  content_type=content_type,
226
  details=analysis_result.get("details"),
227
+ )
228
+
229
+ from app.api.factcheck_router import router as factcheck_router
230
+ router.include_router(factcheck_router) #kupczak tu był
backend/app/models/factcheck_schemas.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+
4
+ class FactCheckRequest(BaseModel):
5
+ statement: str
6
+
7
+ class FactCheckSource(BaseModel):
8
+ title: str
9
+ url: str
10
+ snippet: str
11
+
12
+ class FactCheckResponse(BaseModel):
13
+ verdict: str # "PRAWDA", "FAŁSZ", "SPORNE"
14
+ explanation: str
15
+ confidence: float
16
+ sources: List[FactCheckSource]
backend/app/services/factcheck_service.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import json
3
+ import re
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Dict, Any, List
7
+ import google.generativeai as genai
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def load_env_fallback():
12
+ """
13
+ Ręcznie wczytuje plik .env do os.environ z pełną diagnostyką w konsoli.
14
+ """
15
+ if os.getenv("GEMINI_API_KEY"):
16
+ return
17
+
18
+ possible_paths = [
19
+ Path(".env"), # Bieżący folder roboczy
20
+ Path("backend/.env"), # Folder backend
21
+ Path(__file__).resolve().parent.parent.parent / ".env" # Ścieżka relatywna do serwisu
22
+ ]
23
+
24
+ print(f"\n🔍 [DIAGNOSTYKA .ENV] Bieżący katalog roboczy (CWD): {os.getcwd()}")
25
+
26
+ found_any = False
27
+ for path_obj in possible_paths:
28
+ resolved_path = path_obj.resolve()
29
+ exists = resolved_path.exists()
30
+ print(f"👉 Sprawdzam ścieżkę: {resolved_path} -> [Znaleziono: {'TAK' if exists else 'NIE'}]")
31
+
32
+ if exists:
33
+ found_any = True
34
+ print(f"📖 Próba wczytania pliku: {resolved_path}")
35
+ try:
36
+ loaded_keys = []
37
+ with open(resolved_path, "r", encoding="utf-8") as f:
38
+ for line in f:
39
+ line = line.strip()
40
+ if line and not line.startswith("#") and "=" in line:
41
+ key, val = line.split("=", 1)
42
+ k_clean = key.strip()
43
+ v_clean = val.strip().strip("'\"")
44
+ os.environ[k_clean] = v_clean
45
+ loaded_keys.append(k_clean)
46
+ print(f"✅ Pomyślnie wczytano klucze z pliku: {loaded_keys}")
47
+ break
48
+ except Exception as e:
49
+ print(f"❌ Błąd odczytu pliku .env: {e}")
50
+
51
+ if not found_any:
52
+ print("❌ Nie znaleziono pliku .env w żadnej z badanych lokalizacji!")
53
+
54
+ final_key = os.getenv("GEMINI_API_KEY")
55
+ print(f"🔑 Status GEMINI_API_KEY: {'ZNAJDZIONO (zaczyna się od: ' + final_key[:6] + '...)' if final_key else 'NIE ZNAJDZIONO!'}\n")
56
+
57
+ # Uruchamiamy wczytywanie środowiska przy imporcie tego serwisu
58
+ load_env_fallback()
59
+
60
+
61
+ async def analyze_with_gemini_grounding(statement: str) -> Dict[str, Any]:
62
+ """
63
+ Analizuje stwierdzenie, automatycznie przeszukując internet za pomocą
64
+ wbudowanego w Gemini narzędzia Google Search Grounding.
65
+ """
66
+ # Upewniamy się, że środowisko jest załadowane
67
+ load_env_fallback()
68
+
69
+ api_key = os.getenv("GEMINI_API_KEY")
70
+
71
+ if not api_key:
72
+ logger.error("Brak klucza GEMINI_API_KEY w środowisku systemowym!")
73
+ return {
74
+ "verdict": "SPORNE",
75
+ "explanation": "Błąd backendu: Brak skonfigurowanego klucza GEMINI_API_KEY w pliku .env.",
76
+ "confidence": 0.0,
77
+ "sources": []
78
+ }
79
+
80
+ genai.configure(api_key=api_key)
81
+
82
+ prompt = f"""Jesteś zaawansowanym asystentem do weryfikacji faktów (fact-checking).
83
+ Przeanalizuj poniższe stwierdzenie, korzystając z wyszukiwarki Google (masz do niej dostęp jako narzędzie), aby zweryfikować jego prawdziwość w czasie rzeczywistym.
84
+
85
+ STWIERDZENIE DO WERYFIKACJI:
86
+ "{statement}"
87
+
88
+ Twoja odpowiedź musi być wyłącznie poprawnym obiektem JSON (bez bloków kodu typu ```json, bez dodatkowego tekstu na początku ani na końcu).
89
+ Format JSON:
90
+ {{
91
+ "verdict": "PRAWDA" lub "FAŁSZ" lub "SPORNE",
92
+ "explanation": "Zwięzłe (2-4 zdania), merytoryczne i obiektywne uzasadnienie werdyktu w języku polskim, wyjaśniające co mówią fakty."
93
+ }}
94
+
95
+ Wskazówki do werdyktu:
96
+ - "PRAWDA": Najnowsze fakty i wiarygodne źródła w pełni potwierdzają to stwierdzenie.
97
+ - "FAŁSZ": Fakty jednoznacznie zaprzeczają temu stwierdzeniu.
98
+ - "SPORNE": Informacje w sieci są sprzeczne, jest to kwestia opinii lub brak jednoznacznych dowodów.
99
+ """
100
+
101
+ try:
102
+ model = genai.GenerativeModel(
103
+ model_name="gemini-2.5-flash",
104
+ tools=[
105
+ genai.protos.Tool(
106
+ google_search=genai.protos.Tool.GoogleSearch()
107
+ )
108
+ ]
109
+ )
110
+
111
+ response = model.generate_content(
112
+ prompt,
113
+ generation_config=genai.types.GenerationConfig(
114
+ temperature=0.0
115
+ )
116
+ )
117
+
118
+ raw_text = response.text.strip()
119
+ logger.info(f"Surowa odpowiedź Gemini: {raw_text}")
120
+
121
+ if raw_text.startswith("```"):
122
+ match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_text, re.DOTALL)
123
+ if match:
124
+ raw_text = match.group(1)
125
+
126
+ result_json = json.loads(raw_text)
127
+
128
+ sources = []
129
+ candidate = response.candidates[0]
130
+ metadata = getattr(candidate, "grounding_metadata", None)
131
+
132
+ if metadata and getattr(metadata, "grounding_chunks", None):
133
+ for chunk in metadata.grounding_chunks:
134
+ if chunk.web:
135
+ sources.append({
136
+ "title": chunk.web.title,
137
+ "url": chunk.web.uri,
138
+ "snippet": "Źródło zweryfikowane bezpośrednio przez wyszukiwarkę Google."
139
+ })
140
+
141
+ return {
142
+ "verdict": result_json.get("verdict", "SPORNE"),
143
+ "explanation": result_json.get("explanation", "Brak uzasadnienia."),
144
+ "confidence": 0.95 if result_json.get("verdict") in ["PRAWDA", "FAŁSZ"] else 0.5,
145
+ "sources": sources
146
+ }
147
+
148
+ except Exception as e:
149
+ logger.error(f"Błąd analizy Gemini Grounding API: {e}", exc_info=True)
150
+ return {
151
+ "verdict": "SPORNE",
152
+ "explanation": f"Wystąpił błąd komunikacji z modelem językowym: {str(e)}",
153
+ "confidence": 0.0,
154
+ "sources": []
155
+ }
backend/requirements.txt CHANGED
@@ -13,4 +13,6 @@ Pillow
13
  slowapi
14
  pytest==7.4.3
15
  pytest-asyncio==0.21.1
16
- redis
 
 
 
13
  slowapi
14
  pytest==7.4.3
15
  pytest-asyncio==0.21.1
16
+ duckduckgo-search>=6.0.0
17
+ google-generativeai>=0.8.0
18
+ redis
index.js CHANGED
@@ -1,23 +1,23 @@
1
  import dotenv from "dotenv";
2
  dotenv.config();
3
 
4
- import {
5
- Client,
6
- GatewayIntentBits,
7
- Events,
8
- ModalBuilder,
9
- TextInputBuilder,
10
- TextInputStyle,
11
- ActionRowBuilder,
12
- MessageFlags,
13
- ApplicationCommandType,
14
- EmbedBuilder,
15
- ButtonBuilder,
16
- ButtonStyle,
17
- PermissionFlagsBits,
18
- ChannelSelectMenuBuilder,
19
- StringSelectMenuBuilder,
20
- ChannelType
21
  } from "discord.js";
22
 
23
  const client = new Client({
@@ -40,20 +40,28 @@ client.once(Events.ClientReady, async () => {
40
  {
41
  name: "detect",
42
  description: "Otwiera okienko do wklejenia linku lub tekstu do analizy",
43
- type: ApplicationCommandType.ChatInput
44
  },
45
  {
46
  name: "setup",
47
- description: "Ustawienia kanału logów i modeli analizy (Wymaga Administratora)",
48
- default_member_permissions: PermissionFlagsBits.Administrator.toString(),
49
- type: ApplicationCommandType.ChatInput
 
 
50
  },
51
  {
52
  name: "Wykryj deepfake",
53
- type: ApplicationCommandType.Message
54
- }
 
 
 
 
55
  ]);
56
- console.log("Pomyślnie zarejestrowano komendy (/detect, /setup oraz menu kontekstowe)");
 
 
57
  } catch (error) {
58
  console.error("Błąd podczas rejestracji komend:", error);
59
  }
@@ -85,12 +93,15 @@ async function fetchGuildConfig(guildId) {
85
  multiModelWorkflow: data.multi_model_workflow || false,
86
  models: {
87
  text: data.active_text_model || "none",
88
- image: data.active_image_model || "none"
89
- }
90
  };
91
  }
92
  } catch (err) {
93
- console.error(`[CONFIG ERROR] Błąd pobierania konfiguracji dla gildii ${guildId}:`, err.message);
 
 
 
94
  }
95
  return {
96
  logChannelId: null,
@@ -104,37 +115,58 @@ function preparePayload(input, explicitContentType = null) {
104
 
105
  if (explicitContentType) {
106
  if (explicitContentType.startsWith("image/")) {
107
- return { type: "image", payload: { image_url: trimmed, content_type: "image" } };
 
 
 
108
  } else if (explicitContentType.startsWith("video/")) {
109
- return { type: "video", payload: { video_url: trimmed, content_type: "video" } };
 
 
 
110
  } else {
111
- return { type: "file", payload: { file_url: trimmed, content_type: "file" } };
 
 
 
112
  }
113
  }
114
 
115
  const isUrl = trimmed.startsWith("http://") || trimmed.startsWith("https://");
116
 
117
  if (isUrl) {
118
- const cleanUrl = trimmed.toLowerCase().split('?')[0];
119
-
120
  if (cleanUrl.match(/\.(png|jpg|jpeg|webp|gif)$/)) {
121
- return { type: "image", payload: { image_url: trimmed, content_type: "image" } };
 
 
 
122
  } else if (cleanUrl.match(/\.(mp4|webm|mov|avi)$/)) {
123
- return { type: "video", payload: { video_url: trimmed, content_type: "video" } };
 
 
 
124
  } else {
125
- return { type: "file", payload: { file_url: trimmed, content_type: "file" } };
 
 
 
126
  }
127
  }
128
 
129
- return {
130
- type: "text",
131
- payload: { text: trimmed, content_type: "text" }
132
  };
133
  }
134
 
135
  function getProgressBar(confidence, isDeepfake) {
136
  const totalBlocks = 10;
137
- const filledBlocks = Math.min(totalBlocks, Math.max(0, Math.round(confidence * totalBlocks)));
 
 
 
138
  const emptyBlocks = totalBlocks - filledBlocks;
139
  const blockEmoji = isDeepfake ? "🟥" : "🟩";
140
  return blockEmoji.repeat(filledBlocks) + "⬛".repeat(emptyBlocks);
@@ -143,9 +175,11 @@ function getProgressBar(confidence, isDeepfake) {
143
  // CAŁKOWICIE DYNAMICZNY GENERATOR WIDOKU SETUPU
144
  function generateSetupView(tempConfig, availableModels) {
145
  const embed = new EmbedBuilder()
146
- .setColor(0x5865F2)
147
  .setTitle("⚙️ Konfiguracja Systemu Detekcji")
148
- .setDescription("Wybierz kanał do wysyłania logów oraz aktywne modele dla poszczególnych formatów danych.")
 
 
149
  .setTimestamp()
150
  .setFooter({ text: "Wybierz opcje i kliknij Zapisz ustawienia" });
151
 
@@ -165,7 +199,7 @@ function generateSetupView(tempConfig, availableModels) {
165
  embed.addFields({
166
  name: `⚙️ Model dla formatu: ${contentType.toUpperCase()}`,
167
  value: `\`${currentSelected}\``,
168
- inline: true
169
  });
170
  }
171
 
@@ -174,19 +208,17 @@ function generateSetupView(tempConfig, availableModels) {
174
  .setPlaceholder("Wybierz kanał dla raportów")
175
  .addChannelTypes(ChannelType.GuildText);
176
 
177
- const components = [
178
- new ActionRowBuilder().addComponents(channelSelect)
179
- ];
180
 
181
  for (const [contentType, models] of Object.entries(availableModels)) {
182
  if (components.length >= 4) break;
183
 
184
  const currentSelected = tempConfig.models[contentType] || models[0];
185
 
186
- const selectOptions = models.map(model => ({
187
  label: model,
188
  value: model,
189
- default: currentSelected === model
190
  }));
191
 
192
  const modelSelect = new StringSelectMenuBuilder()
@@ -214,14 +246,14 @@ function generateSetupView(tempConfig, availableModels) {
214
  .setCustomId("setup_cancel")
215
  .setLabel("Anuluj")
216
  .setStyle(ButtonStyle.Danger)
217
- .setEmoji("❌")
218
  );
219
 
220
  components.push(buttonsRow);
221
 
222
  return {
223
  embeds: [embed],
224
- components: components
225
  };
226
  }
227
 
@@ -235,11 +267,89 @@ async function sendLogToDiscord(guild, embedToSend) {
235
  await channel.send({ embeds: [embedToSend] });
236
  }
237
  } catch (err) {
238
- console.warn(`Nie można wysłać logu na kanał ${config.logChannelId}:`, err.message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  }
240
  }
241
 
242
- async function handleAnalysis(interaction, userContent, targetMessage = null, explicitContentType = null) {
 
 
 
 
 
 
243
  await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
244
 
245
  try {
@@ -247,7 +357,9 @@ async function handleAnalysis(interaction, userContent, targetMessage = null, ex
247
  payload.guild_id = interaction.guildId;
248
  payload.user_id = interaction.user.id;
249
 
250
- console.log(`Wysyłanie zapytania typu: ${type} do API z modelem: ${payload.model || "domyślny"}...`);
 
 
251
 
252
  const response = await fetch(`${API_URL}/analyze`, {
253
  method: "POST",
@@ -259,13 +371,16 @@ async function handleAnalysis(interaction, userContent, targetMessage = null, ex
259
 
260
  if (!response.ok) {
261
  const errorData = await response.json().catch(() => ({}));
262
- console.error("Szczegóły błędu z FastAPI:", JSON.stringify(errorData, null, 2));
263
-
 
 
 
264
  let errorMsg = `Błąd serwera API (Status ${response.status})`;
265
  if (errorData.detail) {
266
  if (Array.isArray(errorData.detail)) {
267
  errorMsg = errorData.detail
268
- .map(err => `• Pole \`${err.loc.join(".")}\`: ${err.msg}`)
269
  .join("\n");
270
  } else {
271
  errorMsg = errorData.detail;
@@ -279,12 +394,15 @@ async function handleAnalysis(interaction, userContent, targetMessage = null, ex
279
  if (targetMessage) {
280
  try {
281
  if (data.is_deepfake) {
282
- await targetMessage.react('⚠️');
283
  } else {
284
- await targetMessage.react('');
285
  }
286
  } catch (reactError) {
287
- console.warn("Nie udało się dodać reakcji do wiadomości:", reactError.message);
 
 
 
288
  }
289
  }
290
 
@@ -296,8 +414,28 @@ async function handleAnalysis(interaction, userContent, targetMessage = null, ex
296
  .setColor(embedColor)
297
  .setTitle("🛡️ Wynik Analizy Treści")
298
  .setDescription(`**Werdykt:** ${verdictText}`)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  .setTimestamp()
300
- .setFooter({ text: "Deepfake Detection Service", iconURL: client.user.displayAvatarURL() });
 
 
 
301
 
302
  // DYNAMICZNE RENDEROWANIE EMBEDA: Multi-Model vs Single-Model
303
  if (data.details && data.details.length > 0) {
@@ -340,14 +478,13 @@ async function handleAnalysis(interaction, userContent, targetMessage = null, ex
340
  .setCustomId("reportError")
341
  .setLabel("Zgłoś błąd analizy")
342
  .setStyle(ButtonStyle.Danger)
343
- .setEmoji("⚠️")
344
  );
345
 
346
  await interaction.editReply({
347
  embeds: [embed],
348
- components: [buttonRow]
349
  });
350
-
351
  } catch (error) {
352
  console.error("Błąd podczas analizy:", error);
353
  await interaction.editReply({
@@ -356,8 +493,9 @@ async function handleAnalysis(interaction, userContent, targetMessage = null, ex
356
  }
357
  }
358
 
 
 
359
  client.on(Events.InteractionCreate, async (interaction) => {
360
-
361
  if (interaction.isChatInputCommand()) {
362
  if (interaction.commandName === "detect") {
363
  const modal = new ModalBuilder()
@@ -379,7 +517,7 @@ client.on(Events.InteractionCreate, async (interaction) => {
379
 
380
  if (interaction.commandName === "setup") {
381
  const guildId = interaction.guildId;
382
-
383
  await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
384
 
385
  // Pobieramy konfigurację bezpośrednio z FastAPI
@@ -388,7 +526,8 @@ client.on(Events.InteractionCreate, async (interaction) => {
388
 
389
  if (!availableModels || Object.keys(availableModels).length === 0) {
390
  return interaction.editReply({
391
- 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!"
 
392
  });
393
  }
394
 
@@ -402,9 +541,9 @@ client.on(Events.InteractionCreate, async (interaction) => {
402
  }
403
  }
404
 
405
- activeSetupSessions.set(guildId, {
406
- config: { ...currentConfig },
407
- availableModels
408
  });
409
 
410
  const setupView = generateSetupView(currentConfig, availableModels);
@@ -418,7 +557,9 @@ client.on(Events.InteractionCreate, async (interaction) => {
418
  const tempSession = activeSetupSessions.get(guildId);
419
  if (tempSession) {
420
  tempSession.config.logChannelId = interaction.values[0];
421
- await interaction.update(generateSetupView(tempSession.config, tempSession.availableModels));
 
 
422
  }
423
  }
424
  }
@@ -427,13 +568,15 @@ client.on(Events.InteractionCreate, async (interaction) => {
427
  if (interaction.isStringSelectMenu()) {
428
  const guildId = interaction.guildId;
429
  const tempSession = activeSetupSessions.get(guildId);
430
-
431
  if (tempSession) {
432
  // Sprawdzamy czy zmieniany jest model (szukamy przedrostka setup_model_)
433
  if (interaction.customId.startsWith("setup_model_")) {
434
  const contentType = interaction.customId.replace("setup_model_", "");
435
  tempSession.config.models[contentType] = interaction.values[0];
436
- await interaction.update(generateSetupView(tempSession.config, tempSession.availableModels));
 
 
437
  }
438
  }
439
  }
@@ -441,12 +584,12 @@ client.on(Events.InteractionCreate, async (interaction) => {
441
  if (interaction.isMessageContextMenuCommand()) {
442
  if (interaction.commandName === "Wykryj deepfake") {
443
  const targetMessage = interaction.targetMessage;
444
-
445
  let contentToAnalyze = targetMessage.content;
446
  let explicitContentType = null;
447
 
448
  const attachment = targetMessage.attachments.first();
449
-
450
  if (attachment) {
451
  contentToAnalyze = attachment.url;
452
  explicitContentType = attachment.contentType;
@@ -454,14 +597,35 @@ client.on(Events.InteractionCreate, async (interaction) => {
454
 
455
  if (!contentToAnalyze || contentToAnalyze.trim().length === 0) {
456
  return interaction.reply({
457
- content: "❌ Ta wiadomość nie zawiera tekstu ani załączników do analizy.",
458
- flags: [MessageFlags.Ephemeral]
 
459
  });
460
  }
461
 
462
- await handleAnalysis(interaction, contentToAnalyze, targetMessage, explicitContentType);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  }
464
  }
 
465
 
466
  if (interaction.isModalSubmit()) {
467
  if (interaction.customId === "detectModal") {
@@ -480,7 +644,7 @@ client.on(Events.InteractionCreate, async (interaction) => {
480
  const response = await fetch(`${API_URL}/guilds/${guildId}/setup`, {
481
  method: "POST",
482
  headers: {
483
- "Content-Type": "application/json"
484
  },
485
  body: JSON.stringify({
486
  active_text_model: tempSession.config.models?.text || "none",
@@ -497,16 +661,17 @@ client.on(Events.InteractionCreate, async (interaction) => {
497
 
498
  activeSetupSessions.delete(guildId);
499
  await interaction.update({
500
- content: "✅ **Ustawienia zostały pomyślnie zapisane na backendzie!**",
 
501
  embeds: [],
502
- components: []
503
  });
504
  } catch (error) {
505
  console.error("[SETUP ERROR]", error);
506
  await interaction.update({
507
  content: `❌ **Wystąpił błąd podczas zapisywania konfiguracji:** ${error.message}`,
508
  embeds: [],
509
- components: []
510
  });
511
  }
512
  }
@@ -517,7 +682,7 @@ client.on(Events.InteractionCreate, async (interaction) => {
517
  await interaction.update({
518
  content: "❌ **Konfiguracja została anulowana.**",
519
  embeds: [],
520
- components: []
521
  });
522
  }
523
 
@@ -534,30 +699,35 @@ client.on(Events.InteractionCreate, async (interaction) => {
534
  .setLabel("Zgłoś błąd analizy")
535
  .setStyle(ButtonStyle.Danger)
536
  .setEmoji("⚠️")
537
- .setDisabled(true)
538
  );
539
 
540
  // 2 UPDATE BUTTONS
541
  await interaction.update({
542
- embeds: [interaction.message.embeds[0]],
543
- components: [disabledRow]
544
  });
545
 
546
  // 3 CONFIRMATION
547
  await interaction.followUp({
548
- content: "✅ **Dziękujemy!** Twoje zgłoszenie błędu zostało zarejestrowane.",
549
- flags: [MessageFlags.Ephemeral]
 
550
  });
551
 
552
- console.log(`[RAPORT BŁĘDU] Użytkownik ${interaction.user.tag} (ID: ${interaction.user.id}) zgłosił błąd klasyfikacji.`);
 
 
553
 
554
  const originalEmbed = interaction.message.embeds[0];
555
  if (originalEmbed) {
556
  const logEmbed = EmbedBuilder.from(originalEmbed)
557
- .setColor(0xFFAA00)
558
  .setTitle("⚠️ Zgłoszenie błędu analizy")
559
- .setDescription(`Użytkownik **${interaction.user.tag}** (ID: \`${interaction.user.id}\`) zgłosił błąd analizy w poniższym raporcie.`);
560
-
 
 
561
  await sendLogToDiscord(interaction.guild, logEmbed);
562
  }
563
  }
@@ -576,30 +746,35 @@ client.on(Events.InteractionCreate, async (interaction) => {
576
  .setLabel("Zgłoś błąd analizy")
577
  .setStyle(ButtonStyle.Danger)
578
  .setEmoji("⚠️")
579
- .setDisabled(true)
580
  );
581
 
582
  // 2 DISABLE BUTTONS
583
  await interaction.update({
584
- embeds: [interaction.message.embeds[0]],
585
- components: [disabledRow]
586
  });
587
 
588
  // 3 CONFIRMATION
589
  await interaction.followUp({
590
- content: "✅ **Dziękujemy!** Twoje potwierdzenie zostało pomyślnie zapisane.",
591
- flags: [MessageFlags.Ephemeral]
 
592
  });
593
 
594
- console.log(`[POTWIERDZENIE] Użytkownik ${interaction.user.tag} (ID: ${interaction.user.id}) potwierdził poprawną klasyfikację.`);
 
 
595
 
596
  const originalEmbed = interaction.message.embeds[0];
597
  if (originalEmbed) {
598
  const logEmbed = EmbedBuilder.from(originalEmbed)
599
- .setColor(0x00AAFF)
600
  .setTitle("✅ Potwierdzona poprawność analizy")
601
- .setDescription(`Użytkownik **${interaction.user.tag}** (ID: \`${interaction.user.id}\`) potwierdził poprawność raportu.`);
602
-
 
 
603
  await sendLogToDiscord(interaction.guild, logEmbed);
604
  }
605
  }
@@ -619,4 +794,4 @@ client.on(Events.MessageCreate, (message) => {
619
  console.log(`Message from ${message.author.tag}: ${message.content}`);
620
  });
621
 
622
- client.login(process.env.DISCORD_TOKEN);
 
1
  import dotenv from "dotenv";
2
  dotenv.config();
3
 
4
+ import {
5
+ Client,
6
+ GatewayIntentBits,
7
+ Events,
8
+ ModalBuilder,
9
+ TextInputBuilder,
10
+ TextInputStyle,
11
+ ActionRowBuilder,
12
+ MessageFlags,
13
+ ApplicationCommandType,
14
+ EmbedBuilder,
15
+ ButtonBuilder,
16
+ ButtonStyle,
17
+ PermissionFlagsBits,
18
+ ChannelSelectMenuBuilder,
19
+ StringSelectMenuBuilder,
20
+ ChannelType,
21
  } from "discord.js";
22
 
23
  const client = new Client({
 
40
  {
41
  name: "detect",
42
  description: "Otwiera okienko do wklejenia linku lub tekstu do analizy",
43
+ type: ApplicationCommandType.ChatInput,
44
  },
45
  {
46
  name: "setup",
47
+ description:
48
+ "Ustawienia kanału logów i modeli analizy (Wymaga Administratora)",
49
+ default_member_permissions:
50
+ PermissionFlagsBits.Administrator.toString(),
51
+ type: ApplicationCommandType.ChatInput,
52
  },
53
  {
54
  name: "Wykryj deepfake",
55
+ type: ApplicationCommandType.Message,
56
+ },
57
+ {
58
+ name: "Weryfikacja faktów", // <--- TA LINIA
59
+ type: ApplicationCommandType.Message,
60
+ },
61
  ]);
62
+ console.log(
63
+ "Pomyślnie zarejestrowano komendy (/detect, /setup oraz menu kontekstowe)",
64
+ );
65
  } catch (error) {
66
  console.error("Błąd podczas rejestracji komend:", error);
67
  }
 
93
  multiModelWorkflow: data.multi_model_workflow || false,
94
  models: {
95
  text: data.active_text_model || "none",
96
+ image: data.active_image_model || "none",
97
+ },
98
  };
99
  }
100
  } catch (err) {
101
+ console.error(
102
+ `[CONFIG ERROR] Błąd pobierania konfiguracji dla gildii ${guildId}:`,
103
+ err.message,
104
+ );
105
  }
106
  return {
107
  logChannelId: null,
 
115
 
116
  if (explicitContentType) {
117
  if (explicitContentType.startsWith("image/")) {
118
+ return {
119
+ type: "image",
120
+ payload: { image_url: trimmed, content_type: "image" },
121
+ };
122
  } else if (explicitContentType.startsWith("video/")) {
123
+ return {
124
+ type: "video",
125
+ payload: { video_url: trimmed, content_type: "video" },
126
+ };
127
  } else {
128
+ return {
129
+ type: "file",
130
+ payload: { file_url: trimmed, content_type: "file" },
131
+ };
132
  }
133
  }
134
 
135
  const isUrl = trimmed.startsWith("http://") || trimmed.startsWith("https://");
136
 
137
  if (isUrl) {
138
+ const cleanUrl = trimmed.toLowerCase().split("?")[0];
139
+
140
  if (cleanUrl.match(/\.(png|jpg|jpeg|webp|gif)$/)) {
141
+ return {
142
+ type: "image",
143
+ payload: { image_url: trimmed, content_type: "image" },
144
+ };
145
  } else if (cleanUrl.match(/\.(mp4|webm|mov|avi)$/)) {
146
+ return {
147
+ type: "video",
148
+ payload: { video_url: trimmed, content_type: "video" },
149
+ };
150
  } else {
151
+ return {
152
+ type: "file",
153
+ payload: { file_url: trimmed, content_type: "file" },
154
+ };
155
  }
156
  }
157
 
158
+ return {
159
+ type: "text",
160
+ payload: { text: trimmed, content_type: "text" },
161
  };
162
  }
163
 
164
  function getProgressBar(confidence, isDeepfake) {
165
  const totalBlocks = 10;
166
+ const filledBlocks = Math.min(
167
+ totalBlocks,
168
+ Math.max(0, Math.round(confidence * totalBlocks)),
169
+ );
170
  const emptyBlocks = totalBlocks - filledBlocks;
171
  const blockEmoji = isDeepfake ? "🟥" : "🟩";
172
  return blockEmoji.repeat(filledBlocks) + "⬛".repeat(emptyBlocks);
 
175
  // CAŁKOWICIE DYNAMICZNY GENERATOR WIDOKU SETUPU
176
  function generateSetupView(tempConfig, availableModels) {
177
  const embed = new EmbedBuilder()
178
+ .setColor(0x5865f2)
179
  .setTitle("⚙️ Konfiguracja Systemu Detekcji")
180
+ .setDescription(
181
+ "Wybierz kanał do wysyłania logów oraz aktywne modele dla poszczególnych formatów danych.",
182
+ )
183
  .setTimestamp()
184
  .setFooter({ text: "Wybierz opcje i kliknij Zapisz ustawienia" });
185
 
 
199
  embed.addFields({
200
  name: `⚙️ Model dla formatu: ${contentType.toUpperCase()}`,
201
  value: `\`${currentSelected}\``,
202
+ inline: true,
203
  });
204
  }
205
 
 
208
  .setPlaceholder("Wybierz kanał dla raportów")
209
  .addChannelTypes(ChannelType.GuildText);
210
 
211
+ const components = [new ActionRowBuilder().addComponents(channelSelect)];
 
 
212
 
213
  for (const [contentType, models] of Object.entries(availableModels)) {
214
  if (components.length >= 4) break;
215
 
216
  const currentSelected = tempConfig.models[contentType] || models[0];
217
 
218
+ const selectOptions = models.map((model) => ({
219
  label: model,
220
  value: model,
221
+ default: currentSelected === model,
222
  }));
223
 
224
  const modelSelect = new StringSelectMenuBuilder()
 
246
  .setCustomId("setup_cancel")
247
  .setLabel("Anuluj")
248
  .setStyle(ButtonStyle.Danger)
249
+ .setEmoji("❌"),
250
  );
251
 
252
  components.push(buttonsRow);
253
 
254
  return {
255
  embeds: [embed],
256
+ components: components,
257
  };
258
  }
259
 
 
267
  await channel.send({ embeds: [embedToSend] });
268
  }
269
  } catch (err) {
270
+ console.warn(
271
+ `Nie można wysłać logu na kanał ${config.logChannelId}:`,
272
+ err.message,
273
+ );
274
+ }
275
+ }
276
+
277
+ async function handleFactCheck(interaction, statement) {
278
+ await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
279
+
280
+ try {
281
+ console.log(`Wysyłanie zapytania do weryfikacji faktów: "${statement.slice(0, 30)}..."`);
282
+
283
+ const response = await fetch(`${API_URL}/factcheck`, {
284
+ method: "POST",
285
+ headers: {
286
+ "Content-Type": "application/json",
287
+ },
288
+ body: JSON.stringify({ statement: statement }),
289
+ });
290
+
291
+ if (!response.ok) {
292
+ const errorData = await response.json().catch(() => ({}));
293
+ throw new Error(errorData.detail || `Błąd API (Status ${response.status})`);
294
+ }
295
+
296
+ const data = await response.json();
297
+
298
+ let embedColor = 0xFFAA00; // Żółty domyślnie (SPORNE)
299
+ let verdictEmoji = "⚖️";
300
+
301
+ if (data.verdict === "PRAWDA") {
302
+ embedColor = 0x00FF00; // Zielony
303
+ verdictEmoji = "✅";
304
+ } else if (data.verdict === "FAŁSZ") {
305
+ embedColor = 0xFF0000; // Czerwony
306
+ verdictEmoji = "❌";
307
+ }
308
+
309
+ const embed = new EmbedBuilder()
310
+ .setColor(embedColor)
311
+ .setTitle(`${verdictEmoji} Wynik Weryfikacji Faktów`)
312
+ .setDescription(`**Badane stwierdzenie:**\n*"${statement}"*\n\n**Werdykt:** \`${data.verdict}\``)
313
+ .addFields(
314
+ { name: "📝 Analiza merytoryczna", value: data.explanation },
315
+ { name: "🎯 Pewność analizy", value: `\`${(data.confidence * 100).toFixed(0)}%\``, inline: true }
316
+ )
317
+ .setTimestamp()
318
+ .setFooter({ text: "System Fact-checkingowy", iconURL: client.user.displayAvatarURL() });
319
+
320
+ // Formatowanie źródeł
321
+ if (data.sources && data.sources.length > 0) {
322
+ const sourcesText = data.sources
323
+ .map((src, idx) => `**[${idx + 1}]** [${src.title}](${src.url})\n*${src.snippet.slice(0, 150)}...*`)
324
+ .join("\n\n");
325
+
326
+ // Discord ma limit 1024 znaków na jedno pole w Embedzie, zabezpieczamy się przed jego przekroczeniem
327
+ const truncatedSources = sourcesText.length > 1024 ? sourcesText.slice(0, 1000) + "..." : sourcesText;
328
+
329
+ embed.addFields({ name: "🔗 Wykorzystane źródła internetowe", value: truncatedSources });
330
+ } else {
331
+ embed.addFields({ name: "🔗 Wykorzystane źródła internetowe", value: "Brak bezpośrednich źródeł." });
332
+ }
333
+
334
+ await interaction.editReply({
335
+ embeds: [embed]
336
+ });
337
+
338
+ } catch (error) {
339
+ console.error("Błąd podczas weryfikacji faktów:", error);
340
+ await interaction.editReply({
341
+ content: `❌ Nie udało się przeprowadzić weryfikacji faktów.\n\n**Szczegóły błędu:**\n${error.message}`,
342
+ });
343
  }
344
  }
345
 
346
+
347
+ async function handleAnalysis(
348
+ interaction,
349
+ userContent,
350
+ targetMessage = null,
351
+ explicitContentType = null,
352
+ ) {
353
  await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
354
 
355
  try {
 
357
  payload.guild_id = interaction.guildId;
358
  payload.user_id = interaction.user.id;
359
 
360
+ console.log(
361
+ `Wysyłanie zapytania typu: ${type} do API z modelem: ${payload.model || "domyślny"}...`,
362
+ );
363
 
364
  const response = await fetch(`${API_URL}/analyze`, {
365
  method: "POST",
 
371
 
372
  if (!response.ok) {
373
  const errorData = await response.json().catch(() => ({}));
374
+ console.error(
375
+ "Szczegóły błędu z FastAPI:",
376
+ JSON.stringify(errorData, null, 2),
377
+ );
378
+
379
  let errorMsg = `Błąd serwera API (Status ${response.status})`;
380
  if (errorData.detail) {
381
  if (Array.isArray(errorData.detail)) {
382
  errorMsg = errorData.detail
383
+ .map((err) => `• Pole \`${err.loc.join(".")}\`: ${err.msg}`)
384
  .join("\n");
385
  } else {
386
  errorMsg = errorData.detail;
 
394
  if (targetMessage) {
395
  try {
396
  if (data.is_deepfake) {
397
+ await targetMessage.react("⚠️");
398
  } else {
399
+ await targetMessage.react("");
400
  }
401
  } catch (reactError) {
402
+ console.warn(
403
+ "Nie udało się dodać reakcji do wiadomości:",
404
+ reactError.message,
405
+ );
406
  }
407
  }
408
 
 
414
  .setColor(embedColor)
415
  .setTitle("🛡️ Wynik Analizy Treści")
416
  .setDescription(`**Werdykt:** ${verdictText}`)
417
+ .addFields(
418
+ {
419
+ name: "Pewność modelu",
420
+ value: `\`${confidencePercent}%\` \n${progressBar}`,
421
+ },
422
+ {
423
+ name: "Czas przetwarzania",
424
+ value: `\`${data.analysis_time.toFixed(3)}s\``,
425
+ inline: true,
426
+ },
427
+ { name: "Użyty model", value: `\`${data.used_model}\``, inline: true },
428
+ {
429
+ name: "Format danych",
430
+ value: `\`${data.content_type.toUpperCase()}\``,
431
+ inline: true,
432
+ },
433
+ )
434
  .setTimestamp()
435
+ .setFooter({
436
+ text: "Deepfake Detection Service",
437
+ iconURL: client.user.displayAvatarURL(),
438
+ });
439
 
440
  // DYNAMICZNE RENDEROWANIE EMBEDA: Multi-Model vs Single-Model
441
  if (data.details && data.details.length > 0) {
 
478
  .setCustomId("reportError")
479
  .setLabel("Zgłoś błąd analizy")
480
  .setStyle(ButtonStyle.Danger)
481
+ .setEmoji("⚠️"),
482
  );
483
 
484
  await interaction.editReply({
485
  embeds: [embed],
486
+ components: [buttonRow],
487
  });
 
488
  } catch (error) {
489
  console.error("Błąd podczas analizy:", error);
490
  await interaction.editReply({
 
493
  }
494
  }
495
 
496
+ //funkcja kupczaka
497
+
498
  client.on(Events.InteractionCreate, async (interaction) => {
 
499
  if (interaction.isChatInputCommand()) {
500
  if (interaction.commandName === "detect") {
501
  const modal = new ModalBuilder()
 
517
 
518
  if (interaction.commandName === "setup") {
519
  const guildId = interaction.guildId;
520
+
521
  await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
522
 
523
  // Pobieramy konfigurację bezpośrednio z FastAPI
 
526
 
527
  if (!availableModels || Object.keys(availableModels).length === 0) {
528
  return interaction.editReply({
529
+ content:
530
+ "❌ **Błąd konfiguracji:** Nie udało się nawiązać połączenia z backendem (FastAPI). Uruchom swój backend w Pythonie i spróbuj ponownie!",
531
  });
532
  }
533
 
 
541
  }
542
  }
543
 
544
+ activeSetupSessions.set(guildId, {
545
+ config: { ...currentConfig },
546
+ availableModels,
547
  });
548
 
549
  const setupView = generateSetupView(currentConfig, availableModels);
 
557
  const tempSession = activeSetupSessions.get(guildId);
558
  if (tempSession) {
559
  tempSession.config.logChannelId = interaction.values[0];
560
+ await interaction.update(
561
+ generateSetupView(tempSession.config, tempSession.availableModels),
562
+ );
563
  }
564
  }
565
  }
 
568
  if (interaction.isStringSelectMenu()) {
569
  const guildId = interaction.guildId;
570
  const tempSession = activeSetupSessions.get(guildId);
571
+
572
  if (tempSession) {
573
  // Sprawdzamy czy zmieniany jest model (szukamy przedrostka setup_model_)
574
  if (interaction.customId.startsWith("setup_model_")) {
575
  const contentType = interaction.customId.replace("setup_model_", "");
576
  tempSession.config.models[contentType] = interaction.values[0];
577
+ await interaction.update(
578
+ generateSetupView(tempSession.config, tempSession.availableModels),
579
+ );
580
  }
581
  }
582
  }
 
584
  if (interaction.isMessageContextMenuCommand()) {
585
  if (interaction.commandName === "Wykryj deepfake") {
586
  const targetMessage = interaction.targetMessage;
587
+
588
  let contentToAnalyze = targetMessage.content;
589
  let explicitContentType = null;
590
 
591
  const attachment = targetMessage.attachments.first();
592
+
593
  if (attachment) {
594
  contentToAnalyze = attachment.url;
595
  explicitContentType = attachment.contentType;
 
597
 
598
  if (!contentToAnalyze || contentToAnalyze.trim().length === 0) {
599
  return interaction.reply({
600
+ content:
601
+ "❌ Ta wiadomość nie zawiera tekstu ani załączników do analizy.",
602
+ flags: [MessageFlags.Ephemeral],
603
  });
604
  }
605
 
606
+ await handleAnalysis(
607
+ interaction,
608
+ contentToAnalyze,
609
+ targetMessage,
610
+ explicitContentType,
611
+ );
612
+ }
613
+ if (interaction.commandName === "Weryfikacja faktów") {
614
+ const targetMessage = interaction.targetMessage;
615
+ const contentToVerify = targetMessage.content;
616
+
617
+ if (!contentToVerify || contentToVerify.trim().length < 10) {
618
+ return interaction.reply({
619
+ content:
620
+ "❌ Wiadomość musi mieć przynajmniej 10 znaków tekstu, aby można było ją zweryfikować.",
621
+ flags: [MessageFlags.Ephemeral],
622
+ });
623
+ }
624
+
625
+ await handleFactCheck(interaction, contentToVerify);
626
  }
627
  }
628
+ //koniec funkcji kupczaka
629
 
630
  if (interaction.isModalSubmit()) {
631
  if (interaction.customId === "detectModal") {
 
644
  const response = await fetch(`${API_URL}/guilds/${guildId}/setup`, {
645
  method: "POST",
646
  headers: {
647
+ "Content-Type": "application/json",
648
  },
649
  body: JSON.stringify({
650
  active_text_model: tempSession.config.models?.text || "none",
 
661
 
662
  activeSetupSessions.delete(guildId);
663
  await interaction.update({
664
+ content:
665
+ "✅ **Ustawienia zostały pomyślnie zapisane na backendzie!**",
666
  embeds: [],
667
+ components: [],
668
  });
669
  } catch (error) {
670
  console.error("[SETUP ERROR]", error);
671
  await interaction.update({
672
  content: `❌ **Wystąpił błąd podczas zapisywania konfiguracji:** ${error.message}`,
673
  embeds: [],
674
+ components: [],
675
  });
676
  }
677
  }
 
682
  await interaction.update({
683
  content: "❌ **Konfiguracja została anulowana.**",
684
  embeds: [],
685
+ components: [],
686
  });
687
  }
688
 
 
699
  .setLabel("Zgłoś błąd analizy")
700
  .setStyle(ButtonStyle.Danger)
701
  .setEmoji("⚠️")
702
+ .setDisabled(true),
703
  );
704
 
705
  // 2 UPDATE BUTTONS
706
  await interaction.update({
707
+ embeds: [interaction.message.embeds[0]],
708
+ components: [disabledRow],
709
  });
710
 
711
  // 3 CONFIRMATION
712
  await interaction.followUp({
713
+ content:
714
+ "✅ **Dziękujemy!** Twoje zgłoszenie błędu zostało zarejestrowane.",
715
+ flags: [MessageFlags.Ephemeral],
716
  });
717
 
718
+ console.log(
719
+ `[RAPORT BŁĘDU] Użytkownik ${interaction.user.tag} (ID: ${interaction.user.id}) zgłosił błąd klasyfikacji.`,
720
+ );
721
 
722
  const originalEmbed = interaction.message.embeds[0];
723
  if (originalEmbed) {
724
  const logEmbed = EmbedBuilder.from(originalEmbed)
725
+ .setColor(0xffaa00)
726
  .setTitle("⚠️ Zgłoszenie błędu analizy")
727
+ .setDescription(
728
+ `Użytkownik **${interaction.user.tag}** (ID: \`${interaction.user.id}\`) zgłosił błąd analizy w poniższym raporcie.`,
729
+ );
730
+
731
  await sendLogToDiscord(interaction.guild, logEmbed);
732
  }
733
  }
 
746
  .setLabel("Zgłoś błąd analizy")
747
  .setStyle(ButtonStyle.Danger)
748
  .setEmoji("⚠️")
749
+ .setDisabled(true),
750
  );
751
 
752
  // 2 DISABLE BUTTONS
753
  await interaction.update({
754
+ embeds: [interaction.message.embeds[0]],
755
+ components: [disabledRow],
756
  });
757
 
758
  // 3 CONFIRMATION
759
  await interaction.followUp({
760
+ content:
761
+ "✅ **Dziękujemy!** Twoje potwierdzenie zostało pomyślnie zapisane.",
762
+ flags: [MessageFlags.Ephemeral],
763
  });
764
 
765
+ console.log(
766
+ `[POTWIERDZENIE] Użytkownik ${interaction.user.tag} (ID: ${interaction.user.id}) potwierdził poprawną klasyfikację.`,
767
+ );
768
 
769
  const originalEmbed = interaction.message.embeds[0];
770
  if (originalEmbed) {
771
  const logEmbed = EmbedBuilder.from(originalEmbed)
772
+ .setColor(0x00aaff)
773
  .setTitle("✅ Potwierdzona poprawność analizy")
774
+ .setDescription(
775
+ `Użytkownik **${interaction.user.tag}** (ID: \`${interaction.user.id}\`) potwierdził poprawność raportu.`,
776
+ );
777
+
778
  await sendLogToDiscord(interaction.guild, logEmbed);
779
  }
780
  }
 
794
  console.log(`Message from ${message.author.tag}: ${message.content}`);
795
  });
796
 
797
+ client.login(process.env.DISCORD_TOKEN);