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()