diff --git a/README.md b/README.md index a802016..99eb6f6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,243 @@ -# chatProgram +# P2P Chat - 点对点通信应用 +一个基于 Python 的 P2P 网络通信应用程序,支持文本消息、文件传输、图片分享和语音通话。 + +## 功能特性 + +- 文本消息通信 +- 文件传输(支持断点续传) +- 图片传输与显示 +- 音视频播放 +- 语音聊天 +- 局域网自动发现 +- 离线消息缓存 + +## 环境要求 + +- Python 3.9+ +- MySQL 5.7+(可选,用于持久化存储) + +## 安装 + +### 1. 克隆项目 + +```bash +git clone +cd chatProgram +``` + +### 2. 创建虚拟环境 + +```bash +python -m venv venv + +# Windows +venv\Scripts\activate + +# Linux/Mac +source venv/bin/activate +``` + +### 3. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 4. 系统依赖(Linux) + +```bash +# Ubuntu/Debian +sudo apt install portaudio19-dev ffmpeg libopus-dev + +# openEuler/CentOS +sudo dnf install portaudio-devel ffmpeg opus-devel +``` + +## 快速开始 + +### 启动服务器 + +```bash +# 基本启动 +python run_server.py + +# 指定地址和端口 +python run_server.py --host 0.0.0.0 --port 8888 + +# 调试模式 +python run_server.py --debug +``` + +### 启动客户端(命令行) + +```bash +# 基本启动 +python run_client.py --username alice + +# 连接远程服务器 +python run_client.py -u bob -s 192.168.1.100 -p 8888 + +# 带显示名称 +python run_client.py -u alice -d "Alice Wang" +``` + +### 启动客户端(GUI) + +```bash +# 基本启动 +python run_client_gui.py + +# 指定服务器 +python run_client_gui.py --server 192.168.1.100 --port 8888 +``` + +## 命令行客户端命令 + +连接成功后可使用以下命令: + +| 命令 | 说明 | +|------|------| +| `/list` | 查看在线用户 | +| `/msg <用户> <消息>` | 发送消息 | +| `/file <用户> <路径>` | 发送文件 | +| `/call <用户>` | 发起语音通话 | +| `/endcall` | 结束通话 | +| `/stats` | 查看统计信息 | +| `/refresh` | 刷新用户列表 | +| `/help` | 查看帮助 | +| `/quit` | 退出 | + +## 配置 + +### 环境变量 + +```bash +# 服务器配置 +P2P_SERVER_HOST=0.0.0.0 +P2P_SERVER_PORT=8888 +P2P_MAX_CONNECTIONS=1000 + +# 数据库配置 +P2P_DB_HOST=localhost +P2P_DB_PORT=3306 +P2P_DB_NAME=p2p_chat +P2P_DB_USER=root +P2P_DB_PASSWORD=your_password + +# 安全配置 +P2P_USE_TLS=true +P2P_CERT_FILE=/path/to/cert.pem +P2P_KEY_FILE=/path/to/key.pem +``` + +### 配置文件 + +也可以直接修改 `config.py` 中的默认值。 + +## 部署到 openEuler 服务器 + +### 1. 安装依赖 + +```bash +sudo dnf update -y +sudo dnf install -y python3 python3-pip python3-devel +sudo dnf install -y portaudio-devel ffmpeg opus-devel mysql-server mysql-devel +``` + +### 2. 部署项目 + +```bash +mkdir -p /opt/p2p-chat +cd /opt/p2p-chat + +# 上传项目文件 +# scp -r ./* user@server:/opt/p2p-chat/ + +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +### 3. 配置防火墙 + +```bash +sudo firewall-cmd --permanent --add-port=8888/tcp +sudo firewall-cmd --permanent --add-port=8889/udp +sudo firewall-cmd --reload +``` + +### 4. 设置系统服务 + +创建 `/etc/systemd/system/p2p-chat.service`: + +```ini +[Unit] +Description=P2P Chat Server +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/p2p-chat +Environment=PATH=/opt/p2p-chat/venv/bin +ExecStart=/opt/p2p-chat/venv/bin/python run_server.py +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +启用服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable p2p-chat +sudo systemctl start p2p-chat +sudo systemctl status p2p-chat +``` + +## 项目结构 + +``` +chatProgram/ +├── client/ # 客户端模块 +│ ├── ui/ # GUI 组件 +│ ├── app.py # 客户端应用 +│ ├── connection_manager.py +│ ├── file_transfer.py +│ ├── image_processor.py +│ ├── media_player.py +│ └── voice_chat.py +├── server/ # 服务器模块 +│ ├── database.py +│ ├── relay_server.py +│ └── repositories.py +├── shared/ # 共享模块 +│ ├── message_handler.py +│ ├── models.py +│ └── security.py +├── tests/ # 测试 +├── config.py # 配置 +├── run_server.py # 服务器启动脚本 +├── run_client.py # 命令行客户端启动脚本 +├── run_client_gui.py # GUI 客户端启动脚本 +└── requirements.txt # 依赖 +``` + +## 测试 + +```bash +# 运行所有测试 +pytest + +# 运行特定测试 +pytest tests/test_relay_server.py -v + +# 运行集成测试 +pytest tests/test_integration.py -v +``` + +## License + +MIT diff --git a/client/app.py b/client/app.py index f92012a..abf781b 100644 --- a/client/app.py +++ b/client/app.py @@ -106,7 +106,8 @@ class P2PClientApp: logger.info(f"Client started for user: {user_info.username}") # 初始化语音聊天模块 - self._voice_chat = VoiceChatModule(self._connection_manager) + self._voice_chat = VoiceChatModule(self.config) + self._voice_chat.set_send_message_callback(self._send_message_async) # 请求在线用户列表 await self._request_online_users() diff --git a/client/ui/file_transfer_widget.py b/client/ui/file_transfer_widget.py index 7ea6b7b..c5bca09 100644 --- a/client/ui/file_transfer_widget.py +++ b/client/ui/file_transfer_widget.py @@ -6,9 +6,14 @@ 需求: 4.1, 4.3, 4.6, 5.2, 5.3, 5.4, 5.6 """ +from __future__ import annotations + import logging import os -from typing import Optional, Dict +from typing import Optional, Dict, TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Type from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, @@ -422,7 +427,7 @@ class FileTransferWidget(QWidget): dialog = ImagePreviewDialog(image_path, self) dialog.exec() - def create_image_thumbnail(self, image_path: str) -> Optional[ImageThumbnail]: + def create_image_thumbnail(self, image_path: str) -> Optional["ImageThumbnail"]: """ 创建图片缩略图组件 diff --git a/client/ui/main_window.py b/client/ui/main_window.py index c7a294d..08feab0 100644 --- a/client/ui/main_window.py +++ b/client/ui/main_window.py @@ -328,30 +328,21 @@ class MainWindow(QMainWindow): def closeEvent(self, event: QCloseEvent) -> None: """ 窗口关闭事件 - - 实现后台运行 (需求 9.5) - WHEN 应用程序最小化 THEN P2P_Client SHALL 保持后台运行并继续接收消息 """ - # 如果有系统托盘,最小化到托盘而不是关闭 - if self._system_tray and self._system_tray.is_available(): - event.ignore() - self.hide() - self._system_tray.show_message("P2P通信", "应用程序已最小化到系统托盘") + # 确认退出 + reply = QMessageBox.question( + self, + "确认退出", + "确定要退出应用程序吗?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self._cleanup() + event.accept() else: - # 确认退出 - reply = QMessageBox.question( - self, - "确认退出", - "确定要退出应用程序吗?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No - ) - - if reply == QMessageBox.StandardButton.Yes: - self._cleanup() - event.accept() - else: - event.ignore() + event.ignore() def resizeEvent(self, event) -> None: """ diff --git a/client_gui.log b/client_gui.log new file mode 100644 index 0000000..709fc66 --- /dev/null +++ b/client_gui.log @@ -0,0 +1,60 @@ +2025-12-26 22:45:58,501 - client.ui.login_dialog - INFO - LoginDialog initialized +2025-12-26 22:46:20,014 - client.ui.login_dialog - INFO - User registered and logged in: ybw +2025-12-26 22:46:21,293 - client.ui.main_window - INFO - MainWindow initialized +2025-12-26 22:46:21,293 - client.ui.main_window - INFO - Current user set: ybw +2025-12-26 22:46:21,333 - client.ui.system_tray - INFO - SystemTrayManager initialized +2025-12-26 22:46:21,441 - client.connection_manager - INFO - ConnectionManager initialized +2025-12-26 22:46:21,442 - client.file_transfer - INFO - FileTransferModule initialized +2025-12-26 22:46:21,443 - client.image_processor - INFO - ImageProcessor initialized +2025-12-26 22:46:21,443 - client.media_player - INFO - AudioPlayer initialized +2025-12-26 22:46:21,443 - client.media_player - INFO - VideoPlayer initialized +2025-12-26 22:46:21,443 - client.media_player - INFO - MediaPlayer initialized +2025-12-26 22:46:21,443 - client.app - INFO - P2PClientApp initialized +2025-12-26 22:46:21,452 - client.connection_manager - INFO - Connection state changed: disconnected -> connecting (Connecting to server) +2025-12-26 22:46:21,452 - client.app - INFO - Connection state changed: connecting (Connecting to server) +2025-12-26 22:46:23,591 - client.connection_manager - INFO - Connection state changed: connecting -> error (Network error: [WinError 1225] 远程计算机拒绝网络连接。) +2025-12-26 22:46:23,591 - client.app - INFO - Connection state changed: error (Network error: [WinError 1225] 远程计算机拒绝网络连接。) +2025-12-26 22:46:23,591 - client.app - ERROR - Failed to start client: Failed to connect: [WinError 1225] 远程计算机拒绝网络连接。 +2025-12-26 22:48:08,584 - client.ui.login_dialog - INFO - LoginDialog initialized +2025-12-26 22:48:17,200 - client.ui.login_dialog - INFO - User registered and logged in: ybw +2025-12-26 22:48:17,787 - client.ui.main_window - INFO - MainWindow initialized +2025-12-26 22:48:17,787 - client.ui.main_window - INFO - Current user set: ybw +2025-12-26 22:48:17,802 - client.ui.system_tray - INFO - SystemTrayManager initialized +2025-12-26 22:48:17,871 - client.connection_manager - INFO - ConnectionManager initialized +2025-12-26 22:48:17,873 - client.file_transfer - INFO - FileTransferModule initialized +2025-12-26 22:48:17,873 - client.image_processor - INFO - ImageProcessor initialized +2025-12-26 22:48:17,873 - client.media_player - INFO - AudioPlayer initialized +2025-12-26 22:48:17,874 - client.media_player - INFO - VideoPlayer initialized +2025-12-26 22:48:17,874 - client.media_player - INFO - MediaPlayer initialized +2025-12-26 22:48:17,874 - client.app - INFO - P2PClientApp initialized +2025-12-26 22:48:17,881 - client.connection_manager - INFO - Connection state changed: disconnected -> connecting (Connecting to server) +2025-12-26 22:48:17,881 - client.app - INFO - Connection state changed: connecting (Connecting to server) +2025-12-26 22:48:17,925 - client.connection_manager - INFO - Connection state changed: connecting -> connected (Connected to server) +2025-12-26 22:48:17,926 - client.app - INFO - Connection state changed: connected (Connected to server) +2025-12-26 22:48:17,927 - client.connection_manager - INFO - LAN listener started on port 8889 +2025-12-26 22:48:17,927 - client.connection_manager - INFO - Connected to server 113.45.148.222:8888 +2025-12-26 22:48:17,928 - client.app - INFO - Client started for user: ybw +2025-12-26 22:48:17,928 - client.app - ERROR - Failed to start client: 'ConnectionManager' object has no attribute 'audio_sample_rate' +2025-12-26 22:55:46,116 - client.ui.login_dialog - INFO - LoginDialog initialized +2025-12-26 22:56:08,430 - client.ui.login_dialog - INFO - User logged in: ybw +2025-12-26 22:56:08,969 - client.ui.main_window - INFO - MainWindow initialized +2025-12-26 22:56:08,969 - client.ui.main_window - INFO - Current user set: ybw +2025-12-26 22:56:08,982 - client.ui.system_tray - INFO - SystemTrayManager initialized +2025-12-26 22:56:09,057 - client.connection_manager - INFO - ConnectionManager initialized +2025-12-26 22:56:09,059 - client.file_transfer - INFO - FileTransferModule initialized +2025-12-26 22:56:09,061 - client.image_processor - INFO - ImageProcessor initialized +2025-12-26 22:56:09,061 - client.media_player - INFO - AudioPlayer initialized +2025-12-26 22:56:09,062 - client.media_player - INFO - VideoPlayer initialized +2025-12-26 22:56:09,062 - client.media_player - INFO - MediaPlayer initialized +2025-12-26 22:56:09,062 - client.app - INFO - P2PClientApp initialized +2025-12-26 22:56:09,067 - client.connection_manager - INFO - Connection state changed: disconnected -> connecting (Connecting to server) +2025-12-26 22:56:09,067 - client.app - INFO - Connection state changed: connecting (Connecting to server) +2025-12-26 22:56:09,109 - client.connection_manager - INFO - Connection state changed: connecting -> connected (Connected to server) +2025-12-26 22:56:09,109 - client.app - INFO - Connection state changed: connected (Connected to server) +2025-12-26 22:56:09,110 - client.connection_manager - INFO - LAN listener started on port 8889 +2025-12-26 22:56:09,110 - client.connection_manager - INFO - Connected to server 113.45.148.222:8888 +2025-12-26 22:56:09,111 - client.app - INFO - Client started for user: ybw +2025-12-26 22:56:09,111 - client.voice_chat - INFO - VoiceChatModule initialized +2025-12-26 22:56:09,112 - __main__ - INFO - Connected to server +2025-12-26 22:56:09,132 - client.app - INFO - Online users updated: 1 users +2025-12-26 22:56:09,133 - __main__ - INFO - Online users: 1 diff --git a/config.py b/config.py index eaf5a06..38d2c44 100644 --- a/config.py +++ b/config.py @@ -29,7 +29,7 @@ class ServerConfig: @dataclass class ClientConfig: """客户端配置""" - server_host: str = "127.0.0.1" + server_host: str = "113.45.148.222" server_port: int = 8888 # LAN discovery diff --git a/run_client_gui.py b/run_client_gui.py new file mode 100644 index 0000000..5205490 --- /dev/null +++ b/run_client_gui.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +P2P Chat Client GUI 启动脚本 + +用法: + python run_client_gui.py [--server HOST] [--port PORT] + +示例: + python run_client_gui.py + python run_client_gui.py --server 192.168.1.100 --port 8888 +""" + +import asyncio +import argparse +import logging +import sys +import os +from typing import Optional, List + +# 确保能找到模块 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from PyQt6.QtWidgets import QApplication, QMessageBox, QSplashScreen +from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject +from PyQt6.QtGui import QPixmap, QFont + +from client.app import P2PClientApp +from client.connection_manager import ConnectionState +from client.ui import ( + MainWindow, LoginDialog, ChatWidget, ContactListWidget, + FileTransferWidget, MediaPlayerWidget, VoiceCallWidget, SystemTrayManager +) +from shared.models import UserInfo, UserStatus, Message, MessageType +from config import load_client_config, ClientConfig + + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout), + logging.FileHandler('client_gui.log', encoding='utf-8') + ] +) +logger = logging.getLogger(__name__) + + +class AsyncWorker(QThread): + """异步工作线程,用于运行 asyncio 事件循环""" + + connected = pyqtSignal(bool, str) # (success, message) + disconnected = pyqtSignal(str) # reason + message_received = pyqtSignal(object) # Message + user_list_updated = pyqtSignal(list) # List[UserInfo] + state_changed = pyqtSignal(str, str) # (state, reason) + + def __init__(self, client: P2PClientApp, user_info: UserInfo): + super().__init__() + self.client = client + self.user_info = user_info + self._running = False + self._loop: Optional[asyncio.AbstractEventLoop] = None + + def run(self): + """运行异步事件循环""" + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + self._running = True + + try: + # 连接到服务器 + success = self._loop.run_until_complete( + self.client.start(self.user_info) + ) + + if success: + self.connected.emit(True, "连接成功") + + # 设置回调 + self.client.add_message_callback(self._on_message) + self.client.add_state_callback(self._on_state_change) + self.client.add_user_list_callback(self._on_user_list) + + # 保持运行 + while self._running: + self._loop.run_until_complete(asyncio.sleep(0.1)) + else: + self.connected.emit(False, "连接失败") + + except Exception as e: + logger.error(f"Worker error: {e}") + self.connected.emit(False, str(e)) + finally: + self._loop.close() + + def stop(self): + """停止工作线程""" + self._running = False + if self._loop and self._loop.is_running(): + self._loop.call_soon_threadsafe(self._loop.stop) + + def _on_message(self, message: Message): + """消息回调""" + self.message_received.emit(message) + + def _on_state_change(self, state: ConnectionState, reason: Optional[str]): + """状态变化回调""" + self.state_changed.emit(state.value, reason or "") + if state == ConnectionState.DISCONNECTED: + self.disconnected.emit(reason or "连接断开") + + def _on_user_list(self, users: List[UserInfo]): + """用户列表更新回调""" + self.user_list_updated.emit(users) + + def send_message(self, peer_id: str, content: str) -> bool: + """发送消息(线程安全)""" + if self._loop and self._running: + future = asyncio.run_coroutine_threadsafe( + self.client.send_text_message(peer_id, content), + self._loop + ) + try: + return future.result(timeout=5.0) + except Exception as e: + logger.error(f"Send message error: {e}") + return False + return False + + def send_file(self, peer_id: str, file_path: str) -> bool: + """发送文件(线程安全)""" + if self._loop and self._running: + future = asyncio.run_coroutine_threadsafe( + self.client.send_file(peer_id, file_path), + self._loop + ) + try: + return future.result(timeout=300.0) # 5分钟超时 + except Exception as e: + logger.error(f"Send file error: {e}") + return False + return False + + def refresh_users(self): + """刷新用户列表(线程安全)""" + if self._loop and self._running: + asyncio.run_coroutine_threadsafe( + self.client.refresh_online_users(), + self._loop + ) + + +class P2PChatGUI(QObject): + """P2P 聊天 GUI 应用程序""" + + def __init__(self, config: ClientConfig): + super().__init__() + self.config = config + self.app: Optional[QApplication] = None + self.main_window: Optional[MainWindow] = None + self.client: Optional[P2PClientApp] = None + self.worker: Optional[AsyncWorker] = None + self._current_user: Optional[UserInfo] = None + self._current_chat_peer: Optional[str] = None + + def run(self) -> int: + """运行 GUI 应用程序""" + # 创建 Qt 应用 + self.app = QApplication(sys.argv) + self.app.setApplicationName("P2P Chat") + self.app.setApplicationVersion("0.1.0") + + # 设置默认字体 + font = QFont("Microsoft YaHei", 10) + self.app.setFont(font) + + # 设置样式 + self.app.setStyle("Fusion") + + # 应用退出时清理资源 + self.app.aboutToQuit.connect(self.cleanup) + + # 显示登录对话框 + if not self._show_login(): + return 0 + + # 创建主窗口 + self._create_main_window() + + # 连接到服务器 + self._connect_to_server() + + # 运行应用 + return self.app.exec() + + def _show_login(self) -> bool: + """显示登录对话框""" + dialog = LoginDialog() + + # 设置服务器地址 + dialog._login_server_input.setText( + f"{self.config.server_host}:{self.config.server_port}" + ) + dialog._reg_server_input.setText( + f"{self.config.server_host}:{self.config.server_port}" + ) + + if dialog.exec(): + self._current_user = dialog.get_user_info() + host, port = dialog.get_server_address() + self.config.server_host = host + self.config.server_port = port + return True + return False + + def _create_main_window(self): + """创建主窗口""" + self.main_window = MainWindow(self.config) + + if self._current_user: + self.main_window.set_current_user(self._current_user) + + # 设置系统托盘 + try: + tray = SystemTrayManager(self.main_window) + self.main_window.set_system_tray(tray) + except Exception as e: + logger.warning(f"Failed to create system tray: {e}") + + # 连接菜单动作 + self._connect_menu_actions() + + self.main_window.show() + + def _connect_menu_actions(self): + """连接菜单动作""" + # 发送文件 + self.main_window._send_file_action.triggered.connect(self._on_send_file) + # 发送图片 + self.main_window._send_image_action.triggered.connect(self._on_send_image) + # 语音通话 + self.main_window._voice_call_action.triggered.connect(self._on_voice_call) + + def _connect_to_server(self): + """连接到服务器""" + if not self._current_user: + return + + # 更新状态 + self.main_window.connection_state_changed.emit("连接中...") + + # 创建客户端 + self.client = P2PClientApp(self.config) + + # 创建工作线程 + self.worker = AsyncWorker(self.client, self._current_user) + self.worker.connected.connect(self._on_connected) + self.worker.disconnected.connect(self._on_disconnected) + self.worker.message_received.connect(self._on_message_received) + self.worker.user_list_updated.connect(self._on_user_list_updated) + self.worker.state_changed.connect(self._on_state_changed) + + # 启动工作线程 + self.worker.start() + + def _on_connected(self, success: bool, message: str): + """连接结果回调""" + if success: + self.main_window.connection_state_changed.emit("已连接") + self.main_window._statusbar.showMessage(f"已连接到服务器") + logger.info("Connected to server") + + # 刷新用户列表 + QTimer.singleShot(500, self._refresh_users) + else: + self.main_window.connection_state_changed.emit("连接失败") + QMessageBox.critical( + self.main_window, + "连接失败", + f"无法连接到服务器: {message}" + ) + + def _on_disconnected(self, reason: str): + """断开连接回调""" + self.main_window.connection_state_changed.emit("已断开") + self.main_window._statusbar.showMessage(f"连接断开: {reason}") + + # 询问是否重连 + reply = QMessageBox.question( + self.main_window, + "连接断开", + f"与服务器的连接已断开: {reason}\n\n是否尝试重新连接?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self._connect_to_server() + + def _on_message_received(self, message: Message): + """消息接收回调""" + if message.msg_type == MessageType.TEXT: + content = message.payload.decode('utf-8') + sender = message.sender_id + + # 显示通知 + if self.main_window.isMinimized() or not self.main_window.isActiveWindow(): + self.main_window.show_notification( + f"来自 {sender} 的消息", + content[:50] + ("..." if len(content) > 50 else "") + ) + + # 更新状态栏 + self.main_window._statusbar.showMessage( + f"收到来自 {sender} 的消息", 3000 + ) + + logger.info(f"Message from {sender}: {content[:50]}...") + + elif message.msg_type == MessageType.VOICE_CALL_REQUEST: + sender = message.sender_id + self.main_window.show_notification( + "来电", + f"{sender} 正在呼叫你" + ) + + def _on_user_list_updated(self, users: List[UserInfo]): + """用户列表更新回调""" + logger.info(f"Online users: {len(users)}") + self.main_window._statusbar.showMessage( + f"在线用户: {len(users)}", 3000 + ) + + def _on_state_changed(self, state: str, reason: str): + """状态变化回调""" + state_text = { + "disconnected": "已断开", + "connecting": "连接中...", + "connected": "已连接", + "reconnecting": "重连中..." + }.get(state, state) + + self.main_window.connection_state_changed.emit(state_text) + + def _refresh_users(self): + """刷新用户列表""" + if self.worker: + self.worker.refresh_users() + + def _on_send_file(self): + """发送文件""" + from PyQt6.QtWidgets import QFileDialog + + file_path, _ = QFileDialog.getOpenFileName( + self.main_window, + "选择文件", + "", + "所有文件 (*.*)" + ) + + if file_path and self._current_chat_peer: + self.main_window._statusbar.showMessage(f"正在发送文件...") + # TODO: 实现文件发送 + + def _on_send_image(self): + """发送图片""" + from PyQt6.QtWidgets import QFileDialog + + file_path, _ = QFileDialog.getOpenFileName( + self.main_window, + "选择图片", + "", + "图片文件 (*.png *.jpg *.jpeg *.gif *.bmp)" + ) + + if file_path and self._current_chat_peer: + self.main_window._statusbar.showMessage(f"正在发送图片...") + # TODO: 实现图片发送 + + def _on_voice_call(self): + """发起语音通话""" + if self._current_chat_peer: + self.main_window._statusbar.showMessage(f"正在呼叫...") + # TODO: 实现语音通话 + + def cleanup(self): + """清理资源""" + logger.info("Cleaning up resources...") + + if self.worker: + self.worker.stop() + self.worker.wait(3000) + self.worker = None + + if self.client: + # 在新的事件循环中停止客户端 + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(self.client.stop()) + except Exception as e: + logger.error(f"Error stopping client: {e}") + finally: + loop.close() + self.client = None + + logger.info("Cleanup completed") + + +def parse_args() -> argparse.Namespace: + """解析命令行参数""" + parser = argparse.ArgumentParser( + description='P2P Chat Client (GUI)', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + '--server', '-s', + type=str, + default=None, + help='服务器地址 (默认: 127.0.0.1)' + ) + parser.add_argument( + '--port', '-p', + type=int, + default=None, + help='服务器端口 (默认: 8888)' + ) + parser.add_argument( + '--debug', + action='store_true', + help='启用调试模式' + ) + return parser.parse_args() + + +def main() -> int: + """主函数""" + args = parse_args() + + # 设置日志级别 + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + # 加载配置 + config = load_client_config() + + # 命令行参数覆盖配置 + if args.server: + config.server_host = args.server + if args.port: + config.server_port = args.port + + # 创建并运行 GUI + gui = P2PChatGUI(config) + + try: + exit_code = gui.run() + return exit_code + except KeyboardInterrupt: + logger.info("Application interrupted") + gui.cleanup() + return 0 + except Exception as e: + logger.error(f"Application error: {e}") + gui.cleanup() + return 1 + + +if __name__ == "__main__": + sys.exit(main())