Add WS message filtering, idle grace period, webhook env var
- Browser WS clients can now send {"type": "subscribe", "message_types": [...]}
to only receive specific message types. Default is all (no change for browsers).
- Discord bot subscribes to only "rare" and "chat" — eliminates 82GB+ of
unnecessary telemetry/vitals/inventory traffic.
- Idle detection now has a 5-minute grace period before firing Discord alerts,
preventing false positives on brief idle states.
- Added DISCORD_ACLOG_WEBHOOK env var to docker-compose.yml for death/idle alerts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3885b408c9
commit
de2cc3a0e3
3 changed files with 62 additions and 20 deletions
|
|
@ -293,7 +293,15 @@ class DiscordRareMonitor:
|
|||
|
||||
# Send connection established message
|
||||
await self.post_status_to_aclog("🔗 WebSocket connection established")
|
||||
|
||||
|
||||
# Subscribe only to message types we care about (rare + chat)
|
||||
# This dramatically reduces network traffic vs receiving the full firehose
|
||||
await websocket.send(json.dumps({
|
||||
"type": "subscribe",
|
||||
"message_types": ["rare", "chat"]
|
||||
}))
|
||||
logger.info("📋 Subscribed to message types: rare, chat")
|
||||
|
||||
# Simple message processing with comprehensive error handling
|
||||
try:
|
||||
message_count = 0
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ services:
|
|||
SECRET_KEY: "${SECRET_KEY}"
|
||||
LOG_LEVEL: "DEBUG"
|
||||
INVENTORY_SERVICE_URL: "http://inventory-service:8000"
|
||||
DISCORD_ACLOG_WEBHOOK: "${DISCORD_ACLOG_WEBHOOK:-}"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
|
|
|
|||
71
main.py
71
main.py
|
|
@ -2476,8 +2476,11 @@ async def list_equipment_sets_proxy():
|
|||
|
||||
# -------------------- WebSocket endpoints -----------------------
|
||||
## WebSocket connection tracking
|
||||
# Set of browser WebSocket clients subscribed to live updates
|
||||
browser_conns: set[WebSocket] = set()
|
||||
# Browser WebSocket clients subscribed to live updates.
|
||||
# Maps ws → None (all messages) or a set of message types to receive.
|
||||
# Clients can send {"type": "subscribe", "message_types": ["rare", "chat"]}
|
||||
# to filter. Without subscribing, all message types are forwarded (browser default).
|
||||
browser_conns: Dict[WebSocket, set[str] | None] = {}
|
||||
# Mapping of plugin clients by character_name to their WebSocket for command forwarding
|
||||
plugin_conns: Dict[str, WebSocket] = {}
|
||||
|
||||
|
|
@ -2496,6 +2499,8 @@ live_combat_stats: Dict[str, dict] = {}
|
|||
# --- Idle detection + Discord alerts ----------
|
||||
DISCORD_ACLOG_WEBHOOK = os.getenv("DISCORD_ACLOG_WEBHOOK", "")
|
||||
_idle_alerted: set[str] = set() # chars we've already alerted for this idle period
|
||||
_idle_since: Dict[str, float] = {} # char → timestamp when first detected idle
|
||||
_IDLE_GRACE_SECONDS = 300 # 5 minutes before alerting
|
||||
_death_alerted: Dict[str, float] = {} # char → last death alert timestamp
|
||||
|
||||
|
||||
|
|
@ -2531,16 +2536,24 @@ async def _idle_detection_loop():
|
|||
vt_state in ("combat", "hunt") and kph == 0
|
||||
)
|
||||
|
||||
if is_idle and name not in _idle_alerted:
|
||||
_idle_alerted.add(name)
|
||||
state_text = p.get("vt_state") or "idle"
|
||||
await _send_discord_aclog(
|
||||
f"⚠️ **{name}** appears idle (state: {state_text}, KPH: {kph})"
|
||||
)
|
||||
logger.info(f"IDLE_ALERT: {name} state={state_text} kph={kph}")
|
||||
elif not is_idle and name in _idle_alerted:
|
||||
# Character recovered — clear alert
|
||||
now = time.time()
|
||||
if is_idle:
|
||||
if name not in _idle_since:
|
||||
# First time seeing idle — start grace timer
|
||||
_idle_since[name] = now
|
||||
elif name not in _idle_alerted and (now - _idle_since[name]) >= _IDLE_GRACE_SECONDS:
|
||||
# Grace period elapsed — fire alert
|
||||
_idle_alerted.add(name)
|
||||
idle_mins = int((now - _idle_since[name]) / 60)
|
||||
state_text = p.get("vt_state") or "idle"
|
||||
await _send_discord_aclog(
|
||||
f"⚠️ **{name}** appears idle for {idle_mins}min (state: {state_text}, KPH: {kph})"
|
||||
)
|
||||
logger.info(f"IDLE_ALERT: {name} state={state_text} kph={kph} idle_mins={idle_mins}")
|
||||
elif not is_idle:
|
||||
# Character recovered — clear alert and grace timer
|
||||
_idle_alerted.discard(name)
|
||||
_idle_since.pop(name, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Idle detection error: {e}")
|
||||
|
|
@ -2718,14 +2731,23 @@ async def _send_to_browser(ws: WebSocket, data: dict) -> WebSocket | None:
|
|||
|
||||
|
||||
async def _do_broadcast(data: dict):
|
||||
"""Send data to all browser clients concurrently. Runs as a background task."""
|
||||
clients = list(browser_conns)
|
||||
if not clients:
|
||||
"""Send data to all browser clients concurrently. Runs as a background task.
|
||||
|
||||
Respects per-client message type filters: clients that sent a ``subscribe``
|
||||
message only receive the types they asked for.
|
||||
"""
|
||||
msg_type = data.get("type")
|
||||
# Build list of clients that should receive this message
|
||||
targets = []
|
||||
for ws, allowed_types in list(browser_conns.items()):
|
||||
if allowed_types is None or msg_type in allowed_types:
|
||||
targets.append(ws)
|
||||
if not targets:
|
||||
return
|
||||
results = await asyncio.gather(*(_send_to_browser(ws, data) for ws in clients))
|
||||
results = await asyncio.gather(*(_send_to_browser(ws, data) for ws in targets))
|
||||
for ws in results:
|
||||
if ws is not None:
|
||||
browser_conns.discard(ws)
|
||||
browser_conns.pop(ws, None)
|
||||
|
||||
|
||||
async def _broadcast_to_browser_clients(snapshot: dict):
|
||||
|
|
@ -3626,7 +3648,7 @@ async def cleanup_stale_connections():
|
|||
stale_browsers.append(ws)
|
||||
|
||||
for ws in stale_browsers:
|
||||
browser_conns.discard(ws)
|
||||
browser_conns.pop(ws, None)
|
||||
|
||||
if stale_browsers:
|
||||
logger.info(f"Cleaned up {len(stale_browsers)} stale browser connections")
|
||||
|
|
@ -3657,7 +3679,7 @@ async def ws_live_updates(websocket: WebSocket):
|
|||
global _browser_connections
|
||||
# Add new browser client to the set
|
||||
await websocket.accept()
|
||||
browser_conns.add(websocket)
|
||||
browser_conns[websocket] = None # None = receive all message types
|
||||
logger.info(f"Browser WebSocket connected: {websocket.client}")
|
||||
|
||||
# Track browser connection
|
||||
|
|
@ -3673,6 +3695,17 @@ async def ws_live_updates(websocket: WebSocket):
|
|||
except WebSocketDisconnect:
|
||||
logger.info(f"Browser WebSocket disconnected: {websocket.client}")
|
||||
break
|
||||
# Handle subscribe requests — clients can filter which message types they receive
|
||||
if data.get("type") == "subscribe":
|
||||
types = data.get("message_types")
|
||||
if isinstance(types, list) and types:
|
||||
browser_conns[websocket] = set(types)
|
||||
logger.info(f"Browser {websocket.client} subscribed to: {types}")
|
||||
else:
|
||||
browser_conns[websocket] = None # reset to all
|
||||
logger.info(f"Browser {websocket.client} reset to all message types")
|
||||
continue
|
||||
|
||||
# Handle dungeon map requests from browser
|
||||
if data.get("type") == "request_dungeon_map":
|
||||
landblock = data.get("landblock")
|
||||
|
|
@ -3726,7 +3759,7 @@ async def ws_live_updates(websocket: WebSocket):
|
|||
# Track browser disconnection
|
||||
_browser_connections = max(0, _browser_connections - 1)
|
||||
|
||||
browser_conns.discard(websocket)
|
||||
browser_conns.pop(websocket, None)
|
||||
logger.debug(
|
||||
f"Removed browser WebSocket from connection pool: {websocket.client}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue