# 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`