surfaces/docs/research/matrix-spaces.md
Mikhail Putilovskij 67499daa61 feat: extend platform mock + add research docs
platform/interface.py:
- Add Attachment, MessageChunk, AgentEvent types
- Add stream_message() to PlatformClient Protocol (door open for streaming)
- Add WebhookReceiver Protocol

platform/mock.py:
- Add attachment_mode config (url/binary/s3)
- Implement stream_message() — single chunk, ready for real streaming
- Add register_webhook_receiver() + simulate_agent_event() for testing

docs/research/:
- telegram-forum-topics.md — aiogram 3.x Forum Topics API, FSM patterns, UX analysis
- fsm-patterns.md — FSM storage options, StateData best practices
- matrix-spaces.md — matrix-nio Space API, room ordering, invite flow
- matrix-events.md — reactions, threads, typing, sync loop pitfalls
- telegram-chat-alternatives.md — 7 alternatives for multi-chat UX, virtual chats in DM recommended
2026-03-30 14:04:34 +03:00

264 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Research: matrix-nio Space API
Based on: Matrix Client-Server Spec, matrix-nio 0.24+, Element Web behavior.
## Создание Space
Space в Matrix — это комната с room type `m.space`. При создании нужны state events.
### Полный рабочий пример
```python
from nio import AsyncClient, RoomCreateResponse
async def create_personal_space(client: AsyncClient, display_name: str) -> str:
"""
Создаёт персональный Space для пользователя.
Возвращает room_id созданного Space.
"""
response = await client.room_create(
name=f"Lambda — {display_name}",
is_public=False,
room_version="10",
initial_state=[
{
"type": "m.room.topic",
"state_key": "",
"content": {"topic": "Lambda AI workspace"},
},
{
"type": "m.room.join_rules",
"state_key": "",
"content": {"join_rule": "invite"},
},
],
creation_content={
"type": "m.space", # ЭТО ГЛАВНОЕ — иначе создастся обычная комната
},
)
if isinstance(response, RoomCreateResponse):
return response.room_id
raise Exception(f"Failed to create space: {response}")
```
### Подводный камень #1: creation_content обязателен
```python
# НЕПРАВИЛЬНО — создаст обычную комнату, не Space
await client.room_create(name="My Space")
# ПРАВИЛЬНО
await client.room_create(
name="My Space",
creation_content={"type": "m.space"},
)
```
---
## Добавление комнат в Space
Чтобы добавить комнату в Space, нужно установить state event `m.space.child` в самом Space.
### Создание комнаты и добавление в Space
```python
async def create_room_in_space(
client: AsyncClient,
space_room_id: str,
room_name: str,
order: str,
) -> str:
"""
Создаёт комнату и добавляет её в Space.
Возвращает room_id новой комнаты.
"""
# 1. Создаём комнату
room_resp = await client.room_create(
name=room_name,
is_public=False,
room_version="10",
initial_state=[
{
"type": "m.room.join_rules",
"state_key": "",
"content": {"join_rule": "invite"},
},
],
)
if not isinstance(room_resp, RoomCreateResponse):
raise Exception(f"Failed to create room: {room_resp}")
room_id = room_resp.room_id
# 2. Добавляем в Space через m.space.child
# state_key = room_id дочерней комнаты (НЕ произвольное имя!)
await client.room_put_state(
room_id=space_room_id,
event_type="m.space.child",
state_key=room_id,
content={
"via": ["example.com"], # серверы для присоединения — обязательно
"order": order, # строковый порядок
"suggested": True,
},
)
# 3. Обратная ссылка (помогает клиентам понять структуру)
await client.room_put_state(
room_id=room_id,
event_type="m.space.parent",
state_key=space_room_id,
content={
"via": ["example.com"],
"canonical": True,
},
)
return room_id
```
### Подводный камень #2: state_key — это room_id, не имя
```python
# НЕПРАВИЛЬНО
await client.room_put_state(
room_id=space_id,
event_type="m.space.child",
state_key="Настройки", # ОШИБКА: нужен room_id
content={"via": ["example.com"]},
)
# ПРАВИЛЬНО
await client.room_put_state(
room_id=space_id,
event_type="m.space.child",
state_key="!abc123:example.com", # именно room_id
content={"via": ["example.com"]},
)
```
---
## Управление порядком комнат
Порядок в Space контролируется полем `order` в `m.space.child`. Это строковое сравнение.
### Закрепить «Настройки» вверху, чаты ниже
```python
async def setup_space_order(
client: AsyncClient,
space_id: str,
settings_room_id: str,
chat_room_ids: list[str],
) -> None:
# Настройки вверху
await client.room_put_state(
room_id=space_id,
event_type="m.space.child",
state_key=settings_room_id,
content={"via": ["example.com"], "order": "00", "suggested": True},
)
# Чаты с нарастающим порядком
for idx, chat_id in enumerate(chat_room_ids):
await client.room_put_state(
room_id=space_id,
event_type="m.space.child",
state_key=chat_id,
content={"via": ["example.com"], "order": f"{10 + idx:02d}", "suggested": True},
)
```
### Подводный камень #3: order — строковое, нужен padding
```python
# НЕПРАВИЛЬНО: "10" < "2" в строковом сравнении!
orders = ["0", "1", "10", "11", "2", "3"]
# ПРАВИЛЬНО: одинаковая длина
orders = ["00", "01", "02", "03", "10", "11"]
```
---
## Приглашение пользователя в Space и дочерние комнаты
Простого способа пригласить в Space и все дочерние комнаты одним вызовом нет — нужно приглашать в каждую отдельно.
```python
async def invite_user_to_space_and_rooms(
client: AsyncClient,
space_id: str,
child_room_ids: list[str],
user_id: str,
) -> None:
# Приглашаем в Space
await client.room_invite(space_id, user_id)
# Приглашаем в каждую дочернюю комнату
for room_id in child_room_ids:
await client.room_invite(room_id, user_id)
```
### Подводный камень #4: пользователь в Space не видит комнаты автоматически
Приглашение в Space не тянет дочерние комнаты — нужно приглашать каждую явно.
---
## Переименование и удаление из Space
### Переименование комнаты
```python
async def rename_room(client: AsyncClient, room_id: str, new_name: str) -> None:
await client.room_put_state(
room_id=room_id,
event_type="m.room.name",
state_key="",
content={"name": new_name},
)
```
### Удаление комнаты из Space (без удаления самой комнаты)
```python
async def remove_from_space(
client: AsyncClient,
space_id: str,
room_id: str,
) -> None:
# Пустой content = убрать из Space
await client.room_put_state(
room_id=space_id,
event_type="m.space.child",
state_key=room_id,
content={}, # пустое — убирает комнату из Space
)
```
---
## Подводные камни (резюме)
| Проблема | Решение |
|----------|---------|
| Space создаётся как обычная комната | Используй `creation_content={"type": "m.space"}` |
| Комнаты не видны в Space | Добавь `m.space.child` с `state_key=room_id` |
| Неправильный порядок | Строковый padding: `"00"`, `"01"`, `"10"` |
| Пользователь не видит комнаты | Приглашай в каждую дочернюю отдельно |
| Комната не убирается из Space | Установи пустой `content: {}` в `m.space.child` |
| `via` не указан | Всегда указывай `"via": ["homeserver.com"]` |
---
## Выводы для нашей реализации
1. **При регистрации**: `create_personal_space()` → сохранить `space_id` в БД
2. **Комната «Настройки»**: создать первой с `order: "00"`
3. **«Чат 1»**: создать второй с `order: "10"`
4. **Команда `!new`**: создать комнату + `m.space.child` + пригласить пользователя
5. **Команда `!rename`**: обновить `m.room.name`
6. **Команда `!archive`**: установить пустой `m.space.child`