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
|
# Send connection established message
|
||||||
await self.post_status_to_aclog("🔗 WebSocket connection established")
|
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
|
# Simple message processing with comprehensive error handling
|
||||||
try:
|
try:
|
||||||
message_count = 0
|
message_count = 0
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ services:
|
||||||
SECRET_KEY: "${SECRET_KEY}"
|
SECRET_KEY: "${SECRET_KEY}"
|
||||||
LOG_LEVEL: "DEBUG"
|
LOG_LEVEL: "DEBUG"
|
||||||
INVENTORY_SERVICE_URL: "http://inventory-service:8000"
|
INVENTORY_SERVICE_URL: "http://inventory-service:8000"
|
||||||
|
DISCORD_ACLOG_WEBHOOK: "${DISCORD_ACLOG_WEBHOOK:-}"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
|
|
|
||||||
71
main.py
71
main.py
|
|
@ -2476,8 +2476,11 @@ async def list_equipment_sets_proxy():
|
||||||
|
|
||||||
# -------------------- WebSocket endpoints -----------------------
|
# -------------------- WebSocket endpoints -----------------------
|
||||||
## WebSocket connection tracking
|
## WebSocket connection tracking
|
||||||
# Set of browser WebSocket clients subscribed to live updates
|
# Browser WebSocket clients subscribed to live updates.
|
||||||
browser_conns: set[WebSocket] = set()
|
# 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
|
# Mapping of plugin clients by character_name to their WebSocket for command forwarding
|
||||||
plugin_conns: Dict[str, WebSocket] = {}
|
plugin_conns: Dict[str, WebSocket] = {}
|
||||||
|
|
||||||
|
|
@ -2496,6 +2499,8 @@ live_combat_stats: Dict[str, dict] = {}
|
||||||
# --- Idle detection + Discord alerts ----------
|
# --- Idle detection + Discord alerts ----------
|
||||||
DISCORD_ACLOG_WEBHOOK = os.getenv("DISCORD_ACLOG_WEBHOOK", "")
|
DISCORD_ACLOG_WEBHOOK = os.getenv("DISCORD_ACLOG_WEBHOOK", "")
|
||||||
_idle_alerted: set[str] = set() # chars we've already alerted for this idle period
|
_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
|
_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
|
vt_state in ("combat", "hunt") and kph == 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_idle and name not in _idle_alerted:
|
now = time.time()
|
||||||
_idle_alerted.add(name)
|
if is_idle:
|
||||||
state_text = p.get("vt_state") or "idle"
|
if name not in _idle_since:
|
||||||
await _send_discord_aclog(
|
# First time seeing idle — start grace timer
|
||||||
f"⚠️ **{name}** appears idle (state: {state_text}, KPH: {kph})"
|
_idle_since[name] = now
|
||||||
)
|
elif name not in _idle_alerted and (now - _idle_since[name]) >= _IDLE_GRACE_SECONDS:
|
||||||
logger.info(f"IDLE_ALERT: {name} state={state_text} kph={kph}")
|
# Grace period elapsed — fire alert
|
||||||
elif not is_idle and name in _idle_alerted:
|
_idle_alerted.add(name)
|
||||||
# Character recovered — clear alert
|
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_alerted.discard(name)
|
||||||
|
_idle_since.pop(name, None)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Idle detection error: {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):
|
async def _do_broadcast(data: dict):
|
||||||
"""Send data to all browser clients concurrently. Runs as a background task."""
|
"""Send data to all browser clients concurrently. Runs as a background task.
|
||||||
clients = list(browser_conns)
|
|
||||||
if not clients:
|
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
|
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:
|
for ws in results:
|
||||||
if ws is not None:
|
if ws is not None:
|
||||||
browser_conns.discard(ws)
|
browser_conns.pop(ws, None)
|
||||||
|
|
||||||
|
|
||||||
async def _broadcast_to_browser_clients(snapshot: dict):
|
async def _broadcast_to_browser_clients(snapshot: dict):
|
||||||
|
|
@ -3626,7 +3648,7 @@ async def cleanup_stale_connections():
|
||||||
stale_browsers.append(ws)
|
stale_browsers.append(ws)
|
||||||
|
|
||||||
for ws in stale_browsers:
|
for ws in stale_browsers:
|
||||||
browser_conns.discard(ws)
|
browser_conns.pop(ws, None)
|
||||||
|
|
||||||
if stale_browsers:
|
if stale_browsers:
|
||||||
logger.info(f"Cleaned up {len(stale_browsers)} stale browser connections")
|
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
|
global _browser_connections
|
||||||
# Add new browser client to the set
|
# Add new browser client to the set
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
browser_conns.add(websocket)
|
browser_conns[websocket] = None # None = receive all message types
|
||||||
logger.info(f"Browser WebSocket connected: {websocket.client}")
|
logger.info(f"Browser WebSocket connected: {websocket.client}")
|
||||||
|
|
||||||
# Track browser connection
|
# Track browser connection
|
||||||
|
|
@ -3673,6 +3695,17 @@ async def ws_live_updates(websocket: WebSocket):
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
logger.info(f"Browser WebSocket disconnected: {websocket.client}")
|
logger.info(f"Browser WebSocket disconnected: {websocket.client}")
|
||||||
break
|
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
|
# Handle dungeon map requests from browser
|
||||||
if data.get("type") == "request_dungeon_map":
|
if data.get("type") == "request_dungeon_map":
|
||||||
landblock = data.get("landblock")
|
landblock = data.get("landblock")
|
||||||
|
|
@ -3726,7 +3759,7 @@ async def ws_live_updates(websocket: WebSocket):
|
||||||
# Track browser disconnection
|
# Track browser disconnection
|
||||||
_browser_connections = max(0, _browser_connections - 1)
|
_browser_connections = max(0, _browser_connections - 1)
|
||||||
|
|
||||||
browser_conns.discard(websocket)
|
browser_conns.pop(websocket, None)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Removed browser WebSocket from connection pool: {websocket.client}"
|
f"Removed browser WebSocket from connection pool: {websocket.client}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue