Skip to content

GLOBAL PRESENCE AGGREGATOR — DAARION.city

Version: 1.0.0 Location: docs/realtime/GLOBAL_PRESENCE_AGGREGATOR_SPEC.md


0. PURPOSE

Зробити єдиний центр правди про присутність (presence) та активність у місті:

  • збирати Matrix presence/typing/room-activity на сервері,
  • агрегувати їх на рівні кімнат (city_room),
  • публікувати у NATS як події,
  • транслювати у фронтенд через WebSocket з city-service.

Результат: DAARION має "живе місто":

  • список кімнат /city показує:
  • скільки людей онлайн,
  • активність у реальному часі,
  • майбутня City Map (2D/2.5D) живиться цими даними.

1. ARCHITECTURE OVERVIEW

┌─────────────────────────────────────────────────────────────────────────┐
                           DAARION PRESENCE SYSTEM                        
├─────────────────────────────────────────────────────────────────────────┤
                                                                         
  ┌─────────────┐     ┌──────────────────────┐     ┌─────────────────┐   
     Matrix    │────▶│ matrix-presence-     │────▶│      NATS          
     Synapse         aggregator                   JetStream        
                     (sync loop)                                   
  └─────────────┘     └──────────────────────┘     └────────┬────────┘   
                                                                        
                                                                        
  ┌─────────────┐     ┌──────────────────────┐     ┌─────────────────┐   
     Browser   │◀────│   city-service       │◀────│   NATS Sub         
     (WS)              /ws/city/presence                           
  └─────────────┘     └──────────────────────┘     └─────────────────┘   
                                                                         
└─────────────────────────────────────────────────────────────────────────┘

Компоненти

  1. matrix-presence-aggregator (новий сервіс)
  2. читає Matrix sync (presence, typing, room activity),
  3. тримає у пам'яті поточний стан присутності,
  4. публікує агреговані події в NATS.

  5. NATS JetStream

  6. канал для presence/events:

    • city.presence.room.*
    • city.presence.user.*
  7. city-service (розширення)

  8. підписується на NATS події,
  9. тримає WebSocket з'єднання з фронтендом,
  10. пушить presence/room-activity у браузер.

  11. web (Next.js UI)

  12. сторінка /city:
    • показує N online по кожній кімнаті,
    • highlight "active" кімнати.

2. MATRIX SIDE — ЗВІДКИ БРАТИ ПОДІЇ

2.1. Окремий Matrix-юзер для агрегації

Спец-акаунт: - @presence_daemon:daarion.space - права: - читати presence/typing у всіх city_* кімнатах, - бути учасником цих кімнат.

2.2. Sync-loop на сервері

Сервіс matrix-presence-aggregator:

  • використовує /sync Matrix (як клієнт),
  • фільтр:
{
  "presence": {
    "types": ["m.presence"]
  },
  "room": {
    "timeline": { "limit": 0 },
    "state": { "limit": 0 },
    "ephemeral": {
      "types": ["m.typing", "m.receipt"]
    }
  }
}
  • робить long-polling з since + timeout,
  • парсить:
  • presence.eventsm.presence,
  • rooms.join[roomId].ephemeral.eventsm.typing, m.receipt.

3. DATA MODEL (IN-MEMORY AGGREGATOR)

3.1. Room presence state

from dataclasses import dataclass
from typing import Dict, List, Set, Optional
from datetime import datetime

@dataclass
class UserPresence:
    user_id: str               # "@user:domain"
    status: str                # "online" | "offline" | "unavailable"
    last_active_ts: float      # timestamp

@dataclass
class RoomPresence:
    room_id: str               # "!....:daarion.space"
    alias: Optional[str]       # "#city_energy:daarion.space"
    city_room_slug: Optional[str]  # "energy"
    online_count: int
    typing_user_ids: List[str]
    last_event_ts: float

class PresenceState:
    users: Dict[str, UserPresence]
    rooms: Dict[str, RoomPresence]
    room_members: Dict[str, Set[str]]  # room_id -> set of user_ids

3.2. Мапінг Room → City Room

matrix-presence-aggregator має знати matrix_room_idcity_room.slug.

Pull-mode (MVP): - при старті сервісу: - GET /internal/city/rooms - зчитати всі matrix_room_id / matrix_room_alias / slug, - зібрати мапу roomId → slug. - періодично (кожні 5 хвилин) оновлювати.


4. NATS EVENTS

4.1. Room-level presence

Subject:

city.presence.room.<slug>

Event payload:

{
  "type": "room.presence",
  "room_slug": "energy",
  "matrix_room_id": "!gykdLyazhkcSZGHmbG:daarion.space",
  "matrix_room_alias": "#city_energy:daarion.space",
  "online_count": 5,
  "typing_count": 1,
  "typing_users": ["@user1:daarion.space"],
  "last_event_ts": 1732610000000
}

4.2. User-level presence (опційний)

Subject:

city.presence.user.<localpart>

Payload:

{
  "type": "user.presence",
  "matrix_user_id": "@user1:daarion.space",
  "status": "online",
  "last_active_ts": 1732610000000
}

5. EVENT GENERATION LOGIC

5.1. Обробка m.presence

При кожному m.presence: - оновити PresenceState.users[userId], - для всіх кімнат, де є цей юзер — перерахувати onlineCount, - якщо onlineCount змінився — публікувати нову подію.

5.2. Обробка m.typing

При m.typing: - content.user_ids → список typing у кімнаті. - Зберегти в RoomPresence.typing_user_ids. - Згенерувати івент city.presence.room.<slug>.

5.3. Throttling

  • подію публікувати тільки якщо onlineCount змінився,
  • або не частіше ніж раз на 3 секунди на кімнату.

6. CITY REALTIME GATEWAY (WEBSOCKET)

6.1. WebSocket endpoint

GET /ws/city/presence

Auth: JWT токен у query param або header.

6.2. Формат повідомлень

Snapshot (при підключенні):

{
  "type": "snapshot",
  "rooms": [
    { "room_slug": "general", "online_count": 3, "typing_count": 0 },
    { "room_slug": "welcome", "online_count": 1, "typing_count": 0 }
  ]
}

Incremental update:

{
  "type": "room.presence",
  "room_slug": "energy",
  "online_count": 5,
  "typing_count": 1
}

7. FRONTEND INTEGRATION

7.1. Список кімнат /city

State:

type RoomPresenceUI = {
  onlineCount: number;
  typingCount: number;
};

const [presenceBySlug, setPresenceBySlug] = useState<Record<string, RoomPresenceUI>>({});

WebSocket handler:

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'snapshot') {
    const presence: Record<string, RoomPresenceUI> = {};
    data.rooms.forEach(r => {
      presence[r.room_slug] = {
        onlineCount: r.online_count,
        typingCount: r.typing_count
      };
    });
    setPresenceBySlug(presence);
  }

  if (data.type === 'room.presence') {
    setPresenceBySlug(prev => ({
      ...prev,
      [data.room_slug]: {
        onlineCount: data.online_count,
        typingCount: data.typing_count
      }
    }));
  }
};

7.2. UI

  • Room card: X online, typing badge
  • Active room: glow effect
  • Typing animation

8. CONFIG / ENV

matrix-presence-aggregator

MATRIX_HS_URL=https://app.daarion.space
MATRIX_ACCESS_TOKEN=<presence_daemon_token>
MATRIX_USER_ID=@presence_daemon:daarion.space
CITY_SERVICE_INTERNAL_URL=http://city-service:7001
NATS_URL=nats://nats:4222
ROOM_PRESENCE_THROTTLE_MS=3000

city-service (realtime gateway)

NATS_URL=nats://nats:4222
JWT_SECRET=<secret>

9. ACCEPTANCE CRITERIA

  • [ ] matrix-presence-aggregator запущений і синхронізується з Matrix
  • [ ] NATS отримує події city.presence.room.*
  • [ ] city-service має endpoint /ws/city/presence
  • [ ] При підключенні WS клієнт отримує snapshot
  • [ ] При зміні presence клієнт отримує update
  • [ ] UI /city показує online count для кожної кімнати
  • [ ] Typing indicator відображається

10. FUTURE ENHANCEMENTS

  1. Agent presence — окремі статуси для AI-агентів
  2. City Map — візуалізація presence на 2D карті
  3. Push notifications — сповіщення про активність
  4. Historical analytics — статистика активності