| """ |
| 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: |
| |
| filter_key = {"user_id": str(user_id)} |
| display_name = username |
| elif anonymous_id: |
| |
| filter_key = {"anonymous_id": anonymous_id} |
| display_name = "Guest" |
| elif ip_address: |
| |
| 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 "" |
| |
| 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: |
| |
| 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") |
| |
| 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}}) |
|
|
|
|
|
|