{"id":31967727,"url":"https://github.com/coderooz/socket-python-chat-app","last_synced_at":"2025-10-14T18:41:20.954Z","repository":{"id":314198873,"uuid":"1054554951","full_name":"coderooz/Socket-Python-Chat-App","owner":"coderooz","description":"A simple multi-client chat application with desktop (Tkinter) and web (browser) clients. The server supports both raw TCP socket clients (desktop) and WebSocket clients (browser) and relays messages between them in a shared chat channel. Text and file transfer are supported using a small JSON message protocol with base64 encoding for files.","archived":false,"fork":false,"pushed_at":"2025-09-11T02:39:41.000Z","size":15,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-11T06:01:05.533Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/coderooz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-11T02:33:25.000Z","updated_at":"2025-09-11T02:39:44.000Z","dependencies_parsed_at":"2025-09-11T06:01:08.214Z","dependency_job_id":"e937b488-0170-4373-8f64-8c93148d19c4","html_url":"https://github.com/coderooz/Socket-Python-Chat-App","commit_stats":null,"previous_names":["coderooz/socket-python-chat-app"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/coderooz/Socket-Python-Chat-App","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coderooz%2FSocket-Python-Chat-App","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coderooz%2FSocket-Python-Chat-App/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coderooz%2FSocket-Python-Chat-App/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coderooz%2FSocket-Python-Chat-App/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/coderooz","download_url":"https://codeload.github.com/coderooz/Socket-Python-Chat-App/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coderooz%2FSocket-Python-Chat-App/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279020361,"owners_count":26086866,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-14T02:00:06.444Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-14T18:41:07.210Z","updated_at":"2025-10-14T18:41:20.946Z","avatar_url":"https://github.com/coderooz.png","language":"Python","readme":"# Python Socket + Tkinter Chat App\n\nA simple multi-client chat application with **desktop (Tkinter)** and **web (browser)** clients. The server supports both **raw TCP socket clients** (desktop) and **WebSocket clients** (browser) and relays messages between them in a shared chat channel. Text and file transfer are supported using a small JSON message protocol with base64 encoding for files.\n\n---\n\n## Features\n\n* Single channel (chat group) supporting multiple members.\n* Desktop client built with Tkinter (Python) using raw TCP sockets.\n* Web client (browser) using WebSocket protocol.\n* Server written in Python `asyncio` that accepts both TCP and WebSocket connections and broadcasts messages between all connected clients.\n* Send text messages and files (files are base64-encoded and delivered to all clients).\n* Simple, clear UI for both desktop and web clients.\n\n---\n\n## Project Structure\n\n```\npython-tk-socket-chat/\n├── README.md           # This documentation\n├── server.py           # Central server (asyncio) - accepts TCP + WebSocket\n├── desktop_client.py   # Tkinter desktop client (TCP socket)\n├── web/\n│   ├── index.html      # Browser UI + JS WebSocket client\n│   └── static/         # optional folder for JS/CSS\n├── requirements.txt\n└── LICENSE\n```\n\n---\n\n## Protocol (JSON-over-socket)\n\nAll messages are JSON strings followed by a newline (`\\n`). Each JSON object has at least these fields:\n\n* `type` — `join` | `text` | `file` | `leave` | `system`\n* `username` — sender's display name\n* `channel` — chat channel name (we use `main` by default)\n* `message` — for `text` and `system` messages\n* `filename` \u0026 `data` — for `file` messages; `data` is base64-encoded file bytes\n* `timestamp` — ISO 8601 timestamp (optional)\n\nExamples:\n\nText message:\n\n```json\n{\"type\":\"text\",\"username\":\"alice\",\"channel\":\"main\",\"message\":\"hello everyone\"}\n```\n\nFile message (shortened):\n\n```json\n{\"type\":\"file\",\"username\":\"bob\",\"channel\":\"main\",\"filename\":\"cat.png\",\"data\":\"iVBORw0KG...\"}\n```\n\nJoin message (sent when client connects):\n\n```json\n{\"type\":\"join\",\"username\":\"alice\",\"channel\":\"main\"}\n```\n\n---\n\n## Server: `server.py`\n\nThis server uses `asyncio` and the `websockets` library to accept WebSocket clients and `asyncio`'s TCP server to accept raw socket clients. It tracks connected clients and relays messages between them.\n\n\u003e Requirements: Python 3.8+ (3.10+ recommended), `websockets` library.\n\n```python\n# server.py\nimport asyncio\nimport json\nimport datetime\nfrom websockets import serve as websocket_serve\n\n# TCP clients: use a list because dicts are unhashable\nTCP_CLIENTS = []\nWS_CLIENTS = set()\nCHANNEL = \"main\"\n\ndef now_iso():\n    return datetime.datetime.utcnow().isoformat() + \"Z\"\n\nasync def broadcast(message_json: str):\n    \"\"\"Send message_json (string) to all clients (both TCP and WS).\"\"\"\n    # TCP clients: write newline-delimited JSON\n    for client in list(TCP_CLIENTS):\n        try:\n            writer = client[\"writer\"]\n            writer.write((message_json + \"\\n\").encode())\n            await writer.drain()\n        except Exception as e:\n            print(\"Error writing to tcp client:\", e)\n            try:\n                TCP_CLIENTS.remove(client)\n            except ValueError:\n                pass\n\n    # WebSocket clients\n    for ws in list(WS_CLIENTS):\n        try:\n            await ws.send(message_json)\n        except Exception as e:\n            print(\"Error writing to ws client:\", e)\n            WS_CLIENTS.discard(ws)\n\nasync def handle_tcp_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):\n    addr = writer.get_extra_info(\"peername\")\n    client = {\"type\": \"tcp\", \"writer\": writer, \"username\": None}\n    TCP_CLIENTS.append(client)\n    print(\"TCP client connected\", addr)\n\n    try:\n        while True:\n            data = await reader.readline()\n            if not data:\n                break\n            try:\n                msg = data.decode().strip()\n                obj = json.loads(msg)\n            except Exception as e:\n                print(\"Invalid message from\", addr, e)\n                continue\n\n            # Set username if join\n            if obj.get(\"type\") == \"join\":\n                client[\"username\"] = obj.get(\"username\") or f\"tcp-{addr}\"\n                sysmsg = json.dumps(\n                    {\n                        \"type\": \"system\",\n                        \"username\": \"__server__\",\n                        \"channel\": CHANNEL,\n                        \"message\": f\"{client['username']} joined.\",\n                        \"timestamp\": now_iso(),\n                    }\n                )\n                await broadcast(sysmsg)\n            else:\n                # attach timestamp and broadcast\n                obj.setdefault(\"timestamp\", now_iso())\n                await broadcast(json.dumps(obj))\n\n    except Exception as e:\n        print(\"TCP handler error:\", e)\n\n    finally:\n        try:\n            TCP_CLIENTS.remove(client)\n        except ValueError:\n            pass\n        try:\n            writer.close()\n            await writer.wait_closed()\n        except Exception:\n            pass\n        print(\"TCP client disconnected\", addr)\n\n        # broadcast leave\n        if client.get(\"username\"):\n            leave = json.dumps(\n                {\n                    \"type\": \"system\",\n                    \"username\": \"__server__\",\n                    \"channel\": CHANNEL,\n                    \"message\": f\"{client['username']} left.\",\n                    \"timestamp\": now_iso(),\n                }\n            )\n            await broadcast(leave)\n\n# Accept optional `path` to be compatible with different websockets versions\nasync def handle_ws(ws, path=None):\n    WS_CLIENTS.add(ws)\n    print(\"WS client connected\")\n    try:\n        async for msg in ws:\n            try:\n                obj = json.loads(msg)\n            except Exception as e:\n                print(\"Invalid ws message:\", e)\n                continue\n            obj.setdefault(\"timestamp\", now_iso())\n            await broadcast(json.dumps(obj))\n    except Exception as e:\n        print(\"WS handler error:\", e)\n    finally:\n        WS_CLIENTS.discard(ws)\n        print(\"WS client disconnected\")\n\nasync def main():\n    print(\"Starting server...\")\n    tcp_server = await asyncio.start_server(handle_tcp_client, \"0.0.0.0\", 8765)\n    # websockets.serve will pass either (ws, path) or just (ws) depending on version;\n    # our handler accepts an optional `path`.\n    ws_server = await websocket_serve(handle_ws, \"0.0.0.0\", 8766)\n\n    addrs = \", \".join(str(sock.getsockname()) for sock in tcp_server.sockets)\n    print(f\"TCP server listening on {addrs}, WebSocket on 8766\")\n\n    async with tcp_server:\n        await asyncio.Future()  # run forever\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except KeyboardInterrupt:\n        print(\"Server stopped\")\n\n```\n\n**Notes:**\n\n* TCP server: port **8765** — for desktop client using raw sockets.\n* WebSocket server: port **8766** — for browser clients using the WebSocket API.\n* Messages are forwarded between both pools so clients on different protocols share the same chat.\n\n---\n\n## Desktop client: `desktop_client.py` (Tkinter + raw sockets)\n\nThis is a simple Tkinter GUI. It connects to the TCP server on port 8765 and sends/receives JSON messages as described in the protocol. Files are sent base64 encoded.\n\n```python\n# desktop_client.py\nimport socket\nimport threading\nimport json\nimport base64\nimport tkinter as tk\nfrom tkinter import scrolledtext, filedialog, simpledialog, messagebox\nimport time\n\nSERVER_HOST = '127.0.0.1'\nSERVER_PORT = 8765\nBUFFER = 4096\nCHANNEL = 'main'\n\nclass ChatClient:\n    def __init__(self, root):\n        self.root = root\n        self.root.title('Tk Chat')\n        self.username = simpledialog.askstring('Username', 'Enter display name', parent=root) or f'user-{int(time.time()%1000)}'\n\n        top = tk.Frame(root)\n        tk.Label(top, text=f'User: {self.username}').pack(side=tk.LEFT)\n        tk.Button(top, text='Send File', command=self.send_file).pack(side=tk.RIGHT)\n        top.pack(fill=tk.X)\n\n        self.txt = scrolledtext.ScrolledText(root, state='disabled', height=20)\n        self.txt.pack(fill=tk.BOTH, expand=True)\n\n        bottom = tk.Frame(root)\n        self.entry = tk.Entry(bottom)\n        self.entry.pack(side=tk.LEFT, fill=tk.X, expand=True)\n        self.entry.bind('\u003cReturn\u003e', lambda e: self.send_text())\n        tk.Button(bottom, text='Send', command=self.send_text).pack(side=tk.RIGHT)\n        bottom.pack(fill=tk.X)\n\n        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        try:\n            self.sock.connect((SERVER_HOST, SERVER_PORT))\n        except Exception as e:\n            messagebox.showerror('Connection Error', str(e))\n            root.destroy()\n            return\n\n        # start receiver thread\n        self.running = True\n        t = threading.Thread(target=self.receiver, daemon=True)\n        t.start()\n\n        # send join message\n        join = json.dumps({'type':'join','username':self.username,'channel':CHANNEL})\n        self.sock.sendall((join + '\\n').encode())\n\n        root.protocol('WM_DELETE_WINDOW', self.on_close)\n\n    def append(self, text):\n        self.txt['state'] = 'normal'\n        self.txt.insert(tk.END, text + '\\n')\n        self.txt.yview(tk.END)\n        self.txt['state'] = 'disabled'\n\n    def receiver(self):\n        buff = b''\n        while self.running:\n            try:\n                data = self.sock.recv(BUFFER)\n                if not data:\n                    break\n                buff += data\n                while b'\\n' in buff:\n                    line, buff = buff.split(b'\\n', 1)\n                    try:\n                        obj = json.loads(line.decode())\n                        self.handle_msg(obj)\n                    except Exception as e:\n                        print('Invalid message', e)\n            except Exception as e:\n                print('Receiver error', e)\n                break\n        self.running = False\n\n    def handle_msg(self, obj):\n        t = obj.get('type')\n        if t == 'text' or t == 'system':\n            user = obj.get('username')\n            msg = obj.get('message')\n            ts = obj.get('timestamp','')\n            self.root.after(0, lambda: self.append(f'[{user}] {msg}'))\n        elif t == 'file':\n            user = obj.get('username')\n            fname = obj.get('filename')\n            data = obj.get('data')\n            # ask to save\n            def ask_save():\n                path = filedialog.asksaveasfilename(initialfile=fname)\n                if path:\n                    with open(path, 'wb') as f:\n                        f.write(base64.b64decode(data))\n                    messagebox.showinfo('Saved', f'Saved file to {path}')\n                self.append(f'[{user}] sent file: {fname} (saved: {bool(path)})')\n            self.root.after(0, ask_save)\n\n    def send_text(self):\n        text = self.entry.get().strip()\n        if not text:\n            return\n        obj = {'type':'text','username':self.username,'channel':CHANNEL,'message':text}\n        try:\n            self.sock.sendall((json.dumps(obj) + '\\n').encode())\n            self.entry.delete(0, tk.END)\n        except Exception as e:\n            messagebox.showerror('Send error', str(e))\n\n    def send_file(self):\n        path = filedialog.askopenfilename()\n        if not path:\n            return\n        with open(path, 'rb') as f:\n            data = base64.b64encode(f.read()).decode()\n        fname = path.split('/')[-1]\n        obj = {'type':'file','username':self.username,'channel':CHANNEL,'filename':fname,'data':data}\n        try:\n            self.sock.sendall((json.dumps(obj) + '\\n').encode())\n            self.append(f'[you] sent file: {fname}')\n        except Exception as e:\n            messagebox.showerror('Send error', str(e))\n\n    def on_close(self):\n        try:\n            leave = json.dumps({'type':'leave','username':self.username,'channel':CHANNEL})\n            self.sock.sendall((leave + '\\n').encode())\n        except: pass\n        self.running = False\n        try:\n            self.sock.close()\n        except: pass\n        self.root.destroy()\n\nif __name__ == '__main__':\n    root = tk.Tk()\n    app = ChatClient(root)\n    root.mainloop()\n```\n\n**Notes:**\n\n* Desktop client connects to `SERVER_HOST:8765`. Modify `SERVER_HOST` if server runs elsewhere.\n* Incoming file messages trigger a file-save dialog.\n\n---\n\n## Web client: `web/index.html`\n\nA lightweight HTML+JS client using the browser WebSocket API to connect to the server's WebSocket port (8766). It implements the same JSON protocol and presents a simple, clean UI.\n\n```html\n\u003c!-- web/index.html --\u003e\n\u003c!doctype html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"utf-8\" /\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\" /\u003e\n  \u003ctitle\u003eWeb Chat\u003c/title\u003e\n  \u003cstyle\u003e\n    body{font-family:system-ui,Arial;margin:0;display:flex;flex-direction:column;height:100vh}\n    header{padding:12px;background:#111;color:#fff}\n    #chat{flex:1;overflow:auto;padding:12px;background:#f6f6f6}\n    #inputBar{display:flex;padding:8px}\n    #msg{flex:1;padding:8px}\n    .msg{margin:6px 0;padding:8px;border-radius:6px;background:white;box-shadow:0 1px 2px rgba(0,0,0,0.05)}\n    .meta{font-size:12px;color:#666}\n  \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cheader\u003e\n    \u003cspan id=\"me\"\u003eWeb User\u003c/span\u003e\n  \u003c/header\u003e\n  \u003cdiv id=\"chat\"\u003e\u003c/div\u003e\n  \u003cdiv id=\"inputBar\"\u003e\n    \u003cinput id=\"msg\" placeholder=\"Type a message\" /\u003e\n    \u003cinput type=\"file\" id=\"file\" /\u003e\n    \u003cbutton id=\"send\"\u003eSend\u003c/button\u003e\n  \u003c/div\u003e\n\n\u003cscript\u003e\n  const WS_URL = 'ws://' + location.hostname + ':8766'; // adjust host if needed\n  let username = prompt('Enter display name') || 'web-' + Math.floor(Math.random()*1000);\n  document.getElementById('me').textContent = username;\n\n  const ws = new WebSocket(WS_URL);\n  const chat = document.getElementById('chat');\n  const msgInput = document.getElementById('msg');\n  const fileInput = document.getElementById('file');\n\n  function addMessage(text){\n    const d = document.createElement('div'); d.className='msg'; d.innerHTML = text; chat.appendChild(d); chat.scrollTop = chat.scrollHeight;\n  }\n\n  ws.onopen = () =\u003e {\n    const join = {type:'join',username:username,channel:'main'};\n    ws.send(JSON.stringify(join));\n  }\n\n  ws.onmessage = (e) =\u003e {\n    try{\n      const obj = JSON.parse(e.data);\n      if(obj.type === 'text' || obj.type === 'system'){\n        addMessage(`\u003cdiv class=\"meta\"\u003e${obj.username}\u003c/div\u003e\u003cdiv\u003e${obj.message}\u003c/div\u003e`);\n      } else if(obj.type === 'file'){\n        const link = document.createElement('a');\n        link.href = 'data:application/octet-stream;base64,' + obj.data;\n        link.download = obj.filename;\n        link.textContent = `${obj.username} sent file: ${obj.filename} (click to download)`;\n        const wrapper = document.createElement('div'); wrapper.className='msg'; wrapper.appendChild(link); chat.appendChild(wrapper);\n      }\n    }catch(err){console.error(err)}\n  }\n\n  document.getElementById('send').addEventListener('click', send);\n  msgInput.addEventListener('keydown', (e)=\u003e{ if(e.key==='Enter') send(); });\n\n  function send(){\n    const text = msgInput.value.trim();\n    if(text){\n      ws.send(JSON.stringify({type:'text',username:username,channel:'main',message:text}));\n      msgInput.value = '';\n    } else if(fileInput.files.length){\n      const f = fileInput.files[0];\n      const reader = new FileReader();\n      reader.onload = () =\u003e {\n        const b64 = reader.result.split(',')[1];\n        ws.send(JSON.stringify({type:'file',username:username,channel:'main',filename:f.name,data:b64}));\n        fileInput.value = '';\n      };\n      reader.readAsDataURL(f); // results like data:...;base64,AAA\n    }\n  }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n**Hosting the web client for local testing:**\n\n* Serve the `web/` folder via any simple static server (e.g. `python -m http.server 8000` in the `web/` directory) and then open `http://localhost:8000/index.html` in your browser.\n* The page connects to `ws://\u003chost\u003e:8766`. If you open the page from a different machine, adjust the WebSocket URL accordingly.\n\n---\n\n## Requirements (`requirements.txt`)\n\n```\nwebsockets\u003e=10.0\n```\n\n(Desktop and web clients use only Python stdlib modules.)\n\n---\n\n## Installation \u0026 Run (quick)\n\n1. Clone the repo.\n\n2. Create a virtualenv (recommended) and install dependencies:\n\n```bash\npython -m venv venv\nsource venv/bin/activate  # mac/linux\nvenv\\Scripts\\Activate     # windows\npip install -r requirements.txt\n```\n\n3. Run the server:\n\n```bash\npython server.py\n```\n\nYou should see the server start and listen on TCP port 8765 and WebSocket 8766.\n\n4. Start the desktop client (on the same or another machine):\n\n```bash\npython desktop_client.py\n```\n\n5. Start the web client: serve `web/` statically and open `web/index.html` in a browser. For local testing:\n\n```bash\ncd web\npython -m http.server 8000\n# open http://localhost:8000 in your browser\n```\n\n6. Test:\n\n* Desktop client can send text and files; browser client receives them. Browser can upload files (base64) and desktop clients can save them.\n\n---\n\n## Security \u0026 Limitations\n\n* This is a demonstration project. Do **not** expose the server to the public internet without TLS and authentication.\n* Files are fully loaded into memory and sent base64-encoded — not suitable for very large files.\n* No authentication, rate-limiting, or spam protection.\n* WebSocket endpoint is plain `ws://`. For production, use `wss://` behind TLS.\n\n---\n\n## Extending the Project (ideas)\n\n* Add channels/rooms and UI to select rooms.\n* Add persistent message history (database) and message retrieval on join.\n* Replace the simple TCP protocol with `socket.io` for richer features, reconnection, and namespaces.\n* Add authentication (JWT) and TLS support for secure transport.\n* Chunked file upload for very large files.\n\n---\n\n## License\n\nMIT\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoderooz%2Fsocket-python-chat-app","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcoderooz%2Fsocket-python-chat-app","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoderooz%2Fsocket-python-chat-app/lists"}