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 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-16 06:53:00 +02:00
parent 21635bb3ab
commit 0c82047ba8

91
bot.py
View File

@@ -2,6 +2,7 @@ import os
import json import json
import asyncio import asyncio
import logging import logging
import re
import time import time
import uuid import uuid
@@ -179,7 +180,13 @@ class Bot:
logger.info("Auto-trusted device %s of %s", device.device_id, user_id) logger.info("Auto-trusted device %s of %s", device.device_id, user_id)
async def on_unknown(self, room, event: UnknownEvent): 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: if event.type != CALL_MEMBER_TYPE:
return return
if event.sender == BOT_USER: if event.sender == BOT_USER:
@@ -374,17 +381,21 @@ class Bot:
if doc_context: if doc_context:
messages.append({"role": "system", "content": doc_context}) messages.append({"role": "system", "content": doc_context})
# Last N messages from room timeline as context # Fetch last N messages from room via API
timeline = room.timeline try:
history = list(timeline)[-10:] if timeline else [] resp = await self.client.room_messages(
for evt in history: 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"): if not hasattr(evt, "body"):
continue continue
role = "assistant" if evt.sender == BOT_USER else "user" role = "assistant" if evt.sender == BOT_USER else "user"
messages.append({"role": role, "content": evt.body}) 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 # Add current user message
if not messages or messages[-1].get("content") != user_message:
messages.append({"role": "user", "content": user_message}) messages.append({"role": "user", "content": user_message})
try: try:
@@ -399,6 +410,23 @@ class Bot:
logger.exception("LLM call failed") logger.exception("LLM call failed")
await self._send_text(room.room_id, "Sorry, I couldn't generate a response.") 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"<pre><code>\2</code></pre>", safe, flags=re.DOTALL)
# Inline code
safe = re.sub(r"`([^`]+)`", r"<code>\1</code>", safe)
# Bold
safe = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", safe)
# Italic
safe = re.sub(r"\*(.+?)\*", r"<em>\1</em>", safe)
# Line breaks
safe = safe.replace("\n", "<br/>")
return safe
async def _send_text(self, room_id: str, text: str): async def _send_text(self, room_id: str, text: str):
await self.client.room_send( await self.client.room_send(
room_id, room_id,
@@ -407,32 +435,65 @@ class Bot:
"msgtype": "m.text", "msgtype": "m.text",
"body": text, "body": text,
"format": "org.matrix.custom.html", "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): async def on_room_unknown(self, room, event: RoomMessageUnknown):
"""Handle in-room verification events.""" """Handle in-room verification events."""
source = event.source or {} source = event.source or {}
content = source.get("content", {}) content = source.get("content", {})
event_type = source.get("type", "") 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 return
if event.sender == BOT_USER: if event.sender == BOT_USER:
return 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) 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) 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) 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) 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", "") txn = content.get("m.relates_to", {}).get("event_id", "")
self._verifications.pop(txn, None) self._verifications.pop(txn, None)
logger.info("Verification cancelled: %s", txn) logger.info("Verification cancelled: %s", txn)