|
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Optional
|
|
|
|
from devplacepy.config import PRESENCE_ONLINE_LIMIT
|
|
from devplacepy.database import db, get_users_by_uids
|
|
from devplacepy.services import presence
|
|
from devplacepy.services.base import BaseService
|
|
from devplacepy.services.pubsub import publish as pubsub_publish
|
|
from devplacepy.services.pubsub.hub import pubsub
|
|
|
|
ROSTER_TOPIC = "public.presence.roster"
|
|
TOPIC_PATTERN = re.compile(r"^public\.presence\.(?P<uid>[A-Za-z0-9_-]{1,128})$")
|
|
|
|
|
|
class PresenceRelayService(BaseService):
|
|
title = "Presence relay"
|
|
description = (
|
|
"Single source of truth for LIVE online status. Each tick it recomputes one online "
|
|
"set with hysteresis - a user is online at the timeout window but only drops after an "
|
|
"extra grace margin - and drives BOTH the per-user avatar dots (public.presence.{uid}) "
|
|
"and the shared feed roster (public.presence.roster) from that one set, so they never "
|
|
"disagree. It publishes ONLY on change (a real online<->offline transition or a first-seen "
|
|
"subscriber), reads due users in one batched query per tick, and does nothing when idle. "
|
|
"Runs on the service lock owner where every subscriber converges."
|
|
)
|
|
default_enabled = True
|
|
|
|
def __init__(self):
|
|
super().__init__(name="presence_relay", interval_seconds=2)
|
|
self._online: set[str] = set()
|
|
self._published: dict[str, bool] = {}
|
|
self._roster_uids: Optional[frozenset] = None
|
|
|
|
async def run_once(self) -> None:
|
|
if "users" not in db.tables:
|
|
return
|
|
dot_pairs: list[tuple[str, str]] = []
|
|
roster_subscribed = False
|
|
for entry in pubsub.topics():
|
|
topic = entry["topic"]
|
|
if not entry["subscribers"] or "*" in topic:
|
|
continue
|
|
if topic == ROSTER_TOPIC:
|
|
roster_subscribed = True
|
|
continue
|
|
match = TOPIC_PATTERN.match(topic)
|
|
if match is not None:
|
|
dot_pairs.append((topic, match.group("uid")))
|
|
|
|
roster_rows = presence.online_candidates() if roster_subscribed else []
|
|
rows: dict[str, dict] = {}
|
|
dot_uids = {uid for _, uid in dot_pairs}
|
|
if dot_uids:
|
|
rows.update(get_users_by_uids(list(dot_uids)))
|
|
for row in roster_rows:
|
|
rows[row["uid"]] = row
|
|
|
|
prev = self._online
|
|
online: set[str] = set()
|
|
for uid, row in rows.items():
|
|
elapsed = presence.seconds_since(row.get("last_seen"))
|
|
if presence.stays_online(elapsed, uid in prev):
|
|
online.add(uid)
|
|
self._online = online
|
|
|
|
await self._publish_dots(dot_pairs, online, rows)
|
|
await self._publish_roster(roster_subscribed, roster_rows, online)
|
|
|
|
async def _publish_dots(self, dot_pairs, online, rows) -> None:
|
|
active = {topic for topic, _ in dot_pairs}
|
|
self._published = {t: v for t, v in self._published.items() if t in active}
|
|
published = 0
|
|
for topic, uid in dot_pairs:
|
|
is_on = uid in online
|
|
if self._published.get(topic) == is_on:
|
|
continue
|
|
row = rows.get(uid) or {}
|
|
published += await pubsub_publish(
|
|
topic, {"online": is_on, "last_seen": row.get("last_seen")}
|
|
)
|
|
self._published[topic] = is_on
|
|
if published:
|
|
self.log(f"pushed {published} presence change(s)")
|
|
|
|
async def _publish_roster(self, subscribed, roster_rows, online) -> None:
|
|
if not subscribed:
|
|
self._roster_uids = None
|
|
return
|
|
users = [row for row in roster_rows if row["uid"] in online][:PRESENCE_ONLINE_LIMIT]
|
|
uids = frozenset(row["uid"] for row in users)
|
|
if uids == self._roster_uids:
|
|
return
|
|
self._roster_uids = uids
|
|
await pubsub_publish(
|
|
ROSTER_TOPIC,
|
|
{
|
|
"count": len(users),
|
|
"users": [
|
|
{
|
|
"uid": row["uid"],
|
|
"username": row["username"],
|
|
"avatar_seed": row.get("avatar_seed") or row["username"],
|
|
}
|
|
for row in users
|
|
],
|
|
},
|
|
)
|
|
self.log(f"roster changed: {len(users)} online")
|
|
|
|
def collect_metrics(self) -> dict:
|
|
return {
|
|
"online": len(self._online),
|
|
"tracked_dots": len(self._published),
|
|
}
|