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:
150
welcome.py
Normal file
150
welcome.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user