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 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,18 +381,22 @@ 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 not hasattr(evt, "body"):
|
)
|
||||||
continue
|
if hasattr(resp, "chunk"):
|
||||||
role = "assistant" if evt.sender == BOT_USER else "user"
|
for evt in reversed(resp.chunk):
|
||||||
messages.append({"role": role, "content": evt.body})
|
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
|
# 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:
|
||||||
resp = await self.llm.chat.completions.create(
|
resp = await self.llm.chat.completions.create(
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user