minhvtt commited on
Commit
f018db8
·
verified ·
1 Parent(s): 48eca3e

Update app/routers/screenshots.py

Browse files
Files changed (1) hide show
  1. app/routers/screenshots.py +267 -226
app/routers/screenshots.py CHANGED
@@ -1,226 +1,267 @@
1
- import asyncio
2
- import uuid
3
- from datetime import datetime, timedelta, timezone
4
-
5
- from fastapi import APIRouter, Depends, File, Form, UploadFile
6
-
7
- from app.core.auth import AuthUser, get_current_web_user
8
- from app.core.config import settings
9
- from app.models.schemas import ScreenshotIngestResponse, ViolationScreenshot
10
- from app.services.alert_hub import alert_hub
11
- from app.services.classifier import (
12
- classify_game_with_ocr_llm,
13
- classify_sensitive_content,
14
- )
15
- from app.services.policy_window import is_restriction_active
16
- from app.services.repos import (
17
- audit_repo,
18
- policy_repo,
19
- review_repo,
20
- url_verdict_repo,
21
- violation_repo,
22
- )
23
- from app.services.storage import storage_service
24
-
25
- router = APIRouter(prefix="/screenshots", tags=["screenshots"])
26
-
27
-
28
- async def _cleanup_expired_violations() -> None:
29
- expired = await violation_repo.list_expired()
30
- for row in expired:
31
- absolute_path = row.get("absolute_path")
32
- if isinstance(absolute_path, str):
33
- storage_service.delete(absolute_path)
34
- await violation_repo.delete_expired()
35
-
36
-
37
- @router.post("/ingest", response_model=ScreenshotIngestResponse)
38
- async def ingest_screenshot(
39
- device_id: str = Form(...),
40
- timestamp: str = Form(...),
41
- suspected_game: bool = Form(False),
42
- file: UploadFile = File(...),
43
- ) -> ScreenshotIngestResponse:
44
- screenshot_id = str(uuid.uuid4())
45
- _, absolute_path, stored_name = await storage_service.save_upload(file)
46
-
47
- try:
48
- moment = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
49
- except ValueError:
50
- moment = datetime.now(timezone.utc)
51
- policy_doc = await policy_repo.get(device_id)
52
- policy_payload = policy_doc.get("policy", {}) if policy_doc else {}
53
- windows = policy_payload.get("restriction_windows", [])
54
- if not is_restriction_active(windows, moment):
55
- storage_service.delete(absolute_path)
56
- await audit_repo.log(
57
- {
58
- "screenshot_id": screenshot_id,
59
- "device_id": device_id,
60
- "timestamp": timestamp,
61
- "filename": file.filename or "unknown.png",
62
- "action": "deleted_outside_window",
63
- "reason": "outside_restriction_window",
64
- "created_at": datetime.now(timezone.utc),
65
- }
66
- )
67
- return ScreenshotIngestResponse(screenshot_id=screenshot_id)
68
-
69
- game_task = asyncio.to_thread(
70
- classify_game_with_ocr_llm,
71
- absolute_path,
72
- file.filename or "capture.png",
73
- suspected_game,
74
- )
75
- sensitive_task = asyncio.to_thread(
76
- classify_sensitive_content,
77
- absolute_path,
78
- file.filename or "capture.png",
79
- )
80
- game_raw, sensitive_raw = await asyncio.gather(game_task, sensitive_task, return_exceptions=True)
81
-
82
- if isinstance(game_raw, Exception):
83
- game_result = {
84
- "verdict": "not_game",
85
- "confidence": 0.1,
86
- "reason": "game-check-error",
87
- "urls": [],
88
- "source": "error-fallback",
89
- }
90
- else:
91
- game_result = game_raw
92
-
93
- if isinstance(sensitive_raw, Exception):
94
- sensitive_detected, sensitive_confidence, sensitive_reason = (False, 0.0, "sensitive-check-error")
95
- else:
96
- sensitive_detected, sensitive_confidence, sensitive_reason = sensitive_raw
97
-
98
- ocr_urls = [str(x) for x in game_result.get("urls", [])]
99
-
100
- cached_game_url = await url_verdict_repo.get_cached_game_url(ocr_urls)
101
- if cached_game_url:
102
- game_result = {
103
- **game_result,
104
- "verdict": "game",
105
- "confidence": 0.99,
106
- "reason": f"cached-game-url:{cached_game_url}",
107
- "source": "url-cache",
108
- }
109
-
110
- verdict = str(game_result.get("verdict", "not_game"))
111
- confidence = float(game_result.get("confidence", 0.1))
112
- reason = str(game_result.get("reason", "no-game-evidence"))
113
-
114
- if sensitive_detected:
115
- image_url = f"{settings.media_prefix}/{stored_name}"
116
- await violation_repo.insert(
117
- {
118
- "screenshot_id": screenshot_id,
119
- "device_id": device_id,
120
- "timestamp": timestamp,
121
- "filename": file.filename or "unknown.png",
122
- "created_at": datetime.now(timezone.utc).isoformat(),
123
- "image_url": image_url,
124
- "confidence": sensitive_confidence,
125
- "reason": sensitive_reason,
126
- "absolute_path": absolute_path,
127
- "visibility_scope": "sensitive_admin",
128
- "ocr_urls": ocr_urls,
129
- "expires_at": datetime.now(timezone.utc) + timedelta(hours=24),
130
- }
131
- )
132
- await alert_hub.broadcast_admin(
133
- {
134
- "type": "sensitive_detected",
135
- "device_id": device_id,
136
- "message": "Sensitive content detected. Routed to sensitive admin scope.",
137
- "visibility_scope": "sensitive_admin",
138
- }
139
- )
140
- await _cleanup_expired_violations()
141
- return ScreenshotIngestResponse(screenshot_id=screenshot_id)
142
-
143
- if verdict == "game":
144
- await url_verdict_repo.upsert_game_urls(ocr_urls, source=str(game_result.get("source", "llm")))
145
-
146
- if verdict == "not_game":
147
- storage_service.delete(absolute_path)
148
- await audit_repo.log(
149
- {
150
- "screenshot_id": screenshot_id,
151
- "device_id": device_id,
152
- "timestamp": timestamp,
153
- "filename": file.filename or "unknown.png",
154
- "action": "deleted_non_violation",
155
- "reason": reason,
156
- "created_at": datetime.now(timezone.utc),
157
- }
158
- )
159
- return ScreenshotIngestResponse(screenshot_id=screenshot_id)
160
-
161
- image_url = f"{settings.media_prefix}/{stored_name}"
162
- await violation_repo.insert(
163
- {
164
- "screenshot_id": screenshot_id,
165
- "device_id": device_id,
166
- "timestamp": timestamp,
167
- "filename": file.filename or "unknown.png",
168
- "created_at": datetime.now(timezone.utc).isoformat(),
169
- "image_url": image_url,
170
- "confidence": confidence,
171
- "reason": reason,
172
- "absolute_path": absolute_path,
173
- "visibility_scope": "admin",
174
- "ocr_urls": ocr_urls,
175
- "expires_at": datetime.now(timezone.utc) + timedelta(hours=24),
176
- }
177
- )
178
-
179
- if verdict == "uncertain":
180
- review_id = str(uuid.uuid4())
181
- await review_repo.create_pending(
182
- {
183
- "review_id": review_id,
184
- "screenshot_id": screenshot_id,
185
- "device_id": device_id,
186
- "timestamp": timestamp,
187
- "status": "pending",
188
- "reason": reason,
189
- "ocr_urls": ocr_urls,
190
- "created_at": datetime.now(timezone.utc),
191
- }
192
- )
193
- await alert_hub.broadcast_admin(
194
- {
195
- "type": "game_lock_review_required",
196
- "device_id": device_id,
197
- "message": "Model uncertain. Admin confirmation is required before lock.",
198
- "review_id": review_id,
199
- "screenshot_id": screenshot_id,
200
- "ocr_urls": ocr_urls,
201
- }
202
- )
203
-
204
- await _cleanup_expired_violations()
205
- return ScreenshotIngestResponse(screenshot_id=screenshot_id)
206
-
207
-
208
- @router.get("/violations", response_model=list[ViolationScreenshot])
209
- async def list_violations(user: AuthUser = Depends(get_current_web_user)) -> list[ViolationScreenshot]:
210
- await _cleanup_expired_violations()
211
- rows = await violation_repo.list_active(user.role)
212
- return [
213
- ViolationScreenshot(
214
- screenshot_id=row.get("screenshot_id", ""),
215
- device_id=row.get("device_id", ""),
216
- timestamp=row.get("timestamp", ""),
217
- filename=row.get("filename", ""),
218
- created_at=row.get("created_at", ""),
219
- image_url=row.get("image_url", ""),
220
- confidence=float(row.get("confidence", 0.0)),
221
- reason=row.get("reason", ""),
222
- visibility_scope=row.get("visibility_scope", "admin"),
223
- ocr_urls=[str(x) for x in row.get("ocr_urls", [])],
224
- )
225
- for row in rows
226
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ import uuid
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ from fastapi import APIRouter, Depends, File, Form, UploadFile
7
+
8
+ from app.core.auth import AuthUser, get_current_web_user
9
+ from app.core.config import settings
10
+ from app.models.schemas import ScreenshotIngestResponse, ViolationScreenshot
11
+ from app.services.alert_hub import alert_hub
12
+ from app.services.classifier import (
13
+ classify_game_with_ocr_llm,
14
+ classify_sensitive_content,
15
+ )
16
+ from app.services.policy_window import is_restriction_active
17
+ from app.services.repos import (
18
+ audit_repo,
19
+ policy_repo,
20
+ review_repo,
21
+ url_verdict_repo,
22
+ violation_repo,
23
+ )
24
+ from app.services.storage import storage_service
25
+
26
+ router = APIRouter(prefix="/screenshots", tags=["screenshots"])
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ async def _cleanup_expired_violations() -> None:
31
+ expired = await violation_repo.list_expired()
32
+ for row in expired:
33
+ absolute_path = row.get("absolute_path")
34
+ if isinstance(absolute_path, str):
35
+ storage_service.delete(absolute_path)
36
+ await violation_repo.delete_expired()
37
+
38
+
39
+ @router.post("/ingest", response_model=ScreenshotIngestResponse)
40
+ async def ingest_screenshot(
41
+ device_id: str = Form(...),
42
+ timestamp: str = Form(...),
43
+ suspected_game: bool = Form(False),
44
+ file: UploadFile = File(...),
45
+ ) -> ScreenshotIngestResponse:
46
+ screenshot_id = str(uuid.uuid4())
47
+ _, absolute_path, stored_name = await storage_service.save_upload(file)
48
+ logger.info(
49
+ "ingest start screenshot_id=%s device_id=%s filename=%s suspected_game=%s",
50
+ screenshot_id,
51
+ device_id,
52
+ file.filename or "unknown.png",
53
+ suspected_game,
54
+ )
55
+
56
+ try:
57
+ moment = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
58
+ except ValueError:
59
+ moment = datetime.now(timezone.utc)
60
+ policy_doc = await policy_repo.get(device_id)
61
+ policy_payload = policy_doc.get("policy", {}) if policy_doc else {}
62
+ windows = policy_payload.get("restriction_windows", [])
63
+ if not is_restriction_active(windows, moment):
64
+ storage_service.delete(absolute_path)
65
+ logger.info(
66
+ "ingest drop screenshot_id=%s reason=outside_restriction_window",
67
+ screenshot_id,
68
+ )
69
+ await audit_repo.log(
70
+ {
71
+ "screenshot_id": screenshot_id,
72
+ "device_id": device_id,
73
+ "timestamp": timestamp,
74
+ "filename": file.filename or "unknown.png",
75
+ "action": "deleted_outside_window",
76
+ "reason": "outside_restriction_window",
77
+ "created_at": datetime.now(timezone.utc),
78
+ }
79
+ )
80
+ return ScreenshotIngestResponse(screenshot_id=screenshot_id)
81
+
82
+ game_task = asyncio.to_thread(
83
+ classify_game_with_ocr_llm,
84
+ absolute_path,
85
+ file.filename or "capture.png",
86
+ suspected_game,
87
+ )
88
+ sensitive_task = asyncio.to_thread(
89
+ classify_sensitive_content,
90
+ absolute_path,
91
+ file.filename or "capture.png",
92
+ )
93
+ game_raw, sensitive_raw = await asyncio.gather(game_task, sensitive_task, return_exceptions=True)
94
+
95
+ if isinstance(game_raw, Exception):
96
+ logger.warning("ingest game-check-error screenshot_id=%s err=%s", screenshot_id, repr(game_raw))
97
+ game_result = {
98
+ "verdict": "not_game",
99
+ "confidence": 0.1,
100
+ "reason": "game-check-error",
101
+ "urls": [],
102
+ "source": "error-fallback",
103
+ }
104
+ else:
105
+ game_result = game_raw
106
+
107
+ if isinstance(sensitive_raw, Exception):
108
+ logger.warning("ingest sensitive-check-error screenshot_id=%s err=%s", screenshot_id, repr(sensitive_raw))
109
+ sensitive_detected, sensitive_confidence, sensitive_reason = (False, 0.0, "sensitive-check-error")
110
+ else:
111
+ sensitive_detected, sensitive_confidence, sensitive_reason = sensitive_raw
112
+
113
+ ocr_urls = [str(x) for x in game_result.get("urls", [])]
114
+
115
+ cached_game_url = await url_verdict_repo.get_cached_game_url(ocr_urls)
116
+ if cached_game_url:
117
+ logger.info(
118
+ "ingest cache-hit screenshot_id=%s game_url=%s",
119
+ screenshot_id,
120
+ cached_game_url,
121
+ )
122
+ game_result = {
123
+ **game_result,
124
+ "verdict": "game",
125
+ "confidence": 0.99,
126
+ "reason": f"cached-game-url:{cached_game_url}",
127
+ "source": "url-cache",
128
+ }
129
+
130
+ verdict = str(game_result.get("verdict", "not_game"))
131
+ confidence = float(game_result.get("confidence", 0.1))
132
+ reason = str(game_result.get("reason", "no-game-evidence"))
133
+
134
+ if sensitive_detected:
135
+ logger.info(
136
+ "ingest verdict screenshot_id=%s verdict=sensitive confidence=%.2f reason=%s",
137
+ screenshot_id,
138
+ sensitive_confidence,
139
+ sensitive_reason,
140
+ )
141
+ image_url = f"{settings.media_prefix}/{stored_name}"
142
+ await violation_repo.insert(
143
+ {
144
+ "screenshot_id": screenshot_id,
145
+ "device_id": device_id,
146
+ "timestamp": timestamp,
147
+ "filename": file.filename or "unknown.png",
148
+ "created_at": datetime.now(timezone.utc).isoformat(),
149
+ "image_url": image_url,
150
+ "confidence": sensitive_confidence,
151
+ "reason": sensitive_reason,
152
+ "absolute_path": absolute_path,
153
+ "visibility_scope": "sensitive_admin",
154
+ "ocr_urls": ocr_urls,
155
+ "expires_at": datetime.now(timezone.utc) + timedelta(hours=24),
156
+ }
157
+ )
158
+ await alert_hub.broadcast_admin(
159
+ {
160
+ "type": "sensitive_detected",
161
+ "device_id": device_id,
162
+ "message": "Sensitive content detected. Routed to sensitive admin scope.",
163
+ "visibility_scope": "sensitive_admin",
164
+ }
165
+ )
166
+ await _cleanup_expired_violations()
167
+ return ScreenshotIngestResponse(screenshot_id=screenshot_id)
168
+
169
+ if verdict == "game":
170
+ await url_verdict_repo.upsert_game_urls(ocr_urls, source=str(game_result.get("source", "llm")))
171
+
172
+ if verdict == "not_game":
173
+ storage_service.delete(absolute_path)
174
+ logger.info(
175
+ "ingest verdict screenshot_id=%s verdict=not_game confidence=%.2f reason=%s urls=%d",
176
+ screenshot_id,
177
+ confidence,
178
+ reason,
179
+ len(ocr_urls),
180
+ )
181
+ await audit_repo.log(
182
+ {
183
+ "screenshot_id": screenshot_id,
184
+ "device_id": device_id,
185
+ "timestamp": timestamp,
186
+ "filename": file.filename or "unknown.png",
187
+ "action": "deleted_non_violation",
188
+ "reason": reason,
189
+ "created_at": datetime.now(timezone.utc),
190
+ }
191
+ )
192
+ return ScreenshotIngestResponse(screenshot_id=screenshot_id)
193
+
194
+ image_url = f"{settings.media_prefix}/{stored_name}"
195
+ logger.info(
196
+ "ingest verdict screenshot_id=%s verdict=%s confidence=%.2f reason=%s urls=%d",
197
+ screenshot_id,
198
+ verdict,
199
+ confidence,
200
+ reason,
201
+ len(ocr_urls),
202
+ )
203
+ await violation_repo.insert(
204
+ {
205
+ "screenshot_id": screenshot_id,
206
+ "device_id": device_id,
207
+ "timestamp": timestamp,
208
+ "filename": file.filename or "unknown.png",
209
+ "created_at": datetime.now(timezone.utc).isoformat(),
210
+ "image_url": image_url,
211
+ "confidence": confidence,
212
+ "reason": reason,
213
+ "absolute_path": absolute_path,
214
+ "visibility_scope": "admin",
215
+ "ocr_urls": ocr_urls,
216
+ "expires_at": datetime.now(timezone.utc) + timedelta(hours=24),
217
+ }
218
+ )
219
+
220
+ if verdict == "uncertain":
221
+ review_id = str(uuid.uuid4())
222
+ await review_repo.create_pending(
223
+ {
224
+ "review_id": review_id,
225
+ "screenshot_id": screenshot_id,
226
+ "device_id": device_id,
227
+ "timestamp": timestamp,
228
+ "status": "pending",
229
+ "reason": reason,
230
+ "ocr_urls": ocr_urls,
231
+ "created_at": datetime.now(timezone.utc),
232
+ }
233
+ )
234
+ await alert_hub.broadcast_admin(
235
+ {
236
+ "type": "game_lock_review_required",
237
+ "device_id": device_id,
238
+ "message": "Model uncertain. Admin confirmation is required before lock.",
239
+ "review_id": review_id,
240
+ "screenshot_id": screenshot_id,
241
+ "ocr_urls": ocr_urls,
242
+ }
243
+ )
244
+
245
+ await _cleanup_expired_violations()
246
+ return ScreenshotIngestResponse(screenshot_id=screenshot_id)
247
+
248
+
249
+ @router.get("/violations", response_model=list[ViolationScreenshot])
250
+ async def list_violations(user: AuthUser = Depends(get_current_web_user)) -> list[ViolationScreenshot]:
251
+ await _cleanup_expired_violations()
252
+ rows = await violation_repo.list_active(user.role)
253
+ return [
254
+ ViolationScreenshot(
255
+ screenshot_id=row.get("screenshot_id", ""),
256
+ device_id=row.get("device_id", ""),
257
+ timestamp=row.get("timestamp", ""),
258
+ filename=row.get("filename", ""),
259
+ created_at=row.get("created_at", ""),
260
+ image_url=row.get("image_url", ""),
261
+ confidence=float(row.get("confidence", 0.0)),
262
+ reason=row.get("reason", ""),
263
+ visibility_scope=row.get("visibility_scope", "admin"),
264
+ ocr_urls=[str(x) for x in row.get("ocr_urls", [])],
265
+ )
266
+ for row in rows
267
+ ]