diff --git a/chat.py b/chat.py index 4a0874c..d0ac0b4 100644 --- a/chat.py +++ b/chat.py @@ -25,7 +25,7 @@ import html import urllib.parse PORT = 9999 -VERSION = "10" +VERSION = "11" TUNNEL_TARGETS = { "parspack": ("185.208.174.152", 22), "mequ": ("188.213.68.133", 2022), @@ -86,17 +86,25 @@ CHAT_HTML = r""" - + + + + + + + Chat +
+ Install as app for notifications & fullscreen + + +
+
-
Shift+Enter for newline · Enter to send
- + - - +
""" +PWA_MANIFEST = json.dumps({ + "name": "Chat", + "short_name": "Chat", + "description": "Minimal multi-user chat", + "start_url": "/chat", + "display": "standalone", + "background_color": "#1a1a2e", + "theme_color": "#1a1a2e", + "icons": [ + {"src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable"} + ] +}) + +# Minimal SVG icon (chat bubble) +PWA_ICON = """ + + + + + +""" + +SERVICE_WORKER = """ +const CACHE = 'chat-v1'; +self.addEventListener('install', e => { self.skipWaiting(); }); +self.addEventListener('activate', e => { e.waitUntil(clients.claim()); }); +self.addEventListener('fetch', e => { + // Let all requests go to network (chat is real-time, caching would break it) + // But cache the shell for offline "you're offline" experience + if (e.request.mode === 'navigate') { + e.respondWith( + fetch(e.request).catch(() => new Response( + '

Offline - connect to the internet

', + {headers: {'Content-Type': 'text/html'}} + )) + ); + } +}); +""" + # ── Multipart parser (minimal, for file uploads) ─────────────────────── def parse_multipart(body: bytes, boundary: str) -> dict: @@ -455,6 +560,40 @@ async def handle_ws_tunnel(ws_reader, ws_writer, target): async def handle_http(reader, writer, first_line): method, path, headers, body = await parse_http_request(reader, first_line) + # PWA assets + if method == "GET" and path == "/manifest.json": + resp = PWA_MANIFEST.encode() + writer.write(b"HTTP/1.1 200 OK\r\n") + writer.write(b"Content-Type: application/manifest+json\r\n") + writer.write(f"Content-Length: {len(resp)}\r\n".encode()) + writer.write(b"\r\n") + writer.write(resp) + await writer.drain() + writer.close() + return + + if method == "GET" and path == "/icon.svg": + resp = PWA_ICON.encode() + writer.write(b"HTTP/1.1 200 OK\r\n") + writer.write(b"Content-Type: image/svg+xml\r\n") + writer.write(f"Content-Length: {len(resp)}\r\n".encode()) + writer.write(b"\r\n") + writer.write(resp) + await writer.drain() + writer.close() + return + + if method == "GET" and path == "/sw.js": + resp = SERVICE_WORKER.encode() + writer.write(b"HTTP/1.1 200 OK\r\n") + writer.write(b"Content-Type: application/javascript\r\n") + writer.write(f"Content-Length: {len(resp)}\r\n".encode()) + writer.write(b"\r\n") + writer.write(resp) + await writer.drain() + writer.close() + return + # GET /version if method == "GET" and path == "/version": resp = json.dumps({"version": VERSION}).encode()