anicove / api /models /user_activity.py
mwask's picture
tracking and nav
79d1711
Raw
History Blame Contribute Delete
4.33 kB
"""
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}})