Files
faction_war_dispatch_bot/services/torn_api.py
2026-01-28 13:00:15 -05:00

164 lines
5.8 KiB
Python

# services/torn_api.py
import aiohttp
import asyncio
from config import TORN_API_KEY
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={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={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