# retoor from .core import TTLCache, datetime, db, timedelta, timezone def record_activity(user_uid: str, action: str) -> int: if not user_uid or not action or "user_activity" not in db.tables: return 0 now = datetime.now(timezone.utc).isoformat() with db: db.query( "INSERT INTO user_activity (user_uid, action, count, first_at, last_at) " "VALUES (:u, :a, 1, :now, :now) " "ON CONFLICT(user_uid, action) DO UPDATE SET " "count = count + 1, last_at = excluded.last_at", u=user_uid, a=action, now=now, ) rows = list( db.query( "SELECT count FROM user_activity WHERE user_uid = :u AND action = :a", u=user_uid, a=action, ) ) return int(rows[0]["count"]) if rows else 0 def record_unique_activity(user_uid: str, action: str, target: str) -> int | None: if not user_uid or not action or "user_activity_seen" not in db.tables: return None now = datetime.now(timezone.utc).isoformat() with db: db.query( "INSERT OR IGNORE INTO user_activity_seen " "(user_uid, action, target, created_at) VALUES (:u, :a, :t, :now)", u=user_uid, a=action, t=str(target), now=now, ) changed = list(db.query("SELECT changes() AS c")) if not changed or not changed[0]["c"]: return None rows = list( db.query( "SELECT COUNT(*) AS c FROM user_activity_seen " "WHERE user_uid = :u AND action = :a", u=user_uid, a=action, ) ) return int(rows[0]["c"]) if rows else 0 def get_user_activity(user_uid: str) -> dict: if not user_uid or "user_activity" not in db.tables: return {} rows = db.query( "SELECT action, count FROM user_activity WHERE user_uid = :u", u=user_uid, ) return {row["action"]: int(row["count"]) for row in rows} _activity_cache = TTLCache(ttl=300, max_size=1000) _ACTIVITY_TABLES = ("posts", "comments", "gists", "projects") def get_activity_calendar(user_uid: str) -> dict: cached = _activity_cache.get(user_uid) if cached is not None: return cached sources = [table for table in _ACTIVITY_TABLES if table in db.tables] calendar: dict[str, int] = {} if sources: cutoff = (datetime.now(timezone.utc) - timedelta(days=364)).date().isoformat() union = " UNION ALL ".join( f"SELECT created_at FROM {table} WHERE user_uid = :u AND deleted_at IS NULL" for table in sources ) rows = db.query( f"SELECT date(created_at) AS day, COUNT(*) AS c FROM ({union}) WHERE date(created_at) >= :cutoff GROUP BY day", u=user_uid, cutoff=cutoff, ) for row in rows: if row["day"]: calendar[row["day"]] = row["c"] _activity_cache.set(user_uid, calendar) return calendar def _activity_level(count: int) -> int: if count <= 0: return 0 if count == 1: return 1 if count <= 3: return 2 if count <= 6: return 3 return 4 def get_first_activity_date(user_uid: str): sources = [table for table in _ACTIVITY_TABLES if table in db.tables] if not sources: return None union = " UNION ALL ".join( f"SELECT MIN(created_at) AS m FROM {table} WHERE user_uid = :u AND deleted_at IS NULL" for table in sources ) for row in db.query(f"SELECT MIN(m) AS first FROM ({union})", u=user_uid): if row["first"]: return datetime.fromisoformat(row["first"]).date() return None HEATMAP_WEEKS = 53 def get_activity_heatmap(user_uid: str) -> list: calendar = get_activity_calendar(user_uid) today = datetime.now(timezone.utc).date() week_start = today - timedelta(days=today.weekday()) start = week_start - timedelta(weeks=HEATMAP_WEEKS - 1) first = get_first_activity_date(user_uid) if first: first_week = first - timedelta(days=first.weekday()) if first_week > start: start = first_week weeks = [] for w in range(HEATMAP_WEEKS): week = [] for d in range(7): day = start + timedelta(days=w * 7 + d) iso = day.isoformat() count = calendar.get(iso, 0) week.append({"date": iso, "count": count, "level": _activity_level(count)}) weeks.append(week) return weeks def get_activity_months(weeks: list) -> list: if not weeks: return [] last = len(weeks) - 1 labels = [] for i in range(6): column = round(i * last / 5) iso = weeks[column][0]["date"] labels.append(datetime.fromisoformat(iso).strftime("%b")) return labels def get_streaks(user_uid: str) -> dict: calendar = get_activity_calendar(user_uid) if not calendar: return {"current": 0, "longest": 0} dates = sorted(datetime.fromisoformat(day).date() for day in calendar) date_set = set(dates) longest = 1 run = 1 for index in range(1, len(dates)): if (dates[index] - dates[index - 1]).days == 1: run += 1 else: run = 1 longest = max(longest, run) today = datetime.now(timezone.utc).date() cursor = today if today not in date_set and (today - timedelta(days=1)) in date_set: cursor = today - timedelta(days=1) current = 0 while cursor in date_set: current += 1 cursor = cursor - timedelta(days=1) return {"current": current, "longest": longest}