import logging from functools import wraps from nio import AsyncClient, RoomMessageText from config import BOT_PREFIX, MATRIX_USER_ID from commands import COMMANDS, metrics logger = logging.getLogger("matrixbot") def handle_command_errors(func): @wraps(func) async def wrapper(client, room_id, sender, args): try: return await func(client, room_id, sender, args) except Exception as e: logger.error(f"Error in command {func.__name__}: {e}", exc_info=True) metrics.record_error(func.__name__) try: from utils import send_text await send_text(client, room_id, "An unexpected error occurred. Please try again later.") except Exception as e2: logger.error(f"Failed to send error message: {e2}", exc_info=True) return wrapper class Callbacks: def __init__(self, client: AsyncClient): self.client = client # Track the sync token so we ignore old messages on startup self.startup_sync_token = None async def message(self, room, event): # Ignore messages from before the bot started if self.startup_sync_token is None: return # Ignore our own messages if event.sender == MATRIX_USER_ID: return body = event.body.strip() if event.body else "" if not body.startswith(BOT_PREFIX): return # Parse command and args without_prefix = body[len(BOT_PREFIX):] parts = without_prefix.split(None, 1) cmd_name = parts[0].lower() if parts else "" args = parts[1] if len(parts) > 1 else "" logger.info(f"Command '{cmd_name}' from {event.sender} in {room.room_id}") handler_entry = COMMANDS.get(cmd_name) if handler_entry is None: return handler, _ = handler_entry metrics.record_command(cmd_name) wrapped = handle_command_errors(handler) await wrapped(self.client, room.room_id, event.sender, args)