Add Wordle, welcome system, integrations, and update roadmap

- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
  infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
  all 12 webhook connections with UUIDs and transform notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 10:29:36 -05:00
parent 5723ac3581
commit dff2f0e2b1
11 changed files with 4324 additions and 167 deletions

View File

@@ -3,6 +3,7 @@ from logging.handlers import RotatingFileHandler
from pathlib import Path
from nio import AsyncClient, RoomSendResponse
from nio.exceptions import OlmUnverifiedDeviceError
from config import MAX_INPUT_LENGTH
@@ -32,10 +33,35 @@ def setup_logging(level="INFO"):
return logger
def _trust_all(client: AsyncClient):
"""Trust all devices in the device store."""
if not client.olm:
return
for user_id, devices in client.device_store.items():
for device_id, olm_device in devices.items():
if not client.olm.is_device_verified(olm_device):
client.verify_device(olm_device)
async def _room_send_trusted(client: AsyncClient, room_id: str, message_type: str, content: dict):
"""Send a message, auto-trusting devices on OlmUnverifiedDeviceError."""
try:
return await client.room_send(
room_id, message_type=message_type, content=content,
ignore_unverified_devices=True,
)
except OlmUnverifiedDeviceError:
_trust_all(client)
return await client.room_send(
room_id, message_type=message_type, content=content,
ignore_unverified_devices=True,
)
async def send_text(client: AsyncClient, room_id: str, text: str):
logger = logging.getLogger("matrixbot")
resp = await client.room_send(
room_id,
resp = await _room_send_trusted(
client, room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": text},
)
@@ -46,8 +72,8 @@ async def send_text(client: AsyncClient, room_id: str, text: str):
async def send_html(client: AsyncClient, room_id: str, plain: str, html: str):
logger = logging.getLogger("matrixbot")
resp = await client.room_send(
room_id,
resp = await _room_send_trusted(
client, room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
@@ -62,8 +88,8 @@ async def send_html(client: AsyncClient, room_id: str, plain: str, html: str):
async def send_reaction(client: AsyncClient, room_id: str, event_id: str, emoji: str):
return await client.room_send(
room_id,
return await _room_send_trusted(
client, room_id,
message_type="m.reaction",
content={
"m.relates_to": {
@@ -75,6 +101,31 @@ async def send_reaction(client: AsyncClient, room_id: str, event_id: str, emoji:
)
async def get_or_create_dm(client: AsyncClient, user_id: str) -> str | None:
"""Find an existing DM room with user_id, or create one. Returns room_id."""
logger = logging.getLogger("matrixbot")
# Check existing rooms for a DM with this user
for room_id, room in client.rooms.items():
if room.member_count == 2 and user_id in (m.user_id for m in room.users.values()):
return room_id
# Create a new DM room
from nio import RoomCreateResponse
resp = await client.room_create(
is_direct=True,
invite=[user_id],
)
if isinstance(resp, RoomCreateResponse):
logger.info("Created DM room %s with %s", resp.room_id, user_id)
# Sync so the new room appears in client.rooms before we try to send
await client.sync(timeout=5000)
return resp.room_id
logger.error("Failed to create DM room with %s: %s", user_id, resp)
return None
def sanitize_input(text: str, max_length: int = MAX_INPUT_LENGTH) -> str:
text = text.strip()[:max_length]
text = "".join(char for char in text if char.isprintable())