- 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>
151 lines
4.9 KiB
Python
151 lines
4.9 KiB
Python
"""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 = (
|
|
"<h3>Welcome to The Lotus Guild!</h3>"
|
|
f"<p>React to this message with {WELCOME_EMOJI} to get invited to all channels.</p>"
|
|
"<p>You'll be added to <b>General</b>, <b>Commands</b>, and <b>Memes</b>.</p>"
|
|
)
|
|
|
|
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")
|