# retoor <retoor@molodetz.nl>
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}