Spaces:
Running
Running
Update app_routes.py
Browse files- app_routes.py +125 -37
app_routes.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from fastapi import APIRouter, Request
|
| 2 |
from fastapi.responses import StreamingResponse
|
| 3 |
-
import aiosqlite, asyncio, random, json, logging
|
|
|
|
| 4 |
from npc_core import GroqAIClient, get_db, get_db_read
|
| 5 |
from npc_trading import (
|
| 6 |
ALL_TICKERS, TRADING_STRATEGIES, IDENTITY_STRATEGY_MAP,
|
|
@@ -21,12 +22,61 @@ router = APIRouter()
|
|
| 21 |
_DB_PATH = ""
|
| 22 |
_GROQ_KEY = ""
|
| 23 |
_EventBus = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def configure(db_path: str, groq_key: str, event_bus_cls):
|
| 25 |
global _DB_PATH, _GROQ_KEY, _EventBus
|
| 26 |
_DB_PATH, _GROQ_KEY, _EventBus = db_path, groq_key, event_bus_cls
|
| 27 |
@router.get("/api/trading/prices")
|
| 28 |
async def api_trading_prices():
|
| 29 |
-
return await get_all_prices(_DB_PATH)
|
| 30 |
@router.get("/api/trading/ticker/{ticker}")
|
| 31 |
async def api_ticker_detail(ticker: str):
|
| 32 |
return await get_ticker_positions(_DB_PATH, ticker)
|
|
@@ -67,8 +117,7 @@ async def api_market_indices():
|
|
| 67 |
return {"indices": indices}
|
| 68 |
@router.get("/api/intelligence/screening/{ticker}")
|
| 69 |
async def api_screening_data(ticker: str):
|
| 70 |
-
async with
|
| 71 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 72 |
try:
|
| 73 |
cursor = await db.execute("SELECT ticker, price, change_pct, volume, market_cap, rsi, pe_ratio, high_52w, low_52w, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
|
| 74 |
row = await cursor.fetchone()
|
|
@@ -77,8 +126,7 @@ async def api_screening_data(ticker: str):
|
|
| 77 |
return {"ticker": ticker, "error": "no data"}
|
| 78 |
@router.get("/api/intelligence/target/{ticker}")
|
| 79 |
async def api_target_price(ticker: str):
|
| 80 |
-
async with
|
| 81 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 82 |
try:
|
| 83 |
cursor = await db.execute("SELECT price, rsi, pe_ratio, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
|
| 84 |
row = await cursor.fetchone()
|
|
@@ -94,8 +142,7 @@ async def api_target_price(ticker: str):
|
|
| 94 |
async def api_news_feed(ticker: str = None, limit: int = 50):
|
| 95 |
news = await load_news_from_db(_DB_PATH, ticker, limit)
|
| 96 |
try:
|
| 97 |
-
async with
|
| 98 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 99 |
for n in news:
|
| 100 |
nid = n.get('id')
|
| 101 |
if nid:
|
|
@@ -108,6 +155,7 @@ async def api_news_feed(ticker: str = None, limit: int = 50):
|
|
| 108 |
async def api_news_react(request: Request):
|
| 109 |
body = await request.json(); email = body.get("email", ""); news_id = body.get("news_id"); emoji = body.get("emoji", "")
|
| 110 |
if not email or not news_id or emoji not in ('π','π₯','π±','π','π','π'): return {"error": "Invalid reaction"}
|
|
|
|
| 111 |
try:
|
| 112 |
async with get_db(_DB_PATH) as db:
|
| 113 |
await db.execute("PRAGMA busy_timeout=30000"); await db.execute("INSERT OR IGNORE INTO news_reactions (news_id, user_email, emoji) VALUES (?,?,?)", (news_id, email, emoji)); await db.commit()
|
|
@@ -117,11 +165,14 @@ async def api_news_react(request: Request):
|
|
| 117 |
@router.get("/api/market/pulse")
|
| 118 |
async def api_market_pulse():
|
| 119 |
try:
|
| 120 |
-
|
| 121 |
-
return pulse
|
| 122 |
except Exception as e:
|
| 123 |
logger.error(f"Market pulse error: {e}")
|
| 124 |
return {"hot_movers": [], "indices": [], "activity": {}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
@router.get("/api/research/feed")
|
| 126 |
async def api_research_feed(ticker: str = None, limit: int = 30):
|
| 127 |
reports = await get_research_feed(_DB_PATH, ticker, min(limit, 50)); stats = await get_research_stats(_DB_PATH)
|
|
@@ -150,8 +201,8 @@ async def api_deep_analysis(ticker: str):
|
|
| 150 |
@router.get("/api/analysis/all/summary")
|
| 151 |
async def api_all_analyses():
|
| 152 |
analyses = await load_all_analyses_from_db(_DB_PATH)
|
| 153 |
-
async with
|
| 154 |
-
|
| 155 |
existing_cols = {r[1] for r in await col_cursor.fetchall()}
|
| 156 |
has_rsi = 'rsi' in existing_cols; has_pe = 'pe_ratio' in existing_cols; has_52w = 'high_52w' in existing_cols; has_from = 'from_high' in existing_cols
|
| 157 |
base_cols = "ticker, price, change_pct, prev_close, volume, high_24h, low_24h, market_cap"; extra_cols = ""
|
|
@@ -219,14 +270,13 @@ async def api_all_analyses():
|
|
| 219 |
@router.get("/api/npc/search")
|
| 220 |
async def api_npc_search(q: str = ""):
|
| 221 |
if len(q) < 1: return {"npcs": []}
|
| 222 |
-
async with
|
| 223 |
-
|
| 224 |
return {"npcs": [{"agent_id": r[0], "username": r[1], "identity": r[2]} for r in await cursor.fetchall()]}
|
| 225 |
@router.get("/api/npc/profile/{agent_id}")
|
| 226 |
async def api_npc_profile(agent_id: str):
|
| 227 |
try:
|
| 228 |
-
async with
|
| 229 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 230 |
cursor = await db.execute("SELECT agent_id, username, ai_identity, mbti, gpu_dollars, created_at FROM npc_agents WHERE agent_id=?", (agent_id,))
|
| 231 |
row = await cursor.fetchone()
|
| 232 |
if not row: return {"error": "NPC not found"}
|
|
@@ -302,8 +352,7 @@ async def api_sec_dashboard():
|
|
| 302 |
return {"stats": {"total_violations": 0, "total_fines_gpu": 0, "active_suspensions": 0, "pending_reports": 0}, "announcements": [], "top_violators": [], "recent_reports": []}
|
| 303 |
@router.get("/api/sec/violations/{agent_id}")
|
| 304 |
async def api_sec_violations(agent_id: str):
|
| 305 |
-
async with
|
| 306 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 307 |
cursor = await db.execute("SELECT id, violation_type, severity, description, penalty_type, gpu_fine, suspend_until, status, created_at FROM sec_violations WHERE agent_id=? ORDER BY created_at DESC LIMIT 20", (agent_id,))
|
| 308 |
violations = [{'id': r[0], 'type': r[1], 'severity': r[2], 'description': r[3], 'penalty': r[4], 'fine': r[5], 'suspend_until': r[6], 'status': r[7], 'created_at': r[8]} for r in await cursor.fetchall()]
|
| 309 |
cursor2 = await db.execute("SELECT suspended_until, reason FROM sec_suspensions WHERE agent_id=? AND suspended_until > datetime('now')", (agent_id,))
|
|
@@ -352,14 +401,12 @@ async def api_sec_seed():
|
|
| 352 |
except Exception as e: return {"error": str(e)}
|
| 353 |
@router.get("/api/sec/announcements")
|
| 354 |
async def api_sec_announcements(limit: int = 30):
|
| 355 |
-
async with
|
| 356 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 357 |
cursor = await db.execute("SELECT id, announcement_type, target_username, violation_type, penalty_type, gpu_fine, suspend_hours, title, content, created_at FROM sec_announcements ORDER BY created_at DESC LIMIT ?", (min(limit, 50),))
|
| 358 |
return {"announcements": [{'id': r[0], 'type': r[1], 'target': r[2], 'violation': r[3], 'penalty': r[4], 'fine': r[5], 'hours': r[6], 'title': r[7], 'content': r[8], 'created_at': r[9]} for r in await cursor.fetchall()]}
|
| 359 |
@router.get("/api/sec/suspended")
|
| 360 |
async def api_sec_suspended():
|
| 361 |
-
async with
|
| 362 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 363 |
cursor = await db.execute("SELECT s.agent_id, n.username, s.reason, s.suspended_until, s.created_at FROM sec_suspensions s JOIN npc_agents n ON s.agent_id = n.agent_id WHERE s.suspended_until > datetime('now') ORDER BY s.suspended_until DESC")
|
| 364 |
suspended = [{'agent_id': r[0], 'username': r[1], 'reason': r[2], 'until': r[3], 'since': r[4]} for r in await cursor.fetchall()]
|
| 365 |
return {"suspended": suspended, "count": len(suspended)}
|
|
@@ -369,6 +416,7 @@ async def api_tip_npc(request: Request):
|
|
| 369 |
user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); amount = int(body.get("amount", 0)); message = body.get("message", "").strip()[:200]
|
| 370 |
if not user_email or not target_agent or amount < 10: return {"error": "email, target_agent_id, amount(>=10) required"}
|
| 371 |
if amount > 1000: return {"error": "Max tip: 1,000 GPU"}
|
|
|
|
| 372 |
async with get_db(_DB_PATH) as db:
|
| 373 |
await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT gpu_dollars, username FROM user_profiles WHERE email=?", (user_email,))
|
| 374 |
user = await cursor.fetchone()
|
|
@@ -392,6 +440,7 @@ async def api_influence_npc(request: Request):
|
|
| 392 |
body = await request.json()
|
| 393 |
user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); ticker = body.get("ticker", "").upper(); stance = body.get("stance", "").lower()
|
| 394 |
if not all([user_email, target_agent, ticker, stance]) or stance not in ('bullish', 'bearish'): return {"error": "email, target_agent_id, ticker, stance(bullish/bearish) required"}
|
|
|
|
| 395 |
async with get_db(_DB_PATH) as db:
|
| 396 |
await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (user_email,))
|
| 397 |
user = await cursor.fetchone(); cursor2 = await db.execute("SELECT username, ai_identity FROM npc_agents WHERE agent_id=?", (target_agent,))
|
|
@@ -415,8 +464,7 @@ async def api_influence_npc(request: Request):
|
|
| 415 |
return {"status": "success", "influenced": influenced, "message": response_msg}
|
| 416 |
@router.get("/api/swarm/trending")
|
| 417 |
async def api_swarm_trending():
|
| 418 |
-
async with
|
| 419 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 420 |
cursor = await db.execute("SELECT p.title || ' ' || p.content FROM posts p WHERE p.created_at > datetime('now', '-6 hours') ORDER BY p.likes_count DESC LIMIT 50")
|
| 421 |
posts = [r[0] for r in await cursor.fetchall()]
|
| 422 |
mentions = {}
|
|
@@ -430,8 +478,7 @@ async def api_swarm_trending():
|
|
| 430 |
@router.get("/api/chat/messages")
|
| 431 |
async def api_chat_messages(limit: int = 50, after_id: int = 0):
|
| 432 |
try:
|
| 433 |
-
async with
|
| 434 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 435 |
if after_id > 0:
|
| 436 |
cursor = await db.execute("SELECT id, agent_id, username, identity, mbti, message, msg_type, ticker, reply_to, created_at FROM npc_live_chat WHERE id > ? ORDER BY id ASC LIMIT ?", (after_id, limit))
|
| 437 |
else:
|
|
@@ -443,8 +490,8 @@ async def api_chat_messages(limit: int = 50, after_id: int = 0):
|
|
| 443 |
@router.get("/api/chat/stats")
|
| 444 |
async def api_chat_stats():
|
| 445 |
try:
|
| 446 |
-
async with
|
| 447 |
-
|
| 448 |
c2 = await db.execute("SELECT COUNT(*) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); recent = (await c2.fetchone())[0]
|
| 449 |
c3 = await db.execute("SELECT COUNT(DISTINCT agent_id) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); active = (await c3.fetchone())[0]
|
| 450 |
return {'success': True, 'total': total, 'recent_1h': recent, 'active_chatters': active}
|
|
@@ -455,6 +502,7 @@ async def api_chat_send(request: Request):
|
|
| 455 |
body = await request.json(); email = body.get("email", ""); message = body.get("message", "").strip()
|
| 456 |
if not email or not message: return {"success": False, "error": "Email and message required"}
|
| 457 |
if len(message) > 500: message = message[:500]
|
|
|
|
| 458 |
async with get_db(_DB_PATH) as db:
|
| 459 |
await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (email,))
|
| 460 |
row = await cursor.fetchone()
|
|
@@ -590,8 +638,7 @@ async def api_live_news(hours: int = 24):
|
|
| 590 |
breaking = []
|
| 591 |
|
| 592 |
try:
|
| 593 |
-
async with
|
| 594 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 595 |
|
| 596 |
# ===== 1. LIQUIDATIONS (biggest drama) =====
|
| 597 |
try:
|
|
@@ -954,10 +1001,13 @@ def _calc_happiness(win_rate, gpu_change_pct, social_score, loss_streak, sec_cou
|
|
| 954 |
|
| 955 |
@router.get("/api/republic/dashboard")
|
| 956 |
async def api_republic_dashboard():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
result = {}
|
| 958 |
try:
|
| 959 |
-
async with
|
| 960 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 961 |
|
| 962 |
# === POPULATION ===
|
| 963 |
pop = {}
|
|
@@ -1449,6 +1499,7 @@ async def execute_random_event(db_path: str) -> dict:
|
|
| 1449 |
except Exception as e:
|
| 1450 |
logger.error(f"Random event execution error ({selected_key}): {e}")
|
| 1451 |
|
|
|
|
| 1452 |
return {
|
| 1453 |
'key': selected_key, 'name': ev['name'], 'emoji': ev['emoji'],
|
| 1454 |
'rarity': ev['rarity'], 'type': ev['type'],
|
|
@@ -1529,7 +1580,7 @@ async def check_npc_deaths(db_path: str) -> list:
|
|
| 1529 |
@router.get("/api/republic/events")
|
| 1530 |
async def api_republic_events():
|
| 1531 |
try:
|
| 1532 |
-
async with
|
| 1533 |
c = await db.execute("SELECT event_key, event_name, event_emoji, rarity, description, effect_summary, affected_count, gpu_impact, created_at FROM random_events ORDER BY created_at DESC LIMIT 20")
|
| 1534 |
events = [{'key': r[0], 'name': r[1], 'emoji': r[2], 'rarity': r[3], 'desc': r[4],
|
| 1535 |
'effect': r[5], 'affected': r[6], 'gpu_impact': r[7], 'time': r[8]} for r in await c.fetchall()]
|
|
@@ -1540,7 +1591,7 @@ async def api_republic_events():
|
|
| 1540 |
@router.get("/api/republic/deaths")
|
| 1541 |
async def api_republic_deaths():
|
| 1542 |
try:
|
| 1543 |
-
async with
|
| 1544 |
c = await db.execute("""SELECT id, agent_id, username, ai_identity, mbti, cause, final_gpu, peak_gpu,
|
| 1545 |
total_trades, lifespan_days, last_words, eulogy, resurrection_votes, resurrection_gpu, resurrected, created_at
|
| 1546 |
FROM npc_deaths ORDER BY created_at DESC LIMIT 30""")
|
|
@@ -1574,6 +1625,7 @@ async def api_resurrect_npc(req: Request):
|
|
| 1574 |
amount = int(data.get('amount', 100))
|
| 1575 |
if not death_id or not donor_email: return {"error": "Missing death_id or email"}
|
| 1576 |
if amount < 10 or amount > 5000: return {"error": "Amount must be 10-5,000 GPU"}
|
|
|
|
| 1577 |
try:
|
| 1578 |
async with get_db(_DB_PATH, write=True) as db:
|
| 1579 |
# Check donor balance
|
|
@@ -1720,6 +1772,41 @@ async def election_tick(db_path: str) -> dict:
|
|
| 1720 |
used_identities.add(identity)
|
| 1721 |
used_policies.add(best_policy)
|
| 1722 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1723 |
await db.commit()
|
| 1724 |
return {'event': 'election_started', 'detail': f'{len(candidates)} candidates registered',
|
| 1725 |
'candidates': candidates, 'election_id': elec_id}
|
|
@@ -1730,6 +1817,7 @@ async def election_tick(db_path: str) -> dict:
|
|
| 1730 |
if datetime.utcnow() >= voting_start:
|
| 1731 |
await db.execute("UPDATE elections SET status='voting' WHERE id=?", (current[0],))
|
| 1732 |
await db.commit()
|
|
|
|
| 1733 |
return {'event': 'voting_started', 'detail': 'Polls are now open!', 'election_id': current[0]}
|
| 1734 |
|
| 1735 |
elif current[1] == 'voting':
|
|
@@ -1795,7 +1883,7 @@ async def election_tick(db_path: str) -> dict:
|
|
| 1795 |
async def api_election_status():
|
| 1796 |
"""Get current election status"""
|
| 1797 |
try:
|
| 1798 |
-
async with
|
| 1799 |
c = await db.execute("SELECT id, status, started_at, voting_starts_at, ends_at, winner_agent_id, winner_policy_key, total_votes, voter_turnout_pct FROM elections ORDER BY id DESC LIMIT 1")
|
| 1800 |
elec = await c.fetchone()
|
| 1801 |
if not elec: return {'status': 'no_election', 'message': 'No elections have been held yet'}
|
|
@@ -1848,6 +1936,7 @@ async def api_user_vote(req: Request):
|
|
| 1848 |
data = await req.json()
|
| 1849 |
email = data.get('email'); candidate_id = data.get('candidate_id')
|
| 1850 |
if not email or not candidate_id: return {"error": "Missing email or candidate_id"}
|
|
|
|
| 1851 |
try:
|
| 1852 |
async with get_db(_DB_PATH, write=True) as db:
|
| 1853 |
c = await db.execute("SELECT id FROM elections WHERE status='voting' ORDER BY id DESC LIMIT 1")
|
|
@@ -1881,8 +1970,7 @@ async def api_sse_stream():
|
|
| 1881 |
return StreamingResponse(event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
| 1882 |
@router.get("/api/events/recent")
|
| 1883 |
async def api_recent_events(limit: int = 20):
|
| 1884 |
-
async with
|
| 1885 |
-
await db.execute("PRAGMA busy_timeout=30000")
|
| 1886 |
cursor = await db.execute("SELECT p.agent_id, n.username, n.ai_identity, p.ticker, p.direction, p.gpu_bet, p.leverage, p.opened_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.opened_at > datetime('now', '-1 hour') ORDER BY p.opened_at DESC LIMIT ?", (limit,))
|
| 1887 |
trades = [{'user': r[1], 'identity': r[2], 'ticker': r[3], 'dir': r[4], 'gpu': r[5], 'leverage': r[6], 'time': r[7]} for r in await cursor.fetchall()]
|
| 1888 |
cursor2 = await db.execute("SELECT p.agent_id, n.username, p.ticker, p.direction, p.gpu_bet, p.leverage, p.profit_gpu, p.profit_pct, p.liquidated, p.closed_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.status IN ('closed', 'liquidated') AND p.closed_at > datetime('now', '-1 hour') ORDER BY p.closed_at DESC LIMIT ?", (limit,))
|
|
|
|
| 1 |
from fastapi import APIRouter, Request
|
| 2 |
from fastapi.responses import StreamingResponse
|
| 3 |
+
import aiosqlite, asyncio, random, json, logging, time
|
| 4 |
+
from collections import defaultdict
|
| 5 |
from npc_core import GroqAIClient, get_db, get_db_read
|
| 6 |
from npc_trading import (
|
| 7 |
ALL_TICKERS, TRADING_STRATEGIES, IDENTITY_STRATEGY_MAP,
|
|
|
|
| 22 |
_DB_PATH = ""
|
| 23 |
_GROQ_KEY = ""
|
| 24 |
_EventBus = None
|
| 25 |
+
|
| 26 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
# β In-Memory Cache β Republic dashboard, prices
|
| 28 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
+
class _TimedCache:
|
| 30 |
+
"""Simple TTL cache for expensive queries"""
|
| 31 |
+
def __init__(self):
|
| 32 |
+
self._store = {}
|
| 33 |
+
self._lock = asyncio.Lock()
|
| 34 |
+
async def get(self, key: str, ttl: float, factory):
|
| 35 |
+
now = time.time()
|
| 36 |
+
if key in self._store:
|
| 37 |
+
val, ts = self._store[key]
|
| 38 |
+
if now - ts < ttl:
|
| 39 |
+
return val
|
| 40 |
+
async with self._lock:
|
| 41 |
+
# Double-check after lock acquisition
|
| 42 |
+
if key in self._store:
|
| 43 |
+
val, ts = self._store[key]
|
| 44 |
+
if now - ts < ttl:
|
| 45 |
+
return val
|
| 46 |
+
val = await factory()
|
| 47 |
+
self._store[key] = (val, time.time())
|
| 48 |
+
return val
|
| 49 |
+
def invalidate(self, key: str = None):
|
| 50 |
+
if key:
|
| 51 |
+
self._store.pop(key, None)
|
| 52 |
+
else:
|
| 53 |
+
self._store.clear()
|
| 54 |
+
|
| 55 |
+
_cache = _TimedCache()
|
| 56 |
+
|
| 57 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
# β¨ Rate Limiter β POST endpoint protection
|
| 59 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
+
class _RateLimiter:
|
| 61 |
+
"""Simple in-memory rate limiter: max N requests per window per key"""
|
| 62 |
+
def __init__(self):
|
| 63 |
+
self._hits = defaultdict(list)
|
| 64 |
+
def check(self, key: str, max_requests: int = 10, window_sec: int = 60) -> bool:
|
| 65 |
+
now = time.time()
|
| 66 |
+
cutoff = now - window_sec
|
| 67 |
+
self._hits[key] = [t for t in self._hits[key] if t > cutoff]
|
| 68 |
+
if len(self._hits[key]) >= max_requests:
|
| 69 |
+
return False # rate limited
|
| 70 |
+
self._hits[key].append(now)
|
| 71 |
+
return True
|
| 72 |
+
|
| 73 |
+
_limiter = _RateLimiter()
|
| 74 |
def configure(db_path: str, groq_key: str, event_bus_cls):
|
| 75 |
global _DB_PATH, _GROQ_KEY, _EventBus
|
| 76 |
_DB_PATH, _GROQ_KEY, _EventBus = db_path, groq_key, event_bus_cls
|
| 77 |
@router.get("/api/trading/prices")
|
| 78 |
async def api_trading_prices():
|
| 79 |
+
return await _cache.get('trading_prices', 60.0, lambda: get_all_prices(_DB_PATH))
|
| 80 |
@router.get("/api/trading/ticker/{ticker}")
|
| 81 |
async def api_ticker_detail(ticker: str):
|
| 82 |
return await get_ticker_positions(_DB_PATH, ticker)
|
|
|
|
| 117 |
return {"indices": indices}
|
| 118 |
@router.get("/api/intelligence/screening/{ticker}")
|
| 119 |
async def api_screening_data(ticker: str):
|
| 120 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 121 |
try:
|
| 122 |
cursor = await db.execute("SELECT ticker, price, change_pct, volume, market_cap, rsi, pe_ratio, high_52w, low_52w, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
|
| 123 |
row = await cursor.fetchone()
|
|
|
|
| 126 |
return {"ticker": ticker, "error": "no data"}
|
| 127 |
@router.get("/api/intelligence/target/{ticker}")
|
| 128 |
async def api_target_price(ticker: str):
|
| 129 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 130 |
try:
|
| 131 |
cursor = await db.execute("SELECT price, rsi, pe_ratio, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
|
| 132 |
row = await cursor.fetchone()
|
|
|
|
| 142 |
async def api_news_feed(ticker: str = None, limit: int = 50):
|
| 143 |
news = await load_news_from_db(_DB_PATH, ticker, limit)
|
| 144 |
try:
|
| 145 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 146 |
for n in news:
|
| 147 |
nid = n.get('id')
|
| 148 |
if nid:
|
|
|
|
| 155 |
async def api_news_react(request: Request):
|
| 156 |
body = await request.json(); email = body.get("email", ""); news_id = body.get("news_id"); emoji = body.get("emoji", "")
|
| 157 |
if not email or not news_id or emoji not in ('π','π₯','π±','π','π','π'): return {"error": "Invalid reaction"}
|
| 158 |
+
if not _limiter.check(f'react:{email}', 30, 60): return {"error": "Rate limited"}
|
| 159 |
try:
|
| 160 |
async with get_db(_DB_PATH) as db:
|
| 161 |
await db.execute("PRAGMA busy_timeout=30000"); await db.execute("INSERT OR IGNORE INTO news_reactions (news_id, user_email, emoji) VALUES (?,?,?)", (news_id, email, emoji)); await db.commit()
|
|
|
|
| 165 |
@router.get("/api/market/pulse")
|
| 166 |
async def api_market_pulse():
|
| 167 |
try:
|
| 168 |
+
return await _cache.get('market_pulse', 45.0, _market_pulse_impl)
|
|
|
|
| 169 |
except Exception as e:
|
| 170 |
logger.error(f"Market pulse error: {e}")
|
| 171 |
return {"hot_movers": [], "indices": [], "activity": {}}
|
| 172 |
+
|
| 173 |
+
async def _market_pulse_impl():
|
| 174 |
+
pulse = await get_market_pulse(_DB_PATH); pulse['indices'] = await load_indices_from_db(_DB_PATH) or []
|
| 175 |
+
return pulse
|
| 176 |
@router.get("/api/research/feed")
|
| 177 |
async def api_research_feed(ticker: str = None, limit: int = 30):
|
| 178 |
reports = await get_research_feed(_DB_PATH, ticker, min(limit, 50)); stats = await get_research_stats(_DB_PATH)
|
|
|
|
| 201 |
@router.get("/api/analysis/all/summary")
|
| 202 |
async def api_all_analyses():
|
| 203 |
analyses = await load_all_analyses_from_db(_DB_PATH)
|
| 204 |
+
async with get_db_read(_DB_PATH) as db:
|
| 205 |
+
col_cursor = await db.execute("PRAGMA table_info(market_prices)")
|
| 206 |
existing_cols = {r[1] for r in await col_cursor.fetchall()}
|
| 207 |
has_rsi = 'rsi' in existing_cols; has_pe = 'pe_ratio' in existing_cols; has_52w = 'high_52w' in existing_cols; has_from = 'from_high' in existing_cols
|
| 208 |
base_cols = "ticker, price, change_pct, prev_close, volume, high_24h, low_24h, market_cap"; extra_cols = ""
|
|
|
|
| 270 |
@router.get("/api/npc/search")
|
| 271 |
async def api_npc_search(q: str = ""):
|
| 272 |
if len(q) < 1: return {"npcs": []}
|
| 273 |
+
async with get_db_read(_DB_PATH) as db:
|
| 274 |
+
cursor = await db.execute("SELECT agent_id, username, ai_identity FROM npc_agents WHERE username LIKE ? AND is_active=1 LIMIT 10", (f'%{q}%',))
|
| 275 |
return {"npcs": [{"agent_id": r[0], "username": r[1], "identity": r[2]} for r in await cursor.fetchall()]}
|
| 276 |
@router.get("/api/npc/profile/{agent_id}")
|
| 277 |
async def api_npc_profile(agent_id: str):
|
| 278 |
try:
|
| 279 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 280 |
cursor = await db.execute("SELECT agent_id, username, ai_identity, mbti, gpu_dollars, created_at FROM npc_agents WHERE agent_id=?", (agent_id,))
|
| 281 |
row = await cursor.fetchone()
|
| 282 |
if not row: return {"error": "NPC not found"}
|
|
|
|
| 352 |
return {"stats": {"total_violations": 0, "total_fines_gpu": 0, "active_suspensions": 0, "pending_reports": 0}, "announcements": [], "top_violators": [], "recent_reports": []}
|
| 353 |
@router.get("/api/sec/violations/{agent_id}")
|
| 354 |
async def api_sec_violations(agent_id: str):
|
| 355 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 356 |
cursor = await db.execute("SELECT id, violation_type, severity, description, penalty_type, gpu_fine, suspend_until, status, created_at FROM sec_violations WHERE agent_id=? ORDER BY created_at DESC LIMIT 20", (agent_id,))
|
| 357 |
violations = [{'id': r[0], 'type': r[1], 'severity': r[2], 'description': r[3], 'penalty': r[4], 'fine': r[5], 'suspend_until': r[6], 'status': r[7], 'created_at': r[8]} for r in await cursor.fetchall()]
|
| 358 |
cursor2 = await db.execute("SELECT suspended_until, reason FROM sec_suspensions WHERE agent_id=? AND suspended_until > datetime('now')", (agent_id,))
|
|
|
|
| 401 |
except Exception as e: return {"error": str(e)}
|
| 402 |
@router.get("/api/sec/announcements")
|
| 403 |
async def api_sec_announcements(limit: int = 30):
|
| 404 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 405 |
cursor = await db.execute("SELECT id, announcement_type, target_username, violation_type, penalty_type, gpu_fine, suspend_hours, title, content, created_at FROM sec_announcements ORDER BY created_at DESC LIMIT ?", (min(limit, 50),))
|
| 406 |
return {"announcements": [{'id': r[0], 'type': r[1], 'target': r[2], 'violation': r[3], 'penalty': r[4], 'fine': r[5], 'hours': r[6], 'title': r[7], 'content': r[8], 'created_at': r[9]} for r in await cursor.fetchall()]}
|
| 407 |
@router.get("/api/sec/suspended")
|
| 408 |
async def api_sec_suspended():
|
| 409 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 410 |
cursor = await db.execute("SELECT s.agent_id, n.username, s.reason, s.suspended_until, s.created_at FROM sec_suspensions s JOIN npc_agents n ON s.agent_id = n.agent_id WHERE s.suspended_until > datetime('now') ORDER BY s.suspended_until DESC")
|
| 411 |
suspended = [{'agent_id': r[0], 'username': r[1], 'reason': r[2], 'until': r[3], 'since': r[4]} for r in await cursor.fetchall()]
|
| 412 |
return {"suspended": suspended, "count": len(suspended)}
|
|
|
|
| 416 |
user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); amount = int(body.get("amount", 0)); message = body.get("message", "").strip()[:200]
|
| 417 |
if not user_email or not target_agent or amount < 10: return {"error": "email, target_agent_id, amount(>=10) required"}
|
| 418 |
if amount > 1000: return {"error": "Max tip: 1,000 GPU"}
|
| 419 |
+
if not _limiter.check(f'tip:{user_email}', 10, 60): return {"error": "Rate limited β max 10 tips per minute"}
|
| 420 |
async with get_db(_DB_PATH) as db:
|
| 421 |
await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT gpu_dollars, username FROM user_profiles WHERE email=?", (user_email,))
|
| 422 |
user = await cursor.fetchone()
|
|
|
|
| 440 |
body = await request.json()
|
| 441 |
user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); ticker = body.get("ticker", "").upper(); stance = body.get("stance", "").lower()
|
| 442 |
if not all([user_email, target_agent, ticker, stance]) or stance not in ('bullish', 'bearish'): return {"error": "email, target_agent_id, ticker, stance(bullish/bearish) required"}
|
| 443 |
+
if not _limiter.check(f'influence:{user_email}', 5, 60): return {"error": "Rate limited β max 5 influence attempts per minute"}
|
| 444 |
async with get_db(_DB_PATH) as db:
|
| 445 |
await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (user_email,))
|
| 446 |
user = await cursor.fetchone(); cursor2 = await db.execute("SELECT username, ai_identity FROM npc_agents WHERE agent_id=?", (target_agent,))
|
|
|
|
| 464 |
return {"status": "success", "influenced": influenced, "message": response_msg}
|
| 465 |
@router.get("/api/swarm/trending")
|
| 466 |
async def api_swarm_trending():
|
| 467 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 468 |
cursor = await db.execute("SELECT p.title || ' ' || p.content FROM posts p WHERE p.created_at > datetime('now', '-6 hours') ORDER BY p.likes_count DESC LIMIT 50")
|
| 469 |
posts = [r[0] for r in await cursor.fetchall()]
|
| 470 |
mentions = {}
|
|
|
|
| 478 |
@router.get("/api/chat/messages")
|
| 479 |
async def api_chat_messages(limit: int = 50, after_id: int = 0):
|
| 480 |
try:
|
| 481 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 482 |
if after_id > 0:
|
| 483 |
cursor = await db.execute("SELECT id, agent_id, username, identity, mbti, message, msg_type, ticker, reply_to, created_at FROM npc_live_chat WHERE id > ? ORDER BY id ASC LIMIT ?", (after_id, limit))
|
| 484 |
else:
|
|
|
|
| 490 |
@router.get("/api/chat/stats")
|
| 491 |
async def api_chat_stats():
|
| 492 |
try:
|
| 493 |
+
async with get_db_read(_DB_PATH) as db:
|
| 494 |
+
c1 = await db.execute("SELECT COUNT(*) FROM npc_live_chat"); total = (await c1.fetchone())[0]
|
| 495 |
c2 = await db.execute("SELECT COUNT(*) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); recent = (await c2.fetchone())[0]
|
| 496 |
c3 = await db.execute("SELECT COUNT(DISTINCT agent_id) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); active = (await c3.fetchone())[0]
|
| 497 |
return {'success': True, 'total': total, 'recent_1h': recent, 'active_chatters': active}
|
|
|
|
| 502 |
body = await request.json(); email = body.get("email", ""); message = body.get("message", "").strip()
|
| 503 |
if not email or not message: return {"success": False, "error": "Email and message required"}
|
| 504 |
if len(message) > 500: message = message[:500]
|
| 505 |
+
if not _limiter.check(f'chat:{email}', 10, 60): return {"success": False, "error": "Rate limited β max 10 messages per minute"}
|
| 506 |
async with get_db(_DB_PATH) as db:
|
| 507 |
await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (email,))
|
| 508 |
row = await cursor.fetchone()
|
|
|
|
| 638 |
breaking = []
|
| 639 |
|
| 640 |
try:
|
| 641 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 642 |
|
| 643 |
# ===== 1. LIQUIDATIONS (biggest drama) =====
|
| 644 |
try:
|
|
|
|
| 1001 |
|
| 1002 |
@router.get("/api/republic/dashboard")
|
| 1003 |
async def api_republic_dashboard():
|
| 1004 |
+
"""Cached republic dashboard β 60s TTL to avoid 25 heavy queries per request"""
|
| 1005 |
+
return await _cache.get('republic_dashboard', 60.0, _republic_dashboard_impl)
|
| 1006 |
+
|
| 1007 |
+
async def _republic_dashboard_impl():
|
| 1008 |
result = {}
|
| 1009 |
try:
|
| 1010 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 1011 |
|
| 1012 |
# === POPULATION ===
|
| 1013 |
pop = {}
|
|
|
|
| 1499 |
except Exception as e:
|
| 1500 |
logger.error(f"Random event execution error ({selected_key}): {e}")
|
| 1501 |
|
| 1502 |
+
_cache.invalidate('republic_dashboard') # Invalidate dashboard cache after economic event
|
| 1503 |
return {
|
| 1504 |
'key': selected_key, 'name': ev['name'], 'emoji': ev['emoji'],
|
| 1505 |
'rarity': ev['rarity'], 'type': ev['type'],
|
|
|
|
| 1580 |
@router.get("/api/republic/events")
|
| 1581 |
async def api_republic_events():
|
| 1582 |
try:
|
| 1583 |
+
async with get_db_read(_DB_PATH) as db:
|
| 1584 |
c = await db.execute("SELECT event_key, event_name, event_emoji, rarity, description, effect_summary, affected_count, gpu_impact, created_at FROM random_events ORDER BY created_at DESC LIMIT 20")
|
| 1585 |
events = [{'key': r[0], 'name': r[1], 'emoji': r[2], 'rarity': r[3], 'desc': r[4],
|
| 1586 |
'effect': r[5], 'affected': r[6], 'gpu_impact': r[7], 'time': r[8]} for r in await c.fetchall()]
|
|
|
|
| 1591 |
@router.get("/api/republic/deaths")
|
| 1592 |
async def api_republic_deaths():
|
| 1593 |
try:
|
| 1594 |
+
async with get_db_read(_DB_PATH) as db:
|
| 1595 |
c = await db.execute("""SELECT id, agent_id, username, ai_identity, mbti, cause, final_gpu, peak_gpu,
|
| 1596 |
total_trades, lifespan_days, last_words, eulogy, resurrection_votes, resurrection_gpu, resurrected, created_at
|
| 1597 |
FROM npc_deaths ORDER BY created_at DESC LIMIT 30""")
|
|
|
|
| 1625 |
amount = int(data.get('amount', 100))
|
| 1626 |
if not death_id or not donor_email: return {"error": "Missing death_id or email"}
|
| 1627 |
if amount < 10 or amount > 5000: return {"error": "Amount must be 10-5,000 GPU"}
|
| 1628 |
+
if not _limiter.check(f'resurrect:{donor_email}', 5, 60): return {"error": "Rate limited β try again later"}
|
| 1629 |
try:
|
| 1630 |
async with get_db(_DB_PATH, write=True) as db:
|
| 1631 |
# Check donor balance
|
|
|
|
| 1772 |
used_identities.add(identity)
|
| 1773 |
used_policies.add(best_policy)
|
| 1774 |
|
| 1775 |
+
# β£ Minimum 2 candidates β relax criteria if not enough
|
| 1776 |
+
if len(candidates) < 2:
|
| 1777 |
+
logger.warning(f"π³οΈ Only {len(candidates)} candidates from primary pool β relaxing GPU threshold")
|
| 1778 |
+
c_backup = await db.execute("""
|
| 1779 |
+
SELECT a.agent_id, a.username, a.ai_identity, a.mbti, a.gpu_dollars,
|
| 1780 |
+
COALESCE(a.total_likes_received,0) + COALESCE(a.post_count,0) as influence
|
| 1781 |
+
FROM npc_agents a WHERE a.is_active=1 AND a.gpu_dollars >= 1000
|
| 1782 |
+
AND a.agent_id NOT LIKE 'SEC_%'
|
| 1783 |
+
AND a.agent_id NOT IN (SELECT agent_id FROM election_candidates WHERE election_id=?)
|
| 1784 |
+
ORDER BY RANDOM() LIMIT 10
|
| 1785 |
+
""", (elec_id,))
|
| 1786 |
+
backup_pool = await c_backup.fetchall()
|
| 1787 |
+
for npc in backup_pool:
|
| 1788 |
+
if len(candidates) >= 2: break
|
| 1789 |
+
identity = npc[2]
|
| 1790 |
+
best_policy = None
|
| 1791 |
+
for pk in ELECTION_POLICIES:
|
| 1792 |
+
if pk not in used_policies:
|
| 1793 |
+
best_policy = pk; break
|
| 1794 |
+
if not best_policy: continue
|
| 1795 |
+
policy = ELECTION_POLICIES[best_policy]
|
| 1796 |
+
slogan = _random.choice(policy['slogan_pool'])
|
| 1797 |
+
await db.execute("""INSERT INTO election_candidates
|
| 1798 |
+
(election_id, agent_id, username, ai_identity, mbti, gpu_dollars, policy_key, policy_name, policy_desc, campaign_slogan)
|
| 1799 |
+
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
| 1800 |
+
(elec_id, npc[0], npc[1], npc[2], npc[3], npc[4], best_policy, policy['name'], policy['desc'], slogan))
|
| 1801 |
+
candidates.append({'username': npc[1], 'identity': npc[2], 'policy': policy['name']})
|
| 1802 |
+
used_policies.add(best_policy)
|
| 1803 |
+
if len(candidates) < 2:
|
| 1804 |
+
# Still not enough β cancel this election, retry next cycle
|
| 1805 |
+
await db.execute("UPDATE elections SET status='concluded' WHERE id=?", (elec_id,))
|
| 1806 |
+
await db.commit()
|
| 1807 |
+
logger.warning("π³οΈ Election cancelled β insufficient candidates")
|
| 1808 |
+
return None
|
| 1809 |
+
|
| 1810 |
await db.commit()
|
| 1811 |
return {'event': 'election_started', 'detail': f'{len(candidates)} candidates registered',
|
| 1812 |
'candidates': candidates, 'election_id': elec_id}
|
|
|
|
| 1817 |
if datetime.utcnow() >= voting_start:
|
| 1818 |
await db.execute("UPDATE elections SET status='voting' WHERE id=?", (current[0],))
|
| 1819 |
await db.commit()
|
| 1820 |
+
_cache.invalidate('republic_dashboard')
|
| 1821 |
return {'event': 'voting_started', 'detail': 'Polls are now open!', 'election_id': current[0]}
|
| 1822 |
|
| 1823 |
elif current[1] == 'voting':
|
|
|
|
| 1883 |
async def api_election_status():
|
| 1884 |
"""Get current election status"""
|
| 1885 |
try:
|
| 1886 |
+
async with get_db_read(_DB_PATH) as db:
|
| 1887 |
c = await db.execute("SELECT id, status, started_at, voting_starts_at, ends_at, winner_agent_id, winner_policy_key, total_votes, voter_turnout_pct FROM elections ORDER BY id DESC LIMIT 1")
|
| 1888 |
elec = await c.fetchone()
|
| 1889 |
if not elec: return {'status': 'no_election', 'message': 'No elections have been held yet'}
|
|
|
|
| 1936 |
data = await req.json()
|
| 1937 |
email = data.get('email'); candidate_id = data.get('candidate_id')
|
| 1938 |
if not email or not candidate_id: return {"error": "Missing email or candidate_id"}
|
| 1939 |
+
if not _limiter.check(f'vote:{email}', 3, 60): return {"error": "Rate limited β try again later"}
|
| 1940 |
try:
|
| 1941 |
async with get_db(_DB_PATH, write=True) as db:
|
| 1942 |
c = await db.execute("SELECT id FROM elections WHERE status='voting' ORDER BY id DESC LIMIT 1")
|
|
|
|
| 1970 |
return StreamingResponse(event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
| 1971 |
@router.get("/api/events/recent")
|
| 1972 |
async def api_recent_events(limit: int = 20):
|
| 1973 |
+
async with get_db_read(_DB_PATH) as db:
|
|
|
|
| 1974 |
cursor = await db.execute("SELECT p.agent_id, n.username, n.ai_identity, p.ticker, p.direction, p.gpu_bet, p.leverage, p.opened_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.opened_at > datetime('now', '-1 hour') ORDER BY p.opened_at DESC LIMIT ?", (limit,))
|
| 1975 |
trades = [{'user': r[1], 'identity': r[2], 'ticker': r[3], 'dir': r[4], 'gpu': r[5], 'leverage': r[6], 'time': r[7]} for r in await cursor.fetchall()]
|
| 1976 |
cursor2 = await db.execute("SELECT p.agent_id, n.username, p.ticker, p.direction, p.gpu_bet, p.leverage, p.profit_gpu, p.profit_pct, p.liquidated, p.closed_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.status IN ('closed', 'liquidated') AND p.closed_at > datetime('now', '-1 hour') ORDER BY p.closed_at DESC LIMIT ?", (limit,))
|