From 0c82047ba80117995f2bcdad9c0740d1169926c3 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Mon, 16 Feb 2026 06:53:00 +0200 Subject: [PATCH] fix(CF-1189): Fix room.timeline crash, add markdown rendering, improve verification routing - Replace room.timeline (non-existent in nio) with client.room_messages() API - Add markdown-to-HTML conversion for formatted Matrix messages - Route in-room verification events from both UnknownEvent and RoomMessageUnknown Co-Authored-By: Claude Opus 4.6 --- bot.py | 101 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 20 deletions(-) diff --git a/bot.py b/bot.py index ee80f9a..b65f74c 100644 --- a/bot.py +++ b/bot.py @@ -2,6 +2,7 @@ import os import json import asyncio import logging +import re import time import uuid @@ -179,7 +180,13 @@ class Bot: logger.info("Auto-trusted device %s of %s", device.device_id, user_id) async def on_unknown(self, room, event: UnknownEvent): - """Handle call member state events to join calls.""" + """Handle call member state events and in-room verification.""" + # Route verification events + if event.type.startswith("m.key.verification."): + if event.sender != BOT_USER: + await self._route_verification(room, event) + return + if event.type != CALL_MEMBER_TYPE: return if event.sender == BOT_USER: @@ -374,18 +381,22 @@ class Bot: if doc_context: messages.append({"role": "system", "content": doc_context}) - # Last N messages from room timeline as context - timeline = room.timeline - history = list(timeline)[-10:] if timeline else [] - for evt in history: - if not hasattr(evt, "body"): - continue - role = "assistant" if evt.sender == BOT_USER else "user" - messages.append({"role": role, "content": evt.body}) + # Fetch last N messages from room via API + try: + resp = await self.client.room_messages( + room.room_id, start=self.client.next_batch or "", limit=10 + ) + if hasattr(resp, "chunk"): + for evt in reversed(resp.chunk): + if not hasattr(evt, "body"): + continue + role = "assistant" if evt.sender == BOT_USER else "user" + messages.append({"role": role, "content": evt.body}) + except Exception: + logger.debug("Could not fetch room history, proceeding without context") - # Ensure last message is the current user message - if not messages or messages[-1].get("content") != user_message: - messages.append({"role": "user", "content": user_message}) + # Add current user message + messages.append({"role": "user", "content": user_message}) try: resp = await self.llm.chat.completions.create( @@ -399,6 +410,23 @@ class Bot: logger.exception("LLM call failed") await self._send_text(room.room_id, "Sorry, I couldn't generate a response.") + @staticmethod + def _md_to_html(text: str) -> str: + """Minimal markdown to HTML for Matrix formatted_body.""" + import html as html_mod + safe = html_mod.escape(text) + # Code blocks (```...```) + safe = re.sub(r"```(\w*)\n(.*?)```", r"
\2
", safe, flags=re.DOTALL) + # Inline code + safe = re.sub(r"`([^`]+)`", r"\1", safe) + # Bold + safe = re.sub(r"\*\*(.+?)\*\*", r"\1", safe) + # Italic + safe = re.sub(r"\*(.+?)\*", r"\1", safe) + # Line breaks + safe = safe.replace("\n", "
") + return safe + async def _send_text(self, room_id: str, text: str): await self.client.room_send( room_id, @@ -407,32 +435,65 @@ class Bot: "msgtype": "m.text", "body": text, "format": "org.matrix.custom.html", - "formatted_body": text, + "formatted_body": self._md_to_html(text), }, ) + async def _route_verification(self, room, event: UnknownEvent): + """Route in-room verification events from UnknownEvent.""" + source = event.source or {} + verify_type = event.type + logger.info("Verification event: %s from %s", verify_type, event.sender) + + if verify_type == "m.key.verification.request": + await self._handle_verification_request(room, source) + elif verify_type == "m.key.verification.start": + await self._handle_verification_start(room, source) + elif verify_type == "m.key.verification.key": + await self._handle_verification_key(room, source) + elif verify_type == "m.key.verification.mac": + await self._handle_verification_mac(room, source) + elif verify_type == "m.key.verification.cancel": + content = source.get("content", {}) + txn = content.get("m.relates_to", {}).get("event_id", "") + self._verifications.pop(txn, None) + logger.info("Verification cancelled: %s", txn) + elif verify_type == "m.key.verification.done": + pass # Other side confirmed done + async def on_room_unknown(self, room, event: RoomMessageUnknown): """Handle in-room verification events.""" source = event.source or {} content = source.get("content", {}) event_type = source.get("type", "") + msgtype = content.get("msgtype", "") - if not event_type.startswith("m.key.verification."): + logger.info("RoomMessageUnknown: type=%s msgtype=%s sender=%s", event_type, msgtype, event.sender) + + # In-room verification events can come as m.room.message with msgtype=m.key.verification.* + # or as direct event types m.key.verification.* + verify_type = "" + if event_type.startswith("m.key.verification."): + verify_type = event_type + elif msgtype.startswith("m.key.verification."): + verify_type = msgtype + + if not verify_type: return if event.sender == BOT_USER: return - logger.info("Verification event: %s from %s", event_type, event.sender) + logger.info("Verification event: %s from %s", verify_type, event.sender) - if event_type == "m.key.verification.request": + if verify_type == "m.key.verification.request": await self._handle_verification_request(room, source) - elif event_type == "m.key.verification.start": + elif verify_type == "m.key.verification.start": await self._handle_verification_start(room, source) - elif event_type == "m.key.verification.key": + elif verify_type == "m.key.verification.key": await self._handle_verification_key(room, source) - elif event_type == "m.key.verification.mac": + elif verify_type == "m.key.verification.mac": await self._handle_verification_mac(room, source) - elif event_type == "m.key.verification.cancel": + elif verify_type == "m.key.verification.cancel": txn = content.get("m.relates_to", {}).get("event_id", "") self._verifications.pop(txn, None) logger.info("Verification cancelled: %s", txn)