""" User Activity model for real-time presence tracking. Tracks who is currently on the site and what page they're viewing. """ from datetime import datetime, timezone, timedelta from pymongo import DESCENDING from ..core.db_connector import db ACTIVITY_COLLECTION = "user_activity" activity_collection = db[ACTIVITY_COLLECTION] def _ensure_indexes(): try: activity_collection.create_index([("last_seen", DESCENDING)]) activity_collection.create_index([("user_id", DESCENDING)]) activity_collection.create_index("last_seen", expireAfterSeconds=300) except Exception: pass _ensure_indexes() def ping_user(user_id, username, avatar, page_url, page_title="", anonymous_id=None, ip_address=None): """Record or update a user's presence on the site. For authenticated users: - keyed by user_id (MongoDB ObjectId str) - shows their username & avatar For anonymous visitors: - keyed by anonymous_id (client-generated UUID stored in localStorage) - deduplicated by ip_address if anonymous_id not provided - shows "Guest" in the activity feed """ now = datetime.now(timezone.utc) if user_id and username: # Authenticated user filter_key = {"user_id": str(user_id)} display_name = username elif anonymous_id: # Anonymous user with client ID filter_key = {"anonymous_id": anonymous_id} display_name = "Guest" elif ip_address: # Anonymous user identified by IP (fallback) filter_key = {"ip_address": ip_address, "user_id": {"$exists": False}} display_name = "Guest" else: return False doc = { "last_seen": now, "page_url": page_url, "page_title": page_title, } if user_id and username: doc["user_id"] = str(user_id) doc["username"] = username doc["avatar"] = avatar or "" # Remove any anonymous fields if user later logs in activity_collection.update_many( {"ip_address": ip_address, "user_id": {"$exists": False}}, {"$set": {"user_id": str(user_id), "username": username, "avatar": avatar or ""}} ) else: doc["is_anonymous"] = True doc["username"] = "Guest" doc["avatar"] = "" if anonymous_id: doc["anonymous_id"] = anonymous_id if ip_address: doc["ip_address"] = ip_address activity_collection.update_one( filter_key, {"$set": doc, "$setOnInsert": {"first_seen": now}}, upsert=True ) return True def get_active_users(minutes=5): """Get users active within the last N minutes. Returns: list of dicts with user info including is_anonymous flag. """ cutoff = datetime.now(timezone.utc) - timedelta(minutes=minutes) cursor = activity_collection.find( {"last_seen": {"$gte": cutoff}} ).sort("last_seen", DESCENDING) seen_ids = set() users = [] for doc in cursor: # Deduplicate: prefer authenticated version over anonymous uid = doc.get("user_id") or doc.get("anonymous_id") or doc.get("ip_address") or "" if uid in seen_ids: continue seen_ids.add(uid) is_anon = doc.get("is_anonymous", False) or not doc.get("user_id") display_name = doc.get("username", "Guest") # Differentiate anonymous guests with a short ID suffix if is_anon and doc.get("anonymous_id"): short_id = str(doc["anonymous_id"])[-5:] display_name = f"Guest_{short_id}" users.append({ "user_id": doc.get("user_id", ""), "username": display_name, "avatar": doc.get("avatar", ""), "page_url": doc.get("page_url", ""), "page_title": doc.get("page_title", ""), "last_seen": doc["last_seen"].isoformat() if doc.get("last_seen") else None, "first_seen": doc["first_seen"].isoformat() if doc.get("first_seen") else None, "is_anonymous": is_anon, }) return users def get_active_user_count(minutes=5): """Count users active within the last N minutes.""" cutoff = datetime.now(timezone.utc) - timedelta(minutes=minutes) return activity_collection.count_documents({"last_seen": {"$gte": cutoff}})