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 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,17 +381,21 @@ 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:
# 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:
# Add current user message
messages.append({"role": "user", "content": user_message})
try:
@@ -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"<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):
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)