feat(matrix): land QA follow-ups and refresh docs
- harden Matrix onboarding/chat lifecycle after manual QA - refresh README and Matrix docs to match current behavior - add local ignores for runtime artifacts and include current planning/report docs Closes #7 Closes #9 Closes #14
This commit is contained in:
parent
7fce4c9b3e
commit
6ced154124
35 changed files with 8380 additions and 67 deletions
|
|
@ -14,6 +14,7 @@ from nio import (
|
|||
RoomMemberEvent,
|
||||
RoomMessageText,
|
||||
)
|
||||
from nio.responses import SyncResponse
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from adapter.matrix.converter import from_room_event
|
||||
|
|
@ -115,12 +116,20 @@ class MatrixBot:
|
|||
self.runtime.platform,
|
||||
self.runtime.store,
|
||||
self.runtime.auth_mgr,
|
||||
self.runtime.chat_mgr,
|
||||
)
|
||||
|
||||
async def _send_all(self, room_id: str, outgoing: list[OutgoingEvent]) -> None:
|
||||
for event in outgoing:
|
||||
await send_outgoing(self.client, room_id, event, store=self.runtime.store)
|
||||
|
||||
|
||||
async def prepare_live_sync(client: AsyncClient) -> str | None:
|
||||
response = await client.sync(timeout=0, full_state=True)
|
||||
if isinstance(response, SyncResponse):
|
||||
return response.next_batch
|
||||
return None
|
||||
|
||||
async def send_outgoing(
|
||||
client: AsyncClient,
|
||||
room_id: str,
|
||||
|
|
@ -197,6 +206,8 @@ async def main() -> None:
|
|||
elif password:
|
||||
await client.login(password=password, device_name="surfaces-bot")
|
||||
|
||||
since_token = await prepare_live_sync(client)
|
||||
|
||||
bot = MatrixBot(client, runtime)
|
||||
client.add_event_callback(bot.on_room_message, RoomMessageText)
|
||||
client.add_event_callback(bot.on_member, (InviteMemberEvent, RoomMemberEvent))
|
||||
|
|
@ -209,7 +220,7 @@ async def main() -> None:
|
|||
request_timeout=client_config.request_timeout,
|
||||
)
|
||||
try:
|
||||
await client.sync_forever(timeout=30000)
|
||||
await client.sync_forever(timeout=30000, since=since_token)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from adapter.matrix.handlers.chat import (
|
|||
)
|
||||
from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_confirm
|
||||
from adapter.matrix.handlers.settings import (
|
||||
handle_help,
|
||||
handle_settings,
|
||||
handle_settings_connectors,
|
||||
handle_settings_plan,
|
||||
|
|
@ -27,6 +28,7 @@ def register_matrix_handlers(dispatcher: EventDispatcher, client=None, store=Non
|
|||
dispatcher.register(IncomingCommand, "chats", handle_list_chats)
|
||||
dispatcher.register(IncomingCommand, "rename", make_handle_rename(client, store))
|
||||
dispatcher.register(IncomingCommand, "archive", make_handle_archive(client, store))
|
||||
dispatcher.register(IncomingCommand, "help", handle_help)
|
||||
dispatcher.register(IncomingCommand, "settings", handle_settings)
|
||||
dispatcher.register(IncomingCommand, "settings_skills", handle_settings_skills)
|
||||
dispatcher.register(IncomingCommand, "settings_connectors", handle_settings_connectors)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import structlog
|
||||
from typing import Any
|
||||
|
||||
from nio.api import RoomVisibility
|
||||
from nio.responses import RoomCreateError
|
||||
|
||||
from adapter.matrix.store import (
|
||||
|
|
@ -15,7 +16,7 @@ from adapter.matrix.store import (
|
|||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def handle_invite(client: Any, room: Any, event: Any, platform, store, auth_mgr) -> None:
|
||||
async def handle_invite(client: Any, room: Any, event: Any, platform, store, auth_mgr, chat_mgr) -> None:
|
||||
matrix_user_id = getattr(event, "sender", "")
|
||||
display_name = getattr(room, "display_name", None) or matrix_user_id
|
||||
|
||||
|
|
@ -37,7 +38,8 @@ async def handle_invite(client: Any, room: Any, event: Any, platform, store, aut
|
|||
space_resp = await client.room_create(
|
||||
name=f"Lambda — {display_name}",
|
||||
space=True,
|
||||
visibility="private",
|
||||
visibility=RoomVisibility.private,
|
||||
invite=[matrix_user_id],
|
||||
)
|
||||
if isinstance(space_resp, RoomCreateError):
|
||||
logger.error(
|
||||
|
|
@ -50,8 +52,9 @@ async def handle_invite(client: Any, room: Any, event: Any, platform, store, aut
|
|||
|
||||
chat_resp = await client.room_create(
|
||||
name="Чат 1",
|
||||
visibility="private",
|
||||
visibility=RoomVisibility.private,
|
||||
is_direct=False,
|
||||
invite=[matrix_user_id],
|
||||
)
|
||||
if isinstance(chat_resp, RoomCreateError):
|
||||
logger.error(
|
||||
|
|
@ -69,9 +72,6 @@ async def handle_invite(client: Any, room: Any, event: Any, platform, store, aut
|
|||
state_key=chat_room_id,
|
||||
)
|
||||
|
||||
await client.room_invite(space_id, matrix_user_id)
|
||||
await client.room_invite(chat_room_id, matrix_user_id)
|
||||
|
||||
chat_id = await next_chat_id(store, matrix_user_id)
|
||||
|
||||
user_meta = await get_user_meta(store, matrix_user_id) or {}
|
||||
|
|
@ -89,6 +89,13 @@ async def handle_invite(client: Any, room: Any, event: Any, platform, store, aut
|
|||
"space_id": space_id,
|
||||
},
|
||||
)
|
||||
await chat_mgr.get_or_create(
|
||||
user_id=matrix_user_id,
|
||||
chat_id=chat_id,
|
||||
platform="matrix",
|
||||
surface_ref=chat_room_id,
|
||||
name="Чат 1",
|
||||
)
|
||||
|
||||
welcome = (
|
||||
f"Привет, {user.display_name or matrix_user_id}! Пиши — я здесь.\n\n"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any, Awaitable, Callable
|
||||
|
||||
import structlog
|
||||
from nio.api import RoomVisibility
|
||||
from nio.responses import RoomCreateError
|
||||
|
||||
from adapter.matrix.store import get_user_meta, next_chat_id, set_room_meta
|
||||
|
|
@ -11,6 +12,10 @@ 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:
|
||||
|
|
@ -68,8 +73,9 @@ def make_handle_new_chat(
|
|||
|
||||
response = await client.room_create(
|
||||
name=room_name,
|
||||
visibility="private",
|
||||
visibility=RoomVisibility.private,
|
||||
is_direct=False,
|
||||
invite=[event.user_id],
|
||||
)
|
||||
if isinstance(response, RoomCreateError):
|
||||
logger.error(
|
||||
|
|
@ -90,7 +96,6 @@ def make_handle_new_chat(
|
|||
content={"via": [homeserver]},
|
||||
state_key=room_id,
|
||||
)
|
||||
await client.room_invite(room_id, event.user_id)
|
||||
|
||||
await set_room_meta(
|
||||
store,
|
||||
|
|
@ -141,11 +146,23 @@ def make_handle_rename(
|
|||
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_set_name(ctx.surface_ref, new_name)
|
||||
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}")]
|
||||
|
||||
|
|
@ -159,7 +176,19 @@ def make_handle_archive(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,25 @@ from adapter.matrix.reactions import build_skills_text
|
|||
from core.protocol import IncomingCommand, OutgoingMessage, SettingsAction
|
||||
|
||||
|
||||
HELP_TEXT = "\n".join(
|
||||
[
|
||||
"Команды",
|
||||
"",
|
||||
"!new [название] создать новый чат",
|
||||
"!chats список активных чатов",
|
||||
"!rename <название> переименовать текущий чат",
|
||||
"!archive архивировать текущий чат",
|
||||
"!settings общий обзор настроек",
|
||||
"!skills список навыков",
|
||||
"!soul [поле значение] показать или изменить личность",
|
||||
"!safety [триггер on/off] показать или изменить безопасность",
|
||||
"!status краткий статус",
|
||||
"!whoami показать ваш id",
|
||||
"!yes / !no подтвердить или отменить действие",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _render_mapping(title: str, data: dict | None) -> str:
|
||||
data = data or {}
|
||||
lines = [title]
|
||||
|
|
@ -66,6 +85,12 @@ async def handle_settings(
|
|||
return [OutgoingMessage(chat_id=event.chat_id, text=dashboard)]
|
||||
|
||||
|
||||
async def handle_help(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list:
|
||||
return [OutgoingMessage(chat_id=event.chat_id, text=HELP_TEXT)]
|
||||
|
||||
|
||||
async def handle_settings_skills(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import asyncio
|
|||
import os
|
||||
|
||||
import structlog
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.types import BotCommand
|
||||
|
|
@ -41,9 +44,9 @@ def build_event_dispatcher() -> EventDispatcher:
|
|||
|
||||
|
||||
async def main() -> None:
|
||||
token = os.environ.get("BOT_TOKEN")
|
||||
token = os.environ.get("BOT_TOKEN") or os.environ.get("TELEGRAM_BOT_TOKEN")
|
||||
if not token:
|
||||
raise RuntimeError("BOT_TOKEN env variable is not set")
|
||||
raise RuntimeError("BOT_TOKEN (or TELEGRAM_BOT_TOKEN) env variable is not set")
|
||||
|
||||
db.init_db()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue