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:
101
bot.py
101
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"<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)
|
||||
|
||||
Reference in New Issue
Block a user