forked from retoor/devplacepy
Add `last_seen` column to users table with index, implement `set_last_seen` and `get_online_users` database functions, expose presence config env vars (`PRESENCE_TIMEOUT_SECONDS`, `PRESENCE_ONLINE_LIMIT`, `PRESENCE_ONLINE_MARGIN_SECONDS`), include `last_seen` in follow list responses, and update profile docs to mention online indicator.
78 lines
2.2 KiB
Python
78 lines
2.2 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Optional
|
|
|
|
from devplacepy.config import (
|
|
PRESENCE_ONLINE_LIMIT,
|
|
PRESENCE_ONLINE_MARGIN_SECONDS,
|
|
PRESENCE_TIMEOUT_SECONDS,
|
|
PRESENCE_WRITE_SECONDS,
|
|
)
|
|
from devplacepy.database import get_online_users, set_last_seen
|
|
|
|
_last_write: dict[str, float] = {}
|
|
|
|
|
|
def touch(user_uid: str) -> None:
|
|
if not user_uid:
|
|
return
|
|
now = time.monotonic()
|
|
if now - _last_write.get(user_uid, 0.0) < PRESENCE_WRITE_SECONDS:
|
|
return
|
|
_last_write[user_uid] = now
|
|
set_last_seen(user_uid, datetime.now(timezone.utc).isoformat())
|
|
|
|
|
|
def seconds_since(last_seen: Optional[str]) -> Optional[float]:
|
|
if not last_seen:
|
|
return None
|
|
try:
|
|
seen = datetime.fromisoformat(last_seen)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
if seen.tzinfo is None:
|
|
seen = seen.replace(tzinfo=timezone.utc)
|
|
return (datetime.now(timezone.utc) - seen).total_seconds()
|
|
|
|
|
|
def is_online(user: Optional[dict]) -> bool:
|
|
if not user:
|
|
return False
|
|
elapsed = seconds_since(user.get("last_seen"))
|
|
return elapsed is not None and elapsed < PRESENCE_TIMEOUT_SECONDS
|
|
|
|
|
|
def stays_online(elapsed: Optional[float], was_online: bool) -> bool:
|
|
if elapsed is None:
|
|
return False
|
|
grace = PRESENCE_TIMEOUT_SECONDS + PRESENCE_ONLINE_MARGIN_SECONDS
|
|
return elapsed < (grace if was_online else PRESENCE_TIMEOUT_SECONDS)
|
|
|
|
|
|
def _cutoff_iso(seconds: int) -> str:
|
|
return (datetime.now(timezone.utc) - timedelta(seconds=seconds)).isoformat()
|
|
|
|
|
|
def online_cutoff_iso() -> str:
|
|
return _cutoff_iso(PRESENCE_TIMEOUT_SECONDS)
|
|
|
|
|
|
def sort_by_username(rows: list) -> list:
|
|
return sorted(rows, key=lambda row: (row.get("username") or "").lower())
|
|
|
|
|
|
def online_users(limit: int = PRESENCE_ONLINE_LIMIT) -> list:
|
|
return sort_by_username(get_online_users(online_cutoff_iso(), limit))
|
|
|
|
|
|
def online_candidates(limit: int = PRESENCE_ONLINE_LIMIT) -> list:
|
|
return sort_by_username(
|
|
get_online_users(
|
|
_cutoff_iso(PRESENCE_TIMEOUT_SECONDS + PRESENCE_ONLINE_MARGIN_SECONDS), limit
|
|
)
|
|
)
|