Back to json for server side storage
This commit is contained in:
@@ -9,8 +9,9 @@ class Member(BaseModel):
|
||||
level: int
|
||||
estimate: str
|
||||
type: str # "friendly" or "enemy"
|
||||
group: Optional[str] = None # "group1", "group2", ... or None
|
||||
group: Optional[str] = None # "1", "2", ...
|
||||
hits: int = 0
|
||||
status: str = "Unknown" # Added status field for Torn API status
|
||||
|
||||
class ServerState:
|
||||
def __init__(self, group_count: int = 5):
|
||||
@@ -30,16 +31,11 @@ class ServerState:
|
||||
# concurrency lock for async safety
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
# Assignment helpers (all use the lock when called from async endpoints)
|
||||
# Assignment helpers
|
||||
async def assign_member(self, member_id: int, kind: str, group_key: str):
|
||||
"""
|
||||
Assign a member (by id) of kind ('friendly'|'enemy') to group_key ("1".."N").
|
||||
This removes it from any previous group of the same kind.
|
||||
"""
|
||||
async with self.lock:
|
||||
if kind not in ("friendly", "enemy"):
|
||||
raise ValueError("invalid kind")
|
||||
|
||||
if group_key not in self.groups:
|
||||
raise ValueError("invalid group_key")
|
||||
|
||||
@@ -48,7 +44,7 @@ class ServerState:
|
||||
if member_id in buckets[kind]:
|
||||
buckets[kind].remove(member_id)
|
||||
|
||||
# add to new group if not present
|
||||
# add to new group
|
||||
if member_id not in self.groups[group_key][kind]:
|
||||
self.groups[group_key][kind].append(member_id)
|
||||
|
||||
@@ -58,7 +54,6 @@ class ServerState:
|
||||
coll[member_id].group = group_key
|
||||
|
||||
async def remove_member_assignment(self, member_id: int):
|
||||
"""Remove member from any group (both friendly and enemy)."""
|
||||
async with self.lock:
|
||||
for gk, buckets in self.groups.items():
|
||||
if member_id in buckets["friendly"]:
|
||||
@@ -66,33 +61,24 @@ class ServerState:
|
||||
if member_id in buckets["enemy"]:
|
||||
buckets["enemy"].remove(member_id)
|
||||
|
||||
# clear group attr if member in maps
|
||||
# clear group attribute
|
||||
if member_id in self.friendly:
|
||||
self.friendly[member_id].group = None
|
||||
if member_id in self.enemy:
|
||||
self.enemy[member_id].group = None
|
||||
|
||||
async def clear_all_assignments(self):
|
||||
"""Clear all group lists and member.group fields and save to disk."""
|
||||
async with self.lock:
|
||||
# clear in-memory groups
|
||||
for gk in self.groups:
|
||||
self.groups[gk]["friendly"].clear()
|
||||
self.groups[gk]["enemy"].clear()
|
||||
|
||||
# clear group for known members
|
||||
for m in self.friendly.values():
|
||||
m.group = None
|
||||
for m in self.enemy.values():
|
||||
m.group = None
|
||||
|
||||
# save to disk so clients get empty state
|
||||
self.save_assignments()
|
||||
|
||||
async def get_assignments_snapshot(self) -> Dict[str, Dict[str, List[int]]]:
|
||||
"""Return a copy snapshot of the groups dictionary."""
|
||||
async with self.lock:
|
||||
# shallow copy of structure
|
||||
snap = {}
|
||||
for gk, buckets in self.groups.items():
|
||||
snap[gk] = {
|
||||
@@ -101,12 +87,8 @@ class ServerState:
|
||||
}
|
||||
return snap
|
||||
|
||||
# member add/update helpers
|
||||
# member helpers
|
||||
async def upsert_member(self, member_data: dict, kind: str):
|
||||
"""
|
||||
Insert or update member. member_data expected to have id,name,level,estimate.
|
||||
kind must be 'friendly' or 'enemy'.
|
||||
"""
|
||||
async with self.lock:
|
||||
if kind not in ("friendly", "enemy"):
|
||||
raise ValueError("invalid kind")
|
||||
@@ -121,19 +103,18 @@ class ServerState:
|
||||
estimate=str(member_data.get("estimate", "?")),
|
||||
type=kind,
|
||||
group=existing_group,
|
||||
hits=int(member_data.get("hits", 0)) if "hits" in member_data else 0
|
||||
hits=int(member_data.get("hits", 0)) if "hits" in member_data else 0,
|
||||
status=member_data.get("status", "Unknown") # Initialize status if provided
|
||||
)
|
||||
coll[mid] = member
|
||||
|
||||
async def remove_missing_members(self, received_ids: List[int], kind: str):
|
||||
"""Remove any existing members not present in received_ids (useful after a populate)."""
|
||||
async with self.lock:
|
||||
coll = self.friendly if kind == "friendly" else self.enemy
|
||||
to_remove = [mid for mid in coll.keys() if mid not in set(received_ids)]
|
||||
for mid in to_remove:
|
||||
# remove from groups too
|
||||
await self.remove_member_assignment(mid)
|
||||
del coll[mid]
|
||||
|
||||
# single global state (5 groups)
|
||||
# Single global state
|
||||
STATE = ServerState(group_count=5)
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
# services/torn_api.py
|
||||
import aiohttp
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from config import TORN_API_KEY
|
||||
from .ffscouter import fetch_batch_stats
|
||||
from .server_state import STATE
|
||||
|
||||
FRIENDLY_MEMBERS_FILE = Path("data/friendly_members.json")
|
||||
FRIENDLY_STATUS_FILE = Path("data/friendly_status.json")
|
||||
ENEMY_MEMBERS_FILE = Path("data/enemy_members.json")
|
||||
ENEMY_STATUS_FILE = Path("data/enemy_status.json")
|
||||
|
||||
# -----------------------------
|
||||
# Tasks
|
||||
# -----------------------------
|
||||
friendly_status_task = None
|
||||
enemy_status_task = None
|
||||
|
||||
# Locks
|
||||
# Locks for safe async updates
|
||||
friendly_lock = asyncio.Lock()
|
||||
enemy_lock = asyncio.Lock()
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Static population (once)
|
||||
# Populate faction (memory only)
|
||||
# -----------------------------
|
||||
async def populate_faction(faction_id: int, members_file: Path, status_file: Path):
|
||||
"""Fetch members + FFScouter estimates once and save static info + initial status."""
|
||||
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}"
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -42,42 +40,37 @@ async def populate_faction(faction_id: int, members_file: Path, status_file: Pat
|
||||
if not member_ids:
|
||||
return False
|
||||
|
||||
# Fetch FFScouter data once
|
||||
# Fetch FFScouter estimates
|
||||
ff_data = await fetch_batch_stats(member_ids)
|
||||
|
||||
# Build static member list
|
||||
members = []
|
||||
status_data = {}
|
||||
for m in members_list:
|
||||
pid = m["id"]
|
||||
est = ff_data.get(str(pid), {}).get("bs_estimate_human", "?")
|
||||
member = {
|
||||
"id": pid,
|
||||
"name": m.get("name", "Unknown"),
|
||||
"level": m.get("level", 0),
|
||||
"estimate": est,
|
||||
}
|
||||
members.append(member)
|
||||
# initial status
|
||||
status_data[pid] = {"status": m.get("status", {}).get("state", "Unknown")}
|
||||
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)
|
||||
|
||||
# Save members
|
||||
members_file.parent.mkdir(exist_ok=True, parents=True)
|
||||
with open(members_file, "w", encoding="utf-8") as f:
|
||||
json.dump(members, f, indent=2)
|
||||
|
||||
# Save initial status
|
||||
with open(status_file, "w", encoding="utf-8") as f:
|
||||
json.dump(status_data, f, indent=2)
|
||||
# Remove missing members from STATE
|
||||
await STATE.remove_missing_members(received_ids, kind)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Status refresh loop
|
||||
# -----------------------------
|
||||
async def refresh_status_loop(faction_id: int, status_file: Path, lock: asyncio.Lock, interval: int):
|
||||
"""Refresh only status from Torn API periodically."""
|
||||
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}"
|
||||
@@ -90,43 +83,39 @@ async def refresh_status_loop(faction_id: int, status_file: Path, lock: asyncio.
|
||||
data = await resp.json()
|
||||
|
||||
members_list = data.get("members", [])
|
||||
status_data = {m["id"]: {"status": m.get("status", {}).get("state", "Unknown")} for m in members_list}
|
||||
|
||||
# Save status safely
|
||||
async with lock:
|
||||
with open(status_file, "w", encoding="utf-8") as f:
|
||||
json.dump(status_data, f, indent=2)
|
||||
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 {faction_id}: {e}")
|
||||
print(f"Error in status loop for {kind} faction {faction_id}: {e}")
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Helper functions for endpoints
|
||||
# Public API helpers
|
||||
# -----------------------------
|
||||
async def populate_friendly(faction_id: int):
|
||||
return await populate_faction(faction_id, FRIENDLY_MEMBERS_FILE, FRIENDLY_STATUS_FILE)
|
||||
|
||||
return await populate_faction(faction_id, "friendly")
|
||||
|
||||
async def populate_enemy(faction_id: int):
|
||||
return await populate_faction(faction_id, ENEMY_MEMBERS_FILE, ENEMY_STATUS_FILE)
|
||||
|
||||
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_STATUS_FILE, friendly_lock, interval)
|
||||
refresh_status_loop(faction_id, "friendly", friendly_lock, interval)
|
||||
)
|
||||
|
||||
|
||||
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_STATUS_FILE, enemy_lock, interval)
|
||||
refresh_status_loop(faction_id, "enemy", enemy_lock, interval)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user