Files
web/backend/services/main/presence_service.py
T

64 lines
2.2 KiB
Python

"""In-memory user presence derived from WebSocket connections only."""
from __future__ import annotations
from datetime import datetime
from fastapi import WebSocket
class PresenceService:
def __init__(self) -> None:
self._connections: dict[int, set[WebSocket]] = {}
self._last_seen: dict[int, datetime] = {}
def register_connection(self, user_id: int, websocket: WebSocket) -> bool:
"""Track a live connection. Returns True if the user became online."""
connections = self._connections.setdefault(user_id, set())
was_online = bool(connections)
connections.add(websocket)
return not was_online
def unregister_connection(self, user_id: int, websocket: WebSocket) -> tuple[bool, datetime | None]:
"""Remove a connection. Returns (became_offline, last_seen) when the last conn drops."""
connections = self._connections.get(user_id)
if not connections:
return False, self._last_seen.get(user_id)
connections.discard(websocket)
if connections:
return False, None
del self._connections[user_id]
last_seen = datetime.now()
self._last_seen[user_id] = last_seen
return True, last_seen
def touch(self, user_id: int) -> None:
"""Refresh activity timestamp while online."""
if self.is_online(user_id):
self._last_seen[user_id] = datetime.now()
def is_online(self, user_id: int) -> bool:
connections = self._connections.get(user_id)
return bool(connections)
def get_last_seen(self, user_id: int) -> datetime | None:
if self.is_online(user_id):
return self._last_seen.get(user_id) or datetime.now()
return self._last_seen.get(user_id)
def get_presence(self, user_id: int) -> tuple[bool, datetime | None]:
online = self.is_online(user_id)
if online:
return True, self.get_last_seen(user_id)
last_seen = self._last_seen.get(user_id)
return False, last_seen
def remove_user(self, user_id: int) -> None:
"""Drop all presence state for a deleted user."""
self._connections.pop(user_id, None)
self._last_seen.pop(user_id, None)
presence_service = PresenceService()