""" Plugin for welcoming new users to the room. Features: * Automatically greets users when they first join the target room. * !welcome command – manually trigger the welcome message for yourself. * Per-user cooldown to prevent welcome spam on repeated part/join cycles. Restricted to room: !NXdVjDXPxXowPkrJJY:matrix.org """ import logging import time import simplematrixbotlib as botlib import nio # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- ALLOWED_ROOM_ID = "!NXdVjDXPxXowPkrJJY:matrix.org" # How many seconds must pass before a user can receive another auto-welcome. # Default: 10 minutes. Raise this if you still see abuse. WELCOME_COOLDOWN_SECONDS = 600 # --------------------------------------------------------------------------- # Cooldown state # Maps Matrix user ID → monotonic timestamp of last auto-welcome sent. # Resets on bot restart, which is fine — a fresh start is a clean slate. # --------------------------------------------------------------------------- _last_welcomed: dict[str, float] = {} def _is_on_cooldown(user_id: str) -> bool: """Return True if the user was welcomed recently and should be skipped.""" last = _last_welcomed.get(user_id) if last is None: return False return (time.monotonic() - last) < WELCOME_COOLDOWN_SECONDS def _record_welcome(user_id: str) -> None: """Mark that this user was just welcomed.""" _last_welcomed[user_id] = time.monotonic() # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- def _build_welcome_message(display_name: str) -> str: """Return the Markdown welcome message for a given display name.""" return f"Hey **{display_name}**, welcome in — so glad to have you here! 🎉 Feel free to tell us a bit about what you're into (self‑hosting, security, homelabs, programming… or just lurk and say hi whenever 🍄)" # --------------------------------------------------------------------------- # Plugin setup - called by funguy.py's run() after the bot object is created # --------------------------------------------------------------------------- def setup(bot): """ Register the member-join listener. funguy.py must call plugin_module.setup(self.bot) for each loaded plugin (after self.bot is created) for this listener to fire. """ @bot.listener.on_custom_event(nio.RoomMemberEvent) async def _on_member_event(room, event): logging.debug( "RoomMemberEvent received: room=%s, sender=%s, membership=%s", room.room_id, event.sender, event.membership, ) # Only the configured room if room.room_id != ALLOWED_ROOM_ID: return # Only on actual join (not invite / leave / ban) if event.membership != "join": return # Ignore the bot itself if event.sender == bot.async_client.user_id: logging.debug("Ignoring bot's own join event") return # Check if this is a genuine first join by looking for previous membership is_first_join = True # Check prev_content directly on the event if hasattr(event, 'prev_content') and event.prev_content: prev_membership = event.prev_content.get('membership') if prev_membership is not None: is_first_join = False logging.debug(f"User {event.sender} had previous membership '{prev_membership}' in prev_content") # Check unsigned field (some events store prev_content there) if is_first_join and hasattr(event, 'unsigned') and event.unsigned: prev_content = event.unsigned.get('prev_content', {}) prev_membership = prev_content.get('membership') if prev_membership is not None: is_first_join = False logging.debug(f"User {event.sender} had previous membership '{prev_membership}' in unsigned") if not is_first_join: logging.info(f"Skipping welcome for {event.sender} - not a first join (name change or rejoin)") return # --- Cooldown check --- if _is_on_cooldown(event.sender): remaining = WELCOME_COOLDOWN_SECONDS - ( time.monotonic() - _last_welcomed[event.sender] ) logging.info( "Skipping welcome for %s – cooldown active (%.0fs remaining)", event.sender, remaining, ) return # Derive a friendly display name from the Matrix ID (@name:server → name) display_name = event.sender.split(":")[0].lstrip("@") logging.info( "User %s joined room %s – sending welcome", event.sender, room.room_id ) try: await bot.api.send_markdown_message( room.room_id, _build_welcome_message(display_name) ) _record_welcome(event.sender) # Only recorded after a successful send logging.info("Welcome message sent successfully to %s", display_name) except Exception as exc: logging.error("Failed to send welcome message: %s", exc) logging.info("Join listener registered for room %s", ALLOWED_ROOM_ID) # --------------------------------------------------------------------------- # Plugin entry point - called by FunguyBot for every message event # --------------------------------------------------------------------------- async def handle_command(room, message, bot, prefix, config): """Handle the !welcome command (not subject to the auto-welcome cooldown).""" match = botlib.MessageMatch(room, message, bot, prefix) if not ( match.is_not_from_this_bot() and match.prefix() and match.command("welcome") ): return if room.room_id != ALLOWED_ROOM_ID: await bot.api.send_text_message( room.room_id, "The !welcome command only works in the designated room.", ) return sender = str(message.sender) display_name = sender.split(":")[0].lstrip("@") await bot.api.send_markdown_message( room.room_id, _build_welcome_message(display_name) ) logging.info("Sent manual !welcome to %s", sender)