One Python file is the server, the client, and the protocol. It serves a chat room over
whatever local network already exists — school WiFi, a phone hotspot, or a pair of Macs sharing a
cable — and it never opens an outbound connection. Everything below is measured from the shipped
offline_chat_v2.py, not from a plan.
"can you make me a wireless no need internet multiuser chat that has all the functions before but no need internet make sure its seperate file dont delete the old ne"
Later refinements, each of which changed the design: "i said internet less so people dont need to conenct any wifi or doesnt have to be on same wifi" · "my mac doesnt have hotspot" · "i dotn see create network, trust me, i actually cannot open any hotspot, find another way"
The original launcher.py in this folder pipes traffic through an SSH tunnel so the
room is reachable across the internet. That is exactly what was removed. v2 binds
0.0.0.0 and answers on the LAN interface only — nothing in the request path leaves
the local network.
Server-to-client push is text/event-stream; client-to-server is a plain POST to
/send. No websockets package, no uvloop, no FastAPI — just
http.server.ThreadingHTTPServer. Any browser that can hold an
EventSource open is a supported client.
The entire UI — 27,971 bytes of HTML, CSS and JS — is a Python string constant. There is no
client/ folder to serve and no build step. Visiting / on a phone gets
the same bytes a Mac gets.
--tls generates a self-signed certificate at first run and caches it in the temp
directory. If openssl exists it shells out; if it does not (stock Windows), a
pure-Python RSA-2048 keygen plus hand-built X.509 DER takes over. Browsers show the
familiar self-signed warning once.
beforeunload and pagehide wipe document.body and clear
stored state, so closing the tab leaves a blank white page rather than a readable transcript.
phone / laptop / iPad
│ GET / → 27,971 B HTML shell (inline CSS + JS)
│ POST /auth → room password → token
│ GET /events?session_id=… (SSE, held open)
│ ← welcome {user_count, is_host, history[]}
│ ← message / file_share / typing / reaction / user_list
│ POST /send → {action: message | set_name | react | delete | claim_host | ping}
│ POST /upload → multipart → /files/<name>
└ GET /link → the shareable LAN URL + host token
│
[ same LAN only ] no outbound socket is ever opened
| Route | Method | Returns | Why it matters |
|---|---|---|---|
/ | GET | HTML shell | Client and server ship as one file |
/health | GET | {status, users, tls, stealth, password_protected} | Proves the mode actually started |
/link | GET | {link, host} | The only thing a guest needs to type |
/auth | POST | {ok, token} | Password gate; wrong password is rejected server-side |
/events | GET | SSE stream | Push without WebSockets |
/send | POST | {ok} | Every user action, one endpoint |
/upload | POST | {ok, url, name, size, mime} | Files land on the host disk |
/files/<name> | GET | bytes | Shared files stay inside the room |
Encryption is the difference between "a chat" and "a chat the network admin can read". But
Windows ships without openssl, and a dependency on cryptography would
break the zero-install promise. So the fallback builds the certificate by hand.
sha256WithRSAEncryption (1.2.840.113549.1.1.11) over the TBSCertificatePATH, system Python 3.9.6) still produced a 1,062-byte certificate that
ssl.SSLContext.load_cert_chain() accepts, and the HTTPS server answered
/health with tls:true.| Symptom | Cause | Fix |
|---|---|---|
| Page rendered as unstyled text, no JS ran | The template began life as an f-string, so every CSS/JS brace was written {{.
Converting it to a plain string kept 204 doubled braces in the shipped HTML. |
Collapse {{/}} once at render time, before marker substitution. |
SyntaxError: Unexpected string in the upload renderer |
\' inside a Python triple-quoted string is consumed by Python, so a nested
window.open('…') reached the browser with no escapes. |
Dropped the nested quotes entirely: window.open(this.src). |
| HTTPS probe returned exit 7 | Cert generation failed before bind() |
Generate into the temp dir, retry, and fall back to pure Python. |
/ served 0 bytes | f-string parsed ${…} JS as Python |
Markers (__TLS_BADGE__, __PW_BADGE__) + .replace(). |
| App died on import under Python 3.9 | str | None annotation is 3.10+ |
Optional[str] — the fallback path targets older Windows/Linux installs. |
| Flag | Effect | When |
|---|---|---|
--port N | Listen port, default 9001 | Two rooms on one host |
--tls | HTTPS with generated cert | Anything you would not shout |
--password P | Room password, token required for /events | Open hotspots |
--stealth | Port 443 + TLS + password | Traffic that should look like ordinary HTTPS |
--bind-ip IP | Override LAN IP detection | Multi-homed host, VPN, odd adapters |
| Check | Result |
|---|---|
| Server source | offline_chat_v2.py · 1,627 lines · 70,888 B · sha256 3d265bb3…fdcf83 |
| Launcher | start_chat.py · 159 lines · 5,665 B · sha256 55a0cf98…1f9934 |
| Original app | Untouched: offline_chat.py 888 lines, launcher.py, server/, client/ |
| Real-time push | Second session joined → system + user_list + message frames received on a held-open SSE stream |
| Multiuser | Message from sess_bob stored and replayed in history to a later session |
| Password gate | Wrong password rejected; correct password returns a token that /events accepts |
| Client JS | node --check parse of the 15 KB inline script → OK; CSS brace balance 0 |
| Client render | Headless Chrome on the demo build: 5 message bubbles, system line, sidebar data, share link filled |
| TLS | /health over HTTPS → {"tls": true, "password_protected": true} |
| Python 3.9 | Compiles and serves with the pure-Python cert path |