- GLOBAL_PRESENCE_AGGREGATOR_SPEC.md documentation - matrix-presence-aggregator service (Python/FastAPI) - Matrix sync loop for presence/typing - NATS publishing for room presence - city-service: presence_gateway for WS broadcast - Frontend: real-time online count in room list - useGlobalPresence hook - Live typing indicators - Active room highlighting
345 lines
10 KiB
Markdown
345 lines
10 KiB
Markdown
# 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 (новий сервіс)**
|
||
- читає Matrix sync (presence, typing, room activity),
|
||
- тримає у пам'яті поточний стан присутності,
|
||
- публікує агреговані події в NATS.
|
||
|
||
2. **NATS JetStream**
|
||
- канал для presence/events:
|
||
- `city.presence.room.*`
|
||
- `city.presence.user.*`
|
||
|
||
3. **city-service (розширення)**
|
||
- підписується на NATS події,
|
||
- тримає WebSocket з'єднання з фронтендом,
|
||
- пушить presence/room-activity у браузер.
|
||
|
||
4. **web (Next.js UI)**
|
||
- сторінка `/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 (як клієнт),
|
||
- фільтр:
|
||
|
||
```json
|
||
{
|
||
"presence": {
|
||
"types": ["m.presence"]
|
||
},
|
||
"room": {
|
||
"timeline": { "limit": 0 },
|
||
"state": { "limit": 0 },
|
||
"ephemeral": {
|
||
"types": ["m.typing", "m.receipt"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- робить long-polling з `since` + `timeout`,
|
||
- парсить:
|
||
- `presence.events` → `m.presence`,
|
||
- `rooms.join[roomId].ephemeral.events` → `m.typing`, `m.receipt`.
|
||
|
||
---
|
||
|
||
## 3. DATA MODEL (IN-MEMORY AGGREGATOR)
|
||
|
||
### 3.1. Room presence state
|
||
|
||
```python
|
||
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_id` ↔ `city_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:
|
||
```json
|
||
{
|
||
"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:
|
||
```json
|
||
{
|
||
"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 (при підключенні):**
|
||
```json
|
||
{
|
||
"type": "snapshot",
|
||
"rooms": [
|
||
{ "room_slug": "general", "online_count": 3, "typing_count": 0 },
|
||
{ "room_slug": "welcome", "online_count": 1, "typing_count": 0 }
|
||
]
|
||
}
|
||
```
|
||
|
||
**Incremental update:**
|
||
```json
|
||
{
|
||
"type": "room.presence",
|
||
"room_slug": "energy",
|
||
"online_count": 5,
|
||
"typing_count": 1
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. FRONTEND INTEGRATION
|
||
|
||
### 7.1. Список кімнат `/city`
|
||
|
||
State:
|
||
```typescript
|
||
type RoomPresenceUI = {
|
||
onlineCount: number;
|
||
typingCount: number;
|
||
};
|
||
|
||
const [presenceBySlug, setPresenceBySlug] = useState<Record<string, RoomPresenceUI>>({});
|
||
```
|
||
|
||
WebSocket handler:
|
||
```typescript
|
||
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
|
||
|
||
```env
|
||
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)
|
||
|
||
```env
|
||
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** — статистика активності
|
||
|