1231. 2025. 2. 21. 12:40
import tkinter as tk
from tkinter import ttk, scrolledtext
import requests
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
import base64
import threading
import time
import random
import string

class ConsoleStyle:
    BG_COLOR = 'black'
    FG_COLOR = '#00ff00'  # 연한 녹색 (터미널 스타일)
    FONT = ('Consolas', 10)  # 고정폭 폰트
    CURSOR_COLOR = '#00ff00'
    SELECT_BG = '#003300'  # 선택 영역 배경색
    SELECT_FG = '#00ff00'  # 선택 영역 텍스트색

class ChatClientGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Secure Terminal Chat")
        self.setup_gui()
        self.setup_client()
        self.apply_console_style()
        self.setup_shortcuts()  # 단축키 설정 추가

    def setup_shortcuts(self):
        # Ctrl+O 단축키 바인딩
        self.root.bind('<Control-o>', lambda e: self.clear_chat())

    def clear_chat(self):
        # 채팅 영역의 모든 내용을 삭제
        self.chat_area.delete(1.0, tk.END)
        self.show_system_message("대화 내용이 삭제되었습니다.")

    def apply_console_style(self):
        # 윈도우 스타일
        self.root.configure(bg=ConsoleStyle.BG_COLOR)
        
        # 스타일 설정
        style = ttk.Style()
        style.configure('Console.TFrame', background=ConsoleStyle.BG_COLOR)
        style.configure('Console.TLabel', 
                       background=ConsoleStyle.BG_COLOR,
                       foreground=ConsoleStyle.FG_COLOR,
                       font=ConsoleStyle.FONT)
        style.configure('Console.TButton',
                       background=ConsoleStyle.BG_COLOR,
                       foreground=ConsoleStyle.FG_COLOR,
                       font=ConsoleStyle.FONT)
        style.configure('Console.TEntry',
                       background=ConsoleStyle.BG_COLOR,
                       foreground=ConsoleStyle.FG_COLOR,
                       font=ConsoleStyle.FONT,
                       fieldbackground=ConsoleStyle.BG_COLOR)

        # 채팅 영역 스타일
        self.chat_area.configure(
            bg=ConsoleStyle.BG_COLOR,
            fg=ConsoleStyle.FG_COLOR,
            font=ConsoleStyle.FONT,
            insertbackground=ConsoleStyle.CURSOR_COLOR,
            selectbackground=ConsoleStyle.SELECT_BG,
            selectforeground=ConsoleStyle.SELECT_FG
        )

        # 입력 영역 스타일
        self.message_entry.configure(
            style='Console.TEntry',
            font=ConsoleStyle.FONT
        )

    def setup_client(self):
        self.client_id = self.generate_client_id()
        self.running = True
        self.server_url = None
        
        # 서버 연결 프레임
        self.connect_frame = ttk.Frame(self.root, padding="10", style='Console.TFrame')
        self.connect_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        ttk.Label(self.connect_frame, text="서버 IP:", style='Console.TLabel').grid(row=0, column=0, padx=5)
        self.server_ip = ttk.Entry(self.connect_frame, width=20, style='Console.TEntry')
        self.server_ip.insert(0, "localhost")
        self.server_ip.grid(row=0, column=1, padx=5)
        
        self.connect_btn = ttk.Button(self.connect_frame, text="연결", 
                                    command=self.connect_to_server, 
                                    style='Console.TButton')
        self.connect_btn.grid(row=0, column=2, padx=5)

        # 메시지 영역은 처음에는 숨김
        self.message_frame.grid_remove()

    def setup_gui(self):
        # 메시지 프레임
        self.message_frame = ttk.Frame(self.root, padding="10", style='Console.TFrame')
        self.message_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 채팅 영역
        self.chat_area = scrolledtext.ScrolledText(
            self.message_frame, 
            wrap=tk.WORD, 
            width=80,  # 더 넓게
            height=24  # 더 높게
        )
        self.chat_area.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 메시지 입력
        self.message_entry = ttk.Entry(
            self.message_frame,
            style='Console.TEntry',
            width=70  # 더 넓게
        )
        self.message_entry.grid(row=1, column=0, sticky=(tk.W, tk.E), padx=5, pady=5)
        self.message_entry.bind('<Return>', lambda e: self.send_message())
        
        # 전송 버튼
        self.send_button = ttk.Button(
            self.message_frame, 
            text="전송", 
            command=self.send_message,
            style='Console.TButton'
        )
        self.send_button.grid(row=1, column=1, sticky=(tk.E), pady=5)
        
        # Grid 설정
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(1, weight=1)
        self.message_frame.columnconfigure(0, weight=1)

    def generate_client_id(self):
        return ''.join(random.choices(string.ascii_letters + string.digits, k=8))

    def connect_to_server(self):
        server_ip = self.server_ip.get()
        self.server_url = f'http://{server_ip}:5000'
        
        try:
            response = requests.post(f"{self.server_url}/register", 
                                   json={"client_id": self.client_id})
            if response.status_code == 200:
                self.show_system_message("서버에 연결되었습니다.")
                self.connect_frame.grid_remove()
                self.message_frame.grid()
                self.receive_thread = threading.Thread(target=self.receive_messages)
                self.receive_thread.daemon = True
                self.receive_thread.start()
            else:
                self.show_system_message("서버 연결 실패")
        except Exception as e:
            self.show_system_message(f"연결 오류: {str(e)}")

    def encrypt_message(self, message):
        message_bytes = message.encode('utf-8')
        padded_message = pad(message_bytes, AES.block_size)
        cipher = AES.new(SECRET_KEY, AES.MODE_CBC, IV=IV)
        encrypted_message = cipher.encrypt(padded_message)
        return base64.b64encode(encrypted_message).decode('utf-8')

    def decrypt_message(self, encrypted_message):
        encrypted_bytes = base64.b64decode(encrypted_message)
        cipher = AES.new(SECRET_KEY, AES.MODE_CBC, IV=IV)
        decrypted_padded = cipher.decrypt(encrypted_bytes)
        return unpad(decrypted_padded, AES.block_size).decode('utf-8')

    def send_message(self):
        if not self.server_url:
            return
            
        message = self.message_entry.get().strip()
        if message:
            try:
                encrypted_message = self.encrypt_message(message)
                response = requests.post(
                    f"{self.server_url}/send",
                    json={
                        "sender_id": self.client_id,
                        "message": encrypted_message
                    }
                )
                if response.status_code == 200:
                    self.message_entry.delete(0, tk.END)
                    self.show_message("me", message)
            except Exception as e:
                self.show_system_message(f"전송 오류: {str(e)}")

    def receive_messages(self):
        while self.running:
            if not self.server_url:
                time.sleep(1)
                continue
                
            try:
                response = requests.post(
                    f"{self.server_url}/receive",
                    json={"client_id": self.client_id}
                )
                if response.status_code == 200:
                    messages = response.json()['messages']
                    for encrypted_message in messages:
                        try:
                            decrypted_message = self.decrypt_message(encrypted_message)
                            self.root.after(0, self.show_message, "other", decrypted_message)
                        except Exception as e:
                            print(f"메시지 복호화 오류: {str(e)}")
            except Exception as e:
                print(f"수신 오류: {str(e)}")
            time.sleep(1)

    def show_message(self, sender, message):
        timestamp = time.strftime("%H:%M:%S")
        if sender == "me":
            formatted_message = f"[{timestamp}] >>> {message}\n"
        else:
            # 상대방 메시지는 ID 없이 표시
            formatted_message = f"[{timestamp}] <<< {message}\n"
        self.chat_area.insert(tk.END, formatted_message)
        self.chat_area.see(tk.END)

    def show_system_message(self, message):
        timestamp = time.strftime("%H:%M:%S")
        formatted_message = f"[{timestamp}] === {message} ===\n"
        self.chat_area.insert(tk.END, formatted_message)
        self.chat_area.see(tk.END)

    def on_closing(self):
        self.running = False
        if self.server_url:
            try:
                requests.post(f"{self.server_url}/unregister", 
                            json={"client_id": self.client_id})
            except:
                pass
        self.root.destroy()

def main():
    root = tk.Tk()
    root.configure(bg='black')
    # 윈도우 크기 설정
    root.geometry("800x600")  # 더 큰 창 크기
    app = ChatClientGUI(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()

if __name__ == '__main__':
    SECRET_KEY = b'01234567890123456789012345678901'
    IV = b'0123456789012345'
    main()