"""Welcome module — DM new Space members. When a user joins the Space, the bot sends them a DM with a welcome message and a reaction button. When they react, the bot invites them to the standard public channels (General, Commands, Memes). """ import json import logging from pathlib import Path from nio import AsyncClient from utils import send_html, send_reaction, get_or_create_dm from config import MATRIX_USER_ID logger = logging.getLogger("matrixbot") # The Space room to watch for new members SPACE_ROOM_ID = "!-1ZBnAH-JiCOV8MGSKN77zDGTuI3pgSdy8Unu_DrDyc" # Public channels to invite new members to (skip Management + Cool Kids) INVITE_ROOMS = [ "!wfokQ1-pE896scu_AOcCBA2s3L4qFo-PTBAFTd0WMI0", # General (v12) "!ou56mVZQ8ZB7AhDYPmBV5_BR28WMZ4x5zwZkPCqjq1s", # Commands (v12) "!GK6v5cLEEnowIooQJv5jECfISUjADjt8aKhWv9VbG5U", # Memes (v12) ] WELCOME_EMOJI = "\u2705" # checkmark STATE_FILE = Path("welcome_state.json") def _load_state() -> dict: if STATE_FILE.exists(): try: return json.loads(STATE_FILE.read_text()) except (json.JSONDecodeError, OSError): pass return {} def _save_state(state: dict): try: tmp = STATE_FILE.with_suffix(".tmp") tmp.write_text(json.dumps(state, indent=2)) tmp.rename(STATE_FILE) except OSError as e: logger.error("Failed to save welcome state: %s", e) async def handle_space_join(client: AsyncClient, sender: str): """Called when a new user joins the Space. DM them a welcome message.""" state = _load_state() welcomed = state.get("welcomed_users", []) if sender in welcomed: return logger.info("New Space member %s — sending welcome DM", sender) dm_room = await get_or_create_dm(client, sender) if not dm_room: logger.error("Could not create DM with %s for welcome", sender) return plain = ( "Welcome to The Lotus Guild!\n\n" f"React to this message with {WELCOME_EMOJI} to get invited to all channels.\n\n" "You'll be added to General, Commands, and Memes." ) html = ( "

Welcome to The Lotus Guild!

" f"

React to this message with {WELCOME_EMOJI} to get invited to all channels.

" "

You'll be added to General, Commands, and Memes.

" ) resp = await send_html(client, dm_room, plain, html) if hasattr(resp, "event_id"): # Track the welcome message per user so we can match their reaction dm_messages = state.get("dm_welcome_messages", {}) dm_messages[resp.event_id] = {"user": sender, "dm_room": dm_room} state["dm_welcome_messages"] = dm_messages _save_state(state) # React to our own message to show what to click await send_reaction(client, dm_room, resp.event_id, WELCOME_EMOJI) logger.info("Sent welcome DM to %s (event %s)", sender, resp.event_id) else: logger.error("Failed to send welcome DM to %s: %s", sender, resp) async def handle_welcome_reaction( client: AsyncClient, room_id: str, sender: str, reacted_event_id: str, key: str ): """Handle a reaction to a welcome DM. Invite user to channels.""" if sender == MATRIX_USER_ID: return if key != WELCOME_EMOJI: return state = _load_state() dm_messages = state.get("dm_welcome_messages", {}) entry = dm_messages.get(reacted_event_id) if not entry: return if entry["user"] != sender: return logger.info("Welcome reaction from %s — sending invites", sender) invited_count = 0 for invite_room_id in INVITE_ROOMS: room = client.rooms.get(invite_room_id) if room and sender in (m.user_id for m in room.users.values()): logger.debug("%s already in %s, skipping", sender, invite_room_id) continue try: resp = await client.room_invite(invite_room_id, sender) logger.info("Invited %s to %s: %s", sender, invite_room_id, resp) invited_count += 1 except Exception as e: logger.error("Failed to invite %s to %s: %s", sender, invite_room_id, e) # Mark user as welcomed welcomed = state.get("welcomed_users", []) if sender not in welcomed: welcomed.append(sender) state["welcomed_users"] = welcomed # Remove the DM message entry (one-time use) del dm_messages[reacted_event_id] state["dm_welcome_messages"] = dm_messages _save_state(state) # Confirm in DM from utils import send_text if invited_count > 0: await send_text(client, room_id, f"You've been invited to {invited_count} channel(s). Check your invites!") else: await send_text(client, room_id, "You're already in all the channels!") async def post_welcome_message(client: AsyncClient): """No-op kept for backward compatibility with bot.py startup.""" logger.info("Welcome module ready — watching Space for new members")