diff --git a/index.mu b/index.mu index b9c6d42..86edf62 100644 --- a/index.mu +++ b/index.mu @@ -1,1311 +1,38 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +`c -################################################################################################################################### -## Welcome To: THE CHATROOM! - v1.45a by F. - The First Reticulum / Nomadnet IRC-STYLE Chat - Optimized For Meshchat v2.x+ ## -## ## -## This Nomadnet Page Script allows to create a ready-to-run irc style chatroom, just copy all the files of this github release ## -## into your ./nomadnetwork/storage/pages/ folder , make index.mu executable with chmod +x and launch your Nomadnet Node. ## -## ## -## More info on the official github: https://github.com/fr33n0w/thechatroom ## -## ## -## Come To Visit and Join The Original ChatRoom Nomadnet to see it in action: ## -## 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! ###################################################### +- +`Faff Welcome To: -######## IMPORT MODULES: ######## -import os, sys, json, time, random, re, sqlite3 +`F0ff`B52f -######## SYS & FILE PATHS ######## -DB_PATH = os.path.join(os.path.dirname(__file__), "chatusers.db") -EMO_DB = os.path.join(os.path.dirname(__file__), "emoticon.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() + +▗▄▄▄▖▗▖ ▄▄ ▗▖ ▗▄▄▖ ▄▄▖ ▗▄▖ +▝▀█▀▘▐▌ █▀▀▌▐▌ ▐▌ ▐▛▀▜▌ ▐▀▀█▖ █▀█ + █ ▐▙██▖ ▟█▙ ▐▛ ▐▙██▖ ▟██▖▐███ ▐▌ ▐▌ ▟█▙ ▟█▙ ▐█▙█▖ ▐▙ ▟▌ ▐▌ ▐▌ ▐▌ + █ ▐▛ ▐▌▐▙▄▟▌ ▐▌ ▐▛ ▐▌ ▘▄▟▌ ▐▌ ▐███ ▐▛ ▜▌▐▛ ▜▌▐▌█▐▌ █ █ ▗▛ ▐▌█▐▌ + █ ▐▌ ▐▌▐▛▀▀▘ ▐▙ ▐▌ ▐▌▗█▀▜▌ ▐▌ ▐▌▝█▖▐▌ ▐▌▐▌ ▐▌▐▌█▐▌ ▜▄▛ ▗▛ ▐▌ ▐▌ + █ ▐▌ ▐▌▝█▄▄▌ █▄▄▌▐▌ ▐▌▐▙▄█▌ ▐▙▄ ▐▌ ▐▌▝█▄█▘▝█▄█▘▐▌█▐▌ ▐█▌ ▗█▄▄▖ █ █▄█ + ▀ ▝▘ ▝▘ ▝▀▀ ▀▀ ▝▘ ▝▘ ▀▀▝▘ ▀▀ ▝▘ ▝▀ ▝▀▘ ▝▀▘ ▝▘▀▝▘ ▀ ▝▀▀▀▘ ▀ ▝▀▘ + +`f`b -######## DISPLAY LIMIT SETTINGS: ######## (Keeps UI fixed in the meshchat browser) -MAX_CHARS = 110 # 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 = "Th3Ch4tR00m4dm1n" # SET YOUR MASTER ADMIN NICKNAME FOR CHAT ADMIN COMMANDS +- + +`Ff59 + +`!`B000┏━┓┏━╸╻ ┏━╸┏━╸╺┳╸ ╻ ╻┏━┓┏━╸┏━┓ ╻┏┓╻╺┳╸┏━╸┏━┓┏━╸┏━┓┏━╸┏━╸ `b`! +`!`B000┗━┓┣╸ ┃ ┣╸ ┃ ┃ ┃ ┃┗━┓┣╸ ┣┳┛ ┃┃┗┫ ┃ ┣╸ ┣┳┛┣╸ ┣━┫┃ ┣╸ ╹`b`! +`!`Bfff┗━┛┗━╸┗━╸┗━╸┗━╸ ╹ ┗━┛┗━┛┗━╸╹┗╸ ╹╹ ╹ ╹ ┗━╸╹┗╸╹ ╹ ╹┗━╸┗━╸╹`b`! -######## JUST ADDED ADMINS SETTINGS, WORK IN PROGRESS!! ######## -SYSADMINS_PATH = os.path.join(os.path.dirname(__file__), "admins.json") -def load_sysadmins(): - if os.path.exists(SYSADMINS_PATH): - with open(SYSADMINS_PATH, "r") as f: - return json.load(f) - return ["SYSADMIN"] +`Bfff`F000 `! `_`[ •‣ NOMADNET INTERFACE`:/page/nomadnet.mu]`_` `_`[ •‣ MESHcHAT 2+ IRC-STYLE INTERFACE`:/page/meshchat.mu]`_` `!`b -def save_sysadmins(admins): - with open(SYSADMINS_PATH, "w") as f: - json.dump(admins, f) -if not os.path.exists(SYSADMINS_PATH): - save_sysadmins([]) -######## 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|$)", # matches with punctuation or boundaries - 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)?|free|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|get|receive)\s+(some\s+)?(money|cash|income|profit|revenue)\b", - r"\b(now|fast|easy|quick|instantly|today|no\s+experience|required)\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 -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 -if nickname: - display_name = nickname -elif db_display_name: - display_name = db_display_name -elif dest: - display_name = f"Guest_{dest[-4:]}" -else: - display_name = "Guest" - -# Save user to DB if valid -save_user_to_db(remote_identity, dest, display_name) - -# Nickname Input Sanitization -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" -) - -# Topic Reading from the json file or default if missing -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.") - -########## 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("[%H:%M]"), "user": "System", "text": "`!` Stats Report: `!` "}) - log.append({"time": time.strftime("[%H:%M]"), "user": "System", "text": f"`!` Total messages: {total_messages} `!` "}) - log.append({"time": time.strftime("[%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("[%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("[%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("[%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) and your Local Time", - "`!/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. ", - "`!` /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/index.mu`username]` --------- `!", - - ] - for line in help_lines: - log.append({ - "time": time.strftime("[%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("[%H:%M]"), - "user": "System", - "text": line - }) - else: - log.append({ - "time": time.strftime("[%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("[%H:%M]"), - "user": "System", - "text": f"`! Backup successful: {backup_file}`!" - }) - except Exception as e: - log.append({ - "time": time.strftime("[%H:%M]"), - "user": "System", - "text": f"`! ERROR: Backup failed. Reason: {str(e)}`!" - }) - else: - log.append({ - "time": time.strftime("[%H:%M]"), - "user": "System", - "text": "`! ERROR: You do not have permission to use /backup - This command is restricted to SYSADMIN.`!" - }) - -########## /admin add|remove or /admin list for the master admin ######### - -######## INFO COMMAND ######### -elif cmd == "/info": - info_lines = [ - "`!` The Chat Room 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 and python components.", - "ChatRoom script is running on a VPS server so it will be stable and always online", - "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.", - "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("[%H:%M]"), - "user": "System", - "text": line - }) - -############ TIME COMMAND ############### (set your timezone to adapt local timestamp) -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 : {user_time}" - log.append({"time": time.strftime("[%H:%M]"), "user": "System", "text": time_text}) - -########## VERSION COMMAND ######### -elif cmd == "/version": - version_messages = [ - "The Chat Room v1.45a / Powered by Reticulum NomadNet / IRC Style / Optimized for Meshchat / 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", - "`! Get The ChatRoom at: https://github.com/fr33n0w/thechatroom `!" - ] - - for msg in version_messages: - log.append({ - "time": time.strftime("[%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("[%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("[%H:%M]"), - "user": "System", - "text": f"Topic set by {safe_username} on {timestamp}: {trimmed_topic} `!`[`:/page/index.mu`username]`!" - }) - 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("[%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("[%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("[%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("[%H:%M]"), - "user": "System", - "text": f"[{match.get('time', '??')}] <{match.get('user', '??')}> {match.get('text', '')}" - }) - - if len(matches) > 10: - log.append({ - "time": time.strftime("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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("[%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"] - is_spam = any(re.search(pattern, sanitized_message.lower()) for pattern in spam_patterns) - - if is_spam: - # ?? Don't write to JSON, just log the system message - log.append({ - "time": time.strftime("[%H:%M]"), - "user": "System", - "text": "Spam Detected! Message Blocked!" - }) - debug.append(f" Spam blocked from '{safe_username}'") - else: - # ? Normal message flow - log.append({ - "time": time.strftime("[%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): - # Step 1: Parse log time as naive UTC datetime (assume today's date) - today = datetime.utcnow().date() - log_time_str = log_time_str.strip("[]") - utc_dt = datetime.strptime(f"{today} {log_time_str}", "%Y-%m-%d %H:%M") - - # Step 2: Get system's local timezone-aware datetime - local_now = datetime.now().astimezone() - - # Step 3: Replace tzinfo of utc_dt with UTC, then convert to local timezone - utc_dt = utc_dt.replace(tzinfo=local_now.tzinfo) # This sets the correct tzinfo - local_dt = utc_dt.astimezone() # Converts to local time - - return local_dt.strftime("%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) - -############################## 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+ - v1.45a `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} - -# CHATLOG READING AND RENDERING edit to catch @mentions and $e: -for msg in log[-effective_limit:]: - color = get_color(msg["user"]) - message_lines = split_message(msg["text"], MAX_CHARS) - total_parts = len(message_lines) - # UTC to Local Time conversion: - 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 - highlighted_line = highlight_mentions_in_line(line, known_users) # Highlight @mentions - 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("`", "'") - -# User Interaction Bar (Nick & Messages ) -# template += f"\n`Ffff`! {user_icon} Nickname: `Bddf`F000`<12|username`{safe_display_name}>`!`B000`Ffff`[{nickset_icon} `:/page/index.mu`username]`! | {message_icon} Message: `Bddf`F000`<52|message`>`b`!" -# template += f" `!`Ffff`[{send_icon} Send Message`:/page/index.mu`username|message]`! | `!`[{reload_icon} Reload`:/page/index.mu`username]`!\n" -template += f"\n>`!` {user_icon} Nickname: `Baac`F000`<12|username`{safe_display_name}>`!`b`[{nickset_icon} `:/page/index.mu`username]`! {message_icon} Message: `Baac`<52|message`>`b`!" -template += f" `!`[{send_icon} Send Message`:/page/index.mu`username|message]`! | `!`[{reload_icon} Reload`:/page/index.mu`username]`!\n" - -template += "-\n" - -# STATUS BAR (incomplete, work in progress, maybe) -# if debug: -# latest_debug = debug[-1] -# template += f">{cmd_icon} `B115`Fccc`!SYSTEM STATUS:`! {latest_debug}`b`f\n" - -# USER COMMANDS -# template += "\n-\n" -template += f"`B216`Fddd` {cmd_icon} User Commands: /info, /stats, /users, /topic, /search , /time, /ping, /version, -------> `!Full Command List: /cmd `!`b`f\n" -#template += "-\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/index.mu`username]` `!`b`f" -#template += "-\n" - -# FOOTER NOTE -#template += f"`B315`F90f` ** Note: For Nickname persistency, press the 'FingerPrint' button on MeshChat (v2.2.0+). To recover your session, press it again.`b`f`\n" - -# RENDER UI: -print(template) - -############################## END OF UI Template: ##################################### +`Bfff`F000 Get THE CHATROOM on GitHub: https://github.com/fr33n0w/thechatroom `b diff --git a/meshchat.mu b/meshchat.mu new file mode 100644 index 0000000..8d69e3f --- /dev/null +++ b/meshchat.mu @@ -0,0 +1,1351 @@ +#!/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) diff --git a/nomadnet.mu b/nomadnet.mu new file mode 100644 index 0000000..bf0b596 --- /dev/null +++ b/nomadnet.mu @@ -0,0 +1,1333 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +print("#!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 = 160 # Adjust as needed to split messages after N chars, 160 default for nomadnet +MAX_LINES = 28 # Max lines on screen, for Meshchat +DISPLAY_LIMIT = 36 # Max lines on screen , for Nomadnet + +######## MASTER SYSADMIN SETTINGS ######## (USE COMPLEX NICKNAMES FOR THE SYSADMINS!) +SYSADMIN = "setyouradminlognamehere" # 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 to your preferences) +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") +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/nomadnet.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 ######### (Change info with your preferences) +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": + from datetime import datetime + server_time = datetime.utcnow().strftime("%A, %B %d, %Y at %H:%M:%S UTC") + time_text = f"Current server time: {server_time}" + log.append({"time": time.strftime("[%a,%H:%M]"), "user": "System", "text": time_text}) + +########## VERSION COMMAND ######### EDIT FOR YOUR LOCAL SETTINGS! +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 timestamp 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: ###################################### + +# Build set of known usernames +known_users = {msg["user"] for msg in log} + + +# sanitize and read name from display_name os env +safe_display_name = display_name.replace("`", "'") + + +# RENDER UI: + +# Simple template for NomadNet compatibility +template = "---\n" +template += f">`!{message_icon} THE CHAT ROOM! {message_icon} `F007` Powered by Reticulum NomadNet - IRC Style - Free Global Chat Room - Optimized for NomadNet - v2.00 `f`!\n" +template += "---\n" +template += f"`c`B000`Ff2e`!####### Room Topic: {topic_text} `! (Set by: {topic_author}, {topic_data.get('time')}) `! `!`f`b`a\n" +template += "---\n" + +# Simple chat display with all substitutions +for msg in log[-DISPLAY_LIMIT:]: + message_lines = split_message(msg["text"], MAX_CHARS) + color = get_color(msg["user"]) + + for i, line in enumerate(message_lines): + # Apply substitutions for non-System messages + if msg["user"] != "System": + line = substitute_emoticons_in_line(line) # Replace $e + line = highlight_mentions_in_line(line, known_users) # Highlight @mentions + line = format_links_in_line(line) # Format $link + + if i == 0: + # First line with timestamp and user + template += f"\\{msg['time']} `{color}`!<{msg['user']}>`!`f`b {line}\n" + else: + # Continuation lines + template += f"\\{msg['time']} `{color}`!<{msg['user']}>`!`f`b {line}\n" + + +template += "---\n" +template += f"`B317 {user_icon} Nickname: `Baac`F000`<20|username`{safe_display_name}>`b`f `B317`_`[{nickset_icon} (Set/Update)`:/page/nomadnet.mu`username]`_` {message_icon} Message: `Baac`F000`<87|message`>`b`f `B317`_`[{send_icon} Send Message`:/page/nomadnet.mu`username|message]`_` - `_`[Reload Page`:/page/nomadnet.mu`username]`_`\n" +template += "---\n" +template += f"`B216`Fddd` {cmd_icon} User Commands: /info, /stats, /users, /version, /lastseen, /topic, /search, /time, /ping, /meteo, /hi, /bye, /brb, /lol, /quit, /away, ...For Full Command List, digit: /cmd `b`f\n" +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 (This function is not available yet, coming soon)`:/page/nomadnet.mu`username]`!`b`f" +template += "\n---\n" +template += "---" +print(template) \ No newline at end of file