feat: Implement Matrix Chat Client

This commit is contained in:
Apple
2025-11-26 13:15:01 -08:00
parent 871812ef92
commit e9c04f6bcd
7 changed files with 1264 additions and 8 deletions

View File

@@ -0,0 +1,413 @@
# MATRIX CHAT CLIENT — DAARION.city
Version: 1.0.0
## 0. PURPOSE
Зробити так, щоб сторінка `/city/[slug]` у DAARION UI була **повноцінним Matrix-чатом**:
- використовує реальні Matrix rooms (`matrix_room_id`, `matrix_room_alias`),
- працює від імені поточного користувача DAARION,
- показує історію, нові повідомлення, статус підключення,
- використовує існуючий Chat Layout (UI), але замість тимчасового WebSocket — Matrix.
Це базовий крок для подальшого:
- Presence / Typing / Read receipts,
- агентів як ботів,
- 2D/2.5D City Map з live-активністю.
---
## 1. ARCHITECTURE OVERVIEW
```
┌─────────────────────────────────────────────────────────────────┐
│ DAARION Frontend │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ /city/[slug] Page │ │
│ │ ┌─────────────────┐ ┌────────────────────────────────┐ │ │
│ │ │ Room Info │ │ Matrix Chat Client │ │ │
│ │ │ (from API) │ │ - Connect to Synapse │ │ │
│ │ └─────────────────┘ │ - Send/receive messages │ │ │
│ │ │ - Show history │ │ │
│ │ └────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Backend │
│ ┌─────────────┐ ┌──────────────────┐ ┌───────────────────┐ │
│ │ auth-service│ │ city-service │ │ matrix-gateway │ │
│ │ (7020) │ │ (7001) │ │ (7025) │ │
│ │ │ │ │ │ │ │
│ │ JWT tokens │ │ /chat/bootstrap │ │ /user/token │ │
│ │ User→Matrix │ │ matrix_room_id │ │ Create rooms │ │
│ └─────────────┘ └──────────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Matrix Synapse (8018) │
│ - Rooms: !xxx:daarion.space │
│ - Users: @daarion_xxx:daarion.space │
│ - Messages, history, sync │
└─────────────────────────────────────────────────────────────────┘
```
### Компоненти:
- **auth-service** (7020)
- знає `user_id`, email, Matrix user mapping.
- **matrix-gateway** (7025)
- вміє створювати кімнати (вже реалізовано),
- буде видавати Matrix access tokens для користувачів.
- **city-service** (7001)
- надає `matrix_room_id` / `matrix_room_alias`,
- новий endpoint `/chat/bootstrap`.
- **web (Next.js UI)**
- сторінка `/city/[slug]`,
- компонент `ChatRoom`,
- Matrix chat client.
---
## 2. AUTH MODEL
### Допущення (MVP):
- Користувач уже залогінений у DAARION (JWT).
- Для кожного `user_id` вже існує Matrix-акаунт (авто-provisioning реалізовано раніше).
- Потрібен **bootstrap endpoint**, який:
- по JWT → знаходить Matrix user,
- видає Matrix access token,
- повертає `matrix_room_id` для кімнати.
### Matrix User Mapping
| DAARION user_id | Matrix user_id |
|-----------------|----------------|
| `87838688-d7c4-436c-...` | `@daarion_87838688:daarion.space` |
---
## 3. BACKEND: CHAT BOOTSTRAP API
### 3.1. Endpoint: `GET /api/city/chat/bootstrap`
**Розташування:** `city-service` (логічно відповідає за City+Matrix інтеграцію)
**Вхід:**
- HTTP заголовок `Authorization: Bearer <access_token>` (DAARION JWT)
- query param: `room_slug`, наприклад `energy`
**Логіка:**
1. Валідувати JWT → отримати `user_id`.
2. Знайти `city_room` по `slug`:
- витягнути `matrix_room_id` / `matrix_room_alias`.
3. Через internal виклик до `matrix-gateway`:
- отримати Matrix access token для цього `user_id`.
4. Повернути фронтенду:
```json
{
"matrix_hs_url": "https://app.daarion.space",
"matrix_user_id": "@daarion_87838688:daarion.space",
"matrix_access_token": "syt_...",
"matrix_room_id": "!gykdLyazhkcSZGHmbG:daarion.space",
"matrix_room_alias": "#city_energy:daarion.space",
"room": {
"id": "room_city_energy",
"slug": "energy",
"name": "Energy"
}
}
```
### 3.2. Matrix Gateway: User Token Endpoint
**Endpoint:** `POST /internal/matrix/users/token`
**Request:**
```json
{
"user_id": "87838688-d7c4-436c-9466-4ab0947d7730"
}
```
**Response:**
```json
{
"matrix_user_id": "@daarion_87838688:daarion.space",
"access_token": "syt_...",
"device_id": "DEVICE_ID"
}
```
**Логіка:**
1. Побудувати Matrix username: `daarion_{user_id[:8]}`
2. Спробувати логін з відомим паролем
3. Якщо користувач не існує — створити через admin API
4. Повернути access token
### 3.3. Security
* Endpoint вимагає валідний DAARION JWT.
* `matrix_access_token` — короткоживучий (30 хв) або session-based.
* Internal endpoints (`/internal/*`) доступні тільки з Docker network.
---
## 4. FRONTEND: MATRIX CHAT CLIENT
### 4.1. Поточний Chat Layout
Вже існує:
* сторінка `/city/[slug]`,
* компонент `ChatRoom`:
* `messages[]`,
* `onSend(message)`,
* індикатор підключення.
Зараз він працює через свій WebSocket/stub.
### 4.2. Нова схема
1. **При завантаженні сторінки:**
```tsx
// /city/[slug]/page.tsx
const [bootstrap, setBootstrap] = useState(null);
const [status, setStatus] = useState<'loading' | 'connecting' | 'online' | 'error'>('loading');
useEffect(() => {
async function init() {
// 1. Отримати bootstrap дані
const res = await fetch(`/api/city/chat/bootstrap?room_slug=${slug}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
setBootstrap(data);
// 2. Ініціалізувати Matrix client
setStatus('connecting');
}
init();
}, [slug]);
```
2. **Створення Matrix клієнта:**
```tsx
// Використовуємо REST API напряму (без matrix-js-sdk для простоти MVP)
const matrixClient = new MatrixRestClient({
baseUrl: bootstrap.matrix_hs_url,
accessToken: bootstrap.matrix_access_token,
userId: bootstrap.matrix_user_id,
roomId: bootstrap.matrix_room_id
});
```
3. **Отримання історії:**
```tsx
const messages = await matrixClient.getMessages(roomId, { limit: 50 });
```
4. **Відправка повідомлень:**
```tsx
await matrixClient.sendMessage(roomId, {
msgtype: 'm.text',
body: text
});
```
5. **Підписка на нові повідомлення:**
```tsx
// Long-polling або sync
matrixClient.onMessage((event) => {
setMessages(prev => [...prev, mapMatrixEvent(event)]);
});
```
### 4.3. Matrix Event → Chat Message Mapping
```tsx
function mapMatrixEvent(event: MatrixEvent): ChatMessage {
return {
id: event.event_id,
senderId: event.sender,
senderName: event.sender.split(':')[0].replace('@daarion_', 'User '),
text: event.content.body,
timestamp: new Date(event.origin_server_ts),
isUser: event.sender === bootstrap.matrix_user_id,
};
}
```
---
## 5. MATRIX REST CLIENT (Lightweight)
Замість важкого `matrix-js-sdk`, створимо легкий REST клієнт:
```typescript
// lib/matrix-client.ts
export class MatrixRestClient {
private baseUrl: string;
private accessToken: string;
private userId: string;
constructor(config: MatrixClientConfig) {
this.baseUrl = config.baseUrl;
this.accessToken = config.accessToken;
this.userId = config.userId;
}
// Get room messages
async getMessages(roomId: string, options?: { limit?: number; from?: string }) {
const params = new URLSearchParams({
dir: 'b',
limit: String(options?.limit || 50)
});
if (options?.from) params.set('from', options.from);
const res = await fetch(
`${this.baseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages?${params}`,
{ headers: this.authHeaders() }
);
return res.json();
}
// Send text message
async sendMessage(roomId: string, body: string) {
const txnId = `m${Date.now()}`;
const res = await fetch(
`${this.baseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
{
method: 'PUT',
headers: this.authHeaders(),
body: JSON.stringify({
msgtype: 'm.text',
body: body
})
}
);
return res.json();
}
// Join room
async joinRoom(roomId: string) {
const res = await fetch(
`${this.baseUrl}/_matrix/client/v3/join/${encodeURIComponent(roomId)}`,
{
method: 'POST',
headers: this.authHeaders()
}
);
return res.json();
}
// Sync (for real-time updates)
async sync(since?: string) {
const params = new URLSearchParams({ timeout: '30000' });
if (since) params.set('since', since);
const res = await fetch(
`${this.baseUrl}/_matrix/client/v3/sync?${params}`,
{ headers: this.authHeaders() }
);
return res.json();
}
private authHeaders() {
return {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
};
}
}
```
---
## 6. UI / UX REQUIREMENTS
### 6.1. Стан підключення
| Status | UI |
|--------|-----|
| `loading` | Skeleton loader |
| `connecting` | "Підключення до Matrix…" + spinner |
| `online` | Зелений індикатор "Онлайн" |
| `error` | Червоний індикатор + "Помилка підключення" + кнопка "Повторити" |
### 6.2. Відображення історії
* При завантаженні показувати останні 50 повідомлень
* Infinite scroll для старіших повідомлень
* Показувати дату-роздільник між днями
### 6.3. Надсилання повідомлень
* Enter — відправити
* Shift+Enter — новий рядок
* Показувати "sending..." стан
* При помилці — показати "Не вдалося відправити" + retry
---
## 7. LIMITATIONS / MVP
Поки що:
* ✅ Тільки текстові повідомлення (`m.text`)
* ❌ Без файлів/зображень
* ❌ Без threads/reactions
* ❌ Без read receipts
* ❌ Без typing indicators
Це все буде додано у наступних фазах.
---
## 8. API SUMMARY
### City Service (7001)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/city/chat/bootstrap?room_slug=X` | Bootstrap Matrix chat |
### Matrix Gateway (7025)
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/internal/matrix/users/token` | Get/create user token |
---
## 9. ROADMAP AFTER THIS
Після Matrix Chat Client:
1. **Presence & Typing:**
* слухати `m.presence`, `m.typing` → показувати "online/typing".
2. **Reactions & read receipts.**
3. **Attachments (фото/файли).**
4. **City Map інтеграція** (активність кімнат → візуалізація).
---
## 10. ACCEPTANCE CRITERIA
- [ ] `/api/city/chat/bootstrap` повертає Matrix credentials для авторизованого користувача
- [ ] Frontend підключається до Matrix і показує історію повідомлень
- [ ] Користувач може надсилати повідомлення через DAARION UI
- [ ] Повідомлення з'являються в Element Web і навпаки
- [ ] Обробляються стани: loading, connecting, online, error