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:
100
bot.py
100
bot.py
@@ -8,8 +8,11 @@ from pathlib import Path
|
||||
from nio import (
|
||||
AsyncClient,
|
||||
AsyncClientConfig,
|
||||
InviteMemberEvent,
|
||||
LoginResponse,
|
||||
RoomMemberEvent,
|
||||
RoomMessageText,
|
||||
UnknownEvent,
|
||||
)
|
||||
|
||||
from config import (
|
||||
@@ -17,11 +20,13 @@ from config import (
|
||||
MATRIX_USER_ID,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
MATRIX_DEVICE_ID,
|
||||
MATRIX_PASSWORD,
|
||||
LOG_LEVEL,
|
||||
ConfigValidator,
|
||||
)
|
||||
from callbacks import Callbacks
|
||||
from utils import setup_logging
|
||||
from welcome import post_welcome_message
|
||||
|
||||
logger = setup_logging(LOG_LEVEL)
|
||||
|
||||
@@ -40,16 +45,28 @@ def save_credentials(resp, homeserver):
|
||||
logger.info("Credentials saved to %s", CREDENTIALS_FILE)
|
||||
|
||||
|
||||
def trust_devices(client: AsyncClient):
|
||||
"""Auto-trust all devices for all users we share rooms with."""
|
||||
async def trust_devices(client: AsyncClient):
|
||||
"""Query keys and trust all devices for all users we share rooms with."""
|
||||
if not client.olm:
|
||||
logger.warning("Olm not loaded, skipping device trust")
|
||||
return
|
||||
|
||||
# Collect all users across all joined rooms
|
||||
users = set()
|
||||
for room in client.rooms.values():
|
||||
for user_id in room.users:
|
||||
users.add(user_id)
|
||||
|
||||
# Fetch device keys so the store is complete
|
||||
if users:
|
||||
await client.keys_query()
|
||||
|
||||
# Trust every device
|
||||
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)
|
||||
logger.info("Trusted all known devices")
|
||||
logger.info("Trusted all known devices (%d users)", len(users))
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -70,21 +87,73 @@ async def main():
|
||||
client = AsyncClient(
|
||||
MATRIX_HOMESERVER,
|
||||
MATRIX_USER_ID,
|
||||
device_id=MATRIX_DEVICE_ID,
|
||||
device_id=MATRIX_DEVICE_ID or None,
|
||||
config=client_config,
|
||||
store_path=str(STORE_PATH),
|
||||
)
|
||||
|
||||
# Restore access token (no password login needed)
|
||||
client.access_token = MATRIX_ACCESS_TOKEN
|
||||
client.user_id = MATRIX_USER_ID
|
||||
client.device_id = MATRIX_DEVICE_ID
|
||||
# Try saved credentials first, then .env token, then password login
|
||||
logged_in = False
|
||||
has_creds = False
|
||||
|
||||
# Load the olm/e2ee store if it exists
|
||||
client.load_store()
|
||||
if CREDENTIALS_FILE.exists():
|
||||
creds = json.loads(CREDENTIALS_FILE.read_text())
|
||||
client.access_token = creds["access_token"]
|
||||
client.user_id = creds["user_id"]
|
||||
client.device_id = creds["device_id"]
|
||||
has_creds = True
|
||||
logger.info("Loaded credentials from %s", CREDENTIALS_FILE)
|
||||
elif MATRIX_ACCESS_TOKEN and MATRIX_DEVICE_ID:
|
||||
client.access_token = MATRIX_ACCESS_TOKEN
|
||||
client.user_id = MATRIX_USER_ID
|
||||
client.device_id = MATRIX_DEVICE_ID
|
||||
has_creds = True
|
||||
logger.info("Using access token from .env")
|
||||
|
||||
# Load the olm/e2ee store only if we have a device_id
|
||||
if has_creds:
|
||||
client.load_store()
|
||||
|
||||
# Test the token with a sync; if it fails, fall back to password login
|
||||
if has_creds and client.access_token:
|
||||
logger.info("Testing existing access token...")
|
||||
sync_resp = await client.sync(timeout=30000, full_state=True)
|
||||
if hasattr(sync_resp, "next_batch"):
|
||||
logged_in = True
|
||||
logger.info("Existing token is valid")
|
||||
else:
|
||||
logger.warning("Existing token is invalid, will try password login")
|
||||
client.access_token = ""
|
||||
|
||||
if not logged_in:
|
||||
if not MATRIX_PASSWORD:
|
||||
logger.error("No valid token and no MATRIX_PASSWORD set — cannot authenticate")
|
||||
await client.close()
|
||||
sys.exit(1)
|
||||
logger.info("Logging in with password...")
|
||||
login_resp = await client.login(MATRIX_PASSWORD, device_name="LotusBot")
|
||||
if isinstance(login_resp, LoginResponse):
|
||||
logger.info("Password login successful, device_id=%s", login_resp.device_id)
|
||||
save_credentials(login_resp, MATRIX_HOMESERVER)
|
||||
client.load_store()
|
||||
sync_resp = await client.sync(timeout=30000, full_state=True)
|
||||
else:
|
||||
logger.error("Password login failed: %s", login_resp)
|
||||
await client.close()
|
||||
sys.exit(1)
|
||||
|
||||
callbacks = Callbacks(client)
|
||||
client.add_event_callback(callbacks.message, RoomMessageText)
|
||||
client.add_event_callback(callbacks.reaction, UnknownEvent)
|
||||
client.add_event_callback(callbacks.member, RoomMemberEvent)
|
||||
|
||||
# Auto-accept room invites
|
||||
async def _auto_accept_invite(room, event):
|
||||
if event.membership == "invite" and event.state_key == MATRIX_USER_ID:
|
||||
logger.info("Auto-accepting invite to %s", room.room_id)
|
||||
await client.join(room.room_id)
|
||||
|
||||
client.add_event_callback(_auto_accept_invite, InviteMemberEvent)
|
||||
|
||||
# Graceful shutdown
|
||||
loop = asyncio.get_running_loop()
|
||||
@@ -97,11 +166,7 @@ async def main():
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
loop.add_signal_handler(sig, _signal_handler)
|
||||
|
||||
logger.info("Starting initial sync...")
|
||||
|
||||
# Do a first sync to catch up, then mark startup complete so we only
|
||||
# process new messages going forward.
|
||||
sync_resp = await client.sync(timeout=30000, full_state=True)
|
||||
# Mark startup complete from the initial sync
|
||||
if hasattr(sync_resp, "next_batch"):
|
||||
callbacks.startup_sync_token = sync_resp.next_batch
|
||||
logger.info("Initial sync complete, token: %s", sync_resp.next_batch[:20])
|
||||
@@ -111,7 +176,10 @@ async def main():
|
||||
sys.exit(1)
|
||||
|
||||
# Trust devices after initial sync loads the device store
|
||||
trust_devices(client)
|
||||
await trust_devices(client)
|
||||
|
||||
# Post welcome message (idempotent — only posts if not already stored)
|
||||
await post_welcome_message(client)
|
||||
|
||||
logger.info("Bot ready as %s — listening for commands", MATRIX_USER_ID)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user