396 lines
15 KiB
Python
396 lines
15 KiB
Python
import socket
|
||
import threading
|
||
import rsa
|
||
import os
|
||
from pathlib import Path
|
||
import time
|
||
from datetime import datetime
|
||
import urwid
|
||
import sys
|
||
import locale
|
||
|
||
|
||
class ChatClient:
|
||
def __init__(self, host='127.0.0.1', port=5555):
|
||
# Настройка локализации
|
||
locale.setlocale(locale.LC_ALL, '')
|
||
os.environ['LANG'] = 'ru_RU.UTF-8'
|
||
|
||
self.host = host
|
||
self.port = port
|
||
self.nickname = None
|
||
self.client_public_key = None
|
||
self.client_private_key = None
|
||
self.server_public_key = None
|
||
self.current_chat = []
|
||
self.file_transfers = {}
|
||
self.current_file = None
|
||
self.input_history = []
|
||
self.history_index = -1
|
||
self.last_update = time.time()
|
||
self.update_interval = 5 # Интервал обновления в секундах
|
||
|
||
# Цветовая схема
|
||
self.palette = [
|
||
('header', 'white', 'dark blue'),
|
||
('chat', 'light gray', 'black'),
|
||
('input', 'white', 'black'),
|
||
('status', 'white', 'dark blue'),
|
||
('nick', 'light cyan', 'black'),
|
||
('server', 'light green', 'black'),
|
||
('file', 'yellow', 'black'),
|
||
('error', 'light red', 'black'),
|
||
('own', 'light magenta', 'black'),
|
||
('highlight', 'black', 'light gray'),
|
||
]
|
||
|
||
self.generate_keys()
|
||
self.setup_client()
|
||
self.setup_ui()
|
||
|
||
def generate_keys(self):
|
||
(self.client_public_key, self.client_private_key) = rsa.newkeys(2048)
|
||
|
||
def setup_client(self):
|
||
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
try:
|
||
self.client.connect((self.host, self.port))
|
||
except Exception as e:
|
||
print(f"Не удалось подключиться к серверу: {e}")
|
||
sys.exit(1)
|
||
|
||
def setup_ui(self):
|
||
# Создаем виджеты чата
|
||
self.chat_content = urwid.SimpleListWalker([])
|
||
self.chat_list = urwid.ListBox(self.chat_content)
|
||
|
||
# Исправленная строка - правильно закрываем скобки Frame
|
||
chat_frame = urwid.Frame(
|
||
urwid.AttrMap(self.chat_list, 'chat'),
|
||
header=urwid.AttrMap(urwid.Text(" ЧАТ "), 'header'))
|
||
|
||
# Поле ввода
|
||
self.input_edit = urwid.Edit(caption="Сообщение: ")
|
||
input_pile = urwid.Pile([
|
||
urwid.AttrMap(self.input_edit, 'input'),
|
||
urwid.Divider('-')
|
||
])
|
||
|
||
# Статус бар
|
||
self.status_text = urwid.Text("")
|
||
status_bar = urwid.AttrMap(self.status_text, 'status')
|
||
|
||
# Главный layout
|
||
self.main_frame = urwid.Frame(
|
||
body=chat_frame,
|
||
footer=urwid.Pile([
|
||
input_pile,
|
||
status_bar
|
||
])
|
||
)
|
||
|
||
# Главный цикл
|
||
self.loop = urwid.MainLoop(
|
||
self.main_frame,
|
||
palette=self.palette,
|
||
unhandled_input=self.handle_input
|
||
)
|
||
|
||
def handle_input(self, key):
|
||
if key == 'enter':
|
||
self.process_command(self.input_edit.get_edit_text())
|
||
self.input_edit.set_edit_text("")
|
||
self.history_index = -1
|
||
elif key == 'up':
|
||
self.navigate_history(-1)
|
||
elif key == 'down':
|
||
self.navigate_history(1)
|
||
elif key == 'f1':
|
||
self.show_help()
|
||
elif key == 'ctrl u':
|
||
self.input_edit.set_edit_text("")
|
||
|
||
def navigate_history(self, direction):
|
||
if not self.input_history:
|
||
return
|
||
|
||
if direction == -1 and abs(self.history_index) < len(self.input_history):
|
||
self.history_index -= 1
|
||
elif direction == 1 and self.history_index < -1:
|
||
self.history_index += 1
|
||
|
||
if -len(self.input_history) <= self.history_index <= -1:
|
||
self.input_edit.set_edit_text(self.input_history[self.history_index])
|
||
|
||
def process_command(self, msg):
|
||
if not msg.strip():
|
||
return
|
||
|
||
# Добавляем в историю
|
||
self.input_history.append(msg)
|
||
if len(self.input_history) > 100:
|
||
self.input_history.pop(0)
|
||
|
||
if msg.lower() == '/exit':
|
||
self.client.close()
|
||
raise urwid.ExitMainLoop()
|
||
elif msg.lower() == '/help':
|
||
self.show_help()
|
||
elif msg.lower() in ('y', 'n') and hasattr(self, 'current_file') and self.current_file.get('awaiting_response'):
|
||
response = '/accept_file' if msg.lower() == 'y' else '/reject_file'
|
||
encrypted_response = rsa.encrypt(response.encode('utf-8'), self.server_public_key)
|
||
self.client.send(encrypted_response)
|
||
|
||
if msg.lower() == 'y':
|
||
self.add_message(f"[ФАЙЛ] Ожидайте получения файла {self.current_file['file_name']}...", 'file')
|
||
else:
|
||
self.add_message(f"[ФАЙЛ] Вы отказались от получения файла {self.current_file['file_name']}", 'file')
|
||
|
||
del self.current_file
|
||
self.set_status("")
|
||
elif msg.startswith('/file'):
|
||
self.send_file(msg)
|
||
else:
|
||
# Отображаем свое сообщение
|
||
self.add_message(f"{self.nickname}: {msg}", 'own')
|
||
|
||
# Отправляем на сервер
|
||
try:
|
||
encrypted_msg = rsa.encrypt(msg.encode('utf-8'), self.server_public_key)
|
||
self.client.send(encrypted_msg)
|
||
except Exception as e:
|
||
self.add_message(f"[ОШИБКА] Не удалось отправить сообщение: {str(e)}", 'error')
|
||
|
||
def send_file(self, command):
|
||
try:
|
||
parts = command.split(' ', 2)
|
||
if len(parts) < 3:
|
||
self.add_message("[ОШИБКА] Использование: /file <получатель> <путь_к_файлу>", 'error')
|
||
self.set_status("Используйте: /file <ник> <путь_к_файлу>")
|
||
return
|
||
|
||
recipient = parts[1]
|
||
file_path = parts[2]
|
||
|
||
if not os.path.exists(file_path):
|
||
self.add_message(f"[ОШИБКА] Файл {file_path} не найден", 'error')
|
||
self.set_status(f"Файл не найден: {file_path}")
|
||
return
|
||
|
||
file_size = os.path.getsize(file_path)
|
||
if file_size > 1024 * 1024: # 1MB лимит
|
||
self.add_message("[ОШИБКА] Файл слишком большой (макс. 1MB)", 'error')
|
||
self.set_status("Файл слишком большой (макс. 1MB)")
|
||
return
|
||
|
||
with open(file_path, 'rb') as f:
|
||
file_data = f.read()
|
||
|
||
file_name = os.path.basename(file_path)
|
||
full_command = f"/file {recipient} {file_name} {file_data.decode('latin1')}"
|
||
|
||
encrypted_msg = rsa.encrypt(full_command.encode('latin1'), self.server_public_key)
|
||
self.client.send(encrypted_msg)
|
||
|
||
self.add_message(f"[ФАЙЛ] Запрос на отправку {file_name} пользователю {recipient} отправлен", 'file')
|
||
self.set_status(f"Запрос на отправку файла {file_name} отправлен")
|
||
|
||
except Exception as e:
|
||
self.add_message(f"[ОШИБКА при отправке файла] {str(e)}", 'error')
|
||
self.set_status("Ошибка при отправке файла")
|
||
|
||
def receive(self):
|
||
try:
|
||
# Получаем публичный ключ сервера
|
||
self.server_public_key = rsa.PublicKey.load_pkcs1(self.client.recv(2048))
|
||
|
||
# Отправляем свой публичный ключ
|
||
self.client.send(self.client_public_key.save_pkcs1())
|
||
|
||
# Получаем никнейм
|
||
self.set_status("Введите ваш никнейм и нажмите Enter")
|
||
while not self.nickname:
|
||
time.sleep(0.1)
|
||
|
||
# Отправляем никнейм на сервер
|
||
encrypted_nickname = rsa.encrypt(self.nickname.encode('utf-8'), self.server_public_key)
|
||
self.client.send(encrypted_nickname)
|
||
self.set_status(f"Подключено как {self.nickname}")
|
||
|
||
while True:
|
||
try:
|
||
encrypted_msg = self.client.recv(4096)
|
||
if not encrypted_msg:
|
||
break
|
||
|
||
# Расшифровываем сообщение
|
||
msg = rsa.decrypt(encrypted_msg, self.client_private_key).decode('utf-8')
|
||
|
||
if msg.startswith('/file_notification'):
|
||
self.handle_file_notification(msg)
|
||
elif msg.startswith('/file_data'):
|
||
self.handle_file_data(msg)
|
||
else:
|
||
self.add_message(msg, 'server' if msg.startswith('[СЕРВЕР]') else 'nick')
|
||
|
||
except Exception as e:
|
||
self.add_message(f"[ОШИБКА] {str(e)}", 'error')
|
||
break
|
||
except Exception as e:
|
||
self.add_message(f"[ОШИБКА] {str(e)}", 'error')
|
||
finally:
|
||
self.client.close()
|
||
raise urwid.ExitMainLoop()
|
||
|
||
def handle_file_notification(self, notification):
|
||
parts = notification.split(' ', 2)
|
||
if len(parts) < 3:
|
||
return
|
||
|
||
sender = parts[1]
|
||
file_name = parts[2]
|
||
|
||
self.add_message(f"[ФАЙЛ] {sender} хочет отправить вам файл: {file_name}", 'file')
|
||
self.set_status(f"Принять файл {file_name} от {sender}? (y/n)")
|
||
|
||
self.current_file = {
|
||
'sender': sender,
|
||
'file_name': file_name,
|
||
'awaiting_response': True
|
||
}
|
||
|
||
def handle_file_data(self, file_data):
|
||
if not hasattr(self, 'current_file'):
|
||
return
|
||
|
||
try:
|
||
downloads_dir = Path("downloads")
|
||
downloads_dir.mkdir(exist_ok=True)
|
||
|
||
file_path = downloads_dir / self.current_file['file_name']
|
||
|
||
with open(file_path, 'wb') as f:
|
||
f.write(file_data[len('/file_data '):].encode('latin1'))
|
||
|
||
self.add_message(f"[ФАЙЛ] Файл {self.current_file['file_name']} сохранен в {file_path}", 'file')
|
||
self.set_status(f"Файл получен: {self.current_file['file_name']}")
|
||
|
||
except Exception as e:
|
||
self.add_message(f"[ОШИБКА] Не удалось сохранить файл: {str(e)}", 'error')
|
||
finally:
|
||
if hasattr(self, 'current_file'):
|
||
del self.current_file
|
||
|
||
def add_message(self, text, style=None):
|
||
if style:
|
||
self.chat_content.append(urwid.AttrMap(urwid.Text(text), style))
|
||
else:
|
||
self.chat_content.append(urwid.Text(text))
|
||
|
||
# Автопрокрутка вниз
|
||
if len(self.chat_content) > 0:
|
||
self.chat_list.set_focus(len(self.chat_content) - 1)
|
||
|
||
def set_status(self, message, timeout=3):
|
||
self.status_text.set_text(f" {message} ")
|
||
if timeout > 0:
|
||
threading.Timer(timeout, lambda: self.set_status("")).start()
|
||
|
||
def update_ui(self, loop=None, user_data=None):
|
||
"""Функция для обновления интерфейса"""
|
||
current_time = time.time()
|
||
if current_time - self.last_update >= self.update_interval:
|
||
self.last_update = current_time
|
||
# Здесь можно добавить любые обновления интерфейса
|
||
# Например, обновление времени в статус баре
|
||
self.loop.draw_screen()
|
||
|
||
# Планируем следующее обновление
|
||
self.loop.set_alarm_in(self.update_interval, self.update_ui)
|
||
|
||
def show_help(self):
|
||
help_text = [
|
||
"СПРАВКА ПО МЕССЕНДЖЕРУ",
|
||
"",
|
||
"Основные команды:",
|
||
"/exit - Выйти из программы",
|
||
"/help - Показать эту справку",
|
||
"",
|
||
"Отправка файлов:",
|
||
"/file <ник> <путь> - Отправить файл указанному пользователю",
|
||
"Пример: /file user2 /home/user/image.jpg",
|
||
"",
|
||
"При получении файла:",
|
||
"1. Вы получите уведомление о входящем файле",
|
||
"2. Введите 'y' для принятия или 'n' для отказа",
|
||
"",
|
||
"Горячие клавиши:",
|
||
"F1 - Показать/скрыть справку",
|
||
"Стрелки вверх/вниз - История сообщений",
|
||
"Ctrl+U - Очистить поле ввода"
|
||
]
|
||
|
||
help_box = urwid.ListBox(urwid.SimpleListWalker([
|
||
urwid.AttrMap(urwid.Text(line), 'highlight') for line in help_text
|
||
]))
|
||
|
||
overlay = urwid.Overlay(
|
||
urwid.LineBox(help_box, title="Помощь"),
|
||
self.main_frame,
|
||
align='center',
|
||
width=('relative', 80),
|
||
valign='middle',
|
||
height=('relative', 80)
|
||
)
|
||
|
||
def exit_help(key):
|
||
if key in ('f1', 'esc', 'enter'):
|
||
self.loop.widget = self.main_frame
|
||
|
||
self.loop.widget = overlay
|
||
self.loop.unhandled_input = exit_help
|
||
|
||
def run(self):
|
||
# Получаем никнейм
|
||
self.set_status("Введите ваш никнейм и нажмите Enter")
|
||
|
||
def set_nickname(edit, new_edit_text):
|
||
if new_edit_text.strip():
|
||
self.nickname = new_edit_text.strip()
|
||
self.input_edit.set_edit_text("")
|
||
self.loop.unhandled_input = self.handle_input
|
||
self.set_status(f"Подключено как {self.nickname}")
|
||
self.input_edit.set_caption("Сообщение: ")
|
||
# Запускаем периодическое обновление интерфейса
|
||
self.loop.set_alarm_in(self.update_interval, self.update_ui)
|
||
|
||
self.input_edit.set_caption("Никнейм: ")
|
||
self.loop.unhandled_input = lambda key: (
|
||
key == 'enter' and set_nickname(None, self.input_edit.get_edit_text())
|
||
)
|
||
|
||
# Запускаем поток для получения сообщений
|
||
receive_thread = threading.Thread(target=self.receive, daemon=True)
|
||
receive_thread.start()
|
||
|
||
# Запускаем главный цикл
|
||
self.loop.run()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
if len(sys.argv) > 1:
|
||
host = sys.argv[1]
|
||
port = int(sys.argv[2]) if len(sys.argv) > 2 else 5555
|
||
client = ChatClient(host, port)
|
||
else:
|
||
client = ChatClient()
|
||
|
||
client.run()
|
||
except KeyboardInterrupt:
|
||
if hasattr(client, 'client'):
|
||
client.client.close()
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
print(f"Ошибка: {str(e)}")
|
||
sys.exit(1) |