#!/usr/bin/env python3 # -*- coding: utf-8 -*- #!c=0 # ^^^^ <--> Nomadnet specific to prevent page caching, may change if you like. ################################################################################################################################### ## Welcome To: THE CHATROOM! - v2.00 by F. - The First Reticulum / Nomadnet IRC-STYLE Chat - Nomadnet & Meshchat compatible! ## ## ## ## ## ## Full info on the official GitHub ReadMe: https://github.com/fr33n0w/thechatroom ## ## ## ## Come To Visit and Join The Original ChatRoom Nomadnet to see it in action: ## ## NomadNet Link: d251bfd8e30540b5bd219bbbfcc3afc5:/page/index.mu ## ## ## ## ## ## THIS NOMADNET PAGE PYTHON SCRIPT IS FREE AND OPEN SOURCE, PLEASE KEEP ORIGINAL LINKS INSIDE TO SUPPORT THE DEVELOPER'S WORK! ## ################################################### ENJOY YOUR NEW CHATROOM! ###################################################### ######## IMPORT MODULES: ######## import os, sys, json, time, random, re, sqlite3 ######## INITIALIZE LOG (LOCAL SYSTEM INFO MESSAGES) ##### log = [] ######## SYS & FILE PATHS ######## DB_PATH = os.path.join(os.path.dirname(__file__), "chatusers.db") EMO_DB = os.path.join(os.path.dirname(__file__), "emoticons.txt") ######## DB CREATION IF MISSING (on first start usually) ###### if not os.path.exists(DB_PATH): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" CREATE TABLE users ( id INTEGER PRIMARY KEY, remote_identity TEXT, dest TEXT, display_name TEXT ); """) conn.commit() conn.close() ######## DISPLAY LIMIT SETTINGS: ######## (Keeps UI fixed in the meshchat browser) MAX_CHARS = 103 # Adjust as needed to split messages after N chars MAX_LINES = 28 # Max lines on screen ######## MASTER SYSADMIN SETTINGS ######## (USE COMPLEX NICKNAMES FOR THE SYSADMINS!) SYSADMIN = "setyouradminlongnamehere" # SET YOUR MASTER ADMIN NICKNAME FOR CHAT ADMIN COMMANDS ######## UI Unicode Emojis: ######## user_icon = "\U0001F464" # "\U0001F464" # "\U0001F465" - "\U0001FAAA" message_icon = "\U0001F4AC" msg2_icon = "\u2709\ufe0f" send_icon = "\U0001F4E4" totmsg_icon = "\U0001F4E9" reload_icon = "\u21BB" setup_icon = "\u2699\ufe0f" cmd_icon = "\U0001F4BB" # \U0001F579 nickset_icon = "\U0001F504" info_icon = "\u1F6C8" stats_icon = "\u1F4DD" ######## Antispam filters: ######## (Add or remove what you want to allow or not) spam_patterns = [ r"buy\s+now", r"free\s+money", r"fr[e3]{2}\s+m[o0]ney", r"click\s+here", r"cl[i1]ck\s+h[e3]re", r"subscribe\s+(now|today)", r"win\s+big", r"w[i1]n\s+b[i1]g", r"limited\s+offer", r"act\s+now", r"get\s+rich\s+quick", r"make\s+money\s+fast", r"easy\s+cash", r"work\s+from\s+home", r"double\s+your\s+income", r"guaranteed\s+results", r"risk[-\s]*free", r"lowest\s+price", r"no\s+credit\s+check", r"instant\s+approval", r"earn\s+\$\d+", r"cheap\s+meds", r"online\s+pharmacy", r"lose\s+weight\s+fast", r"miracle\s+cure", r"bitcoin\s+offer", r"b[i1]tcoin\s+deal", r"earn\s+bitcoin", r"make\s+money\s+with\s+bitcoin", r"crypto\s+investment", r"crypto\s+deal", r"get\s+rich\s+with\s+crypto", r"eth[e3]reum\s+promo", r"buy\s+crypto\s+now", r"invest\s+in\s+(crypto|bitcoin|ethereum)", r"\bfree\s+(bitcoin|crypto|ethereum)\b", r"\bsell\s+(bitcoin|crypto|ethereum)\b", r"\bi\s+sell\s+(bitcoin|bitcoins|crypto|ethereum)\b", r"\bbuy\s+(bitcoin|crypto|ethereum)\b", r"\bget\s+(bitcoin|crypto|ethereum)\b", r"\bmake\s+money\s+(with|from)\s+(bitcoin|crypto|ethereum)\b", r"\binvest\s+(in|into)\s+(bitcoin|crypto|ethereum)\b", r"\bbitcoin\s+(promo|deal|offer|discount)\b", r"\bcrypto\s+(promo|deal|offer|discount)\b", r"\bfree\s+(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\b", r"\b(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\s+for\s+you\b", r"\bsell\s+(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\b", r"\bbuy\s+(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\b", r"\bi\s+sell\s+(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\b", r"\bget\s+(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\b", r"\bmake\s+money\s+(with|from)\s+(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\b", r"\binvest\s+(in|into)\s+(bitcoin|bitcoins|coin|coins|crypto|tokens|ethereum)\b", r"(?:\W|^)(bitcoin|bitcoins|crypto|ethereum|tokens|coins)(?:\W|$)", r"\b(bitcoin|bitcoins|crypto|ethereum|tokens|coins)\s+for\s+(free|you)\b", r"\bfree\s+(bitcoin|bitcoins|crypto|ethereum|tokens|coins)\b", r"\bget\s+(bitcoin|bitcoins|crypto|ethereum|tokens|coins)\b", r"\bmake\s+money\s+(with|from)\s+(bitcoin|bitcoins|crypto|ethereum|tokens|coins)\b", r"\b(sex|porn|xxx|nude|nudes|nsfw|onlyfans|camgirl|camgirls|adult\s+video|erotic|blowjob|anal|fetish|strip|escort|hardcore|incest|milf|hentai|boobs|naked|cumshot|threesome|gangbang|squirting|deepthroat)\b", r"\b(pornhub|xvideos|redtube|xnxx|xhamster|cam4|chaturbate|brazzers|bangbros|spankbang|fleshlight|adultfriendfinder|livejasmin|myfreecams|stripchat|sex.com)\b", r"\bwatch\s+(live\s+)?(sex|porn|camgirls|nudes)\b", r"\bfree\s+(porn|cams|nudes|xxx|sex\s+videos)\b", r"\bhot\s+(girls|milfs|teens|models)\s+(live|online|waiting)\b", r"\bclick\s+(here|link)\s+(for|to)\s+(sex|porn|nudes|xxx|cam)\b", r"\b(see|watch|join)\s+(my|our)?\s*(onlyfans|cam|sex\s+show)\b", r"\b(win(?:ner)?|guaranteed|prize|cash|credit|loan|investment|rich|easy\s+money|urgent)\b", r"\b(click\s+here|act\s+now|limited\s+time|exclusive\s+deal|verify\s+your\s+account|update\s+required|login\s+now|reset\s+password)\b", r"\b(discount|sale|offer|promo|buy\s+now|order\s+today|lowest\s+price|cheap|bargain|deal)\b", r"\b(bit\.ly|tinyurl\.com|goo\.gl|freegift|get-rich|fastcash|adult|xxx|cams|nudes)\b", r"\b(make\s+\$\d{2,}|earn\s+\$\d{2,}|work\s+from\s+home|no\s+experience\s+needed)\b", r"\b(earn|make)\s+(money|cash)\s+(from\s+home|online|fast|easily)\b", r"\b(work\s+from\s+home|no\s+experience\s+needed|easy\s+income|passive\s+income)\b", r"\b(start\s+earning|get\s+paid\s+daily|quick\s+cash|instant\s+money)\b", r"\b(earn|make)\s+(money|cash)\s+(from|at)\s+home\b", r"\b(work\s+(from|at)\s+home|easy\s+income|passive\s+income)\b", r"\b(start\s+earning|get\s+paid\s+(daily|instantly)|quick\s+cash|instant\s+money)\b", r"\b(work\s+(from|at)\s+home|easy\s+income|passive\s+income|get\s+paid\s+(daily|instantly)|quick\s+cash|instant\s+money)\b", r"\b(earn|make|get)\s+(money|cash|income)\s*(now|fast|quickly|easily)?\b", r"\b(passive\s+income|easy\s+money|no\s+experience|required|work\s+online|get\s+paid\s+(daily|instantly))\b", r"\b(earn|make|receive)\s+(some\s+)?(money|cash|income|profit|revenue)\b", ] ################### Nickname Auto-Color System ##################### (Change colors if you want) colors = [ "B900", "B090", "B009", "B099", "B909", "B066", "B933", "B336", "B939", "B660", "B030", "B630", "B363", "B393", "B606", "B060", "B003", "B960", "B999", "B822", "B525", "B255", "B729", "B279", "B297", "B972", "B792", "B227", "B277", "B377", "B773", "B737", "B003", "B111", "B555", "B222", "B088", "B808", "B180" ] def get_color(name): return colors[sum(ord(c) for c in name.lower()) % len(colors)] ######### Recover input from Os Environment variables ######## def recover_input(key_suffix): for k, v in os.environ.items(): if k.lower().endswith(key_suffix): return v.strip() return "" raw_username = recover_input("username") message = recover_input("message") remote_identity = recover_input("remote_identity") nickname = recover_input("field_username") # This is prioritized dest = recover_input("dest") # Fallback to command-line arguments if needed if not raw_username and len(sys.argv) > 1: raw_username = sys.argv[1].strip() if not message and len(sys.argv) > 2: message = sys.argv[2].strip() if not dest and len(sys.argv) > 3: dest = sys.argv[3].strip() # Extract hash code from remote identity and LXMF address hash_code = remote_identity[-4:] if remote_identity else "" dest_code = dest[-4:] if dest else "" # Smart fallback for display name with logging if nickname: display_name = nickname elif dest: display_name = f"Guest_{dest_code}" else: display_name = "Guest" # sql db nick binding and recovering def init_db(): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, remote_identity TEXT, dest TEXT UNIQUE NOT NULL, display_name TEXT ) """) conn.commit() conn.close() def get_display_name_from_db(dest): if not dest: return None conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT display_name FROM users WHERE dest = ?", (dest,)) result = cursor.fetchone() conn.close() return result[0] if result else None def save_user_to_db(remote_identity, dest, display_name): if not remote_identity or not dest: return # Don't save if required info is missing conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" INSERT INTO users (remote_identity, dest, display_name) VALUES (?, ?, ?) ON CONFLICT(dest) DO UPDATE SET remote_identity = excluded.remote_identity, display_name = excluded.display_name """, (remote_identity, dest, display_name)) conn.commit() conn.close() # Initialize DB init_db() # Get environment variables nickname = os.getenv("field_username", "").strip() dest = os.getenv("dest", "").strip() remote_identity = os.getenv("remote_identity", "").strip() # Try to load display_name from DB db_display_name = get_display_name_from_db(dest) # Determine final display_name with logging nickname_recovered_from_db = False if nickname: display_name = nickname log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Nickname recovered from environment: {display_name} `!`" }) elif db_display_name: display_name = db_display_name nickname_recovered_from_db = True log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Nickname recovered from database: {display_name} `!`" }) elif dest: display_name = f"Guest_{dest[-4:]}" log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` No nickname found. Using fingerprint: {display_name} `!`" }) else: display_name = "Guest" log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "`!` No nickname or fingerprint found. Defaulting to Guest `!`" }) # Save user to DB if valid save_user_to_db(remote_identity, dest, display_name) # ----------------------------------------------- safe_username = ( raw_username.replace("`", "").replace("<", "").replace(">", "") .replace("\n", "").replace("\r", "").replace('"', "").replace("'", "") .replace("/", "").replace("\\", "").replace(";", "").replace(":", "") .replace("&", "").replace("=", "").replace("{", "").replace("}", "") .replace("[", "").replace("]", "").replace("(", "").replace(")", "") .replace("\t", "").replace("*", "").replace("+", "").replace("%", "") .replace("#", "").replace("^", "").replace("~", "").replace("|", "") .replace("$", "").replace(" ", "").strip() or "Guest" ) # Chat Topic functions topic_file = os.path.join(os.path.dirname(__file__), "topic.json") try: with open(topic_file, "r") as tf: topic_data = json.load(tf) topic_text = topic_data.get("text", "Welcome to the chatroom!") topic_author = topic_data.get("user", "System") except: topic_text = "Welcome to the chatroom!" topic_author = "System" log_file = os.path.join(os.path.dirname(__file__), "chat_log.json") debug = [] try: with open(log_file, "r") as f: log = json.load(f) debug.append(f" Total {len(log)} messages") except Exception as e: log = [] debug.append(f"Failed to load log: {e}") # USER COMMANDS LOGIC: cmd = message.strip().lower() ##### ADMIN COMMANDS ##### if safe_username == SYSADMIN and cmd.startswith("/clear"): parts = cmd.split() if len(parts) == 1: # /clear ? remove last message if log: removed = log.pop() debug.append(f"Removed last message: <{removed['user']}> {removed['text']}") else: debug.append("No messages to clear.") elif len(parts) == 2 and parts[1].isdigit(): # /clear N remove last N messages count = int(parts[1]) removed_count = 0 while log and removed_count < count: removed = log.pop() debug.append(f"Removed: <{removed['user']}> {removed['text']}") removed_count += 1 debug.append(f"Cleared last {removed_count} messages.") elif len(parts) == 3 and parts[1] == "user": # /clear user NICKNAME remove all messages from that user target_user = parts[2] original_len = len(log) log[:] = [msg for msg in log if msg.get("user") != target_user] removed_count = original_len - len(log) debug.append(f"Cleared {removed_count} messages from user '{target_user}'.") else: debug.append("Invalid /clear syntax. Use /clear, /clear N, or /clear user NICKNAME.") # Save updated log try: with open(log_file, "w", encoding="utf-8") as f: json.dump(log, f, indent=2, ensure_ascii=False) debug.append("Log updated after clearing.") except Exception as e: debug.append(f"Clear command error: {e}") elif safe_username == SYSADMIN and cmd == "/clearall": if log: log.clear() debug.append("All messages cleared by admin.") try: with open(log_file, "w", encoding="utf-8") as f: json.dump(log, f, indent=2, ensure_ascii=False) debug.append("Log successfully emptied.") except Exception as e: debug.append(f"ClearAll error: {e}") else: debug.append("Log already empty. Nothing to clear.") ########## CHAT USERS COMMANDS ######### #### STATS COMMAND #### elif cmd == "/stats": user_stats = {} user_set = set() for msg in log: if msg["user"] != "System": user_stats[msg["user"]] = user_stats.get(msg["user"], 0) + 1 user_set.add(msg["user"]) total_users = len(user_set) total_messages = len(log) top_users = sorted(user_stats.items(), key=lambda x: x[1], reverse=True) # Prepare lines log.append({"time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "`!` Stats Report: `!` "}) log.append({"time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Total messages: {total_messages} `!` "}) log.append({"time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Total users: {total_users} `!` "}) # Combine top chatters in one line top_line = "`!` Top chatters: `!` " + " , ".join([f"`!` {user} ({count} msg) `!`" for user, count in top_users[:5]]) log.append({"time": time.strftime("[%a,%H:%M]"), "user": "System", "text": top_line}) ############ /users COMMAND ############## elif cmd == "/users": # Count messages per user from collections import Counter user_counts = Counter(msg["user"] for msg in log if msg["user"] != "System") # Sort by most active sorted_users = sorted(user_counts.items(), key=lambda x: -x[1]) total_users = len(sorted_users) # Header line log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Active Users List and Stats, Total Users: ({total_users}) `! " }) # Show in chunks of N with message counts for i in range(0, total_users, 7): chunk = ", ".join(f"`!` {user} `!({count}msg)" for user, count in sorted_users[i:i+7]) log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": chunk }) ############# /cmd COMMAND INFO LINES ############ elif cmd == "/cmd": help_lines = [ f"`!{message_icon} THE CHATROOM!{message_icon} \\ EXTENDED USER COMMANDS INFO:`!", f"`!GENERAL USE AND INFORMATIONAL COMMANDS:`!", f"`!/info`! : Show The Chat Room! Informations, Usage and Disclaimer", f"`!/cmd`! : Show all the available user commands", f"`!/stats`! : Show chatroom statistics, including Top 5 Chatters", f"`!/users`! : List all chatroom users", f"`!/version`! : Show THE CHAT ROOM! script version, news and infos", f"`! {cmd_icon} INTERACTIVE CHAT COMMANDS`!", "`!/lastseen `!`: Last seen user info and latest user message", "`!/topic`!` : Show or Change Room Topic, usage: '/topic' or '/topic Your New Topic Here' ", "`!/search `!` : Search for keywords in the full chatlog ", "`!/time`!` : Show current Chat Server Time (UTC)", "`!/ping`!` : Reply with PONG! if the chat system is up and working", "`!/meteo `! : Get weather info for your city, example: /meteo Miami", "--------------------------------------", f"`! {cmd_icon} SOCIAL INTERACTIONS COMMANDS`!", "`!` /e`!` : Sends randomized emojis from the internal emoji list", "`!` /c `!` : Sends a colored chat message with randomized background and font colors", "`!` @nickname`!` : Sends a colored mention to highlight the mentioned user in a reply message", "`!` $e`!` : Sends a random emoticon using '$e', usable in every part of the message. ", "`!` $link`!` : Higlight your links, example: $link d251bfd8e30540b5bd219bbbfcc3afc5:/page/index.mu ", "`!` /welcome`! : Sends a welcome message. Usage: /welcome or /welcome . ", f"`!` {cmd_icon} USER STATUS INTERACTIONS COMMANDS`!`", "`!` /hi, /bye, /brb, /lol, /exit, /quit, /away, /back, /notice `!`", "`!` Commands Usage Example: /hi OR /hi Hello World! `! (Syntax is valid for all the above commands!)", "--------------------------------------", f"`!` {cmd_icon} ADMIN COMMANDS INFO: /admincmd (Only admins allowed to perform this command) `!`", "`!` --------- END OF COMMAND LIST: `[CLICK TO RELOAD THE PAGE`:/page/meshchat.mu`username]` --------- `!", ] for line in help_lines: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": line }) ######## /admincmd admin command ######## elif cmd == "/admincmd": if safe_username == SYSADMIN: admin_lines = [ f"`! {cmd_icon} ADMIN COMMANDS INFO `!", "`! You have access to restricted administrative functions.`!", "`! /clear `! : Deletes last message from the chatroom and database permanently", "`! /clear N`! : Deletes last N messages from the chatroom and database permanently, example: /clear 3", "`! /clear user `! : Delete all messages from a specified user permanently", "`! /clearall `! : Permanently clear the whole chatroom log and database (Irreversible: use with caution!)", "`! /backup `! : Creates a full chat_log.json database backup in the same chatroom script folder", "--------------------------------------", "`! END OF ADMIN COMMANDS LIST `!" ] for line in admin_lines: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": line }) else: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "`! ERROR: You do not have permission to use /admincmd. This command is restricted to SYSADMYN.`!" }) ######### /backup admin command ######## elif cmd == "/backup": if safe_username == SYSADMIN: try: # Create timestamped backup filename in the same directory timestamp = time.strftime("%Y%m%d_%H%M%S") backup_file = os.path.join(os.path.dirname(__file__), f"chat_log_backup_{timestamp}.json") # Perform the backup import shutil shutil.copy(log_file, backup_file) log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`! Backup successful: {backup_file}`!" }) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`! ERROR: Backup failed. Reason: {str(e)}`!" }) else: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "`! ERROR: You do not have permission to use /backup - This command is restricted to SYSADMIN.`!" }) ######## INFO COMMAND ######### elif cmd == "/info": info_lines = [ "`! The Chat Room v2.00 Info - Overview - Usage - Commands - Disclaimer - README! :) `!", "Welcome! This space is designed to connect people through an old school irc-styled interface.", "No registration required, set your nickname and you are ready to message with other users.", "Nicknames are randomly colorized and there is persistent color for every nickname.", "No privacy compromission: use any nick you want. Nothing is recorded or associated to your rns identity.", "This runs on a Nomadnet, so it will be visible internationally. Respect all the user languages in chat.", "This chat is based on micron, sql3 db and python components.", "You can send irc-style messages and use various commands to explore the chatroom.", "`!` Command Reference `!`", "Just Some Examples:", "/users : show active users and message counts", "/lastseen : check a user's recent activity", "/topic : show or change the room topic", "/stats : show chat stats including top chatters", "`!` Use /cmd to view the full list of available commands. `!`", "`!` Technical Notes `!`", "Due to micron limitations, the chatroom does not refresh automatically.", "To see new incoming messages, reload the page using the provided link buttons.", "Especially on Nomadnet: Reload using the provided link in the bottom bar to avoid duplicate messages!", "Refreshing the page using meshchat browser function will remove nickname persistance, so use our Reload button", "To have a nickname persistency, use the Meshchat v2.+ Fingerprint Button to save and recall (lxmf binding).", "The main chatroom shows the last ~30 messages; use the button at the bottom to view the full chat log.", "`!` DISCLAIMER `!`", "This chatroom is a space for connection, collaboration, and respectful interaction.", "Rude, offensive, or inappropriate behavior is not tolerated. Messages may be deleted.", "Suspension or message deletion can occur without prior warning in serious or repeated cases.", "`!` BEFORE FREE SPECH, COMES RESPECT! - WELCOME TO >>THE CHAT ROOM!<< `!`" ] for line in info_lines: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": line }) ############ TIME COMMAND ############### elif cmd == "/time": server_time = time.strftime("%Y-%m-%d %H:%M:%S") try: import pytz, datetime user_time = datetime.datetime.now(pytz.timezone("Europe/Rome")).strftime("%Y-%m-%d %H:%M:%S") except: user_time = "(Local time not available)" time_text = f"Server time: {server_time} // User time (Naples): {user_time}" log.append({"time": time.strftime("[%a,%H:%M]"), "user": "System", "text": time_text}) ########## VERSION COMMAND ######### elif cmd == "/version": version_messages = [ "The Chat Room v2.00 / Powered by Reticulum NomadNet / IRC Style / Nomadnet & Meshchat Compatible / Made by F", "This chat is running on a VPS server, powered by RNS v1.0.0 and Nomadnet v.0.8.0.", "Latest Implementations in v1.3b: AntiSpam Filter and Nickname persistency (Thanks To: Thomas!!)", "Latest Implementations in v1.4b: Improved UI with Message splitting on long messages", "Latest Implementations in v1.44b: Improved UI, resolved few UI bugs, added Menu Bar on the bottom, added /search command, added 'Read Last 100 Messages', started implementing user settings (for future user preferences: custom nickname colors, multiple chat themes and more...coming soon!)", "Latest Implementations in v1.45b:", "Added Social Interactions Commands, for full command list: /cmd", "Improved UI and readability, fixed dysplay limit function!", "Latest Implementations in v1.45a:", "Alpha Stable Version Release Ready - Improved display limit function", "Added SYSADMIN commands (type /admincmd for help, only allowed for SYSADMIN) ", "Improved AntiSpam Filters, Better UI Timestamp, Added /meteo command", "The ChatRoom v2.00 improvements:", "Code Cleaning , Nomadnet and Meshchat supported, new intro page, timestamp mod, overall script and page improvements", "`! Get The ChatRoom at: https://github.com/fr33n0w/thechatroom `!" ] for msg in version_messages: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": msg }) ######## LASTSEEN COMMAND ######## elif cmd.startswith("/lastseen "): target_user = cmd[10:].strip() last = next((msg for msg in reversed(log) if msg["user"] == target_user), None) seen_text = f"Last seen {target_user} at {last['time']}: {last['text']}" if last else f"No record of user '{target_user}'." log.append({"time": time.strftime("[%a,%H:%M]"), "user": "System", "text": seen_text}) ######## TOPIC COMMAND ######## elif cmd.startswith("/topic "): new_topic = message[7:].replace("`", "").strip() if new_topic: trimmed_topic = new_topic[:70] # limit to N characters timestamp = time.strftime("%d %B %Y") topic_data = {"text": trimmed_topic, "user": safe_username, "time": timestamp} try: with open(topic_file, "w") as tf: json.dump(topic_data, tf) log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"Topic set by {safe_username} on {timestamp}: {trimmed_topic} , Reload The Page!" }) except Exception as e: debug.append(f"Topic update error: {e}") else: debug.append("No topic text provided.") elif cmd == "/topic": log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"Current Topic: {topic_text} (set by {topic_author} on {topic_data.get('time')})" }) ######## SEARCH COMMAND ######## elif cmd.startswith("/search"): search_input = message[8:].strip().lower() if not search_input: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "`!` Error! Command Usage: /search - Please provide one or more keywords! `!`" }) else: keywords = search_input.split() matches = [] for msg in log: if msg.get("user") == "System": continue text = msg.get("text", "").lower() if all(kw in text for kw in keywords): matches.append(msg) log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Search Results for: '{search_input}' - {len(matches)} match(es) found. `!`" }) for match in matches[:10]: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"[{match.get('time', '??')}] <{match.get('user', '??')}> {match.get('text', '')}" }) if len(matches) > 10: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "`!` Showing first 10 results. Refine your search for more specific matches. `!`" }) ######## PING COMMAND ######## elif cmd == "/ping": log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "PONG! (System is up and working!)" }) ######### /e RANDOM EMOJIS COMMAND ######## elif cmd == "/e": try: with open(EMO_DB, "r", encoding="utf-8") as f: emojis = [line.strip() for line in f if line.strip()] if emojis and safe_username: import random chosen = random.choice(emojis) # Treat emoji as a normal message log.append({ "time": time.strftime("[%a,%H:%M]"), "user": safe_username, "text": chosen }) try: with open(log_file, "w") as f: json.dump(log, f) debug.append(f" Emoji by '{safe_username}' sent: {chosen}") except Exception as e: debug.append(f" Emoji send error: {e}") else: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": "`!` Emoji list is empty or username missing. `!`" }) debug.append(" Emoji command skipped: missing emoji or username.") except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error loading emojis: {e} `!`" }) debug.append(f" Emoji command error: {e}") ######## ##### COLOR /c COMMAND ######## ###### elif cmd.startswith("/c "): user_message = message[3:].strip().replace("`", "") # Remove backticks to avoid formatting issues if user_message and safe_username: import random, json def hex_brightness(hex_code): r = int(hex_code[0], 16) g = int(hex_code[1], 16) b = int(hex_code[2], 16) return (r + g + b) / 3 # Generate random hex color for background bg_raw = ''.join(random.choices("0123456789ABCDEF", k=3)) bg_color = f"B{bg_raw}" # Calculate brightness brightness = hex_brightness(bg_raw) font_color = "F000" if brightness > 7.5 else "FFF" # Split message into chunks of 80 characters def split_and_colorize(text, chunk_size=80): chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)] return '\n'.join([f"`{bg_color}`{font_color}` {chunk} `b`f" for chunk in chunks]) colorful_text = split_and_colorize(user_message) # Create log entry entry = { "time": time.strftime("[%a,%H:%M]"), "user": safe_username, "text": colorful_text } log.append(entry) # Write to JSON file try: with open(log_file, "w", encoding="utf-8") as f: json.dump(log, f) debug.append(f"Test: Colored Message succesfully sent! by '{safe_username}'") except Exception as e: debug.append(f"Error sending colored message: {e}") else: debug.append("Error: Color command skipped due to missing message or username.") ###### /HI COMMAND ####### elif cmd.startswith("/hi"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} has joined The Chat Room!" if user_message: full_text = f" `!{base_text} Message: {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /hi command: {e} `!`" }) ###### /BYE COMMAND ####### elif cmd.startswith("/bye"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} is leaving The Chat Room!" if user_message: full_text = f" `!{base_text} Message: {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /bye command: {e} `!`" }) ###### /quit COMMAND ####### elif cmd.startswith("/quit"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} has quit The Chat Room!" if user_message: full_text = f" `!{base_text} Message: {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /quit command: {e} `!`" }) ###### /exit COMMAND ####### elif cmd.startswith("/exit"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} has left The Chat Room!" if user_message: full_text = f" `!{base_text} Message: {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /exit command: {e} `!`" }) ###### /BRB COMMAND ####### elif cmd.startswith("/brb"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} has left The Chat Room! I'LL BE RIGHT BACK! BRB!" if user_message: full_text = f" `!{base_text} Message: {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /brb command: {e} `!`" }) ###### /lol COMMAND ####### elif cmd.startswith("/lol"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} is Laughing Out Loud! LOL! :D " if user_message: full_text = f" `!{base_text} Message: {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /lol command: {e} `!`" }) ###### /away COMMAND ####### elif cmd.startswith("/away"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} is away." if user_message: full_text = f" `!{base_text} (Status: {user_message}) `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /away command: {e} `!`" }) ###### /back COMMAND ####### elif cmd.startswith("/back"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} is back! " if user_message: full_text = f" `!{base_text} {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /back command: {e} `!`" }) ###### /welcome COMMAND ####### elif cmd.startswith("/welcome"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"{colored_nickname} Welcomes " if user_message: full_text = f" `!{base_text} {user_message} `!" else: full_text = f" `!{base_text} everyone! `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /welcome command: {e} `!`" }) ###### /notice COMMAND ####### elif cmd.startswith("/notice"): try: parts = cmd.split(" ", 1) user_message = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") # Get color code for nickname nickname_color = get_color(safe_username) # Format nickname using your markup style colored_nickname = f"`{nickname_color}{safe_username}`b" # Build message base_text = f"NOTICE FROM {colored_nickname}:" if user_message: full_text = f" `!{base_text} {user_message} `!" else: full_text = f" `!{base_text} `!" log.append({ "time": timestamp, "user": "System", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"`!` Error processing /notice command: {e} `!`" }) ####### METEO COMMAND ####### elif cmd.startswith("/meteo"): try: from geopy.geocoders import Nominatim import requests # Extract city name parts = cmd.split(" ", 1) city_name = parts[1].strip() if len(parts) > 1 else "" timestamp = time.strftime("[%a,%H:%M]") if not city_name: raise ValueError("No city name provided. Example: /meteo New York") # Geolocation geolocator = Nominatim(user_agent="weather_bot") location = geolocator.geocode(city_name) if not location: raise ValueError(f"Could not find location for '{city_name}'.") lat, lon = location.latitude, location.longitude # Open-Meteo API call weather_url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true" response = requests.get(weather_url) data = response.json() if "current_weather" not in data: raise ValueError("Weather data not available.") temp = data["current_weather"]["temperature"] code = data["current_weather"]["weathercode"] # Weather code mapping (no emojis) weather_codes = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle", 57: "Dense freezing drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain", 71: "Slight snow fall", 73: "Moderate snow fall", 75: "Heavy snow fall", 77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail" } description = weather_codes.get(code, "Unknown weather") # Format nickname nickname_color = get_color(safe_username) colored_nickname = f"`{nickname_color}{safe_username}`b" weather_text = f"Weather in {city_name}: {temp}C, {description}" full_text = f"Meteo Request from {colored_nickname}: {weather_text} " log.append({ "time": timestamp, "user": "Meteo", "text": full_text }) with open(log_file, "w") as f: json.dump(log, f) except Exception as e: log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"Error processing /meteo command: {e} " }) ##################### END OF COMMANDS, CONTINUE SCRIPT ############################## elif raw_username and message and message.lower() != "null": sanitized_message = message.replace("`", "").replace("[", "") # remove backticks and [ to prevent formatting issues ######### Spam detection logic ######## # banned_words = ["buy now", "free money", "click here", "subscribe", "win big", "limited offer", "act now"] , # edit your spam filters on top of the script trigger_word = next((pattern for pattern in spam_patterns if re.search(pattern, sanitized_message.lower())), None) is_spam = trigger_word is not None if is_spam: # ?? Don't write to JSON, just log the system message log.append({ "time": time.strftime("[%a,%H:%M]"), "user": "System", "text": f"Spam Detected! Message Blocked! Triggered by: '{trigger_word}'" }) debug.append(f" Spam blocked from '{safe_username}'") else: # ? Normal message flow log.append({ "time": time.strftime("[%a,%H:%M]"), "user": safe_username, "text": sanitized_message }) try: with open(log_file, "w") as f: json.dump(log, f) debug.append(f" Message by '{safe_username}' sent!") except Exception as e: debug.append(f" Send error: {e}") else: debug.append(" Page Reloaded. Idle. Void Message. Waiting for user interactions. For extended commands info digit: /help") ######### Helper function to split long messages using MAX CHARS ######## def split_message(text, max_chars): words = text.split() lines = [] current_line = "" for word in words: if len(current_line) + len(word) + 1 <= max_chars: current_line += (" " if current_line else "") + word else: lines.append(current_line) current_line = word if current_line: lines.append(current_line) return lines ######### dynamic ui displayed messages adaptation ######## def calculate_effective_limit(log, max_lines, max_chars): total_lines = 0 effective_limit = 0 for msg in reversed(log): lines = len(split_message(msg["text"], max_chars)) if total_lines + lines > max_lines: break total_lines += lines effective_limit += 1 return max(effective_limit, 1), total_lines effective_limit, total_lines = calculate_effective_limit(log, MAX_LINES, MAX_CHARS) ########## UTC server time to local time dynamic conversion ########## from datetime import datetime def convert_log_time_to_local(log_time_str): # Parse log time - handle both [HH:MM] and [Day,HH:MM] formats log_time_str = log_time_str.strip("[]") # Check if it has day of week prefix (with or without space after comma) if "," in log_time_str: # New format: [Tue,14:23] or [Tue, 14:23] parts = log_time_str.split(",") day_part = parts[0].strip() time_part = parts[1].strip() else: # Old format: [14:23] time_part = log_time_str day_part = None # Get today's date today = datetime.utcnow().date() # Parse as UTC utc_dt = datetime.strptime(f"{today} {time_part}", "%Y-%m-%d %H:%M") # Get system's local timezone-aware datetime local_now = datetime.now().astimezone() # Replace tzinfo of utc_dt with UTC, then convert to local timezone utc_dt = utc_dt.replace(tzinfo=local_now.tzinfo) local_dt = utc_dt.astimezone() return local_dt.strftime("%a,%H:%M") ######### mention users def logic on @user message ######## def highlight_mentions_in_line(line, known_users): def replacer(match): nickname = match.group(1) if nickname in known_users: color = get_color(nickname) return f"`!@`{color}{nickname}`b`!" else: return f"@{nickname}" # Leave uncolored return re.sub(r"@(\w+)", replacer, line) ######## $E FOR EMOTICONS ######## with open(EMO_DB, "r", encoding="utf-8") as f: EMOTICONS = [] for line in f: EMOTICONS.extend(line.strip().split()) # $e catching for emoticons in messages def substitute_emoticons_in_line(line): return re.sub(r"\$e", lambda _: random.choice(EMOTICONS), line) ######## LINK MENTIONS ###### def format_links_in_line(line): def replacer(match): link = match.group(1) return f"`*`_`Fb9f`[{link}]`_`*`f` " # Match `$link` followed by a space and then the actual link return re.sub(r"\$link\s+([^\s]+)", replacer, line) ############################## Output UI template: ###################################### #INTRO TITLE: template = f"> `!{message_icon} THE CHAT ROOM! {message_icon} `F007` Powered by Reticulum NomadNet - IRC Style - Free Global Chat Room - Optimized for Meshchat v2.x+ - v2.00 `f`!\n" template += "-\n" # TOPIC READING AND RENDERING: template += f"`c`B000`Ff2e`!###### Room Topic: {topic_text} `! (Set by: {topic_author}, {topic_data.get('time')}) `! `!`f`b`a\n" template += "-\n" # Build set of known usernames known_users = {msg["user"] for msg in log} for msg in log[-effective_limit:]: color = get_color(msg["user"]) message_lines = split_message(msg["text"], MAX_CHARS) total_parts = len(message_lines) local_time = convert_log_time_to_local(msg["time"]) for i, line in enumerate(message_lines, start=1): marker = f"({i}/{total_parts})" if total_parts > 1 else "" if msg["user"] != "System": line = substitute_emoticons_in_line(line) # Replace $e with random emoticons line = highlight_mentions_in_line(line, known_users) # Highlight @mentions line = format_links_in_line(line) # Format $link into [link] highlighted_line = line else: highlighted_line = line # Skip substitutions for System user template += f"`Ffff` \\{msg['time']} `*` `{color}<{msg['user']}>`b `*` {highlighted_line} \n" template += "-" # sanitize and read name from display_name os env safe_display_name = display_name.replace("`", "'") template += f"\n>`!` {user_icon} Nickname: `Baac`F000`<12|username`{safe_display_name}>`!`b`[{nickset_icon} `:/page/meshchat.mu`username]`! {message_icon} Message: `Baac`<52|message`>`b`!" template += f" `!`[{send_icon} Send Message`:/page/meshchat.mu`username|message]`! | `!`[{reload_icon} Reload`:/page/meshchat.mu`username]`!\n" template += "-\n" # USER COMMANDS template += f"`B216`Fddd` {cmd_icon} User Commands: /info, /stats, /users, /topic, /search , /time, /ping, /version, -------> `!Full Command List: /cmd `!`b`f\n" # MENUBAR template += f"`B317`Feee` `!` {message_icon} Total Messages: ({len(log)}) | {message_icon} On Screen Messages: ({total_lines}) | {totmsg_icon} `[Read Last 100`:/page/last100.mu]` | {totmsg_icon} `[Read Full Chat Log (Slow)`:/page/fullchat.mu]`! | `!`[{setup_icon} User Settings `:/page/meshchat.mu`username]` `!`b`f" # NICKNAME DETECTION STATUS ON PAGE LOAD if display_name.startswith("Guest_"): template += f"`B222`Ff00` Fingerprint Detected! Temporary Nickname set to: {display_name}. Set a new nickname and press the {nickset_icon} reload button next to your name.`b`f`" elif display_name == "Guest": template += "`B222`Ff00` ** You're currently using a generic name. Set a nickname to personalize your session. Press Fingerprint to save or recover it.`b`f`" elif nickname_recovered_from_db: template += f"`B060`F0f0` Fingerprint match successful! Nickname recovered from db: {display_name}, Bound to LXMF Address: {dest}`b`f`" # RENDER UI: print(template)