Task 4: stale room blocking + agent_id binding - MatrixBot._check_agent_routing: blocks normal messages when user has no selected agent or room is bound to a different agent - agent_routing_enabled flag on MatrixRuntime activates the check only in real multi-agent mode (RoutedPlatformClient) - make_handle_new_chat now writes agent_id into new room metadata when user already has a selected agent Task 5: durable restart state tests - test_restart_persistence.py proves selected_agent_id, room agent_id, platform_chat_id, and the sequence counter all survive SQLiteStore close/reopen; also covers clean startup with no prior state
209 lines
7.4 KiB
Python
209 lines
7.4 KiB
Python
from __future__ import annotations
|
||
|
||
from collections.abc import Awaitable, Callable
|
||
from typing import Any
|
||
|
||
import structlog
|
||
from nio.api import RoomVisibility
|
||
from nio.responses import RoomCreateError
|
||
|
||
from adapter.matrix.store import (
|
||
get_selected_agent_id,
|
||
get_user_meta,
|
||
next_chat_id,
|
||
next_platform_chat_id,
|
||
set_room_meta,
|
||
)
|
||
from core.protocol import IncomingCommand, OutgoingMessage
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
def _is_unregistered_chat_id(chat_id: str) -> bool:
|
||
return chat_id.startswith("unregistered:")
|
||
|
||
|
||
async def _fallback_new_chat(
|
||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||
) -> list:
|
||
if not await auth_mgr.is_authenticated(event.user_id):
|
||
return [OutgoingMessage(chat_id=event.chat_id, text="Введите !start чтобы начать.")]
|
||
|
||
name = " ".join(event.args).strip() if event.args else ""
|
||
chats = await chat_mgr.list_active(event.user_id)
|
||
chat_id = f"C{len(chats) + 1}"
|
||
ctx = await chat_mgr.get_or_create(
|
||
user_id=event.user_id,
|
||
chat_id=chat_id,
|
||
platform=event.platform,
|
||
surface_ref=event.chat_id,
|
||
name=name or None,
|
||
)
|
||
return [
|
||
OutgoingMessage(
|
||
chat_id=event.chat_id, text=f"Создан чат: {ctx.display_name} ({ctx.chat_id})"
|
||
)
|
||
]
|
||
|
||
|
||
def make_handle_new_chat(
|
||
client: Any | None,
|
||
store: Any | None,
|
||
) -> Callable[..., Awaitable[list]]:
|
||
async def handle_new_chat(
|
||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||
) -> list:
|
||
if client is None or store is None:
|
||
return await _fallback_new_chat(event, auth_mgr, platform, chat_mgr, settings_mgr)
|
||
|
||
if not await auth_mgr.is_authenticated(event.user_id):
|
||
return [
|
||
OutgoingMessage(
|
||
chat_id=event.chat_id,
|
||
text="Сначала примите приглашение бота.",
|
||
)
|
||
]
|
||
|
||
user_meta = await get_user_meta(store, event.user_id)
|
||
space_id = (user_meta or {}).get("space_id")
|
||
if not space_id:
|
||
return [
|
||
OutgoingMessage(
|
||
chat_id=event.chat_id,
|
||
text="Ошибка: Space не найден. Примите приглашение бота заново.",
|
||
)
|
||
]
|
||
|
||
name = " ".join(event.args).strip() if event.args else ""
|
||
chat_id = await next_chat_id(store, event.user_id)
|
||
platform_chat_id = await next_platform_chat_id(store)
|
||
room_name = name or f"Чат {chat_id}"
|
||
|
||
response = await client.room_create(
|
||
name=room_name,
|
||
visibility=RoomVisibility.private,
|
||
is_direct=False,
|
||
invite=[event.user_id],
|
||
)
|
||
if isinstance(response, RoomCreateError):
|
||
logger.error(
|
||
"room_create failed",
|
||
user_id=event.user_id,
|
||
status_code=getattr(response, "status_code", None),
|
||
)
|
||
return [OutgoingMessage(chat_id=event.chat_id, text="Не удалось создать комнату.")]
|
||
|
||
room_id = getattr(response, "room_id", None)
|
||
if not room_id:
|
||
return [OutgoingMessage(chat_id=event.chat_id, text="Не удалось создать комнату.")]
|
||
|
||
homeserver = event.user_id.split(":")[-1]
|
||
await client.room_put_state(
|
||
room_id=space_id,
|
||
event_type="m.space.child",
|
||
content={"via": [homeserver]},
|
||
state_key=room_id,
|
||
)
|
||
|
||
selected_agent_id = await get_selected_agent_id(store, event.user_id)
|
||
room_meta: dict = {
|
||
"room_type": "chat",
|
||
"chat_id": chat_id,
|
||
"display_name": room_name,
|
||
"matrix_user_id": event.user_id,
|
||
"space_id": space_id,
|
||
"platform_chat_id": platform_chat_id,
|
||
}
|
||
if selected_agent_id:
|
||
room_meta["agent_id"] = selected_agent_id
|
||
await set_room_meta(store, room_id, room_meta)
|
||
ctx = await chat_mgr.get_or_create(
|
||
user_id=event.user_id,
|
||
chat_id=chat_id,
|
||
platform=event.platform,
|
||
surface_ref=room_id,
|
||
name=room_name,
|
||
)
|
||
return [
|
||
OutgoingMessage(
|
||
chat_id=event.chat_id,
|
||
text=f"Создан чат: {ctx.display_name} ({ctx.chat_id})",
|
||
)
|
||
]
|
||
|
||
return handle_new_chat
|
||
|
||
|
||
async def handle_list_chats(
|
||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||
) -> list:
|
||
chats = await chat_mgr.list_active(event.user_id)
|
||
if not chats:
|
||
return [OutgoingMessage(chat_id=event.chat_id, text="Нет активных чатов.")]
|
||
lines = [f"• {c.display_name} ({c.chat_id})" for c in chats]
|
||
return [OutgoingMessage(chat_id=event.chat_id, text="\n".join(lines))]
|
||
|
||
|
||
def make_handle_rename(
|
||
client: Any | None,
|
||
store: Any | None,
|
||
) -> Callable[..., Awaitable[list]]:
|
||
async def handle_rename(
|
||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||
) -> list:
|
||
if not event.args:
|
||
return [
|
||
OutgoingMessage(chat_id=event.chat_id, text="Укажите название: !rename Название")
|
||
]
|
||
if _is_unregistered_chat_id(event.chat_id):
|
||
return [
|
||
OutgoingMessage(
|
||
chat_id=event.chat_id,
|
||
text=(
|
||
"Этот чат не найден в локальном состоянии бота. "
|
||
"Открой зарегистрированную комнату или создай новый чат через !new."
|
||
),
|
||
)
|
||
]
|
||
|
||
new_name = " ".join(event.args)
|
||
ctx = await chat_mgr.rename(event.chat_id, new_name, user_id=event.user_id)
|
||
if client is not None and ctx.surface_ref:
|
||
await client.room_put_state(
|
||
room_id=ctx.surface_ref,
|
||
event_type="m.room.name",
|
||
content={"name": new_name},
|
||
state_key="",
|
||
)
|
||
|
||
return [OutgoingMessage(chat_id=event.chat_id, text=f"Переименован в: {ctx.display_name}")]
|
||
|
||
return handle_rename
|
||
|
||
|
||
def make_handle_archive(
|
||
client: Any | None,
|
||
store: Any | None,
|
||
) -> Callable[..., Awaitable[list]]:
|
||
async def handle_archive(
|
||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||
) -> list:
|
||
if _is_unregistered_chat_id(event.chat_id):
|
||
return [
|
||
OutgoingMessage(
|
||
chat_id=event.chat_id,
|
||
text=(
|
||
"Этот чат не найден в локальном состоянии бота. "
|
||
"Создай новый чат через !new."
|
||
),
|
||
)
|
||
]
|
||
ctx = await chat_mgr.get(event.chat_id, user_id=event.user_id)
|
||
if ctx is None:
|
||
return [OutgoingMessage(chat_id=event.chat_id, text="Этот чат не найден.")]
|
||
await chat_mgr.archive(event.chat_id, user_id=event.user_id)
|
||
if client is not None and ctx.surface_ref:
|
||
await client.room_leave(ctx.surface_ref)
|
||
return [OutgoingMessage(chat_id=event.chat_id, text="Чат архивирован.")]
|
||
|
||
return handle_archive
|