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)