Files
matrix/matrixbot/callbacks.py
T
jared 126979f5cb
Lint / Shell (shellcheck) (push) Successful in 9s
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (ruff) (push) Successful in 5s
Lint / Python deps (pip-audit) (push) Successful in 44s
Lint / Secret scan (gitleaks) (push) Successful in 6s
hangman: edit board in place + fix ASCII art rendering; wyr: debug reaction logging
- Add edit_html() to utils using m.replace so messages can be updated
- Hangman board now edits in place on every guess — shows progressing
  ASCII figure as wrong guesses accumulate instead of spamming new messages
- Extract _hangman_board_html() helper for consistent board rendering
- wyr: add INFO-level logging to reaction callback to diagnose vote tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 13:30:46 -04:00

133 lines
4.7 KiB
Python

import logging
from functools import wraps
from nio import AsyncClient
from config import BOT_PREFIX, MATRIX_USER_ID
from commands import COMMANDS, metrics, check_scramble_answer, check_riddle_answer, record_wyr_vote
from welcome import handle_welcome_reaction, handle_space_join, SPACE_ROOM_ID
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 ""
# Check active non-command games that monitor all room messages
if body and not body.startswith(BOT_PREFIX):
await check_scramble_answer(self.client, room.room_id, event.sender, body)
await check_riddle_answer(self.client, room.room_id, event.sender, body)
return
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)
async def reaction(self, room, event):
"""Handle m.reaction events (sent as UnknownEvent by matrix-nio)."""
# Ignore events from before startup
if self.startup_sync_token is None:
return
# Ignore our own reactions
if event.sender == MATRIX_USER_ID:
return
# m.reaction events come as UnknownEvent with type "m.reaction"
if not hasattr(event, "source"):
logger.info("reaction: event has no source attr, type=%s sender=%s", type(event).__name__, event.sender)
return
event_type = event.source.get("type", "")
content = event.source.get("content", {})
relates_to = content.get("m.relates_to", {})
rel_type = relates_to.get("rel_type", "")
reacted_event_id = relates_to.get("event_id", "")
key = relates_to.get("key", "")
logger.info(
"reaction: type=%s rel_type=%s key=%r target=%s sender=%s",
event_type, rel_type, key, reacted_event_id[:16] if reacted_event_id else "", event.sender,
)
if rel_type != "m.annotation":
return
await handle_welcome_reaction(
self.client, room.room_id, event.sender, reacted_event_id, key
)
from commands import _WYR_POLLS
logger.info("reaction: wyr polls active=%s matched=%s", list(_WYR_POLLS.keys()), reacted_event_id in _WYR_POLLS)
record_wyr_vote(reacted_event_id, event.sender, key)
async def member(self, room, event):
"""Handle m.room.member events — watch for Space joins."""
# Ignore events from before startup
if self.startup_sync_token is None:
return
# Only care about the Space
if room.room_id != SPACE_ROOM_ID:
return
# Ignore our own membership changes
if event.state_key == MATRIX_USER_ID:
return
# Only trigger on joins (not leaves, bans, etc.)
if event.membership != "join":
return
# Check if this is a new join (prev was not "join")
prev = event.prev_membership if hasattr(event, "prev_membership") else None
if prev == "join":
return # Already was a member, this is a profile update or similar
await handle_space_join(self.client, event.state_key)