import socket import threading import json import tkinter as tk from tkinter import ttk, scrolledtext, messagebox from datetime import datetime from tkinter import font as tkfont class ChatClient: def __init__(self, root): self.root = root self.root.title("Python Chat") self.root.geometry("800x600") self.root.configure(bg='#f0f0f0') self.current_user = None self.current_chat = None self.is_group_chat = False self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.setup_ui() def setup_ui(self): self.custom_font = tkfont.Font(family="Helvetica", size=10) self.bold_font = tkfont.Font(family="Helvetica", size=10, weight="bold") self.main_frame = ttk.Frame(self.root) self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) self.left_panel = ttk.Frame(self.main_frame, width=200) self.left_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) self.left_panel.pack_propagate(False) self.right_panel = ttk.Frame(self.main_frame) self.right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) self.login_frame = ttk.Frame(self.left_panel) self.login_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(self.login_frame, text="Username:").pack(anchor=tk.W) self.username_entry = ttk.Entry(self.login_frame) self.username_entry.pack(fill=tk.X) ttk.Label(self.login_frame, text="Password:").pack(anchor=tk.W, pady=(5, 0)) self.password_entry = ttk.Entry(self.login_frame, show="*") self.password_entry.pack(fill=tk.X) self.login_button = ttk.Button(self.login_frame, text="Login", command=self.login) self.login_button.pack(fill=tk.X, pady=(5, 0)) self.register_button = ttk.Button(self.login_frame, text="Register", command=self.register) self.register_button.pack(fill=tk.X, pady=(5, 0)) self.user_list_frame = ttk.LabelFrame(self.left_panel, text="Users") self.user_list_frame.pack(fill=tk.BOTH, expand=True) self.user_list = tk.Listbox(self.user_list_frame, font=self.custom_font, selectmode=tk.SINGLE) self.user_list.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.user_list.bind('<>', self.select_user) self.group_list_frame = ttk.LabelFrame(self.left_panel, text="Groups") self.group_list_frame.pack(fill=tk.BOTH, pady=(10, 0)) self.group_list = tk.Listbox(self.group_list_frame, font=self.custom_font, selectmode=tk.SINGLE) self.group_list.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.group_list.bind('<>', self.select_group) self.create_group_button = ttk.Button(self.left_panel, text="Create Group", command=self.show_create_group_dialog) self.create_group_button.pack(fill=tk.X, pady=(10, 0)) self.chat_frame = ttk.Frame(self.right_panel) self.chat_frame.pack(fill=tk.BOTH, expand=True) self.chat_header = ttk.Label(self.chat_frame, text="Select a chat", font=self.bold_font) self.chat_header.pack(anchor=tk.W, padx=5, pady=5) self.chat_display = scrolledtext.ScrolledText( self.chat_frame, wrap=tk.WORD, state=tk.DISABLED, font=self.custom_font, bg='white', padx=10, pady=10 ) self.chat_display.pack(fill=tk.BOTH, expand=True) self.input_frame = ttk.Frame(self.right_panel) self.input_frame.pack(fill=tk.X, pady=(10, 0)) self.message_entry = ttk.Entry(self.input_frame) self.message_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) self.message_entry.bind('', self.send_message) self.send_button = ttk.Button(self.input_frame, text="Send", command=self.send_message) self.send_button.pack(side=tk.RIGHT) self.update_ui_state(False) def update_ui_state(self, logged_in): state = tk.NORMAL if logged_in else tk.DISABLED self.user_list.config(state=state) self.group_list.config(state=state) self.message_entry.config(state=state) self.send_button.config(state=state) self.create_group_button.config(state=state) if not logged_in: self.user_list.delete(0, tk.END) self.group_list.delete(0, tk.END) self.chat_header.config(text="Select a chat") self.chat_display.config(state=tk.NORMAL) self.chat_display.delete(1.0, tk.END) self.chat_display.config(state=tk.DISABLED) self.current_chat = None def login(self): username = self.username_entry.get() password = self.password_entry.get() if not username or not password: messagebox.showerror("Error", "Username and password are required") return try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect(('localhost', 5555)) self.socket.send(json.dumps({ 'type': 'login', 'username': username, 'password': password }).encode('utf-8')) response = json.loads(self.socket.recv(1024).decode('utf-8')) if response['type'] == 'login_success': self.current_user = username self.update_ui_state(True) threading.Thread(target=self.receive_messages, daemon=True).start() else: messagebox.showerror("Error", "Login failed - Invalid username or password") self.socket.close() except Exception as e: messagebox.showerror("Error", f"Connection error: {str(e)}") def register(self): username = self.username_entry.get() password = self.password_entry.get() if not username or not password: messagebox.showerror("Error", "Username and password are required") return try: temp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) temp_socket.connect(('localhost', 5555)) temp_socket.send(json.dumps({ 'type': 'register', 'username': username, 'password': password }).encode('utf-8')) response = json.loads(temp_socket.recv(1024).decode('utf-8')) temp_socket.close() if response['type'] == 'register_success': messagebox.showinfo("Success", "Registration successful. Please login now.") else: messagebox.showerror("Error", "Registration failed - username may be taken or invalid") except Exception as e: messagebox.showerror("Error", f"Connection error: {str(e)}") def receive_messages(self): while True: try: message = self.socket.recv(1024).decode('utf-8') if not message: break data = json.loads(message) if data['type'] == 'user_list': self.user_list.delete(0, tk.END) for user in data['users']: if user != self.current_user: self.user_list.insert(tk.END, user) elif data['type'] == 'group_list': self.group_list.delete(0, tk.END) for group in data['groups']: self.group_list.insert(tk.END, group) elif data['type'] == 'message': self.display_message( data['sender'], data['receiver'], data['message'], data['is_group'] ) elif data['type'] == 'initial_messages': for msg in data['messages']: self.display_message( msg['sender'], msg['receiver'], msg['message'], msg['is_group'], msg['timestamp'] ) elif data['type'] == 'server_shutdown': messagebox.showinfo("Server", "Server is shutting down") self.on_closing() break except Exception as e: print(f"Error receiving message: {e}") break def display_message(self, sender, receiver, message, is_group, timestamp=None): timestamp = timestamp or datetime.now().strftime("%Y-%m-%d %H:%M:%S") if (is_group and receiver == self.current_chat) or \ (not is_group and ((sender == self.current_chat and receiver == self.current_user) or \ (sender == self.current_user and receiver == self.current_chat))): self.chat_display.config(state=tk.NORMAL) if sender == self.current_user: self.chat_display.tag_config('right', justify='right', foreground='blue') self.chat_display.insert(tk.END, f"{message} ({timestamp})\n", 'right') else: sender_label = f"{sender} (group)" if is_group and sender != self.current_chat else sender self.chat_display.tag_config('left', justify='left', foreground='green') self.chat_display.insert(tk.END, f"{sender_label}: {message} ({timestamp})\n", 'left') self.chat_display.config(state=tk.DISABLED) self.chat_display.see(tk.END) def select_user(self, event): selection = event.widget.curselection() if selection: self.current_chat = event.widget.get(selection[0]) self.is_group_chat = False self.chat_header.config(text=f"Chat with {self.current_chat}") self.chat_display.config(state=tk.NORMAL) self.chat_display.delete(1.0, tk.END) self.chat_display.config(state=tk.DISABLED) def select_group(self, event): selection = event.widget.curselection() if selection: self.current_chat = event.widget.get(selection[0]) self.is_group_chat = True self.chat_header.config(text=f"Group: {self.current_chat}") self.chat_display.config(state=tk.NORMAL) self.chat_display.delete(1.0, tk.END) self.chat_display.config(state=tk.DISABLED) def send_message(self, event=None): if not self.current_chat or not self.message_entry.get(): return message = self.message_entry.get() self.socket.send(json.dumps({ 'type': 'message', 'sender': self.current_user, 'receiver': self.current_chat, 'message': message, 'is_group': self.is_group_chat }).encode('utf-8')) self.display_message( self.current_user, self.current_chat, message, self.is_group_chat ) self.message_entry.delete(0, tk.END) def show_create_group_dialog(self): if not self.current_user: return dialog = tk.Toplevel(self.root) dialog.title("Create Group") dialog.geometry("300x300") ttk.Label(dialog, text="Group Name:").pack(anchor=tk.W, padx=10, pady=(10, 0)) group_name_entry = ttk.Entry(dialog) group_name_entry.pack(fill=tk.X, padx=10, pady=(0, 10)) ttk.Label(dialog, text="Select Members:").pack(anchor=tk.W, padx=10, pady=(10, 0)) members_frame = ttk.Frame(dialog) members_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) members_list = tk.Listbox(members_frame, selectmode=tk.MULTIPLE) members_list.pack(fill=tk.BOTH, expand=True) for i in range(self.user_list.size()): members_list.insert(tk.END, self.user_list.get(i)) def create_group(): group_name = group_name_entry.get() selected_indices = members_list.curselection() selected_members = [members_list.get(i) for i in selected_indices] if not group_name or not selected_members: messagebox.showerror("Error", "Group name and at least one member are required") return self.socket.send(json.dumps({ 'type': 'create_group', 'group_name': group_name, 'members': selected_members + [self.current_user] }).encode('utf-8')) dialog.destroy() button_frame = ttk.Frame(dialog) button_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) ttk.Button(button_frame, text="Create", command=create_group).pack(side=tk.RIGHT) ttk.Button(button_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=(0, 5)) def on_closing(self): if hasattr(self, 'socket') and self.current_user: try: self.socket.close() except: pass self.root.destroy() if __name__ == "__main__": root = tk.Tk() client = ChatClient(root) root.protocol("WM_DELETE_WINDOW", client.on_closing) root.mainloop()