카테고리 없음

내부 메신저

1231. 2025. 3. 22. 22:40

 

python -m pip uninstall pycryptodome pycrypto -y

python -m pip install pycryptodome

 

F1,F2 : 대화 확인

F3 : 창 확대

F4 : 삭제

F5 : 호출

F7, F8 : 시간체크

 

방화벽설정을 통한 아이피 통제 필요합니다.

import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
import threading
import time
import hashlib
import socket
import json

# 다크모드 및 UI 스타일 설정
class ConsoleStyle:
    BG_COLOR = '#222222'
    FG_COLOR = '#ffffff'
    FONT = ('Helvetica', 10)
    BUTTON_FONT = ('Helvetica', 10, 'bold')
    ENTRY_FONT = ('Helvetica', 10)
    CURSOR_COLOR = '#00ff00'
    BORDER_COLOR = '#444444'

# 로컬 IP 주소 감지
def get_local_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
    except Exception:
        ip = "127.0.0.1"
    finally:
        s.close()
    return ip

# 채팅 클라이언트 GUI 클래스
class ChatClientGUI:
    def __init__(self, root):
        self.root = root
        self.original_width = 400
        self.original_height = 160
        self.root.geometry(f"{self.original_width}x{self.original_height}")
        self.root.title("로컬 디스크(C:)")
        self.is_large = False
        
        self.configure_dark_mode()
        
        self.local_ip = get_local_ip()
        # client_id는 PIN을 붙여 고유하게 생성됨
        self.client_id = self.local_ip.split('.')[-1]
        self.secret_key = None
        self.iv = None
        self.server_url = None
        self.room = None
        self.running = True
        self.stopwatch_start = None
        self.status_responses = {}
        self.chat_visible = False
        
        self.setup_gui()
        self.setup_bindings()
        
        self.update_clock()
        self.schedule_status_request()
        self.update_active_users()
    
    def configure_dark_mode(self):
        self.root.configure(bg=ConsoleStyle.BG_COLOR)
        style = ttk.Style()
        style.theme_use('clam')
        style.configure('TFrame', background=ConsoleStyle.BG_COLOR)
        style.configure('TLabel', background=ConsoleStyle.BG_COLOR, foreground=ConsoleStyle.FG_COLOR, font=ConsoleStyle.FONT)
        style.configure('TButton', background=ConsoleStyle.BG_COLOR, foreground=ConsoleStyle.FG_COLOR, font=ConsoleStyle.BUTTON_FONT)
        style.configure('TEntry', fieldbackground=ConsoleStyle.BG_COLOR, foreground=ConsoleStyle.FG_COLOR, font=ConsoleStyle.ENTRY_FONT)
    def insert_chat_message(self, text, tag):
        self.chat_area.config(state="normal")
        self.chat_area.insert(tk.END, text, tag)
        self.chat_area.see(tk.END)  # 마지막 줄로 스크롤
        self.chat_area.update_idletasks()  # 강제로 GUI 업데이트
        self.chat_area.config(state="disabled")

    
    def setup_gui(self):
        self.connect_frame = ttk.Frame(self.root, padding="15", relief="groove")
        self.connect_frame.pack(fill=tk.BOTH, padx=20, pady=20)
        self.connect_frame.columnconfigure(0, weight=1)
        self.connect_frame.columnconfigure(1, weight=2)
        self.connect_frame.columnconfigure(2, weight=1)
        
        ttk.Label(self.connect_frame, text="내 IP:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        ttk.Label(self.connect_frame, text=self.local_ip).grid(row=0, column=1, padx=5, pady=5, sticky="w")
        
        ttk.Label(self.connect_frame, text="서버 IP:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.server_ip = ttk.Entry(self.connect_frame, width=20)
        self.server_ip.insert(0, "127.0.0.1")
        self.server_ip.grid(row=1, column=1, padx=5, pady=5, sticky="w")
        
        ttk.Label(self.connect_frame, text="KEY :").grid(row=2, column=0, padx=5, pady=5, sticky="w")
        self.pin_entry = ttk.Entry(self.connect_frame, width=20)
        self.pin_entry.grid(row=2, column=1, padx=5, pady=5, sticky="w")
        
        ttk.Button(self.connect_frame, text="연결", command=self.connect_to_server)\
            .grid(row=0, column=2, rowspan=3, padx=10, pady=5, sticky="ns")
        
        self.clock_label = ttk.Label(self.root, text="", font=('Helvetica', 14, 'bold'), anchor="center")
        self.clock_label.pack(fill=tk.BOTH, expand=True)
        self.clock_label.bind("<Button-1>", lambda e: self.focus_message_entry())
        
        self.chat_area_frame = ttk.Frame(self.root, padding="10")
        self.chat_area = scrolledtext.ScrolledText(self.chat_area_frame, wrap=tk.WORD, height=8,
                                             bg=ConsoleStyle.BG_COLOR, fg=ConsoleStyle.FG_COLOR,
                                             font=ConsoleStyle.FONT, insertbackground=ConsoleStyle.CURSOR_COLOR)
        self.chat_area.pack(fill=tk.BOTH, expand=True)
        self.chat_area.tag_configure("right", justify="right", foreground=ConsoleStyle.FG_COLOR)
        self.chat_area.tag_configure("left", justify="left", foreground="yellow")
        self.chat_area.config(state="disabled")
        self.chat_area.bind("<Button-1>", lambda e: self.message_entry.focus_set())
        
        self.message_entry = ttk.Entry(self.root)
        self.message_entry.pack(fill=tk.X, side="bottom", pady=5)
        self.message_entry.bind('<Return>', lambda e: self.send_message())
        self.message_entry.configure(foreground=ConsoleStyle.BG_COLOR)
    
    def setup_bindings(self):
        self.root.bind('<Control-z>', lambda e: self.chat_area.delete(1.0, tk.END))
        self.root.bind('<F3>', self.toggle_window_size)
        self.root.bind('<F4>', lambda e: self.clear_message())
        self.root.bind('<F5>', self.alert_message)
        self.root.bind('<F7>', self.start_stopwatch)
        self.root.bind('<F8>', self.broadcast_stopwatch)
        self.root.bind('<KeyPress-F1>', self.show_chat)
        self.root.bind('<KeyRelease-F1>', self.hide_chat)
        self.root.bind('<KeyPress-F2>', self.show_chat)
        self.root.bind('<KeyRelease-F2>', self.hide_chat)
        self.root.bind_all("<Button-1>", lambda e: self.focus_message_entry())
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
    
    def toggle_window_size(self, event=None):
        if not self.is_large:
            self.root.geometry(f"{self.original_width*2}x{self.original_height*2}")
            self.is_large = True
        else:
            self.root.geometry(f"{self.original_width}x{self.original_height}")
            self.is_large = False
    
    def focus_message_entry(self):
        if self.connect_frame.winfo_ismapped():
            return
        self.message_entry.focus_set()
    
    # 연결 시 client_id에 PIN을 포함하여 고유하게 생성
    def connect_to_server(self):
        pin = self.pin_entry.get().strip()
        if len(pin) < 4:
            pin = (pin + "0000")[:4]
        elif len(pin) > 4:
            pin = pin[:4]
        
        self.room = pin
        self.client_id = f"{self.local_ip.split('.')[-1]}_{pin}"
        self.secret_key, self.iv = self.generate_key_from_pin(pin)
        self.server_url = f'http://{self.server_ip.get()}:5000'
        
        try:
            response = requests.post(
                f"{self.server_url}/register",
                json={"client_id": self.client_id, "room": self.room}
            )
            if response.status_code == 200:
                self.root.title(f"로컬 디스크(C: PIN {pin})")
                self.connect_frame.pack_forget()
                self.clock_label.pack(fill=tk.BOTH, expand=True)
            else:
                messagebox.showerror("오류", "서버 연결 실패")
        except Exception as e:
            messagebox.showerror("오류", f"연결 실패: {e}")
    
    def show_shortcut_info(self):
        info = ("F3: 창 크기 토글\n"
                "F4: 삭제\n"
                "F5: 호출\n"
                "F7, F8: 시간체크")
        messagebox.showinfo("안내", info)
    
    def generate_key_from_pin(self, pin):
        pin_bytes = pin.encode('utf-8')
        key_hash = hashlib.sha256(pin_bytes).digest()
        iv_hash = hashlib.md5(pin_bytes).digest()
        return key_hash, iv_hash
    
    def encrypt_message(self, message):
        cipher = AES.new(self.secret_key, AES.MODE_CBC, self.iv)
        encrypted = cipher.encrypt(pad(message.encode('utf-8'), AES.block_size))
        return base64.b64encode(encrypted).decode('utf-8')
    
    def decrypt_message(self, encrypted_message):
        cipher = AES.new(self.secret_key, AES.MODE_CBC, self.iv)
        decrypted = unpad(cipher.decrypt(base64.b64decode(encrypted_message)), AES.block_size)
        return decrypted.decode('utf-8')
    
    def send_message(self):
        message = self.message_entry.get().strip()
        if message and self.server_url:
            encrypted = self.encrypt_message(message)
            payload = {"sender_id": self.client_id, "message": encrypted}
            try:
                requests.post(f"{self.server_url}/send", json=payload)
            except Exception as e:
                print(f"메시지 전송 오류: {e}")
            self.insert_chat_message(f"{message}\n", "right")
            self.message_entry.delete(0, tk.END)
    
    def alert_message(self, event=None):
        self.message_entry.delete(0, tk.END)
        self.message_entry.insert(0, "호출쓰")
        self.send_message()
    
    def send_custom_message(self, text, display=True):
        if text and self.server_url:
            encrypted = self.encrypt_message(text)
            payload = {"sender_id": self.client_id, "message": encrypted}
            try:
                requests.post(f"{self.server_url}/send", json=payload)
            except Exception as e:
                print(f"커스텀 메시지 전송 오류: {e}")
            if display:
                self.chat_area.insert(tk.END, f"{text}\n", "right")
    
    def send_silent_message(self, text):
        self.send_custom_message(text, display=False)
    
    # 메시지 수신 시 각 메시지를 개별 try/except로 처리하여 복호화 오류가 전체 루프를 멈추지 않도록 함
    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:
                    for msg in response.json().get('messages', []):
                        try:
                            sender_id = msg.get('sender_id', '상대')
                            encrypted_msg = msg.get('message', '')
                            if not encrypted_msg:
                                continue
                            decrypted_msg = self.decrypt_message(encrypted_msg)
                        except Exception as decryption_error:
                            print(f"메시지 복호화 오류: {decryption_error}")
                            continue
                        
                        if decrypted_msg == "##STATUS_REQUEST##":
                            if sender_id != self.client_id:
                                self.send_silent_message("##STATUS_OK##")
                            continue
                        if decrypted_msg == "##STATUS_OK##":
                            if sender_id != self.client_id:
                                self.status_responses[sender_id] = time.time()
                            continue
                        if decrypted_msg == "호출쓰" and sender_id != self.client_id:
                            messagebox.showinfo("", "")
                        if decrypted_msg == "deldeldel":
                            self.chat_area.delete(1.0, tk.END)
                            continue
                        if sender_id != self.client_id:
                            timestamp = time.strftime("[%H:%M]", time.localtime())
                            display_sender = sender_id.split('_')[0] if '_' in sender_id else sender_id
                            def insert_msg(ts, ds, m):
                                self.insert_chat_message(f"{ts} {ds}: {m}\n", "left")
                            self.chat_area.after(0, insert_msg, timestamp, display_sender, decrypted_msg)

            except Exception as e:
                print(f"메시지 수신 중 오류 발생: {e}")
            time.sleep(1)
    
    def clear_message(self):
        self.send_custom_message("deldeldel", display=False)
        self.chat_area.delete(1.0, tk.END)
    
    def show_chat(self, event=None):
        if not self.chat_visible:
            self.chat_visible = True
            self.clock_label.pack_forget()
            self.chat_area_frame.pack(fill=tk.BOTH, expand=True, before=self.message_entry)
            self.message_entry.focus_set()
    
    def hide_chat(self, event=None):
        if self.chat_visible:
            self.chat_visible = False
            self.chat_area_frame.pack_forget()
            self.clock_label.pack(fill=tk.BOTH, expand=True)
            self.message_entry.focus_set()
    
    def update_clock(self):
        if not self.chat_visible:
            now = time.localtime()
            day = time.strftime("%d", now)
            weekday_number = time.strftime("%w", now)
            weekday_map = {"0": "일", "1": "월", "2": "화", "3": "수", "4": "목", "5": "금", "6": "토"}
            weekday = weekday_map.get(weekday_number, "")
            current_time = f"{day}({weekday}) " + time.strftime("%H:%M:%S", now)
            self.clock_label.config(text=current_time)
        self.root.after(1000, self.update_clock)
    
    def send_status_request(self):
        if self.server_url:
            self.send_silent_message("##STATUS_REQUEST##")
    
    def schedule_status_request(self):
        self.send_status_request()
        self.root.after(30000, self.schedule_status_request)
    
    def update_active_users(self):
        if self.server_url:
            now = time.time()
            active_count = 1
            for ts in self.status_responses.values():
                if now - ts <= 40:
                    active_count += 1
            if self.room:
                self.root.title(f"로컬 디스크(C: {self.room} / {active_count}명)")
            else:
                self.root.title(f"로컬 디스크(C: {active_count}명)")
        else:
            self.root.title("로컬 디스크(C:)")
        self.root.after(10000, self.update_active_users)
    
    def start_stopwatch(self, event=None):
        self.stopwatch_start = time.time()
        self.message_entry.insert(0, "타임워치 시작!")
        self.send_message()
    
    def broadcast_stopwatch(self, event=None):
        if self.stopwatch_start is None:
            print("타임워치가 시작되지 않았습니다.")
        else:
            elapsed = time.time() - self.stopwatch_start
            hours = int(elapsed // 3600)
            minutes = int((elapsed % 3600) // 60)
            elapsed_str = f"{hours}시간 {minutes}분 경과!"
            self.message_entry.insert(0, elapsed_str)
            self.send_message()
            
    
    def on_closing(self):
        self.running = False
        self.root.destroy()

if __name__ == '__main__':
    root = tk.Tk()
    app = ChatClientGUI(root)
    threading.Thread(target=app.receive_messages, daemon=True).start()
    root.mainloop()