Files
thechatroom-ru/nomadnetwork/storage/pages/meshchat.mu

1343 lines
57 KiB
Plaintext
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#!c=0
######## ИМПОРТ МОДУЛЕЙ: ########
import os, sys, json, time, random, re, sqlite3
######## ИНИЦИАЛИЗАЦИЯ ЛОГА (ЛОКАЛЬНЫЕ СИСТЕМНЫЕ ИНФОРМАЦИОННЫЕ СООБЩЕНИЯ) #####
log = []
######## СИСТЕМНЫЕ И ФАЙЛОВЫЕ ПУТИ ########
DB_PATH = os.path.join(os.path.dirname(__file__), "chatusers.db")
EMO_DB = os.path.join(os.path.dirname(__file__), "emoticons.txt")
######## СОЗДАНИЕ БД ЕСЛИ ОТСУТСТВУЕТ (обычно при первом запуске) ######
if not os.path.exists(DB_PATH):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
remote_identity TEXT,
dest TEXT UNIQUE NOT NULL,
display_name TEXT
);
""")
conn.commit()
conn.close()
######## НАСТРОЙКИ ЛИМИТА ОТОБРАЖЕНИЯ: ######## (Сохраняет интерфейс фиксированным в браузере meshchat)
MAX_CHARS = 103 # Настройте по необходимости для разбиения сообщений после N символов
MAX_LINES = 28 # Максимум строк на экране
######## НАСТРОЙКИ ГЛАВНОГО СИСТЕМНОГО АДМИНИСТРАТОРА ######## (ИСПОЛЬЗУЙТЕ СЛОЖНЫЕ НИКНЕЙМЫ ДЛЯ СИСТЕМНЫХ АДМИНИСТРАТОРОВ!)
SYSADMIN = "setyouradminlongnamehere" # УСТАНОВИТЕ ВАШ НИКНЕЙМ ГЛАВНОГО АДМИНИСТРАТОРА ДЛЯ АДМИНИСТРАТИВНЫХ КОМАНД ЧАТА
######## ЭМОДЗИ ИНТЕРФЕЙСА UNICODE: ########
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"
######## Фильтры антиспама: ######## (Добавьте или удалите то, что хотите разрешить или запретить)
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",
]
################### Система автоматической окраски никнеймов ##################### (Измените цвета если хотите)
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)]
######### Восстановление ввода из переменных окружения ОС ########
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")
# Резервный вариант: аргументы командной строки при необходимости
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()
# Извлечение хэш-кода из удалённой идентичности и адреса LXMF
hash_code = remote_identity[-4:] if remote_identity else ""
dest_code = dest[-4:] if dest else ""
# Умный резервный вариант для отображаемого имени с логированием
if nickname:
display_name = nickname
elif dest:
display_name = f"Guest_{dest_code}"
else:
display_name = "Guest"
# Привязка никнейма к SQL БД и восстановление
def init_db():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create new table for identity-based lookups
cursor.execute("""
CREATE TABLE IF NOT EXISTS rns_users (
remote_identity TEXT PRIMARY KEY,
display_name TEXT,
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
def get_display_name_from_db(remote_identity):
if not remote_identity:
return None
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT display_name FROM rns_users WHERE remote_identity = ?", (remote_identity,))
result = cursor.fetchone()
conn.close()
return result[0] if result else None
def save_user_to_db(remote_identity, display_name):
if not remote_identity:
return # Don't save if required info is missing
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO rns_users (remote_identity, display_name)
VALUES (?, ?)
ON CONFLICT(remote_identity) DO UPDATE SET
display_name = excluded.display_name,
last_seen = CURRENT_TIMESTAMP
""", (remote_identity, 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(remote_identity)
# Определение финального display_name с логированием
nickname_recovered_from_db = False
if nickname:
display_name = nickname
log.append({
"time": time.strftime("[%a,%H:%M]"),
"user": "System",
"text": f"`!` Никнейм восстановлен из окружения: {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"`!` Никнейм восстановлен из базы данных: {display_name} `!`"
})
elif remote_identity:
display_name = f"Guest_{remote_identity[:4]}"
log.append({
"time": time.strftime("[%a,%H:%M]"),
"user": "System",
"text": f"`!` Никнейм не найден. Используется отпечаток: {display_name} `!`"
})
else:
display_name = "Guest"
log.append({
"time": time.strftime("[%a,%H:%M]"),
"user": "System",
"text": "`!` Никнейм или отпечаток не найдены. Используется по умолчанию: Guest `!`"
})
# Сохранение пользователя в БД если валидно
save_user_to_db(remote_identity, 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"
)
# Функции темы чата
topic_file = os.path.join(os.path.dirname(__file__), "topic.json")
topic_data = {}
try:
with open(topic_file, "r") as tf:
topic_data = json.load(tf)
topic_text = topic_data.get("text", "Добро пожаловать в чат!")
topic_author = topic_data.get("user", "System")
except (IOError, json.JSONDecodeError, KeyError):
topic_text = "Добро пожаловать в чат!"
topic_author = "System"
topic_data = {}
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" Всего {len(log)} сообщений")
except Exception as e:
log = []
debug.append(f"Ошибка загрузки лога: {e}")
# ЛОГИКА КОМАНД ПОЛЬЗОВАТЕЛЯ:
cmd = message.strip().lower()
##### АДМИНИСТРАТИВНЫЕ КОМАНДЫ #####
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.")
########## КОМАНДЫ ПОЛЬЗОВАТЕЛЕЙ ЧАТА #########
#### КОМАНДА STATS ####
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 = "`!` Топ участников: `!` " + " , ".join([f"`!` {user} ({count} сообщ.) `!`" 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"`!` Список активных пользователей и статистика, Всего пользователей: ({total_users}) `! "
})
# Show in chunks of N with message counts
for i in range(0, total_users, 7):
chunk = ", ".join(f"`!` {user} `!({count}сообщ.)" 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} \\ РАСШИРЕННАЯ ИНФОРМАЦИЯ О КОМАНДАХ ПОЛЬЗОВАТЕЛЯ:`!",
f"`!ОБЩИЕ И ИНФОРМАЦИОННЫЕ КОМАНДЫ:`!",
f"`!/info`! : Показать информацию о The Chat Room!, использование и отказ",
f"`!/cmd`! : Показать все доступные команды пользователя",
f"`!/stats`! : Показать статистику чата, включая Топ-5 участников",
f"`!/users`! : Список всех пользователей чата",
f"`!/version`! : Показать версию скрипта THE CHAT ROOM!, новости и информацию",
f"`! {cmd_icon} ИНТЕРАКТИВНЫЕ КОМАНДЫ ЧАТА`!",
"`!/lastseen <username>`!`: Информация о последнем появлении пользователя и последнее сообщение пользователя",
"`!/topic`!` : Показать или изменить тему комнаты, использование: '/topic' или '/topic Ваша новая тема здесь' ",
"`!/search <keyword(s)>`!` : Поиск ключевых слов в полном логе чата ",
"`!/time`!` : Показать текущее время сервера чата (UTC)",
"`!/ping`!` : Ответить PONG! если система чата работает",
"`!/meteo <cityname>`! : Получить информацию о погоде для вашего города, пример: /meteo Москва",
"--------------------------------------",
f"`! {cmd_icon} КОМАНДЫ СОЦИАЛЬНОГО ВЗАИМОДЕЙСТВИЯ`!",
"`!` /e`!` : Отправляет случайные эмодзи из внутреннего списка эмодзи",
"`!` /c <текстовое сообщение>`!` : Отправляет цветное сообщение чата со случайными цветами фона и шрифта",
"`!` @nickname`!` : Отправляет цветное упоминание для выделения упомянутого пользователя в ответном сообщении",
"`!` $e`!` : Отправляет случайный эмотикон используя '$e', можно использовать в любой части сообщения. ",
"`!` $link`!` : Выделить ваши ссылки, пример: $link d251bfd8e30540b5bd219bbbfcc3afc5:/page/index.mu ",
"`!` /welcome`! : Отправляет приветственное сообщение. Использование: /welcome или /welcome <nickname>. ",
f"`!` {cmd_icon} КОМАНДЫ СТАТУСА ПОЛЬЗОВАТЕЛЯ`!`",
"`!` /hi, /bye, /brb, /lol, /exit, /quit, /away, /back, /notice `!`",
"`!` Пример использования команд: /hi ИЛИ /hi Привет Мир! `! (Синтаксис действителен для всех вышеперечисленных команд!)",
"--------------------------------------",
f"`!` {cmd_icon} ИНФОРМАЦИЯ ОБ АДМИНИСТРАТИВНЫХ КОМАНДАХ: /admincmd (Только администраторам разрешено выполнять эту команду) `!`",
"`!` --------- КОНЕЦ СПИСКА КОМАНД: `[НАЖМИТЕ ДЛЯ ПЕРЕЗАГРУЗКИ СТРАНИЦЫ`:/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} ИНФОРМАЦИЯ ОБ АДМИНИСТРАТИВНЫХ КОМАНДАХ `!",
"`! У вас есть доступ к ограниченным административным функциям.`!",
"`! /clear `! : Удаляет последнее сообщение из чата и базы данных навсегда",
"`! /clear N`! : Удаляет последние N сообщений из чата и базы данных навсегда, пример: /clear 3",
"`! /clear user <nickname>`! : Удалить все сообщения от указанного пользователя навсегда",
"`! /clearall `! : Навсегда очистить весь лог чата и базу данных (Необратимо: используйте с осторожностью!)",
"`! /backup `! : Создаёт полную резервную копию базы данных chat_log.json в той же папке скрипта чата",
"--------------------------------------",
"`! КОНЕЦ СПИСКА АДМИНИСТРАТИВНЫХ КОМАНД `!"
]
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! :) `!",
"Добро пожаловать! Это пространство предназначено для соединения людей через интерфейс в старом стиле IRC.",
"Регистрация не требуется, установите ваш никнейм и вы готовы общаться с другими пользователями.",
"Никнеймы случайно окрашиваются и для каждого никнейма есть постоянный цвет.",
"Без компромиссов приватности: используйте любой никнейм. Ничего не записывается и не связывается с вашей rns идентичностью.",
"Это работает на Nomadnet, поэтому будет видно международно. Уважайте все языки пользователей в чате.",
"Этот чат основан на компонентах micron, sql3 db и python.",
"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 <username> : 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 (ImportError, AttributeError, OSError):
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 <keywords> - 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:
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:
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} присоединился к Чату!"
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"`!` Ошибка обработки команды /hi: {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} покидает Чат!"
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"`!` Ошибка обработки команды /bye: {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} вышел из Чата!"
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"`!` Ошибка обработки команды /quit: {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} покинул Чат!"
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"`!` Ошибка обработки команды /exit: {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} покинул Чат! СКОРО ВЕРНУСЬ! BRB!"
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"`!` Ошибка обработки команды /brb: {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} громко смеётся! LOL! :D "
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"`!` Ошибка обработки команды /lol: {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} отсутствует."
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"`!` Ошибка обработки команды /away: {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} вернулся! "
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"`!` Ошибка обработки команды /back: {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} приветствует "
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"`!` Ошибка обработки команды /welcome: {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"УВЕДОМЛЕНИЕ ОТ {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"`!` Ошибка обработки команды /notice: {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}&current_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"Ошибка обработки команды /meteo: {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"Обнаружен спам! Сообщение заблокировано! Сработало на: '{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(" Страница перезагружена. Простой. Пустое сообщение. Ожидание взаимодействия пользователя. Для расширенной информации о командах введите: /help")
######### Вспомогательная функция для разбиения длинных сообщений используя 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 в локальное время ##########
from datetime import datetime, timezone
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.now(timezone.utc).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")
######### логика определения упоминаний пользователей в сообщениях @user ########
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 ДЛЯ ЭМОТИКОНОВ ########
EMOTICONS = []
try:
with open(EMO_DB, "r", encoding="utf-8") as f:
for line in f:
EMOTICONS.extend(line.strip().split())
except (IOError, OSError):
EMOTICONS = [":)", ":(", ":D", ":P"]
# $e catching for emoticons in messages
def substitute_emoticons_in_line(line):
if not EMOTICONS:
return line
return re.sub(r"\$e", lambda _: random.choice(EMOTICONS), line)
######## УПОМИНАНИЯ ССЫЛОК ######
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"> `!\U0001F1F7\U0001F1FA RU Chat \U0001F1F7\U0001F1FA `F007` Работает на Reticulum NomadNet - IRC стиль - Бесплатный глобальный чат - Оптимизировано для Meshchat v2.x+ - v2.00 `f`!\n"
template += "-\n"
# ЧТЕНИЕ И ОТОБРАЖЕНИЕ ТЕМЫ:
template += f"`c`B000`Ff2e`!###### Тема комнаты: {topic_text} `! (Установлена: {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} Никнейм: `Baac`F000`<12|username`{safe_display_name}>`!`b`[{nickset_icon} `:/page/meshchat.mu`username]`! {message_icon} Сообщение: `Baac`<52|message`>`b`!"
template += f" `!`[{send_icon} Отправить сообщение`:/page/meshchat.mu`username|message]`! | `!`[{reload_icon} Перезагрузить`:/page/meshchat.mu`username]`!\n"
template += "-\n"
# КОМАНДЫ ПОЛЬЗОВАТЕЛЯ
template += f"`B216`Fddd` {cmd_icon} Команды пользователя: /info, /stats, /users, /topic, /search <ключевые слова>, /time, /ping, /version, -------> `!Полный список команд: /cmd `!`b`f\n"
# ПАНЕЛЬ МЕНЮ
template += f"`B317`Feee` `!` {message_icon} Всего сообщений: ({len(log)}) | {message_icon} Сообщений на экране: ({total_lines}) | {totmsg_icon} `[Прочитать последние 100`:/page/last100.mu]` | {totmsg_icon} `[Прочитать полный лог чата (Медленно)`:/page/fullchat.mu]`! | `!`[{setup_icon} Настройки пользователя `:/page/meshchat.mu`username]` `!`b`f"
# СТАТУС ОПРЕДЕЛЕНИЯ НИКНЕЙМА ПРИ ЗАГРУЗКЕ СТРАНИЦЫ
if display_name.startswith("Guest_"):
template += f"`B222`Ff00` Отпечаток обнаружен! Временный никнейм установлен: {display_name}. Установите новый никнейм и нажмите кнопку {nickset_icon} перезагрузки рядом с вашим именем.`b`f`"
elif display_name == "Guest":
template += "`B222`Ff00` ** Вы сейчас используете общее имя. Установите никнейм для персонализации вашей сессии. Нажмите Fingerprint для сохранения или восстановления.`b`f`"
elif nickname_recovered_from_db:
template += f"`B060`F0f0` Совпадение отпечатка успешно! Никнейм восстановлен из БД: {display_name}`b`f`"
# RENDER UI:
print(template)