diff --git a/chat.py b/chat.py index d0ac0b4..b06d1fe 100644 --- a/chat.py +++ b/chat.py @@ -25,7 +25,7 @@ import html import urllib.parse PORT = 9999 -VERSION = "11" +VERSION = "12" TUNNEL_TARGETS = { "parspack": ("185.208.174.152", 22), "mequ": ("188.213.68.133", 2022), @@ -36,9 +36,6 @@ MAX_TOTAL_STORAGE = 50 * 1024 * 1024 # 50 MB total # ── Server ────────────────────────────────────────────────────────────── -clients: dict[asyncio.StreamWriter, str] = {} # TCP clients -sse_queues: list[asyncio.Queue] = [] # web clients -history: list[dict] = [] uploaded_files: dict[str, bytes] = {} # file_id -> raw bytes (insertion order) total_file_bytes = 0 @@ -53,31 +50,52 @@ def store_file(file_id: str, data: bytes): total_file_bytes -= len(uploaded_files.pop(oldest_id)) +class Group: + """A chat group with its own history, clients, and optional password.""" + def __init__(self, name: str): + self.name = name + self.password: str | None = None + self.history: list[dict] = [] + self.clients: dict[asyncio.StreamWriter, str] = {} # TCP clients + self.sse_queues: list[asyncio.Queue] = [] -async def broadcast(msg: dict): - history.append(msg) - line = json.dumps(msg) + "\n" + async def broadcast(self, msg: dict): + self.history.append(msg) + line = json.dumps(msg) + "\n" - # TCP clients - dead = [] - for w in clients: - try: - w.write(line.encode()) - await w.drain() - except Exception: - dead.append(w) - for w in dead: - clients.pop(w, None) + dead = [] + for w in self.clients: + try: + w.write(line.encode()) + await w.drain() + except Exception: + dead.append(w) + for w in dead: + self.clients.pop(w, None) - # SSE web clients - dead_q = [] - for q in sse_queues: - try: - q.put_nowait(msg) - except Exception: - dead_q.append(q) - for q in dead_q: - sse_queues.remove(q) + dead_q = [] + for q in self.sse_queues: + try: + q.put_nowait(msg) + except Exception: + dead_q.append(q) + for q in dead_q: + self.sse_queues.remove(q) + + +# All groups keyed by name. Auto-created on first access. +groups: dict[str, Group] = {} +DEFAULT_GROUP = "lobby" + + +def get_group(name: str) -> Group: + """Get or create a group by name.""" + name = name.lower().strip() + if not name: + name = DEFAULT_GROUP + if name not in groups: + groups[name] = Group(name) + return groups[name] # ── HTML / JS chat page ──────────────────────────────────────────────── @@ -139,10 +157,23 @@ CHAT_HTML = r""" #install-bar button { background: #e94560; border: none; color: #fff; padding: 6px 16px; border-radius: 4px; cursor: pointer; margin: 0 4px; } #install-bar .dismiss { background: transparent; color: #666; } + #pw-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.8); + z-index:100; align-items:center; justify-content:center; } + #pw-overlay.show { display:flex; } + #pw-box { background:#16213e; border:1px solid #444; border-radius:8px; padding:24px; + text-align:center; max-width:300px; width:90%; } + #pw-box h3 { margin-bottom:12px; color:#e0e0e0; } + #pw-box input { width:100%; padding:10px; background:#0f3460; border:1px solid #444; + color:#e0e0e0; border-radius:4px; margin-bottom:10px; font-size:16px; } + #pw-box button { padding:8px 20px; background:#e94560; border:none; color:#fff; + border-radius:4px; cursor:pointer; } + #pw-box .pw-err { color:#e94560; font-size:0.85em; margin-bottom:8px; display:none; } + #group-tag { background:#e94560; color:#fff; padding:2px 8px; border-radius:10px; + font-size:0.75em; margin-left:6px; } @media (max-width: 500px) { .msg { font-size: 13px; } .ts { font-size: 11px; } - #input { font-size: 16px; /* prevents iOS zoom */ } + #input { font-size: 16px; } } @@ -152,9 +183,18 @@ CHAT_HTML = r""" +