File size: 9,444 Bytes
e8d43fa
 
4fc93b8
484431d
7fd7594
e8d43fa
a5c8ff6
ea1eac0
e8d43fa
4fc93b8
 
 
 
 
c0426da
4fc93b8
8e30b6a
 
4fc93b8
 
8e30b6a
 
4fc93b8
c0426da
94ddfa7
db0fed3
4fc93b8
 
 
 
e8d43fa
4fc93b8
 
 
 
 
 
 
 
 
 
 
1900ea7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4fc93b8
1900ea7
4fc93b8
 
7c5da40
 
c0426da
4fc93b8
 
484431d
c0426da
 
 
 
4a1fdf8
c0426da
 
 
 
 
 
 
 
 
4a1fdf8
 
 
 
 
 
 
c0426da
 
 
 
 
 
 
 
da164cc
c0426da
 
484431d
db0fed3
 
 
 
 
 
 
 
da164cc
 
db0fed3
 
e8d43fa
 
94ddfa7
dcb5a1a
94ddfa7
dcb5a1a
7c5da40
 
 
e8d43fa
4fc93b8
7c5da40
 
 
94ddfa7
e8d43fa
94ddfa7
7c5da40
dcb5a1a
e8d43fa
7c5da40
 
94ddfa7
7c5da40
 
 
e8d43fa
dcb5a1a
e8d43fa
 
 
 
7c5da40
 
 
db0fed3
 
7c5da40
 
 
e8d43fa
7c5da40
e8d43fa
 
 
 
 
 
 
 
 
 
 
 
 
484431d
e8d43fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c5da40
e8d43fa
 
7c5da40
e8d43fa
db0fed3
8e30b6a
7c5da40
 
 
 
db0fed3
7c5da40
da164cc
c56dba2
 
 
2d188a5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import asyncio
from collections import defaultdict
import logging
from fastapi import APIRouter, Depends, HTTPException, Request, status
from app.dependencies import verify_api_key
from app.services.queue import get_queue_service
from slowapi.errors import RateLimitExceeded
from limits import parse
from redis.exceptions import LockError

from app.models.schemas import (
    AnalysisRequest,
    AnalysisResponse,
    ErrorResponse,
    GuildConfigSchema,
    HealthResponse,
    TextAnalysisRequest,
    ImageAnalysisRequest,
)
from app.services.download import download_file
from app.services.text_analyzer import analyze_text
from app.services.image_analyzer import analyze_image
from app.core.config import get_settings
from app.utils.exceptions import DeepfakeDetectionError, SetupRequiredError
from app.core.limiter import limiter
from app.config_manager import _load_all_configs, save_guild_config

logger = logging.getLogger(__name__)

router = APIRouter()
local_guild_locks = defaultdict(asyncio.Lock)

@router.get(
    "/",
    response_model=HealthResponse,
    tags=["Health"],
    summary="Health check endpoint",
)
async def health_check() -> HealthResponse:
    settings = get_settings()
    logger.info("Health check endpoint accessed")
    
    handlers = {
        "text": analyze_text,
        "image": analyze_image,
    }
    
    models_status = {}
    is_healthy = True
    
    for content_type in settings.AVAILABLE_MODELS.keys():
        handler = handlers.get(content_type)
        
        if handler is not None and callable(handler):
            models_status[content_type] = "ready"
        else:
            models_status[content_type] = "error_not_callable"
            is_healthy = False
            logger.error(f"Krytyczny brak! Handler dla typu '{content_type}' nie jest callable.")

    overall_status = "ok" if is_healthy else "degraded"
    
    return HealthResponse(
        status=overall_status,
        service="Deepfake Detection Service",
        version=settings.APP_VERSION,
        available_models=settings.AVAILABLE_MODELS,
        supported_types=list(settings.AVAILABLE_MODELS.keys()),
        models_status=models_status,
    )

@router.post("/guilds/{guild_id}/setup", tags=["Setup"], dependencies=[Depends(verify_api_key)])
async def save_discord_guild_setup(guild_id: str, payload: GuildConfigSchema):
    # Walidacja modeli z pliku ustawień
    settings = get_settings()
    allowed_text_models = settings.AVAILABLE_MODELS.get("text", [])
    allowed_image_models = settings.AVAILABLE_MODELS.get("image", [])
    
    # Walidujemy tylko wtedy, gdy model nie jest ustawiony na "none"
    if payload.active_text_model and payload.active_text_model.lower() != "none":
        if payload.active_text_model not in allowed_text_models:
            raise HTTPException(
                status_code=400,
                detail=f"Model '{payload.active_text_model}' nie jest dozwolony. Wybierz z: {allowed_text_models}"
            )
            
    if payload.active_image_model and payload.active_image_model.lower() != "none":
        if payload.active_image_model not in allowed_image_models:
            raise HTTPException(
                status_code=400,
                detail=f"Model '{payload.active_image_model}' nie jest dozwolony. Wybierz z: {allowed_image_models}"
            )
            
    # Zapis konfiguracji przez config_manager
    config_dict = payload.dict()
    save_guild_config(guild_id, config_dict)
    
    logger.info(f"Zapisano nową konfigurację dla serwera Discord {guild_id}")
    return {
        "status": "success",
        "message": f"Konfiguracja dla serwera {guild_id} została zapisana.",
        "config": config_dict,
    }

@router.get("/guilds/{guild_id}/config", tags=["Setup"], dependencies=[Depends(verify_api_key)])
async def get_discord_guild_config(guild_id: str):
    """Zwraca zapisaną konfigurację dla konkretnego serwera Discord."""
    configs = _load_all_configs()
    guild_config = configs.get(guild_id, {})
    
    return {
        "active_text_model": guild_config.get("active_text_model", "none"),
        "active_image_model": guild_config.get("active_image_model", "none"),
        "log_channel_id": guild_config.get("log_channel_id", None),
        "multi_model_workflow": guild_config.get("multi_model_workflow", False)
    }

async def _execute_analysis(payload: AnalysisRequest, guild_id: str, settings) -> dict:
    """Funkcja pomocnicza wykonująca właściwy proces pobierania i analizy."""
    if isinstance(payload, TextAnalysisRequest):
        content_type = "text"
    elif isinstance(payload, ImageAnalysisRequest):
        content_type = "image"
    else:
        raise HTTPException(
            status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, 
            detail="Unsupported file/content type."
        )

    try:
        if content_type == "text":
            if len(payload.text) > settings.MAX_CONTENT_SIZES["text"]:
                raise ValueError(f"Text content exceeds maximum length.")
            if len(payload.text) < 50:
                raise ValueError("Text content must be at least 50 characters")
            
            result = await analyze_text(payload.text, guild_id)

        elif content_type == "image":
            image_bytes = await download_file(str(payload.image_url))
            if not image_bytes:
                raise ValueError("Failed to download image")
            if len(image_bytes) > settings.MAX_CONTENT_SIZES["image"]:
                raise ValueError(f"Image size exceeds maximum.")
            
            result = await analyze_image(image_bytes, guild_id)
            
        result["content_type"] = content_type
        return result

    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except SetupRequiredError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except DeepfakeDetectionError as e:
        raise HTTPException(status_code=e.status_code, detail=e.message)
    except Exception as e:
        logger.error(f"Analysis error: {str(e)}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"Failed to analyze {content_type}")
    
@router.post(
    "/analyze",
    response_model=AnalysisResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Bad request"},
        408: {"model": ErrorResponse, "description": "Request timeout"},
        415: {"model": ErrorResponse, "description": "Unsupported media type"},
        429: {"model": ErrorResponse, "description": "Too many requests"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
    tags=["Analysis"],
    summary="Analyze content for deepfake detection",
    dependencies=[Depends(verify_api_key)]
)
async def analyze(request: Request, payload: AnalysisRequest) -> AnalysisResponse:
    guild_id = payload.guild_id
    user_id = payload.user_id
    
    limit_item = parse("1/5seconds")
    if not limiter.limiter.hit(limit_item, f"analyze:user:{user_id}"):
        logger.warning(f"Użytkownik {user_id} przekroczył limit zapytań (1/5s).")
        raise HTTPException(
            status_code=429, 
            detail="Przekroczyłeś limit zapytań. Możesz wykonać tylko 1 analizę na 5 sekund."
        )
    
    settings = get_settings()
    queue_service = get_queue_service()
    
    # 2. Sprawdzamy, czy Redis jest aktywny w pliku konfiguracyjnym oraz czy klient został zainicjalizowany
    if settings.REDIS_ENABLED and queue_service.redis_client is not None:
        logger.info(f"Używam rozproszonej blokady Redis dla użytkownika {user_id}")
        
        # Tworzymy blokadę przy użyciu klienta z QueueService [1.2.6]
        redis_lock = queue_service.redis_client.lock(
            "global_analysis_queue_lock", timeout=60, blocking_timeout=120
        )
        try:
            async with redis_lock:
                analysis_result = await _execute_analysis(payload, guild_id, settings)
        except LockError:
            logger.error(f"Użytkownik {user_id} odrzucony z kolejki Redis z powodu timeoutu.")
            raise HTTPException(
                status_code=503, 
                detail="Serwer jest zbyt zajęty (kolejka Redis przepełniona). Spróbuj ponownie za chwilę."
            )
    else:
        # FALLBACK: Jeśli Redis jest wyłączony, aplikacja automatycznie używa kolejki in-memory
        logger.info(f"Redis jest wyłączony. Używam lokalnej blokady in-memory dla gildii {guild_id}")
        
        local_lock = local_guild_locks[guild_id]
        async with local_lock:
            analysis_result = await _execute_analysis(payload, guild_id, settings)

    # 3. Zwrócenie wyniku analizy
    content_type = analysis_result["content_type"]
    logger.info(f"{content_type.capitalize()} analysis completed. Result: {analysis_result}")
    
    used_model = analysis_result.get("used_model", settings.AVAILABLE_MODELS.get(content_type)[0])
    
    return AnalysisResponse(
        is_deepfake=analysis_result["is_deepfake"],
        confidence=analysis_result["confidence"],
        analysis_time=analysis_result["analysis_time"],
        used_model=used_model,
        content_type=content_type,
        details=analysis_result.get("details"),
    )

from app.api.factcheck_router import router as factcheck_router
router.include_router(factcheck_router) #kupczak tu był