376 lines
14 KiB
Python
376 lines
14 KiB
Python
import tkinter as tk
|
|
from tkinter import messagebox, scrolledtext
|
|
import json
|
|
import threading
|
|
import websockets
|
|
import asyncio
|
|
import requests
|
|
from urllib.parse import urlparse
|
|
from datetime import datetime
|
|
|
|
class ChatClient:
|
|
def __init__(self):
|
|
# 客户端配置
|
|
self.client_version = "1.3"
|
|
self.min_server_version = "1.3"
|
|
self.language = "Python"
|
|
|
|
# 网络组件
|
|
self.websocket = None
|
|
self.loop = None
|
|
self.ws_thread = None
|
|
self.current_user = None
|
|
self.server_url = ""
|
|
self.connected = False
|
|
self.ws_url = "" # 新增的初始化
|
|
|
|
self.ws_url = input('请选择连接IP')
|
|
self.server_url = self.ws_url
|
|
|
|
# 初始化UI
|
|
self.root = tk.Tk()
|
|
self.root.title(f"聊天客户端 v{self.client_version}")
|
|
self.setup_ui()
|
|
self.root.protocol("WM_DELETE_WINDOW", self.cleanup)
|
|
|
|
def setup_ui(self):
|
|
# 登录框架
|
|
self.login_frame = tk.Frame(self.root, padx=20, pady=20)
|
|
self.login_frame.pack()
|
|
|
|
# 服务器配置
|
|
tk.Label(self.login_frame, text="服务器地址:").grid(row=0, column=0, sticky="w")
|
|
self.server_entry = tk.Entry(self.login_frame, width=30)
|
|
self.server_entry.grid(row=0, column=1, sticky="ew")
|
|
self.server_entry.insert(0, "http://127.0.0.1:3000")
|
|
|
|
# 认证信息
|
|
tk.Label(self.login_frame, text="用户名:").grid(row=1, column=0, sticky="w")
|
|
self.username_entry = tk.Entry(self.login_frame)
|
|
self.username_entry.grid(row=1, column=1, sticky="ew")
|
|
|
|
tk.Label(self.login_frame, text="密码:").grid(row=2, column=0, sticky="w")
|
|
self.password_entry = tk.Entry(self.login_frame, show="*")
|
|
self.password_entry.grid(row=2, column=1, sticky="ew")
|
|
|
|
# 操作按钮
|
|
btn_frame = tk.Frame(self.login_frame)
|
|
btn_frame.grid(row=3, columnspan=2, pady=10)
|
|
|
|
tk.Button(btn_frame, text="登录", command=self.on_login).pack(side="left", padx=5)
|
|
tk.Button(btn_frame, text="注册", command=self.on_register).pack(side="left", padx=5)
|
|
|
|
# 状态显示
|
|
self.status_label = tk.Label(self.login_frame, text="等待连接...", fg="gray")
|
|
self.status_label.grid(row=4, columnspan=2)
|
|
|
|
# 聊天主界面(初始隐藏)
|
|
self.chat_frame = tk.Frame(self.root)
|
|
|
|
# 消息显示区域
|
|
self.chat_display = scrolledtext.ScrolledText(
|
|
self.chat_frame,
|
|
wrap=tk.WORD,
|
|
width=60,
|
|
height=20,
|
|
state='disabled'
|
|
)
|
|
self.chat_display.pack(pady=10, fill=tk.BOTH, expand=True)
|
|
|
|
# 消息输入区域
|
|
input_frame = tk.Frame(self.chat_frame)
|
|
input_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.message_entry = tk.Entry(input_frame)
|
|
self.message_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
self.message_entry.bind("<Return>", lambda e: self.send_message())
|
|
|
|
tk.Button(input_frame, text="发送", command=self.send_message).pack(side=tk.LEFT)
|
|
|
|
# 底部状态栏
|
|
self.connection_status = tk.Label(self.chat_frame, text="未连接", fg="red")
|
|
self.connection_status.pack(side=tk.BOTTOM, fill=tk.X)
|
|
|
|
def authenticate(self, action, username, password):
|
|
"""处理认证逻辑(登录/注册)"""
|
|
try:
|
|
# 规范化URL
|
|
self.server_url = self.server_entry.get().strip()
|
|
if not self.server_url.startswith(("http://", "https://")):
|
|
self.server_url = "http://" + self.server_url
|
|
|
|
# 准备请求
|
|
url = f"{self.server_url.rstrip('/')}/api/{action}"
|
|
headers = {"Content-Type": "application/json"}
|
|
payload = json.dumps({"username": username, "password": password})
|
|
|
|
# 发送请求
|
|
self.update_status(f"{action}中...", "blue")
|
|
response = requests.post(url, headers=headers, data=payload, timeout=10)
|
|
|
|
# 检查响应
|
|
if response.status_code != 200:
|
|
error_msg = f"HTTP {response.status_code}"
|
|
if response.text:
|
|
error_msg += f" - {response.text[:100]}{'...' if len(response.text)>100 else ''}"
|
|
raise ConnectionError(error_msg)
|
|
|
|
# 解析JSON
|
|
try:
|
|
data = response.json()
|
|
except json.JSONDecodeError:
|
|
raise ValueError(f"无效的JSON响应: {response.text[:100]}...")
|
|
|
|
# 处理结果
|
|
if not data.get("success"):
|
|
raise ValueError(data.get("message", f"{action}失败"))
|
|
|
|
# 认证成功
|
|
self.current_user = {
|
|
"username": username,
|
|
"is_admin": data.get("is_admin", False)
|
|
}
|
|
self.start_chat_session()
|
|
|
|
except Exception as e:
|
|
error_msg = f"{action}错误: {str(e)}"
|
|
self.update_status(error_msg, "red")
|
|
print(f"[DEBUG] {error_msg}\nURL: {url}\nResponse: {getattr(response, 'text', '')}")
|
|
|
|
def start_chat_session(self):
|
|
"""启动聊天会话"""
|
|
self.login_frame.pack_forget()
|
|
self.chat_frame.pack(fill=tk.BOTH, expand=True)
|
|
self.update_connection_status("连接中...", "orange")
|
|
|
|
# 启动WebSocket连接
|
|
self.loop = asyncio.new_event_loop()
|
|
self.ws_thread = threading.Thread(target=self.run_websocket, daemon=True)
|
|
self.ws_thread.start()
|
|
|
|
def run_websocket(self):
|
|
"""运行WebSocket客户端"""
|
|
asyncio.set_event_loop(self.loop)
|
|
self.loop.run_until_complete(self.websocket_handler())
|
|
|
|
async def websocket_handler(self):
|
|
"""处理WebSocket连接"""
|
|
try:
|
|
# 建立连接前设置ws_url
|
|
self.ws_url = self.server_url.replace("http", "ws") + "/chat"
|
|
|
|
async with websockets.connect(self.ws_url) as websocket:
|
|
self.websocket = websocket
|
|
self.connected = True
|
|
self.update_connection_status("已连接", "green")
|
|
|
|
# 1. 发送客户端信息
|
|
await websocket.send(json.dumps({
|
|
"type": "client-info",
|
|
"version": self.client_version,
|
|
"language": self.language
|
|
}))
|
|
|
|
# 2. 发送认证信息
|
|
await websocket.send(json.dumps({
|
|
"type": "login",
|
|
"username": self.current_user["username"],
|
|
"password": self.password_entry.get()
|
|
}))
|
|
|
|
# 3. 消息处理循环
|
|
while self.connected:
|
|
message = await websocket.recv()
|
|
self.handle_server_message(json.loads(message))
|
|
|
|
except Exception as e:
|
|
error_msg = f"连接错误: {str(e)}"
|
|
self.display_message(error_msg, "system")
|
|
self.update_connection_status("连接断开", "red")
|
|
finally:
|
|
self.connected = False
|
|
|
|
def handle_server_message(self, message):
|
|
"""处理服务端消息"""
|
|
msg_type = message.get("type")
|
|
|
|
if msg_type == "error":
|
|
self.display_message(message.get("message", "未知错误"), "error")
|
|
|
|
elif msg_type == "server-info":
|
|
server_version = message.get("version")
|
|
if server_version != self.client_version:
|
|
warn_msg = f"版本警告: 服务端({server_version})≠客户端({self.client_version})"
|
|
self.display_message(warn_msg, "warning")
|
|
|
|
elif msg_type == "history":
|
|
for msg in message.get("data", []):
|
|
self.display_history_message(msg)
|
|
|
|
elif msg_type == "message":
|
|
self.display_message(
|
|
message.get("content"),
|
|
"admin" if message.get("is_admin") else "user",
|
|
message.get("sender")
|
|
)
|
|
|
|
elif msg_type == "system":
|
|
self.display_message(message.get("message"), "system")
|
|
|
|
def send_message(self):
|
|
"""发送消息到服务器"""
|
|
message = self.message_entry.get().strip()
|
|
if not message or not self.connected:
|
|
return
|
|
|
|
try:
|
|
# 处理命令
|
|
if message.startswith("///"):
|
|
self.handle_command(message)
|
|
else:
|
|
# 发送普通消息
|
|
asyncio.run_coroutine_threadsafe(
|
|
self.send_websocket_message({
|
|
"type": "message",
|
|
"content": message
|
|
}),
|
|
self.loop
|
|
)
|
|
|
|
self.message_entry.delete(0, tk.END)
|
|
|
|
|
|
except Exception as e:
|
|
self.display_message(f"发送失败: {str(e)}", "error")
|
|
|
|
async def send_websocket_message(self, message):
|
|
"""通过WebSocket发送消息"""
|
|
if self.websocket:
|
|
await self.websocket.send(json.dumps(message))
|
|
|
|
def handle_command(self, command):
|
|
"""处理管理员命令"""
|
|
if not self.current_user.get("is_admin"):
|
|
self.display_message("需要管理员权限", "error")
|
|
return
|
|
|
|
# 示例命令处理
|
|
if command == "///help":
|
|
help_text = [
|
|
"可用命令:",
|
|
"///help - 显示帮助",
|
|
"///me - 显示我的信息",
|
|
"///list - 列出在线用户",
|
|
"///op <用户> - 授予管理员权限",
|
|
"///deop <用户> - 取消管理员权限",
|
|
"///kick <用户> - 踢出用户"
|
|
]
|
|
self.display_message("\n".join(help_text), "system")
|
|
else:
|
|
asyncio.run_coroutine_threadsafe(
|
|
self.send_websocket_message({
|
|
"type": "message",
|
|
"content": command
|
|
}),
|
|
self.loop
|
|
)
|
|
|
|
def display_message(self, message, msg_type="normal", sender=None):
|
|
"""显示消息到聊天区域"""
|
|
colors = {
|
|
"system": "blue",
|
|
"error": "red",
|
|
"warning": "orange",
|
|
"admin": "purple",
|
|
"user": "black",
|
|
"normal": "green"
|
|
}
|
|
|
|
self.root.after(0, lambda: self._append_message(
|
|
text=message,
|
|
color=colors.get(msg_type, "black"),
|
|
sender=sender
|
|
))
|
|
|
|
def display_history_message(self, message):
|
|
"""显示历史消息"""
|
|
self._append_message(
|
|
text=message.get("content", ""),
|
|
color="gray",
|
|
sender=message.get("sender"),
|
|
timestamp=message.get("timestamp")
|
|
)
|
|
|
|
def _append_message(self, text, color, sender=None, timestamp=None):
|
|
"""内部方法:添加消息到文本框"""
|
|
self.chat_display.config(state='normal')
|
|
|
|
if timestamp:
|
|
time_str = datetime.fromisoformat(timestamp).strftime("%H:%M:%S")
|
|
self.chat_display.insert(tk.END, f"[{time_str}] ", "time")
|
|
|
|
if sender:
|
|
self.chat_display.insert(tk.END, f"{sender}: ", "sender")
|
|
|
|
self.chat_display.insert(tk.END, text + "\n", color)
|
|
self.chat_display.config(state='disabled')
|
|
self.chat_display.see(tk.END)
|
|
|
|
# 配置标签样式
|
|
for tag in ["time", "sender", color]:
|
|
self.chat_display.tag_config(tag, foreground=color)
|
|
|
|
def update_status(self, text, color="black"):
|
|
"""更新登录界面状态"""
|
|
self.root.after(0, lambda: self.status_label.config(text=text, fg=color))
|
|
|
|
def update_connection_status(self, text, color):
|
|
"""更新连接状态"""
|
|
self.root.after(0, lambda: self.connection_status.config(text=text, fg=color))
|
|
|
|
def on_login(self):
|
|
"""登录按钮事件"""
|
|
username = self.username_entry.get().strip()
|
|
password = self.password_entry.get().strip()
|
|
|
|
if not all([self.server_entry.get(), username, password]):
|
|
self.update_status("所有字段不能为空", "red")
|
|
return
|
|
|
|
threading.Thread(
|
|
target=self.authenticate,
|
|
args=("login", username, password),
|
|
daemon=True
|
|
).start()
|
|
|
|
def on_register(self):
|
|
"""注册按钮事件"""
|
|
username = self.username_entry.get().strip()
|
|
password = self.password_entry.get().strip()
|
|
|
|
if not all([self.server_entry.get(), username, password]):
|
|
self.update_status("所有字段不能为空", "red")
|
|
return
|
|
|
|
threading.Thread(
|
|
target=self.authenticate,
|
|
args=("register", username, password),
|
|
daemon=True
|
|
).start()
|
|
|
|
def cleanup(self):
|
|
"""清理资源"""
|
|
self.connected = False
|
|
try:
|
|
if self.websocket:
|
|
asyncio.run_coroutine_threadsafe(self.websocket.close(), self.loop)
|
|
if self.loop:
|
|
self.loop.call_soon_threadsafe(self.loop.stop)
|
|
except:
|
|
pass
|
|
self.root.destroy()
|
|
|
|
if __name__ == "__main__":
|
|
# 运行客户端
|
|
client = ChatClient()
|
|
client.root.mainloop() |