Rthur2003 commited on
Commit
c3a06a0
·
1 Parent(s): 7f1abd6

fix: enhance error handling in YouTube analysis and implement API key validation with rate limiting

Browse files
app/routes/analyze.py CHANGED
@@ -143,7 +143,7 @@ async def _analyze_youtube(
143
  return AnalyzeResponse(
144
  result=result,
145
  warnings=yt_result.warnings,
146
- errors=[]
147
  )
148
 
149
  except ValueError as e:
 
143
  return AnalyzeResponse(
144
  result=result,
145
  warnings=yt_result.warnings,
146
+ errors=yt_result.errors,
147
  )
148
 
149
  except ValueError as e:
app/routes/commend/router.py CHANGED
@@ -7,9 +7,10 @@ from __future__ import annotations
7
 
8
  import os
9
  import time
 
10
  from typing import Literal, Optional
11
 
12
- from fastapi import APIRouter, HTTPException, status
13
  from pydantic import BaseModel, Field, field_validator
14
 
15
  from .youtube_service import (
@@ -112,6 +113,48 @@ class CommendHealthResponse(BaseModel):
112
  message: str
113
 
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  # --- API Endpoints ---
116
 
117
  @router.get("/health", response_model=CommendHealthResponse)
@@ -156,7 +199,7 @@ async def get_video_info(request: GenerateCommentRequest):
156
  return VideoDetailsResponse(**details)
157
 
158
 
159
- @router.post("/generate", response_model=GenerateCommentResponse)
160
  async def generate_youtube_comment(request: GenerateCommentRequest):
161
  """
162
  Generate an AI-powered comment for a YouTube video.
@@ -217,7 +260,7 @@ async def generate_youtube_comment(request: GenerateCommentRequest):
217
  )
218
 
219
 
220
- @router.post("/post", response_model=PostCommentResponse)
221
  async def post_comment_to_youtube(request: PostCommentRequest):
222
  """
223
  Post a comment to YouTube.
@@ -225,7 +268,14 @@ async def post_comment_to_youtube(request: PostCommentRequest):
225
  Note: This requires valid OAuth credentials with YouTube posting permissions.
226
  Comments include an AI disclaimer as required by platform policies.
227
  Each video can only receive ONE comment to prevent spam/bot detection.
 
228
  """
 
 
 
 
 
 
229
  video_id = extract_video_id(request.videoUrl)
230
 
231
  # Check if already commented on this video
 
7
 
8
  import os
9
  import time
10
+ from collections import defaultdict
11
  from typing import Literal, Optional
12
 
13
+ from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
14
  from pydantic import BaseModel, Field, field_validator
15
 
16
  from .youtube_service import (
 
113
  message: str
114
 
115
 
116
+ # --- Auth & Rate Limiting ---
117
+
118
+ _COMMEND_API_KEY = os.getenv("COMMEND_API_KEY")
119
+ _COMMEND_POSTING_ENABLED = os.getenv("COMMEND_ENABLE_POSTING", "false").lower() == "true"
120
+
121
+ # Simple in-memory rate limiter: IP -> list of timestamps
122
+ _rate_limit_store: dict[str, list[float]] = defaultdict(list)
123
+ _RATE_LIMIT_WINDOW = 60 # seconds
124
+ _RATE_LIMIT_MAX = 10 # requests per window
125
+
126
+
127
+ def _check_rate_limit(client_ip: str) -> None:
128
+ now = time.time()
129
+ window_start = now - _RATE_LIMIT_WINDOW
130
+ timestamps = _rate_limit_store[client_ip]
131
+ # Prune old entries
132
+ _rate_limit_store[client_ip] = [t for t in timestamps if t > window_start]
133
+ if len(_rate_limit_store[client_ip]) >= _RATE_LIMIT_MAX:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
136
+ detail="Rate limit exceeded. Please wait before making more requests."
137
+ )
138
+ _rate_limit_store[client_ip].append(now)
139
+
140
+
141
+ async def _require_api_key(
142
+ request: Request,
143
+ x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
144
+ ) -> None:
145
+ """Validate API key if COMMEND_API_KEY is configured."""
146
+ if not _COMMEND_API_KEY:
147
+ # No key configured — allow (development mode)
148
+ return
149
+ if x_api_key != _COMMEND_API_KEY:
150
+ logger.warning(f"Unauthorized commend request from {request.client.host if request.client else 'unknown'}")
151
+ raise HTTPException(
152
+ status_code=status.HTTP_401_UNAUTHORIZED,
153
+ detail="Invalid or missing API key."
154
+ )
155
+ _check_rate_limit(request.client.host if request.client else "unknown")
156
+
157
+
158
  # --- API Endpoints ---
159
 
160
  @router.get("/health", response_model=CommendHealthResponse)
 
199
  return VideoDetailsResponse(**details)
200
 
201
 
202
+ @router.post("/generate", response_model=GenerateCommentResponse, dependencies=[Depends(_require_api_key)])
203
  async def generate_youtube_comment(request: GenerateCommentRequest):
204
  """
205
  Generate an AI-powered comment for a YouTube video.
 
260
  )
261
 
262
 
263
+ @router.post("/post", response_model=PostCommentResponse, dependencies=[Depends(_require_api_key)])
264
  async def post_comment_to_youtube(request: PostCommentRequest):
265
  """
266
  Post a comment to YouTube.
 
268
  Note: This requires valid OAuth credentials with YouTube posting permissions.
269
  Comments include an AI disclaimer as required by platform policies.
270
  Each video can only receive ONE comment to prevent spam/bot detection.
271
+ Controlled by COMMEND_ENABLE_POSTING env (default: disabled).
272
  """
273
+ if not _COMMEND_POSTING_ENABLED:
274
+ raise HTTPException(
275
+ status_code=status.HTTP_403_FORBIDDEN,
276
+ detail="Comment posting is disabled. Set COMMEND_ENABLE_POSTING=true to enable."
277
+ )
278
+
279
  video_id = extract_video_id(request.videoUrl)
280
 
281
  # Check if already commented on this video
app/routes/commend/youtube_service.py CHANGED
@@ -13,6 +13,7 @@ import os
13
  import re
14
  import json
15
  from typing import Optional, Tuple, List, Dict, Any
 
16
 
17
  from google.oauth2.credentials import Credentials
18
  from google.auth.transport.requests import Request
@@ -92,8 +93,24 @@ def _get_oauth_service():
92
  raise ValueError(f"YouTube OAuth authentication failed: {e}")
93
 
94
 
 
 
 
 
 
 
 
95
  def extract_video_id(video_url: str) -> Optional[str]:
96
- """YouTube URL'sinden video ID'sini çıkarır."""
 
 
 
 
 
 
 
 
 
97
  patterns = [
98
  r"(?<=v=)[^&#?]+",
99
  r"(?<=be/)[^&#?]+",
@@ -104,7 +121,9 @@ def extract_video_id(video_url: str) -> Optional[str]:
104
  for pattern in patterns:
105
  match = re.search(pattern, video_url)
106
  if match:
107
- return match.group(0)
 
 
108
 
109
  return None
110
 
 
13
  import re
14
  import json
15
  from typing import Optional, Tuple, List, Dict, Any
16
+ from urllib.parse import urlparse
17
 
18
  from google.oauth2.credentials import Credentials
19
  from google.auth.transport.requests import Request
 
93
  raise ValueError(f"YouTube OAuth authentication failed: {e}")
94
 
95
 
96
+ _YOUTUBE_HOSTS = frozenset({
97
+ "youtube.com", "www.youtube.com", "m.youtube.com",
98
+ "music.youtube.com", "youtu.be", "www.youtu.be",
99
+ })
100
+ _VIDEO_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{11}$")
101
+
102
+
103
  def extract_video_id(video_url: str) -> Optional[str]:
104
+ """YouTube URL'sinden video ID'sini çıkarır (exact-host + 11-char ID)."""
105
+ try:
106
+ parsed = urlparse(video_url.strip())
107
+ except Exception:
108
+ return None
109
+
110
+ host = (parsed.hostname or "").lower()
111
+ if host not in _YOUTUBE_HOSTS:
112
+ return None
113
+
114
  patterns = [
115
  r"(?<=v=)[^&#?]+",
116
  r"(?<=be/)[^&#?]+",
 
121
  for pattern in patterns:
122
  match = re.search(pattern, video_url)
123
  if match:
124
+ candidate = match.group(0)
125
+ if _VIDEO_ID_RE.match(candidate):
126
+ return candidate
127
 
128
  return None
129
 
app/routes/data_processing.py CHANGED
@@ -22,14 +22,17 @@ async def process_audio_endpoint(
22
  Process an audio file with the given augmentation options.
23
  Returns the processed WAV file.
24
  """
 
25
  logger.info(f"Received audio processing request for file: {file.filename}")
26
-
27
- if not file.content_type.startswith("audio/"):
28
  raise HTTPException(status_code=400, detail="Invalid file type. Must be audio.")
29
 
30
  try:
31
- # Read file content
32
  content = await file.read()
 
 
33
 
34
  # Process audio
35
  processed_audio = process_audio(content, options)
 
22
  Process an audio file with the given augmentation options.
23
  Returns the processed WAV file.
24
  """
25
+ MAX_PAYLOAD_BYTES = 30 * 1024 * 1024 # 30 MB
26
  logger.info(f"Received audio processing request for file: {file.filename}")
27
+
28
+ if not file.content_type or not file.content_type.startswith("audio/"):
29
  raise HTTPException(status_code=400, detail="Invalid file type. Must be audio.")
30
 
31
  try:
32
+ # Read file content with size guard
33
  content = await file.read()
34
+ if len(content) > MAX_PAYLOAD_BYTES:
35
+ raise HTTPException(status_code=413, detail=f"File too large. Maximum size is {MAX_PAYLOAD_BYTES // (1024*1024)} MB.")
36
 
37
  # Process audio
38
  processed_audio = process_audio(content, options)
app/services/preview_model.py CHANGED
@@ -30,22 +30,23 @@ class PreviewModel:
30
 
31
  def analyze(self, fingerprint: str, context: dict | None = None) -> dict:
32
  """
33
- Generate realistic AI detection result from fingerprint.
34
-
35
  Args:
36
  fingerprint: Unique identifier for the content
37
  context: Optional context like warnings or metadata
38
-
39
  Returns:
40
  Analysis result with prediction and features
41
  """
42
  seed_value = self._generate_seed(fingerprint)
 
43
  base_confidence = self._calculate_base_confidence(seed_value)
44
-
45
- is_ai = base_confidence > self.threshold
46
- confidence = self._add_realistic_variance(base_confidence)
47
-
48
- features = self._generate_features(seed_value)
49
  indicators = self._build_indicators(is_ai, confidence, context)
50
 
51
  return {
@@ -83,23 +84,23 @@ class PreviewModel:
83
 
84
  return 0.45 + weighted * 0.45
85
 
86
- def _add_realistic_variance(self, base: float) -> float:
87
- """Add human-like variance to confidence score."""
88
- variance = random.gauss(0, self._variance_scale)
89
  adjusted = base + variance
90
-
91
  if adjusted > 0.95:
92
- adjusted = 0.95 - random.uniform(0, 0.03)
93
  elif adjusted < 0.51:
94
- adjusted = 0.51 + random.uniform(0, 0.02)
95
-
96
  return max(0.51, min(0.97, adjusted))
97
 
98
- def _generate_features(self, seed: float) -> dict:
99
- """Generate realistic feature scores."""
100
  def feature_score(offset: float) -> float:
101
  raw = (seed + offset) % 1.0
102
- noise = random.gauss(0, 0.08)
103
  return max(0.0, min(0.99, raw + noise))
104
 
105
  return {
 
30
 
31
  def analyze(self, fingerprint: str, context: dict | None = None) -> dict:
32
  """
33
+ Generate deterministic AI detection result from fingerprint.
34
+
35
  Args:
36
  fingerprint: Unique identifier for the content
37
  context: Optional context like warnings or metadata
38
+
39
  Returns:
40
  Analysis result with prediction and features
41
  """
42
  seed_value = self._generate_seed(fingerprint)
43
+ rng = random.Random(seed_value)
44
  base_confidence = self._calculate_base_confidence(seed_value)
45
+
46
+ confidence = self._add_realistic_variance(base_confidence, rng)
47
+ is_ai = confidence > self.threshold
48
+
49
+ features = self._generate_features(seed_value, rng)
50
  indicators = self._build_indicators(is_ai, confidence, context)
51
 
52
  return {
 
84
 
85
  return 0.45 + weighted * 0.45
86
 
87
+ def _add_realistic_variance(self, base: float, rng: random.Random) -> float:
88
+ """Add deterministic human-like variance to confidence score."""
89
+ variance = rng.gauss(0, self._variance_scale)
90
  adjusted = base + variance
91
+
92
  if adjusted > 0.95:
93
+ adjusted = 0.95 - rng.uniform(0, 0.03)
94
  elif adjusted < 0.51:
95
+ adjusted = 0.51 + rng.uniform(0, 0.02)
96
+
97
  return max(0.51, min(0.97, adjusted))
98
 
99
+ def _generate_features(self, seed: float, rng: random.Random) -> dict:
100
+ """Generate deterministic feature scores."""
101
  def feature_score(offset: float) -> float:
102
  raw = (seed + offset) % 1.0
103
+ noise = rng.gauss(0, 0.08)
104
  return max(0.0, min(0.99, raw + noise))
105
 
106
  return {