Domify commited on
Commit
7831b98
·
verified ·
1 Parent(s): 78719d1

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +582 -0
app.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import json
4
+ import datetime
5
+ import hashlib
6
+ import asyncio
7
+ import base64
8
+ from collections import defaultdict
9
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import JSONResponse, FileResponse
12
+ from pydantic import BaseModel
13
+ from google.oauth2.service_account import Credentials
14
+ from googleapiclient.discovery import build
15
+ from googleapiclient.http import MediaIoBaseUpload
16
+ import httpx
17
+ import qrcode
18
+ from PIL import Image
19
+
20
+ app = FastAPI()
21
+
22
+ # CORS
23
+ app.add_middleware(
24
+ CORSMiddleware,
25
+ allow_origins=["https://domify-academy.free.nf", "http://localhost:3000"],
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # ==================== SECURE KEYS ====================
31
+ VT_API_KEY = os.getenv("VT_API_KEY")
32
+ if not VT_API_KEY:
33
+ print("⚠️ WARNING: VT_API_KEY not set!")
34
+
35
+ # Google Services
36
+ SHEET_ID = os.getenv("GOOGLE_SHEET_ID")
37
+ DRIVE_FOLDER_ID = os.getenv("GOOGLE_DRIVE_FOLDER_ID")
38
+ SERVICE_ACCOUNT_INFO = json.loads(os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON", "{}"))
39
+
40
+ # Rate Limiting (15 seconds between scans)
41
+ rate_limit_cache = defaultdict(float)
42
+ RATE_LIMIT_SECONDS = 15
43
+
44
+ def check_rate_limit(ip: str):
45
+ """Check if user has exceeded rate limit"""
46
+ now = datetime.datetime.now().timestamp()
47
+ last_scan = rate_limit_cache.get(ip, 0)
48
+ if now - last_scan < RATE_LIMIT_SECONDS:
49
+ wait_time = int(RATE_LIMIT_SECONDS - (now - last_scan))
50
+ return False, wait_time
51
+ rate_limit_cache[ip] = now
52
+ return True, 0
53
+
54
+ def get_services():
55
+ if not SERVICE_ACCOUNT_INFO:
56
+ return None, None
57
+ creds = Credentials.from_service_account_info(
58
+ SERVICE_ACCOUNT_INFO,
59
+ scopes=[
60
+ "https://www.googleapis.com/auth/drive.file",
61
+ "https://www.googleapis.com/auth/spreadsheets"
62
+ ]
63
+ )
64
+ drive = build("drive", "v3", credentials=creds)
65
+ sheets = build("sheets", "v4", credentials=creds)
66
+ return drive, sheets
67
+
68
+ class ScanRequest(BaseModel):
69
+ target: str
70
+ type: str
71
+
72
+ # ==================== URL SCAN ====================
73
+ @app.post("/api/scan/url")
74
+ async def scan_url(request: ScanRequest, client_ip: str = None):
75
+ if not VT_API_KEY:
76
+ return {"status": "error", "message": "API key not configured"}
77
+
78
+ # Rate limiting
79
+ can_scan, wait_time = check_rate_limit(client_ip or "unknown")
80
+ if not can_scan:
81
+ return {"status": "rate_limited", "message": f"Please wait {wait_time} seconds before scanning again"}
82
+
83
+ try:
84
+ async with httpx.AsyncClient() as client:
85
+ # Submit URL
86
+ submit_res = await client.post(
87
+ "https://www.virustotal.com/api/v3/urls",
88
+ headers={"x-apikey": VT_API_KEY},
89
+ data={"url": request.target}
90
+ )
91
+
92
+ if submit_res.status_code != 200:
93
+ return {"status": "error", "message": "Submission failed"}
94
+
95
+ submit_data = submit_res.json()
96
+ scan_id = submit_data.get("data", {}).get("id")
97
+
98
+ # Wait for analysis (longer for better results)
99
+ await asyncio.sleep(8)
100
+
101
+ # Get results
102
+ analysis_res = await client.get(
103
+ f"https://www.virustotal.com/api/v3/analyses/{scan_id}",
104
+ headers={"x-apikey": VT_API_KEY}
105
+ )
106
+
107
+ analysis = analysis_res.json()
108
+ stats = analysis.get("data", {}).get("attributes", {}).get("stats", {})
109
+
110
+ # Get detailed results
111
+ detailed = analysis.get("data", {}).get("attributes", {}).get("results", {})
112
+ top_threats = []
113
+ for engine, result in detailed.items():
114
+ if result.get("category") in ["malicious", "suspicious"]:
115
+ top_threats.append({
116
+ "engine": engine,
117
+ "category": result.get("category"),
118
+ "result": result.get("result", "Detected")
119
+ })
120
+
121
+ await store_scan_result(request.target, "url", stats, scan_id)
122
+
123
+ # Generate report
124
+ report = generate_report(request.target, "url", stats, top_threats[:5])
125
+ report_id = await save_report_to_drive(report, request.target, "url")
126
+
127
+ return {
128
+ "status": "success",
129
+ "scan_id": scan_id,
130
+ "results": {
131
+ "malicious": stats.get("malicious", 0),
132
+ "suspicious": stats.get("suspicious", 0),
133
+ "undetected": stats.get("undetected", 0),
134
+ "harmless": stats.get("harmless", 0),
135
+ "timeout": stats.get("timeout", 0)
136
+ },
137
+ "top_threats": top_threats[:5],
138
+ "report_id": report_id,
139
+ "scan_time": "~8 seconds"
140
+ }
141
+
142
+ except Exception as e:
143
+ return {"status": "error", "message": str(e)}
144
+
145
+ # ==================== IP SCAN ====================
146
+ @app.post("/api/scan/ip")
147
+ async def scan_ip(request: ScanRequest, client_ip: str = None):
148
+ if not VT_API_KEY:
149
+ return {"status": "error", "message": "API key not configured"}
150
+
151
+ can_scan, wait_time = check_rate_limit(client_ip or "unknown")
152
+ if not can_scan:
153
+ return {"status": "rate_limited", "message": f"Please wait {wait_time} seconds"}
154
+
155
+ try:
156
+ async with httpx.AsyncClient() as client:
157
+ response = await client.get(
158
+ f"https://www.virustotal.com/api/v3/ip_addresses/{request.target}",
159
+ headers={"x-apikey": VT_API_KEY}
160
+ )
161
+
162
+ if response.status_code != 200:
163
+ return {"status": "error", "message": "IP not found"}
164
+
165
+ data = response.json()
166
+ attributes = data.get("data", {}).get("attributes", {})
167
+ stats = attributes.get("last_analysis_stats", {})
168
+
169
+ # Get recent communication
170
+ communications = []
171
+ for comm in attributes.get("last_https_certificate", {}).get("subject", {}).items():
172
+ communications.append(f"{comm[0]}: {comm[1]}")
173
+
174
+ await store_scan_result(request.target, "ip", stats)
175
+
176
+ # Generate report
177
+ report = generate_report(request.target, "ip", stats)
178
+ report_id = await save_report_to_drive(report, request.target, "ip")
179
+
180
+ return {
181
+ "status": "success",
182
+ "results": {
183
+ "malicious": stats.get("malicious", 0),
184
+ "suspicious": stats.get("suspicious", 0),
185
+ "undetected": stats.get("undetected", 0),
186
+ "harmless": stats.get("harmless", 0),
187
+ "country": attributes.get("country", "Unknown"),
188
+ "network": attributes.get("network", "Unknown"),
189
+ "as_owner": attributes.get("as_owner", "Unknown")
190
+ },
191
+ "communications": communications[:5],
192
+ "report_id": report_id
193
+ }
194
+
195
+ except Exception as e:
196
+ return {"status": "error", "message": str(e)}
197
+
198
+ # ==================== DOMAIN SCAN ====================
199
+ @app.post("/api/scan/domain")
200
+ async def scan_domain(request: ScanRequest, client_ip: str = None):
201
+ if not VT_API_KEY:
202
+ return {"status": "error", "message": "API key not configured"}
203
+
204
+ can_scan, wait_time = check_rate_limit(client_ip or "unknown")
205
+ if not can_scan:
206
+ return {"status": "rate_limited", "message": f"Please wait {wait_time} seconds"}
207
+
208
+ try:
209
+ async with httpx.AsyncClient() as client:
210
+ response = await client.get(
211
+ f"https://www.virustotal.com/api/v3/domains/{request.target}",
212
+ headers={"x-apikey": VT_API_KEY}
213
+ )
214
+
215
+ if response.status_code != 200:
216
+ return {"status": "error", "message": "Domain not found"}
217
+
218
+ data = response.json()
219
+ attributes = data.get("data", {}).get("attributes", {})
220
+ stats = attributes.get("last_analysis_stats", {})
221
+
222
+ await store_scan_result(request.target, "domain", stats)
223
+
224
+ report = generate_report(request.target, "domain", stats)
225
+ report_id = await save_report_to_drive(report, request.target, "domain")
226
+
227
+ return {
228
+ "status": "success",
229
+ "results": {
230
+ "malicious": stats.get("malicious", 0),
231
+ "suspicious": stats.get("suspicious", 0),
232
+ "undetected": stats.get("undetected", 0),
233
+ "harmless": stats.get("harmless", 0),
234
+ "creation_date": attributes.get("creation_date", "Unknown"),
235
+ "registrar": attributes.get("registrar", "Unknown"),
236
+ "whois": attributes.get("whois", "Not available")[:200]
237
+ },
238
+ "report_id": report_id
239
+ }
240
+
241
+ except Exception as e:
242
+ return {"status": "error", "message": str(e)}
243
+
244
+ # ==================== FILE SCAN ====================
245
+ @app.post("/api/scan/file")
246
+ async def scan_file(file: UploadFile = File(...), client_ip: str = None):
247
+ if not VT_API_KEY:
248
+ return {"status": "error", "message": "API key not configured"}
249
+
250
+ can_scan, wait_time = check_rate_limit(client_ip or "unknown")
251
+ if not can_scan:
252
+ return {"status": "rate_limited", "message": f"Please wait {wait_time} seconds"}
253
+
254
+ try:
255
+ # Read file content
256
+ content = await file.read()
257
+ file_size = len(content)
258
+
259
+ if file_size > 32 * 1024 * 1024: # 32MB limit
260
+ return {"status": "error", "message": "File too large (max 32MB)"}
261
+
262
+ # Calculate hash
263
+ sha256_hash = hashlib.sha256(content).hexdigest()
264
+
265
+ async with httpx.AsyncClient() as client:
266
+ # First check if file already analyzed
267
+ file_check = await client.get(
268
+ f"https://www.virustotal.com/api/v3/files/{sha256_hash}",
269
+ headers={"x-apikey": VT_API_KEY}
270
+ )
271
+
272
+ if file_check.status_code == 200:
273
+ # File already in DB
274
+ file_data = file_check.json()
275
+ stats = file_data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
276
+ return {
277
+ "status": "success",
278
+ "file_name": file.filename,
279
+ "sha256": sha256_hash,
280
+ "results": {
281
+ "malicious": stats.get("malicious", 0),
282
+ "suspicious": stats.get("suspicious", 0),
283
+ "undetected": stats.get("undetected", 0),
284
+ "harmless": stats.get("harmless", 0)
285
+ },
286
+ "message": "File already in VirusTotal database"
287
+ }
288
+
289
+ # Upload file for scanning
290
+ files = {"file": (file.filename, content, file.content_type)}
291
+ upload_res = await client.post(
292
+ "https://www.virustotal.com/api/v3/files",
293
+ headers={"x-apikey": VT_API_KEY},
294
+ files=files
295
+ )
296
+
297
+ if upload_res.status_code != 200:
298
+ return {"status": "error", "message": "Upload failed"}
299
+
300
+ upload_data = upload_res.json()
301
+ scan_id = upload_data.get("data", {}).get("id")
302
+
303
+ # Wait for analysis (longer for files)
304
+ await asyncio.sleep(15)
305
+
306
+ # Get results
307
+ analysis_res = await client.get(
308
+ f"https://www.virustotal.com/api/v3/analyses/{scan_id}",
309
+ headers={"x-apikey": VT_API_KEY}
310
+ )
311
+
312
+ analysis = analysis_res.json()
313
+ stats = analysis.get("data", {}).get("attributes", {}).get("stats", {})
314
+
315
+ await store_scan_result(file.filename, "file", stats, sha256_hash)
316
+
317
+ report = generate_report(file.filename, "file", stats)
318
+ report_id = await save_report_to_drive(report, file.filename, "file")
319
+
320
+ return {
321
+ "status": "success",
322
+ "file_name": file.filename,
323
+ "sha256": sha256_hash,
324
+ "results": {
325
+ "malicious": stats.get("malicious", 0),
326
+ "suspicious": stats.get("suspicious", 0),
327
+ "undetected": stats.get("undetected", 0),
328
+ "harmless": stats.get("harmless", 0)
329
+ },
330
+ "report_id": report_id,
331
+ "scan_time": "~15 seconds"
332
+ }
333
+
334
+ except Exception as e:
335
+ return {"status": "error", "message": str(e)}
336
+
337
+ # ==================== BATCH SCAN ====================
338
+ class BatchRequest(BaseModel):
339
+ targets: list[str]
340
+ type: str
341
+
342
+ @app.post("/api/scan/batch")
343
+ async def batch_scan(request: BatchRequest, client_ip: str = None):
344
+ if len(request.targets) > 10:
345
+ return {"status": "error", "message": "Max 10 targets per batch"}
346
+
347
+ results = []
348
+ for target in request.targets:
349
+ # Simulate batch scanning
350
+ results.append({
351
+ "target": target,
352
+ "status": "queued",
353
+ "message": "Scan initiated"
354
+ })
355
+
356
+ return {
357
+ "status": "success",
358
+ "batch_id": hashlib.md5(str(datetime.datetime.now()).encode()).hexdigest()[:8],
359
+ "total": len(request.targets),
360
+ "results": results
361
+ }
362
+
363
+ # ==================== REAL-TIME ALERTS ====================
364
+ # Store alert subscriptions (in memory, can be moved to DB)
365
+ alert_subscriptions = {}
366
+
367
+ class AlertSubscription(BaseModel):
368
+ email: str
369
+ threshold: int = 1 # Alert if malicious > threshold
370
+ webhook: str = None
371
+
372
+ @app.post("/api/subscribe")
373
+ async def subscribe_alerts(subscription: AlertSubscription):
374
+ alert_subscriptions[subscription.email] = {
375
+ "threshold": subscription.threshold,
376
+ "webhook": subscription.webhook,
377
+ "created": datetime.datetime.now().isoformat()
378
+ }
379
+ return {"status": "success", "message": "Subscribed to threat alerts"}
380
+
381
+ async def send_alert(email: str, target: str, threat_level: int, results: dict):
382
+ """Send alert when threat detected"""
383
+ if email in alert_subscriptions:
384
+ if threat_level >= alert_subscriptions[email]["threshold"]:
385
+ # In production, send email via SendGrid or webhook
386
+ print(f"🔴 ALERT: {email} - {target} detected {threat_level} threats")
387
+ # Store alert in sheet
388
+ await store_alert(email, target, threat_level, results)
389
+
390
+ # ==================== REPORT GENERATION ====================
391
+ def generate_report(target, scan_type, stats, threats=None):
392
+ """Generate detailed threat report"""
393
+ now = datetime.datetime.now()
394
+
395
+ report = f"""
396
+ ========================================
397
+ DOMIFY THREAT REPORT
398
+ ========================================
399
+ Generated: {now.strftime('%Y-%m-%d %H:%M:%S')}
400
+ Report ID: {hashlib.md5(f"{target}{now}".encode()).hexdigest()[:8]}
401
+
402
+ TARGET INFORMATION
403
+ ------------------
404
+ Type: {scan_type.upper()}
405
+ Target: {target}
406
+ Scan Source: VirusTotal API
407
+
408
+ THREAT ANALYSIS
409
+ ---------------
410
+ Malicious: {stats.get('malicious', 0)} vendors
411
+ Suspicious: {stats.get('suspicious', 0)} vendors
412
+ Harmless: {stats.get('harmless', 0)} vendors
413
+ Undetected: {stats.get('undetected', 0)} vendors
414
+ Timeout: {stats.get('timeout', 0)} vendors
415
+
416
+ VERDICT
417
+ -------
418
+ """
419
+
420
+ malicious = stats.get('malicious', 0)
421
+ if malicious > 5:
422
+ report += "CRITICAL: High threat level detected!\n"
423
+ elif malicious > 0:
424
+ report += "WARNING: Malicious activity detected!\n"
425
+ elif stats.get('suspicious', 0) > 0:
426
+ report += "CAUTION: Suspicious activity detected\n"
427
+ else:
428
+ report += "CLEAR: No threats detected\n"
429
+
430
+ if threats:
431
+ report += "\nTOP THREATS\n-----------\n"
432
+ for threat in threats[:5]:
433
+ report += f"• {threat.get('engine')}: {threat.get('category')} - {threat.get('result')}\n"
434
+
435
+ report += f"""
436
+ ========================================
437
+ Generated by Domify Threat Intelligence
438
+ Powered by VirusTotal
439
+ ========================================
440
+ """
441
+ return report
442
+
443
+ async def save_report_to_drive(report, target, scan_type):
444
+ """Save report to Google Drive"""
445
+ try:
446
+ drive, _ = get_services()
447
+ if not drive:
448
+ return None
449
+
450
+ now = datetime.datetime.now()
451
+ report_id = hashlib.md5(f"{target}{now}".encode()).hexdigest()[:8]
452
+ file_name = f"threat_report_{scan_type}_{report_id}.txt"
453
+
454
+ file = drive.files().create(
455
+ body={'name': file_name, 'parents': [DRIVE_FOLDER_ID]},
456
+ media_body=io.BytesIO(report.encode()),
457
+ fields='id,webViewLink'
458
+ ).execute()
459
+
460
+ return file.get('id')
461
+ except Exception as e:
462
+ print(f"Report save failed: {e}")
463
+ return None
464
+
465
+ # ==================== STORE FUNCTIONS ====================
466
+ async def store_scan_result(target, scan_type, stats, scan_id=None):
467
+ try:
468
+ _, sheets = get_services()
469
+ if not sheets:
470
+ return
471
+
472
+ now = datetime.datetime.now().isoformat()
473
+ values = [[
474
+ now,
475
+ target,
476
+ scan_type,
477
+ stats.get("malicious", 0),
478
+ stats.get("suspicious", 0),
479
+ stats.get("undetected", 0),
480
+ stats.get("harmless", 0),
481
+ scan_id or ""
482
+ ]]
483
+
484
+ sheets.spreadsheets().values().append(
485
+ spreadsheetId=SHEET_ID,
486
+ range="ScanHistory!A:H",
487
+ valueInputOption="USER_ENTERED",
488
+ body={"values": values}
489
+ ).execute()
490
+
491
+ except Exception as e:
492
+ print(f"Store failed: {e}")
493
+
494
+ async def store_alert(email, target, threat_level, results):
495
+ try:
496
+ _, sheets = get_services()
497
+ if not sheets:
498
+ return
499
+
500
+ now = datetime.datetime.now().isoformat()
501
+ values = [[
502
+ now,
503
+ email,
504
+ target,
505
+ threat_level,
506
+ json.dumps(results)
507
+ ]]
508
+
509
+ sheets.spreadsheets().values().append(
510
+ spreadsheetId=SHEET_ID,
511
+ range="Alerts!A:E",
512
+ valueInputOption="USER_ENTERED",
513
+ body={"values": values}
514
+ ).execute()
515
+
516
+ except Exception as e:
517
+ print(f"Alert store failed: {e}")
518
+
519
+ # ==================== HISTORY & HEALTH ====================
520
+ @app.get("/api/history")
521
+ async def get_history(limit: int = 10):
522
+ try:
523
+ _, sheets = get_services()
524
+ if not sheets:
525
+ return {"status": "error", "message": "Sheets not configured"}
526
+
527
+ result = sheets.spreadsheets().values().get(
528
+ spreadsheetId=SHEET_ID,
529
+ range="ScanHistory!A:H"
530
+ ).execute()
531
+
532
+ rows = result.get("values", [])
533
+ history = []
534
+
535
+ for row in rows[1:limit+1]:
536
+ if len(row) >= 5:
537
+ history.append({
538
+ "date": row[0],
539
+ "target": row[1],
540
+ "type": row[2],
541
+ "malicious": int(row[3]) if row[3] else 0,
542
+ "suspicious": int(row[4]) if row[4] else 0
543
+ })
544
+
545
+ return {"status": "success", "history": history}
546
+
547
+ except Exception as e:
548
+ return {"status": "error", "message": str(e)}
549
+
550
+ @app.get("/api/report/{report_id}")
551
+ async def get_report(report_id: str):
552
+ try:
553
+ drive, _ = get_services()
554
+ if not drive:
555
+ return {"status": "error", "message": "Drive not configured"}
556
+
557
+ # Search for report
558
+ results = drive.files().list(
559
+ q=f"name contains '{report_id}'",
560
+ fields="files(id, name, webViewLink)"
561
+ ).execute()
562
+
563
+ files = results.get('files', [])
564
+ if files:
565
+ return {
566
+ "status": "success",
567
+ "file_id": files[0]['id'],
568
+ "file_name": files[0]['name'],
569
+ "file_url": files[0]['webViewLink']
570
+ }
571
+ return {"status": "error", "message": "Report not found"}
572
+
573
+ except Exception as e:
574
+ return {"status": "error", "message": str(e)}
575
+
576
+ @app.get("/health")
577
+ async def health():
578
+ return {"status": "ok", "rate_limit_seconds": RATE_LIMIT_SECONDS}
579
+
580
+ if __name__ == "__main__":
581
+ import uvicorn
582
+ uvicorn.run(app, host="0.0.0.0", port=7860)