"""moderator.py: a word filter for your chat rooms, in Python. It reads every message, and when one has a word from words.txt it warns the sender; at the third warning within ten minutes it removes them from the room (REMOVE=kick), or removes them and keeps them out a while (REMOVE=ban). Python 3.10+. pip install -r requirements.txt, then python moderator.py (reads .env). Warnings go to the room, which everyone sees, or with WARN_ALONE=1 to that member only; and REMOVE=kick or REMOVE=ban takes them out of that one room. Its key (dashboard, New API key): rights subscribe and client_events, kind presence, channels: prefix presence-chat-. With REMOVE=kick also the right kick; with REMOVE=ban the right ban. """ import re import time import unicodedata from pathlib import Path from pikoman_bot import Bot, env ROOMS = env("ROOMS", "presence-chat-lobby").split(",") REMOVE = env("REMOVE") # "", "kick" or "ban" (the key needs that right) WARN_ALONE = env("WARN_ALONE") == "1" # warn the member alone BAN_SECONDS = int(env("BAN_SECONDS", "600")) # at most a day STRIKES = 3 # warnings before removal WINDOW = 10 * 60 # warnings older than ten minutes are forgotten def plain(text: str) -> str: """Text as it is compared: lower case, accents off, anything but letters and digits a space.""" text = "".join(c for c in unicodedata.normalize("NFKD", text) if not unicodedata.combining(c)) return " " + re.sub(r"[^\w]+|_", " ", text.lower()) + " " # One word or phrase per line; lines starting with # are comments. lines = (Path(__file__).with_name("words.txt")).read_text(encoding="utf-8").splitlines() WORDS = [plain(line.strip()) for line in lines if line.strip() and not line.strip().startswith("#")] bot = Bot("Moderator", access=3) def moderate(room): strikes = {} # user -> times of their warnings @room.on("message") async def message(sender, data, info): if room.is_bot(sender): return text = plain(str((data or {}).get("text", ""))) if not any(word in text for word in WORDS): return # Who it is: the user id your backend signed into their member data, never what they wrote. member = room.member(sender) user = member.get("user_id", sender) who = member.get("name", "Someone") now = time.time() recent = [t for t in strikes.get(user, []) if now - t < WINDOW] + [now] strikes[user] = recent if len(recent) < STRIKES or not REMOVE: warning = f"{who}, please keep it friendly (warning {min(len(recent), STRIKES)} of {STRIKES})." if WARN_ALONE: await room.send_to(sender, "message", {"text": warning}) else: await room.send("message", {"text": warning}) if len(recent) >= STRIKES: print(f"{room.name}: {who} ({user}) has {len(recent)} warnings") return # The third warning: out of this room. Every connection of that user, when it has a user id. target = {"user_id": member["user_id"]} if "user_id" in member else sender reason = "Language, after two warnings" await room.kick(target, reason, BAN_SECONDS if REMOVE == "ban" else None) strikes.pop(user, None) await room.send("message", {"text": f"{who} was removed from the room."}) for name in ROOMS: moderate(bot.join(name)) print(f"Moderating {', '.join(ROOMS)}: {len(WORDS)} words, {REMOVE or 'warnings only'}") bot.start()