Skip to content

Commit eab4b5d

Browse files
authored
Create Distributed-Chat-Application.py
1 parent 4f6c79b commit eab4b5d

1 file changed

Lines changed: 237 additions & 0 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import socket
2+
import threading
3+
import tkinter as tk
4+
import json
5+
import sqlite3
6+
from datetime import datetime
7+
from typing import Dict
8+
9+
import ttkbootstrap as tb
10+
from ttkbootstrap.constants import *
11+
from ttkbootstrap.widgets.scrolled import ScrolledText
12+
13+
# ================== CONFIG ================== #
14+
HOST = "0.0.0.0" # LAN / WAN support
15+
PORT = 5050
16+
BUFFER_SIZE = 4096
17+
DB_FILE = "chat.db"
18+
19+
# ================== DATABASE ================== #
20+
def init_db():
21+
conn = sqlite3.connect(DB_FILE)
22+
cur = conn.cursor()
23+
cur.execute("""
24+
CREATE TABLE IF NOT EXISTS messages (
25+
id INTEGER PRIMARY KEY AUTOINCREMENT,
26+
sender TEXT,
27+
content TEXT,
28+
reply_to TEXT,
29+
timestamp TEXT
30+
)
31+
""")
32+
conn.commit()
33+
conn.close()
34+
35+
36+
def save_message(sender, content, reply_to=None):
37+
conn = sqlite3.connect(DB_FILE)
38+
cur = conn.cursor()
39+
cur.execute(
40+
"INSERT INTO messages (sender, content, reply_to, timestamp) VALUES (?, ?, ?, ?)",
41+
(sender, content, reply_to, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
42+
)
43+
conn.commit()
44+
conn.close()
45+
46+
47+
def load_history(limit=50):
48+
conn = sqlite3.connect(DB_FILE)
49+
cur = conn.cursor()
50+
cur.execute(
51+
"SELECT sender, content, reply_to, timestamp FROM messages ORDER BY id DESC LIMIT ?",
52+
(limit,)
53+
)
54+
rows = cur.fetchall()
55+
conn.close()
56+
return reversed(rows)
57+
58+
# ================== SERVER ================== #
59+
clients: Dict[socket.socket, str] = {}
60+
server_running = True
61+
62+
63+
def start_server(log_callback):
64+
init_db()
65+
66+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
67+
server.bind((HOST, PORT))
68+
server.listen()
69+
70+
log_callback(f"Server running on {HOST}:{PORT}")
71+
72+
def broadcast(message):
73+
for c in list(clients.keys()):
74+
try:
75+
c.send(message.encode())
76+
except:
77+
clients.pop(c, None)
78+
79+
def handle_client(conn, addr):
80+
try:
81+
username = conn.recv(BUFFER_SIZE).decode()
82+
clients[conn] = username
83+
84+
# Send chat history
85+
for sender, content, reply_to, ts in load_history():
86+
prefix = f"[{ts}] {sender}:"
87+
if reply_to:
88+
conn.send(f"{prefix}{reply_to}\n{content}".encode())
89+
else:
90+
conn.send(f"{prefix} {content}".encode())
91+
92+
broadcast(f"[SERVER] 🟢 {username} joined the chat")
93+
log_callback(f"{username} connected from {addr}")
94+
95+
while server_running:
96+
raw = conn.recv(BUFFER_SIZE).decode()
97+
if not raw:
98+
break
99+
100+
data = json.loads(raw)
101+
msg = data.get("msg", "")
102+
reply_to = data.get("reply_to")
103+
ts = datetime.now().strftime("%H:%M")
104+
105+
save_message(username, msg, reply_to)
106+
107+
if reply_to:
108+
broadcast(f"[{ts}] {username}: ↪ {reply_to}\n{msg}")
109+
else:
110+
broadcast(f"[{ts}] {username}: {msg}")
111+
112+
except Exception as e:
113+
log_callback(f"Client error: {e}")
114+
finally:
115+
name = clients.pop(conn, "Unknown")
116+
conn.close()
117+
broadcast(f"[SERVER] 🔴 {name} left the chat")
118+
119+
while True:
120+
conn, addr = server.accept()
121+
threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()
122+
123+
# ================== CLIENT ================== #
124+
class ChatClient:
125+
def __init__(self, username, host, message_callback):
126+
self.username = username
127+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
128+
self.sock.connect((host, PORT))
129+
self.sock.send(username.encode())
130+
self.message_callback = message_callback
131+
threading.Thread(target=self.listen, daemon=True).start()
132+
133+
def listen(self):
134+
while True:
135+
try:
136+
msg = self.sock.recv(BUFFER_SIZE).decode()
137+
if msg:
138+
self.message_callback(msg)
139+
except:
140+
break
141+
142+
def send(self, msg, reply_to=None):
143+
payload = json.dumps({"msg": msg, "reply_to": reply_to})
144+
self.sock.send(payload.encode())
145+
146+
# ================== UI ================== #
147+
app = tb.Window(
148+
title="Distributed Chat Application",
149+
themename="darkly",
150+
size=(900, 650)
151+
)
152+
153+
username = ""
154+
server_ip = ""
155+
client = None
156+
reply_target = None
157+
158+
# ---------- LOGIN ---------- #
159+
login = tb.Frame(app, padding=30)
160+
login.pack(fill=tk.BOTH, expand=True)
161+
162+
tb.Label(login, text="Username").pack()
163+
user_entry = tb.Entry(login)
164+
user_entry.pack(fill=tk.X, pady=5)
165+
166+
tb.Label(login, text="Server IP (LAN / WAN)").pack()
167+
ip_entry = tb.Entry(login)
168+
ip_entry.insert(0, "127.0.0.1")
169+
ip_entry.pack(fill=tk.X, pady=5)
170+
171+
172+
def join_chat():
173+
global username, server_ip, client
174+
username = user_entry.get().strip()
175+
server_ip = ip_entry.get().strip()
176+
if username and server_ip:
177+
login.pack_forget()
178+
chat.pack(fill=tk.BOTH, expand=True)
179+
client = ChatClient(username, server_ip, display_message)
180+
display_message(f"[SYSTEM] Connected as {username}")
181+
182+
183+
tb.Button(login, text="Join Chat", bootstyle="success", command=join_chat).pack(pady=15)
184+
185+
# ---------- CHAT ---------- #
186+
chat = tb.Frame(app)
187+
188+
chat_box = ScrolledText(chat)
189+
chat_box.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
190+
chat_box.text.configure(state="disabled")
191+
192+
reply_label = tb.Label(chat, text="", bootstyle="warning")
193+
reply_label.pack(anchor=tk.W, padx=10)
194+
195+
entry_frame = tb.Frame(chat)
196+
entry_frame.pack(fill=tk.X, padx=10, pady=5)
197+
198+
msg_entry = tb.Entry(entry_frame)
199+
msg_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
200+
201+
202+
def send_message():
203+
global reply_target
204+
text = msg_entry.get().strip()
205+
if text:
206+
client.send(text, reply_target)
207+
reply_target = None
208+
reply_label.config(text="")
209+
msg_entry.delete(0, tk.END)
210+
211+
212+
msg_entry.bind("<Return>", lambda e: send_message())
213+
214+
215+
def on_click(event):
216+
global reply_target
217+
idx = chat_box.text.index("@%s,%s linestart" % (event.x, event.y))
218+
line = chat_box.text.get(idx, idx + " lineend").strip()
219+
reply_target = line
220+
reply_label.config(text=f"Replying to: {line[:50]}...")
221+
222+
223+
def display_message(msg):
224+
chat_box.text.configure(state="normal")
225+
chat_box.text.insert("end", msg + "\n")
226+
chat_box.text.see("end")
227+
chat_box.text.configure(state="disabled")
228+
229+
230+
chat_box.text.bind("<Button-1>", on_click)
231+
232+
tb.Button(entry_frame, text="Send", bootstyle="primary", command=send_message).pack(side=tk.RIGHT)
233+
234+
# ---------- START SERVER THREAD ---------- #
235+
threading.Thread(target=start_server, args=(display_message,), daemon=True).start()
236+
237+
app.mainloop()

0 commit comments

Comments
 (0)