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
8.6 KiB
8.6 KiB
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.
Полный рабочий пример
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 обязателен
# НЕПРАВИЛЬНО — создаст обычную комнату, не 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
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, не имя
# НЕПРАВИЛЬНО
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. Это строковое сравнение.
Закрепить «Настройки» вверху, чаты ниже
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
# НЕПРАВИЛЬНО: "10" < "2" в строковом сравнении!
orders = ["0", "1", "10", "11", "2", "3"]
# ПРАВИЛЬНО: одинаковая длина
orders = ["00", "01", "02", "03", "10", "11"]
Приглашение пользователя в Space и дочерние комнаты
Простого способа пригласить в Space и все дочерние комнаты одним вызовом нет — нужно приглашать в каждую отдельно.
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
Переименование комнаты
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 (без удаления самой комнаты)
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"] |
Выводы для нашей реализации
- При регистрации:
create_personal_space()→ сохранитьspace_idв БД - Комната «Настройки»: создать первой с
order: "00" - «Чат 1»: создать второй с
order: "10" - Команда
!new: создать комнату +m.space.child+ пригласить пользователя - Команда
!rename: обновитьm.room.name - Команда
!archive: установить пустойm.space.child