# services/torn_api.py import aiohttp import asyncio import config from .ffscouter import fetch_batch_stats from .server_state import STATE # Tasks friendly_status_task = None enemy_status_task = None # Locks for safe async updates friendly_lock = asyncio.Lock() enemy_lock = asyncio.Lock() # Populate faction (memory only) async def populate_faction(faction_id: int, kind: str): #Fetch members + FFScouter estimates once and store in STATE. #kind: "friendly" or "enemy" url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={config.TORN_API_KEY}" try: async with aiohttp.ClientSession() as session: async with session.get(url) as resp: if resp.status != 200: error_text = await resp.text() print(f"[ERROR] Torn API returned {resp.status} for {kind} faction {faction_id}") print(f"[ERROR] Response: {error_text[:200]}") return False data = await resp.json() # Check for API error response if "error" in data: print(f"[ERROR] Torn API error for {kind} faction {faction_id}: {data['error']}") return False members_list = data.get("members", []) if not members_list: print(f"[ERROR] No members found in {kind} faction {faction_id}") return False member_ids = [m.get("id") for m in members_list if "id" in m] if not member_ids: print(f"[ERROR] No valid member IDs in {kind} faction {faction_id}") return False print(f"[INFO] Fetching FFScouter data for {len(member_ids)} members from {kind} faction {faction_id}") # Fetch FFScouter estimates ff_data = await fetch_batch_stats(member_ids) received_ids = [] async with (friendly_lock if kind == "friendly" else enemy_lock): for m in members_list: pid = m["id"] est = ff_data.get(str(pid), {}).get("bs_estimate_human", "?") status = m.get("status", {}).get("state", "Unknown") member_data = { "id": pid, "name": m.get("name", "Unknown"), "level": m.get("level", 0), "estimate": est, "status": status } await STATE.upsert_member(member_data, kind) received_ids.append(pid) # Remove missing members from STATE await STATE.remove_missing_members(received_ids, kind) print(f"[SUCCESS] Populated {len(received_ids)} {kind} members from faction {faction_id}") return True except Exception as e: print(f"[ERROR] Exception in populate_faction for {kind} faction {faction_id}: {type(e).__name__}: {str(e)}") import traceback traceback.print_exc() return False # Status refresh loop async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, interval: int): #Periodically refresh member statuses in STATE. while True: try: url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={config.TORN_API_KEY}" async with aiohttp.ClientSession() as session: async with session.get(url) as resp: if resp.status != 200: print(f"Status fetch error {resp.status}") await asyncio.sleep(interval) continue data = await resp.json() members_list = data.get("members", []) async with lock: coll = STATE.friendly if kind == "friendly" else STATE.enemy for m in members_list: mid = m.get("id") if mid in coll: coll[mid].status = m.get("status", {}).get("state", "Unknown") except Exception as e: print(f"Error in status loop for {kind} faction {faction_id}: {e}") await asyncio.sleep(interval) # Public API helpers async def populate_friendly(faction_id: int): # Store friendly faction ID for chain monitoring STATE.friendly_faction_id = faction_id return await populate_faction(faction_id, "friendly") async def populate_enemy(faction_id: int): # Store enemy faction ID STATE.enemy_faction_id = faction_id return await populate_faction(faction_id, "enemy") async def start_friendly_status_loop(faction_id: int, interval: int): global friendly_status_task if friendly_status_task and not friendly_status_task.done(): friendly_status_task.cancel() friendly_status_task = asyncio.create_task( refresh_status_loop(faction_id, "friendly", friendly_lock, interval) ) # Save state STATE.friendly_status_interval = interval STATE.friendly_status_running = True async def start_enemy_status_loop(faction_id: int, interval: int): global enemy_status_task if enemy_status_task and not enemy_status_task.done(): enemy_status_task.cancel() enemy_status_task = asyncio.create_task( refresh_status_loop(faction_id, "enemy", enemy_lock, interval) ) # Save state STATE.enemy_status_interval = interval STATE.enemy_status_running = True async def stop_friendly_status_loop(): global friendly_status_task if friendly_status_task and not friendly_status_task.done(): friendly_status_task.cancel() try: await friendly_status_task except asyncio.CancelledError: pass STATE.friendly_status_running = False async def stop_enemy_status_loop(): global enemy_status_task if enemy_status_task and not enemy_status_task.done(): enemy_status_task.cancel() try: await enemy_status_task except asyncio.CancelledError: pass STATE.enemy_status_running = False