好友
阅读权限20
听众
最后登录1970-1-1
|
清水飞云
发表于 2026-6-10 10:45
本帖最后由 清水飞云 于 2026-6-10 10:47 编辑
有需要的自行修改。
服务端:
[Python] 纯文本查看 复制代码 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import threading
import sqlite3
import json
import hashlib
import time
import os
import uuid
import re
import math
SERVER_HOST = '0.0.0.0'
SERVER_PORT = 8765
FILE_SERVER_PORT = 8766
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
FILE_STORAGE = os.path.join(BASE_DIR, 'file_storage')
os.makedirs(FILE_STORAGE, exist_ok=True)
online_users = {} # user_id -> socket
user_lock = threading.Lock()
# -------------------- 数据库初始化 --------------------
def init_db():
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.executescript('''
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS friends;
DROP TABLE IF EXISTS friend_requests;
DROP TABLE IF EXISTS groups;
DROP TABLE IF EXISTS group_members;
DROP TABLE IF EXISTS offline_messages;
DROP TABLE IF EXISTS offline_group_messages;
DROP TABLE IF EXISTS files;
DROP TABLE IF EXISTS revoked_messages;
DROP TABLE IF EXISTS countries;
DROP TABLE IF EXISTS states;
DROP TABLE IF EXISTS cities;
''')
c.execute('''CREATE TABLE users (
user_id TEXT PRIMARY KEY,
password TEXT NOT NULL,
nickname TEXT NOT NULL,
gender TEXT,
age INTEGER,
region TEXT,
latitude REAL,
longitude REAL,
avatar TEXT,
created_at REAL
)''')
c.execute('''CREATE TABLE friends (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user1 TEXT,
user2 TEXT,
status INTEGER DEFAULT 1,
created_at REAL,
UNIQUE(user1, user2)
)''')
c.execute('''CREATE TABLE friend_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_user TEXT,
to_user TEXT,
message TEXT,
status INTEGER DEFAULT 0,
created_at REAL
)''')
c.execute('''CREATE TABLE groups (
group_id TEXT PRIMARY KEY,
group_name TEXT,
owner TEXT,
created_at REAL
)''')
c.execute('''CREATE TABLE group_members (
group_id TEXT,
user_id TEXT,
role INTEGER DEFAULT 0,
joined_at REAL,
PRIMARY KEY(group_id, user_id)
)''')
c.execute('''CREATE TABLE offline_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
to_user TEXT,
from_user TEXT,
content TEXT,
msg_type TEXT DEFAULT 'text',
file_id TEXT,
timestamp REAL,
is_read INTEGER DEFAULT 0
)''')
c.execute('''CREATE TABLE offline_group_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id TEXT,
from_user TEXT,
content TEXT,
timestamp REAL,
is_read INTEGER DEFAULT 0
)''')
c.execute('''CREATE TABLE files (
file_id TEXT PRIMARY KEY,
file_name TEXT,
file_size INTEGER,
uploader TEXT,
upload_time REAL,
storage_path TEXT
)''')
c.execute('''CREATE TABLE revoked_messages (
msg_id TEXT PRIMARY KEY,
revoke_time REAL
)''')
c.execute('''CREATE TABLE countries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE
)''')
c.execute('''CREATE TABLE states (
id INTEGER PRIMARY KEY AUTOINCREMENT,
country_id INTEGER,
name TEXT,
FOREIGN KEY(country_id) REFERENCES countries(id)
)''')
c.execute('''CREATE TABLE cities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
state_id INTEGER,
name TEXT,
FOREIGN KEY(state_id) REFERENCES states(id)
)''')
conn.commit()
conn.close()
print("数据库初始化完成")
# -------------------- 预置地区数据(国家、省/州、城市/县)--------------------
def import_region_data():
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM countries")
if c.fetchone()[0] > 0:
conn.close()
return
# 全球主要国家及行政区(可扩展)
regions = {
"中国": {
"北京市": ["东城区", "西城区", "朝阳区", "海淀区", "丰台区"],
"上海市": ["黄浦区", "徐汇区", "长宁区", "静安区", "普陀区"],
"广东省": ["广州市", "深圳市", "珠海市", "汕头市", "佛山市", "东莞市"],
"江苏省": ["南京市", "苏州市", "无锡市", "常州市"],
"浙江省": ["杭州市", "宁波市", "温州市", "绍兴市"],
"四川省": ["成都市", "绵阳市", "德阳市", "宜宾市"]
},
"United States": {
"California": ["Los Angeles", "San Francisco", "San Diego", "Sacramento"],
"New York": ["New York City", "Buffalo", "Rochester", "Albany"],
"Texas": ["Houston", "Dallas", "Austin", "San Antonio"]
},
"日本": {
"東京都": ["千代田区", "中央区", "港区", "新宿区", "渋谷区"],
"大阪府": ["大阪市", "堺市", "東大阪市", "枚方市"]
},
"Germany": {
"Bayern": ["München", "Nürnberg", "Augsburg", "Regensburg"],
"Berlin": ["Berlin-Mitte", "Charlottenburg", "Friedrichshain"]
},
"United Kingdom": {
"England": ["London", "Manchester", "Birmingham", "Liverpool"],
"Scotland": ["Edinburgh", "Glasgow", "Aberdeen"]
},
"France": {
"Île-de-France": ["Paris", "Versailles", "Boulogne-Billancourt"],
"Provence-Alpes-Côte d'Azur": ["Marseille", "Nice", "Toulon"]
},
"Russia": {
"Москва": ["Центральный", "Северный", "Южный"],
"Санкт-Петербург": ["Адмиралтейский", "Василеостровский", "Выборгский"]
},
"India": {
"Maharashtra": ["Mumbai", "Pune", "Nagpur"],
"Delhi": ["New Delhi", "Old Delhi", "Dwarka"]
},
"Brazil": {
"São Paulo": ["São Paulo City", "Guarulhos", "Campinas"],
"Rio de Janeiro": ["Rio de Janeiro City", "Niterói", "Duque de Caxias"]
},
"Australia": {
"New South Wales": ["Sydney", "Newcastle", "Wollongong"],
"Victoria": ["Melbourne", "Geelong", "Ballarat"]
}
}
for country_name, states in regions.items():
c.execute("INSERT INTO countries (name) VALUES (?)", (country_name,))
country_id = c.lastrowid
for state_name, cities in states.items():
c.execute("INSERT INTO states (country_id, name) VALUES (?,?)", (country_id, state_name))
state_id = c.lastrowid
for city_name in cities:
c.execute("INSERT INTO cities (state_id, name) VALUES (?,?)", (state_id, city_name))
conn.commit()
conn.close()
print("地区数据导入完成")
init_db()
import_region_data()
# -------------------- 辅助函数 --------------------
def hash_password(pwd):
return hashlib.sha256(pwd.encode()).hexdigest()
def is_valid_user_id(uid):
return bool(re.match(r'^[A-Za-z0-9]{3,20}$', uid))
def register_user(user_id, password, nickname, gender, age, region, lat, lng):
if not is_valid_user_id(user_id):
return False, "ID必须为3~20位字母或数字组合"
conn = sqlite3.connect('chat.db')
c = conn.cursor()
try:
c.execute('''INSERT INTO users
(user_id, password, nickname, gender, age, region, latitude, longitude, created_at)
VALUES (?,?,?,?,?,?,?,?,?)''',
(user_id, hash_password(password), nickname, gender, age, region, lat, lng, time.time()))
conn.commit()
return True, "注册成功"
except sqlite3.IntegrityError:
return False, "ID已被占用"
finally:
conn.close()
def login_user(user_id, password):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT password, nickname, gender, age, region, latitude, longitude FROM users WHERE user_id=?", (user_id,))
row = c.fetchone()
conn.close()
if row and row[0] == hash_password(password):
return True, {
"nickname": row[1],
"gender": row[2],
"age": row[3],
"region": row[4],
"latitude": row[5],
"longitude": row[6]
}
return False, "ID或密码错误"
def get_user_profile(user_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT nickname, gender, age, region, latitude, longitude FROM users WHERE user_id=?", (user_id,))
row = c.fetchone()
conn.close()
if row:
return {"nickname": row[0], "gender": row[1], "age": row[2], "region": row[3], "latitude": row[4], "longitude": row[5]}
return None
def update_user_location(user_id, lat, lng):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("UPDATE users SET latitude=?, longitude=? WHERE user_id=?", (lat, lng, user_id))
conn.commit()
conn.close()
def haversine(lat1, lon1, lat2, lon2):
R = 6371
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
return R * c
def search_users(user_id, gender=None, age_min=None, age_max=None, region=None):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
query = "SELECT user_id, nickname, gender, age, region FROM users WHERE user_id != ?"
params = [user_id]
if gender and gender != '不限':
query += " AND gender = ?"
params.append(gender)
if age_min is not None:
query += " AND age >= ?"
params.append(age_min)
if age_max is not None:
query += " AND age <= ?"
params.append(age_max)
if region:
query += " AND region LIKE ?"
params.append(f'%{region}%')
c.execute(query, params)
rows = c.fetchall()
conn.close()
return [{"user_id": r[0], "nickname": r[1], "gender": r[2], "age": r[3], "region": r[4]} for r in rows]
def nearby_users(user_id, radius_km=5):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT latitude, longitude FROM users WHERE user_id=?", (user_id,))
my_loc = c.fetchone()
if not my_loc or my_loc[0] is None or my_loc[1] is None:
conn.close()
return []
lat1, lon1 = my_loc
c.execute("SELECT user_id, nickname, gender, age, region, latitude, longitude FROM users WHERE user_id != ? AND latitude IS NOT NULL AND longitude IS NOT NULL", (user_id,))
users = c.fetchall()
conn.close()
result = []
for u in users:
dist = haversine(lat1, lon1, u[5], u[6])
if dist <= radius_km:
result.append({"user_id": u[0], "nickname": u[1], "gender": u[2], "age": u[3], "region": u[4], "distance": round(dist, 2)})
result.sort(key=lambda x: x['distance'])
return result
def get_friends(user_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("""SELECT user2 FROM friends WHERE user1=? AND status=1
UNION SELECT user1 FROM friends WHERE user2=? AND status=1""", (user_id, user_id))
friends = [row[0] for row in c.fetchall()]
conn.close()
return friends
def get_pending_requests(user_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT from_user, message, created_at FROM friend_requests WHERE to_user=? AND status=0", (user_id,))
requests = [{"from": row[0], "msg": row[1], "time": row[2]} for row in c.fetchall()]
conn.close()
return requests
def add_friend_request(from_user, to_user, message):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("INSERT INTO friend_requests (from_user, to_user, message, created_at) VALUES (?,?,?,?)",
(from_user, to_user, message, time.time()))
conn.commit()
conn.close()
def accept_friend_request(from_user, to_user):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("UPDATE friend_requests SET status=1 WHERE from_user=? AND to_user=?", (from_user, to_user))
c.execute("INSERT OR IGNORE INTO friends (user1, user2, status, created_at) VALUES (?,?,1,?)", (from_user, to_user, time.time()))
c.execute("INSERT OR IGNORE INTO friends (user1, user2, status, created_at) VALUES (?,?,1,?)", (to_user, from_user, time.time()))
conn.commit()
conn.close()
def reject_friend_request(from_user, to_user):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("UPDATE friend_requests SET status=2 WHERE from_user=? AND to_user=?", (from_user, to_user))
conn.commit()
conn.close()
def delete_friend(user1, user2):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("DELETE FROM friends WHERE (user1=? AND user2=?) OR (user1=? AND user2=?)", (user1, user2, user2, user1))
conn.commit()
conn.close()
def create_group(group_name, owner):
group_id = str(uuid.uuid4())[:8]
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("INSERT INTO groups (group_id, group_name, owner, created_at) VALUES (?,?,?,?)",
(group_id, group_name, owner, time.time()))
c.execute("INSERT INTO group_members (group_id, user_id, role, joined_at) VALUES (?,?,2,?)", (group_id, owner, time.time()))
conn.commit()
conn.close()
return group_id
def get_user_groups(user_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT group_id FROM group_members WHERE user_id=?", (user_id,))
groups = [row[0] for row in c.fetchall()]
group_info = []
for gid in groups:
c.execute("SELECT group_name FROM groups WHERE group_id=?", (gid,))
name = c.fetchone()[0]
group_info.append({"group_id": gid, "group_name": name})
conn.close()
return group_info
def get_group_members(group_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT user_id FROM group_members WHERE group_id=?", (group_id,))
members = [row[0] for row in c.fetchall()]
conn.close()
return members
def add_group_member(group_id, user_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
try:
c.execute("INSERT INTO group_members (group_id, user_id, role, joined_at) VALUES (?,?,0,?)",
(group_id, user_id, time.time()))
conn.commit()
return True
except sqlite3.IntegrityError:
return False
finally:
conn.close()
def remove_group_member(group_id, user_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("DELETE FROM group_members WHERE group_id=? AND user_id=?", (group_id, user_id))
conn.commit()
conn.close()
def store_offline_msg(to_user, from_user, content, msg_type='text', file_id=None):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("INSERT INTO offline_messages (to_user, from_user, content, msg_type, file_id, timestamp, is_read) VALUES (?,?,?,?,?,?,0)",
(to_user, from_user, content, msg_type, file_id, time.time()))
conn.commit()
conn.close()
def store_offline_group_msg(group_id, from_user, content):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("INSERT INTO offline_group_messages (group_id, from_user, content, timestamp, is_read) VALUES (?,?,?,?,0)",
(group_id, from_user, content, time.time()))
conn.commit()
conn.close()
def get_offline_msgs(user_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT from_user, content, msg_type, file_id, timestamp FROM offline_messages WHERE to_user=? AND is_read=0", (user_id,))
msgs = [{"from": row[0], "content": row[1], "type": row[2], "file_id": row[3], "time": row[4]} for row in c.fetchall()]
c.execute("UPDATE offline_messages SET is_read=1 WHERE to_user=?", (user_id,))
conn.commit()
conn.close()
return msgs
def get_offline_group_msgs(user_id):
groups = get_user_groups(user_id)
group_ids = [g['group_id'] for g in groups]
if not group_ids:
return []
conn = sqlite3.connect('chat.db')
c = conn.cursor()
placeholders = ','.join(['?']*len(group_ids))
c.execute(f"SELECT group_id, from_user, content, timestamp FROM offline_group_messages WHERE group_id IN ({placeholders}) AND is_read=0", group_ids)
msgs = [{"group_id": row[0], "from": row[1], "content": row[2], "time": row[3]} for row in c.fetchall()]
c.execute(f"UPDATE offline_group_messages SET is_read=1 WHERE group_id IN ({placeholders})", group_ids)
conn.commit()
conn.close()
return msgs
def save_file_meta(file_id, file_name, file_size, uploader, storage_path):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("INSERT INTO files (file_id, file_name, file_size, uploader, upload_time, storage_path) VALUES (?,?,?,?,?,?)",
(file_id, file_name, file_size, uploader, time.time(), storage_path))
conn.commit()
conn.close()
def get_file_meta(file_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT file_name, file_size, storage_path FROM files WHERE file_id=?", (file_id,))
row = c.fetchone()
conn.close()
if row:
return {"name": row[0], "size": row[1], "path": row[2]}
return None
def revoke_message(msg_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("INSERT INTO revoked_messages (msg_id, revoke_time) VALUES (?,?)", (msg_id, time.time()))
conn.commit()
conn.close()
def is_message_revoked(msg_id):
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT 1 FROM revoked_messages WHERE msg_id=?", (msg_id,))
row = c.fetchone()
conn.close()
return row is not None
# -------------------- 消息处理 --------------------
def broadcast(message, exclude_sock=None):
with user_lock:
for sock in online_users.values():
if sock != exclude_sock:
try:
sock.send((json.dumps(message) + '\n').encode())
except:
pass
def broadcast_to_group(group_id, message, exclude_user=None):
members = get_group_members(group_id)
with user_lock:
for user in members:
if user == exclude_user:
continue
sock = online_users.get(user)
if sock:
try:
sock.send((json.dumps(message) + '\n').encode())
except:
pass
def handle_client(sock, addr):
user_id = None
try:
data = sock.recv(4096).decode().strip()
if not data:
return
req = json.loads(data)
action = req.get('action')
if action == 'register':
uid = req['user_id']
pwd = req['password']
nick = req.get('nickname', uid)
gender = req.get('gender')
age = req.get('age')
region = req.get('region')
lat = req.get('latitude')
lng = req.get('longitude')
ok, msg = register_user(uid, pwd, nick, gender, age, region, lat, lng)
sock.send((json.dumps({"type": "register_result", "success": ok, "message": msg}) + '\n').encode())
if not ok:
sock.close()
return
user_id = uid
elif action == 'login':
uid = req['user_id']
pwd = req['password']
ok, profile = login_user(uid, pwd)
if ok:
sock.send((json.dumps({"type": "login_result", "success": True, "message": "登录成功", "profile": profile}) + '\n').encode())
else:
sock.send((json.dumps({"type": "login_result", "success": False, "message": profile}) + '\n').encode())
sock.close()
return
user_id = uid
elif action == 'get_countries':
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT id, name FROM countries")
countries = [{"id": row[0], "name": row[1]} for row in c.fetchall()]
conn.close()
sock.send((json.dumps({"type": "countries", "data": countries}) + '\n').encode())
sock.close()
return
elif action == 'get_states':
country_id = req['country_id']
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT id, name FROM states WHERE country_id=?", (country_id,))
states = [{"id": row[0], "name": row[1]} for row in c.fetchall()]
conn.close()
sock.send((json.dumps({"type": "states", "data": states}) + '\n').encode())
sock.close()
return
elif action == 'get_cities':
state_id = req['state_id']
conn = sqlite3.connect('chat.db')
c = conn.cursor()
c.execute("SELECT id, name FROM cities WHERE state_id=?", (state_id,))
cities = [{"id": row[0], "name": row[1]} for row in c.fetchall()]
conn.close()
sock.send((json.dumps({"type": "cities", "data": cities}) + '\n').encode())
sock.close()
return
else:
sock.close()
return
# 登录成功后的主循环
with user_lock:
online_users[user_id] = sock
broadcast({"type": "info", "data": f"{user_id} 上线了"}, exclude_sock=sock)
friends = get_friends(user_id)
pending_reqs = get_pending_requests(user_id)
groups = get_user_groups(user_id)
with user_lock:
online_list = list(online_users.keys())
sock.send((json.dumps({"type": "init_data", "friends": friends, "pending_requests": pending_reqs,
"groups": groups, "online_users": online_list}) + '\n').encode())
for msg in get_offline_msgs(user_id):
sock.send((json.dumps({"type": "offline_private", "from": msg['from'], "content": msg['content'],
"msg_type": msg['type'], "file_id": msg.get('file_id'), "time": msg['time']}) + '\n').encode())
for msg in get_offline_group_msgs(user_id):
sock.send((json.dumps({"type": "offline_group", "group_id": msg['group_id'], "from": msg['from'],
"content": msg['content'], "time": msg['time']}) + '\n').encode())
while True:
data = sock.recv(4096).decode().strip()
if not data:
break
msg = json.loads(data)
msg_type = msg.get('type')
if msg_type == 'private':
target = msg['to']
content = msg['data']
msg_id = msg.get('msg_id', str(time.time()))
with user_lock:
if target in online_users:
online_users[target].send((json.dumps({"type": "private", "msg_id": msg_id, "from": user_id,
"data": content, "time": time.time()}) + '\n').encode())
else:
store_offline_msg(target, user_id, content, 'text')
sock.send((json.dumps({"type": "error", "data": f"用户 {target} 不在线,消息已离线存储"}) + '\n').encode())
elif msg_type == 'group':
group_id = msg['group_id']
content = msg['data']
msg_id = msg.get('msg_id', str(time.time()))
members = get_group_members(group_id)
offline_users = []
with user_lock:
for member in members:
if member == user_id:
continue
if member in online_users:
online_users[member].send((json.dumps({"type": "group", "msg_id": msg_id, "group_id": group_id,
"from": user_id, "data": content, "time": time.time()}) + '\n').encode())
else:
offline_users.append(member)
for off in offline_users:
store_offline_group_msg(group_id, user_id, content)
elif msg_type == 'revoke':
msg_id = msg['msg_id']
revoke_message(msg_id)
broadcast({"type": "revoke_notify", "msg_id": msg_id, "from": user_id})
elif msg_type == 'friend_request':
to_user = msg['to']
message = msg.get('message', '')
add_friend_request(user_id, to_user, message)
with user_lock:
if to_user in online_users:
online_users[to_user].send((json.dumps({"type": "new_friend_request", "from": user_id,
"message": message, "time": time.time()}) + '\n').encode())
elif msg_type == 'handle_friend_request':
from_user = msg['from']
accept = msg['accept']
if accept:
accept_friend_request(from_user, user_id)
for u in [user_id, from_user]:
with user_lock:
if u in online_users:
online_users[u].send((json.dumps({"type": "friend_added", "friend": from_user if u==user_id else user_id}) + '\n').encode())
else:
reject_friend_request(from_user, user_id)
elif msg_type == 'delete_friend':
friend = msg['friend']
delete_friend(user_id, friend)
for u in [user_id, friend]:
with user_lock:
if u in online_users:
online_users[u].send((json.dumps({"type": "friend_deleted", "friend": user_id if u==friend else friend}) + '\n').encode())
elif msg_type == 'create_group':
group_name = msg['group_name']
group_id = create_group(group_name, user_id)
sock.send((json.dumps({"type": "group_created", "group_id": group_id, "group_name": group_name}) + '\n').encode())
elif msg_type == 'invite_to_group':
group_id = msg['group_id']
target = msg['user']
group_name = msg.get('group_name', '')
if add_group_member(group_id, target):
with user_lock:
if target in online_users:
online_users[target].send((json.dumps({"type": "invited_to_group", "group_id": group_id,
"group_name": group_name, "inviter": user_id}) + '\n').encode())
elif msg_type == 'search_users':
gender = msg.get('gender')
age_min = msg.get('age_min')
age_max = msg.get('age_max')
region = msg.get('region')
result = search_users(user_id, gender, age_min, age_max, region)
sock.send((json.dumps({"type": "search_result", "users": result}) + '\n').encode())
elif msg_type == 'nearby_users':
radius = msg.get('radius', 5)
result = nearby_users(user_id, radius)
sock.send((json.dumps({"type": "nearby_result", "users": result}) + '\n').encode())
elif msg_type == 'update_location':
lat = msg.get('latitude')
lng = msg.get('longitude')
update_user_location(user_id, lat, lng)
sock.send((json.dumps({"type": "location_updated"}) + '\n').encode())
elif msg_type == 'file_upload':
# 简化,由文件服务器处理
pass
elif msg_type == 'file_download':
file_id = msg['file_id']
file_info = get_file_meta(file_id)
if file_info:
sock.send((json.dumps({"type": "file_download_info", "file_id": file_id,
"file_name": file_info['name'], "file_size": file_info['size']}) + '\n').encode())
else:
sock.send((json.dumps({"type": "error", "data": "文件不存在"}) + '\n').encode())
elif msg_type == 'get_online_users':
with user_lock:
online_list = list(online_users.keys())
sock.send((json.dumps({"type": "online_users", "users": online_list}) + '\n').encode())
elif msg_type == 'get_friends':
friends = get_friends(user_id)
sock.send((json.dumps({"type": "friends_list", "friends": friends}) + '\n').encode())
elif msg_type == 'get_groups':
groups = get_user_groups(user_id)
sock.send((json.dumps({"type": "groups_list", "groups": groups}) + '\n').encode())
except Exception as e:
print(f"处理出错: {e}")
finally:
if user_id:
with user_lock:
if user_id in online_users:
del online_users[user_id]
broadcast({"type": "info", "data": f"{user_id} 下线了"})
sock.close()
# -------------------- 文件服务器 --------------------
def handle_file_client(file_sock, addr):
try:
data = file_sock.recv(4096).decode().strip()
req = json.loads(data)
if req['type'] == 'upload':
file_id = req['file_id']
file_name = req['file_name']
file_size = req['file_size']
uploader = req['uploader']
storage_path = os.path.join(FILE_STORAGE, file_id)
with open(storage_path, 'wb') as f:
received = 0
while received < file_size:
chunk = file_sock.recv(8192)
if not chunk:
break
f.write(chunk)
received += len(chunk)
save_file_meta(file_id, file_name, file_size, uploader, storage_path)
file_sock.send(b'{"status":"ok"}\n')
elif req['type'] == 'download':
file_id = req['file_id']
file_info = get_file_meta(file_id)
if not file_info:
file_sock.send(b'{"status":"error","message":"file not found"}\n')
return
file_sock.send(json.dumps({"status":"ok", "file_size": file_info['size'], "file_name": file_info['name']}).encode() + b'\n')
with open(file_info['path'], 'rb') as f:
while True:
chunk = f.read(8192)
if not chunk:
break
file_sock.send(chunk)
except Exception as e:
print(f"文件服务器错误: {e}")
finally:
file_sock.close()
def start_file_server():
file_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
file_server.bind((SERVER_HOST, FILE_SERVER_PORT))
file_server.listen(50)
print(f"文件服务器启动,监听 {SERVER_HOST}:{FILE_SERVER_PORT}")
while True:
sock, addr = file_server.accept()
threading.Thread(target=handle_file_client, args=(sock, addr), daemon=True).start()
def main():
threading.Thread(target=start_file_server, daemon=True).start()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((SERVER_HOST, SERVER_PORT))
server.listen(100)
print(f"聊天服务器启动,监听 {SERVER_HOST}:{SERVER_PORT}")
while True:
sock, addr = server.accept()
print(f"新连接 {addr}")
threading.Thread(target=handle_client, args=(sock, addr), daemon=True).start()
if __name__ == '__main__':
main()
客户端
[Asm] 纯文本查看 复制代码 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import socket
import json
import threading
import time
import os
import hashlib
import requests
from datetime import datetime
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
SERVER_HOST = '127.0.0.1'
SERVER_PORT = 8765
FILE_SERVER_HOST = '127.0.0.1'
FILE_SERVER_PORT = 8766
# ---------- 文件传输函数 ----------
def upload_file(file_path, file_id, uploader):
try:
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((FILE_SERVER_HOST, FILE_SERVER_PORT))
req = json.dumps({"type": "upload", "file_id": file_id,
"file_name": file_name, "file_size": file_size,
"uploader": uploader}) + '\n'
sock.send(req.encode())
with open(file_path, 'rb') as f:
while True:
chunk = f.read(8192)
if not chunk:
break
sock.send(chunk)
sock.close()
return True
except Exception:
return False
def download_file(file_id, save_path):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((FILE_SERVER_HOST, FILE_SERVER_PORT))
req = json.dumps({"type": "download", "file_id": file_id}) + '\n'
sock.send(req.encode())
resp = sock.recv(4096).decode().strip()
data = json.loads(resp)
if data.get('status') != 'ok':
return False
file_size = data['file_size']
file_name = data['file_name']
full_path = os.path.join(save_path, file_name)
with open(full_path, 'wb') as f:
received = 0
while received < file_size:
chunk = sock.recv(8192)
if not chunk:
break
f.write(chunk)
received += len(chunk)
sock.close()
return True
except Exception:
return False
# ---------- 登录/注册对话框 ----------
class LoginDialog(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("聊天软件 - 登录/注册")
self.setFixedSize(450, 550)
self.setStyleSheet("""
QDialog { background-color: #f5f5f5; }
QLineEdit, QComboBox { padding: 8px; border: 1px solid #ddd; border-radius: 5px; }
QPushButton { background-color: #07c160; color: white; border: none; border-radius: 5px; padding: 8px; }
QPushButton#register_btn { background-color: #f5f5f5; color: #07c160; border: 1px solid #07c160; }
""")
layout = QVBoxLayout()
title = QLabel("欢迎使用聊天室")
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet("font-size: 20px; font-weight: bold;")
layout.addWidget(title)
# 登录模式控件
self.login_id = QLineEdit()
self.login_id.setPlaceholderText("ID")
self.login_pwd = QLineEdit()
self.login_pwd.setPlaceholderText("密码")
self.login_pwd.setEchoMode(QLineEdit.Password)
self.login_btn = QPushButton("登录")
# 注册模式控件
self.reg_id = QLineEdit()
self.reg_id.setPlaceholderText("ID (3~20位字母/数字)")
self.reg_pwd = QLineEdit()
self.reg_pwd.setPlaceholderText("密码")
self.reg_pwd.setEchoMode(QLineEdit.Password)
self.reg_nick = QLineEdit()
self.reg_nick.setPlaceholderText("昵称")
self.gender_combo = QComboBox()
self.gender_combo.addItems(["保密", "男", "女"])
self.age_input = QLineEdit()
self.age_input.setPlaceholderText("年龄")
self.country_combo = QComboBox()
self.state_combo = QComboBox()
self.city_combo = QComboBox()
self.register_btn = QPushButton("注册")
# 堆叠布局
self.stacked = QStackedWidget()
login_widget = QWidget()
login_layout = QVBoxLayout(login_widget)
login_layout.addWidget(self.login_id)
login_layout.addWidget(self.login_pwd)
login_layout.addWidget(self.login_btn)
reg_widget = QWidget()
reg_layout = QVBoxLayout(reg_widget)
reg_layout.addWidget(QLabel("ID:"))
reg_layout.addWidget(self.reg_id)
reg_layout.addWidget(QLabel("密码:"))
reg_layout.addWidget(self.reg_pwd)
reg_layout.addWidget(QLabel("昵称:"))
reg_layout.addWidget(self.reg_nick)
reg_layout.addWidget(QLabel("性别:"))
reg_layout.addWidget(self.gender_combo)
reg_layout.addWidget(QLabel("年龄:"))
reg_layout.addWidget(self.age_input)
reg_layout.addWidget(QLabel("地区:"))
region_layout = QHBoxLayout()
region_layout.addWidget(self.country_combo)
region_layout.addWidget(self.state_combo)
region_layout.addWidget(self.city_combo)
reg_layout.addLayout(region_layout)
reg_layout.addWidget(self.register_btn)
self.stacked.addWidget(login_widget)
self.stacked.addWidget(reg_widget)
layout.addWidget(self.stacked)
# 切换按钮
self.switch_btn = QPushButton("注册账号")
self.switch_btn.clicked.connect(self.toggle_mode)
layout.addWidget(self.switch_btn)
self.setLayout(layout)
self.mode = 'login'
self.countries = []
self.states = {}
self.cities = {}
self.load_countries()
self.auto_locate_country()
self.login_btn.clicked.connect(self.do_login)
self.register_btn.clicked.connect(self.do_register)
def toggle_mode(self):
if self.mode == 'login':
self.mode = 'register'
self.stacked.setCurrentIndex(1)
self.switch_btn.setText("返回登录")
else:
self.mode = 'login'
self.stacked.setCurrentIndex(0)
self.switch_btn.setText("注册账号")
def load_countries(self):
def fetch():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((SERVER_HOST, SERVER_PORT))
sock.send((json.dumps({"action": "get_countries"}) + '\n').encode())
resp = sock.recv(4096).decode().strip()
data = json.loads(resp)
if data['type'] == 'countries':
self.countries = data['data']
QMetaObject.invokeMethod(self, "populate_countries", Qt.QueuedConnection)
sock.close()
except Exception:
pass
threading.Thread(target=fetch, daemon=True).start()
def populate_countries(self):
self.country_combo.clear()
for c in self.countries:
self.country_combo.addItem(c['name'], c['id'])
self.country_combo.currentIndexChanged.connect(self.on_country_changed)
def on_country_changed(self, index):
if index < 0:
return
country_id = self.country_combo.itemData(index)
def fetch_states():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((SERVER_HOST, SERVER_PORT))
sock.send((json.dumps({"action": "get_states", "country_id": country_id}) + '\n').encode())
resp = sock.recv(4096).decode().strip()
data = json.loads(resp)
if data['type'] == 'states':
self.states[country_id] = data['data']
QMetaObject.invokeMethod(self, "populate_states", Qt.QueuedConnection, Q_ARG(int, country_id))
sock.close()
except Exception:
pass
threading.Thread(target=fetch_states, daemon=True).start()
def populate_states(self, country_id):
self.state_combo.clear()
for s in self.states.get(country_id, []):
self.state_combo.addItem(s['name'], s['id'])
self.state_combo.currentIndexChanged.connect(self.on_state_changed)
def on_state_changed(self, index):
if index < 0:
return
state_id = self.state_combo.itemData(index)
def fetch_cities():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((SERVER_HOST, SERVER_PORT))
sock.send((json.dumps({"action": "get_cities", "state_id": state_id}) + '\n').encode())
resp = sock.recv(4096).decode().strip()
data = json.loads(resp)
if data['type'] == 'cities':
self.cities[state_id] = data['data']
QMetaObject.invokeMethod(self, "populate_cities", Qt.QueuedConnection, Q_ARG(int, state_id))
sock.close()
except Exception:
pass
threading.Thread(target=fetch_cities, daemon=True).start()
def populate_cities(self, state_id):
self.city_combo.clear()
for c in self.cities.get(state_id, []):
self.city_combo.addItem(c['name'], c['id'])
def auto_locate_country(self):
try:
resp = requests.get('http://ip-api.com/json/', timeout=5)
data = resp.json()
if data['status'] == 'success':
country_name = data['country']
self.lat = data['lat']
self.lon = data['lon']
QTimer.singleShot(500, lambda: self.select_country(country_name))
else:
self.lat = None
self.lon = None
except Exception:
self.lat = None
self.lon = None
def select_country(self, country_name):
for i in range(self.country_combo.count()):
if self.country_combo.itemText(i) == country_name:
self.country_combo.setCurrentIndex(i)
break
def get_full_region(self):
country = self.country_combo.currentText()
state = self.state_combo.currentText()
city = self.city_combo.currentText()
return f"{country}, {state}, {city}"
def do_login(self):
uid = self.login_id.text().strip()
pwd = self.login_pwd.text().strip()
if not uid or not pwd:
QMessageBox.warning(self, "提示", "ID和密码不能为空")
return
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((SERVER_HOST, SERVER_PORT))
req = json.dumps({"action": "login", "user_id": uid, "password": pwd}) + '\n'
sock.send(req.encode())
resp = sock.recv(4096).decode().strip()
result = json.loads(resp)
if result.get('success'):
self.login_sock = sock
self.login_profile = result['profile']
self.login_user_id = uid
self.accept()
else:
sock.close()
QMessageBox.critical(self, "登录失败", result['message'])
except Exception as e:
QMessageBox.critical(self, "错误", f"连接服务器失败: {e}")
def do_register(self):
uid = self.reg_id.text().strip()
pwd = self.reg_pwd.text().strip()
nick = self.reg_nick.text().strip()
if not uid or not pwd or not nick:
QMessageBox.warning(self, "提示", "请完整填写ID、密码、昵称")
return
if len(pwd) < 3:
QMessageBox.warning(self, "提示", "密码至少3位")
return
gender = self.gender_combo.currentText()
age_str = self.age_input.text().strip()
age = int(age_str) if age_str.isdigit() else None
region = self.get_full_region()
lat = getattr(self, 'lat', None)
lon = getattr(self, 'lon', None)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((SERVER_HOST, SERVER_PORT))
req = json.dumps({"action": "register", "user_id": uid, "password": pwd, "nickname": nick,
"gender": gender, "age": age, "region": region, "latitude": lat, "longitude": lon}) + '\n'
sock.send(req.encode())
resp = sock.recv(4096).decode().strip()
result = json.loads(resp)
sock.close()
if result.get('success'):
QMessageBox.information(self, "注册成功", "账号注册成功,请登录")
self.toggle_mode()
self.reg_id.clear()
self.reg_pwd.clear()
self.reg_nick.clear()
self.age_input.clear()
else:
QMessageBox.critical(self, "注册失败", result['message'])
except Exception as e:
QMessageBox.critical(self, "错误", f"连接服务器失败: {e}")
def get_result(self):
if hasattr(self, 'login_sock'):
return ('login', self.login_user_id, self.login_profile, self.login_sock)
return None
# ---------- 个人资料对话框 ----------
class ProfileDialog(QDialog):
def __init__(self, user_id, profile, parent=None):
super().__init__(parent)
self.setWindowTitle("个人资料")
self.setFixedSize(300, 250)
layout = QVBoxLayout()
layout.addWidget(QLabel(f"ID: {user_id}"))
layout.addWidget(QLabel(f"昵称: {profile.get('nickname', '')}"))
layout.addWidget(QLabel(f"性别: {profile.get('gender', '保密')}"))
layout.addWidget(QLabel(f"年龄: {profile.get('age', '未知')}"))
layout.addWidget(QLabel(f"地区: {profile.get('region', '未知')}"))
close_btn = QPushButton("关闭")
close_btn.clicked.connect(self.accept)
layout.addWidget(close_btn)
self.setLayout(layout)
# ---------- 查找好友窗口 ----------
class SearchFriendDialog(QDialog):
def __init__(self, sock, user_id, parent=None):
super().__init__(parent)
self.sock = sock
self.user_id = user_id
self.setWindowTitle("查找好友")
self.setMinimumSize(600, 400)
self.tab_widget = QTabWidget()
self.search_tab = QWidget()
self.init_search_tab()
self.tab_widget.addTab(self.search_tab, "条件搜索")
self.nearby_tab = QWidget()
self.init_nearby_tab()
self.tab_widget.addTab(self.nearby_tab, "附近的人")
layout = QVBoxLayout()
layout.addWidget(self.tab_widget)
self.setLayout(layout)
def init_search_tab(self):
layout = QVBoxLayout()
form_layout = QFormLayout()
self.gender_combo = QComboBox()
self.gender_combo.addItems(["不限", "男", "女"])
self.age_min = QLineEdit()
self.age_min.setPlaceholderText("最小年龄")
self.age_max = QLineEdit()
self.age_max.setPlaceholderText("最大年龄")
self.region = QLineEdit()
self.region.setPlaceholderText("地区关键字(国家/省/市)")
form_layout.addRow("性别:", self.gender_combo)
form_layout.addRow("年龄范围:", self.age_min)
form_layout.addRow("至:", self.age_max)
form_layout.addRow("地区:", self.region)
layout.addLayout(form_layout)
search_btn = QPushButton("搜索")
search_btn.clicked.connect(self.do_search)
layout.addWidget(search_btn)
self.result_list = QListWidget()
layout.addWidget(self.result_list)
add_btn = QPushButton("添加选中为好友")
add_btn.clicked.connect(self.add_selected_friend)
layout.addWidget(add_btn)
self.search_tab.setLayout(layout)
def init_nearby_tab(self):
layout = QVBoxLayout()
radius_layout = QHBoxLayout()
radius_layout.addWidget(QLabel("半径(km):"))
self.radius_spin = QSpinBox()
self.radius_spin.setRange(1, 100)
self.radius_spin.setValue(5)
radius_layout.addWidget(self.radius_spin)
nearby_btn = QPushButton("查找附近的人")
nearby_btn.clicked.connect(self.do_nearby)
radius_layout.addWidget(nearby_btn)
layout.addLayout(radius_layout)
self.nearby_list = QListWidget()
layout.addWidget(self.nearby_list)
add_nearby_btn = QPushButton("添加选中为好友")
add_nearby_btn.clicked.connect(self.add_selected_nearby)
layout.addWidget(add_nearby_btn)
self.nearby_tab.setLayout(layout)
def do_search(self):
gender = self.gender_combo.currentText()
if gender == "不限":
gender = None
age_min = self.age_min.text().strip()
age_max = self.age_max.text().strip()
region = self.region.text().strip() or None
req = {"type": "search_users", "gender": gender, "age_min": int(age_min) if age_min else None,
"age_max": int(age_max) if age_max else None, "region": region}
self.sock.send((json.dumps(req) + '\n').encode())
def do_nearby(self):
radius = self.radius_spin.value()
req = {"type": "nearby_users", "radius": radius}
self.sock.send((json.dumps(req) + '\n').encode())
def update_search_result(self, users):
self.result_list.clear()
for u in users:
text = f"{u['nickname']} ({u['user_id']}) | {u['gender']} | {u['age']}岁 | {u['region']}"
item = QListWidgetItem(text)
item.setData(Qt.UserRole, u['user_id'])
self.result_list.addItem(item)
def update_nearby_result(self, users):
self.nearby_list.clear()
for u in users:
text = f"{u['nickname']} ({u['user_id']}) | {u['gender']} | {u['age']}岁 | {u['region']} | 距离 {u['distance']} km"
item = QListWidgetItem(text)
item.setData(Qt.UserRole, u['user_id'])
self.nearby_list.addItem(item)
def add_selected_friend(self):
item = self.result_list.currentItem()
if not item:
QMessageBox.warning(self, "提示", "请先选择一个用户")
return
target_id = item.data(Qt.UserRole)
self.send_friend_request(target_id)
def add_selected_nearby(self):
item = self.nearby_list.currentItem()
if not item:
QMessageBox.warning(self, "提示", "请先选择一个用户")
return
target_id = item.data(Qt.UserRole)
self.send_friend_request(target_id)
def send_friend_request(self, target_id):
message, ok = QInputDialog.getText(self, "添加好友", "请输入验证信息:")
if ok:
req = {"type": "friend_request", "to": target_id, "message": message}
self.sock.send((json.dumps(req) + '\n').encode())
QMessageBox.information(self, "提示", "好友申请已发送")
# ---------- 主聊天窗口 ----------
class ChatWindow(QMainWindow):
sig_display_message = pyqtSignal(str, str, str, float, str, bool, str)
sig_display_system = pyqtSignal(str)
sig_update_session_list = pyqtSignal()
sig_connection_lost = pyqtSignal()
sig_friend_added = pyqtSignal(str)
sig_friend_deleted = pyqtSignal(str)
sig_new_friend_request = pyqtSignal(str, str)
sig_invited_to_group = pyqtSignal(str, str, str)
sig_search_result = pyqtSignal(list)
sig_nearby_result = pyqtSignal(list)
def __init__(self, user_id, profile, sock):
super().__init__()
self.user_id = user_id
self.nickname = profile['nickname']
self.profile = profile
self.sock = sock
self.running = True
self.online_users = []
self.friends = []
self.groups = []
self.current_chat = None
self.current_chat_type = 'private'
self.sent_msg_ids = {}
self.init_ui()
self.start_receive_thread()
self.sig_display_message.connect(self._display_message)
self.sig_display_system.connect(self._display_system_msg)
self.sig_update_session_list.connect(self._update_session_list)
self.sig_connection_lost.connect(self._on_connection_lost)
self.sig_friend_added.connect(self._on_friend_added)
self.sig_friend_deleted.connect(self._on_friend_deleted)
self.sig_new_friend_request.connect(self._on_new_friend_request)
self.sig_invited_to_group.connect(self._on_invited_to_group)
self.sig_search_result.connect(self.on_search_result)
self.sig_nearby_result.connect(self.on_nearby_result)
def init_ui(self):
self.setWindowTitle(f"聊天室 - {self.nickname}")
self.setMinimumSize(1000, 700)
self.setStyleSheet("""
QMainWindow { background-color: #f0f2f5; }
QListWidget { border: none; background-color: white; outline: none; }
QListWidget::item { padding: 10px; border-bottom: 1px solid #f0f0f0; }
QListWidget::item:selected { background-color: #e8f4ff; }
QTextBrowser { border: 1px solid #e0e0e0; border-radius: 5px; background-color: white; }
QPushButton { background-color: #07c160; color: white; border: none; border-radius: 5px; padding: 8px 15px; }
QPushButton:hover { background-color: #06ad56; }
QLineEdit { padding: 8px; border: 1px solid #ddd; border-radius: 5px; }
""")
central = QWidget()
self.setCentralWidget(central)
main_layout = QHBoxLayout(central)
main_layout.setContentsMargins(0,0,0,0)
main_layout.setSpacing(0)
left_panel = QWidget()
left_panel.setFixedWidth(280)
left_panel.setStyleSheet("background-color: white; border-right: 1px solid #e0e0e0;")
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0,0,0,0)
self.header_btn = QPushButton(f" {self.nickname}")
self.header_btn.setStyleSheet("text-align: left; padding: 15px; font-size: 16px; font-weight: bold; border-bottom: 1px solid #e0e0e0; background-color: transparent;")
self.header_btn.clicked.connect(self.show_profile)
left_layout.addWidget(self.header_btn)
self.session_list = QListWidget()
self.session_list.itemClicked.connect(self.on_session_changed)
left_layout.addWidget(self.session_list)
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(10,10,10,10)
self.chat_title = QLabel("聊天")
self.chat_title.setStyleSheet("font-size: 18px; font-weight: bold; padding: 5px; border-bottom: 1px solid #e0e0e0;")
right_layout.addWidget(self.chat_title)
self.msg_display = QTextBrowser()
self.msg_display.setOpenExternalLinks(False)
right_layout.addWidget(self.msg_display)
input_widget = QWidget()
input_layout = QHBoxLayout(input_widget)
self.input_box = QLineEdit()
self.input_box.setPlaceholderText("输入消息...")
self.input_box.returnPressed.connect(self.send_message)
send_btn = QPushButton("发送")
send_btn.clicked.connect(self.send_message)
file_btn = QPushButton("📎")
file_btn.setFixedWidth(40)
file_btn.clicked.connect(self.send_file)
input_layout.addWidget(file_btn)
input_layout.addWidget(self.input_box)
input_layout.addWidget(send_btn)
right_layout.addWidget(input_widget)
main_layout.addWidget(left_panel)
main_layout.addWidget(right_panel)
menubar = self.menuBar()
profile_menu = menubar.addMenu("我的")
profile_action = QAction("个人资料", self)
profile_action.triggered.connect(self.show_profile)
profile_menu.addAction(profile_action)
friend_menu = menubar.addMenu("好友")
add_friend_action = QAction("添加好友", self)
add_friend_action.triggered.connect(self.add_friend_dialog)
friend_menu.addAction(add_friend_action)
search_friend_action = QAction("查找好友", self)
search_friend_action.triggered.connect(self.open_search_window)
friend_menu.addAction(search_friend_action)
group_menu = menubar.addMenu("群组")
create_group_action = QAction("创建群组", self)
create_group_action.triggered.connect(self.create_group_dialog)
group_menu.addAction(create_group_action)
invite_group_action = QAction("邀请成员", self)
invite_group_action.triggered.connect(self.invite_member_dialog)
group_menu.addAction(invite_group_action)
self.msg_display.setContextMenuPolicy(Qt.CustomContextMenu)
self.msg_display.customContextMenuRequested.connect(self.show_msg_menu)
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self.request_online_users)
self.refresh_timer.start(10000)
def show_profile(self):
dialog = ProfileDialog(self.user_id, self.profile, self)
dialog.exec_()
def open_search_window(self):
self.search_dialog = SearchFriendDialog(self.sock, self.user_id, self)
self.search_dialog.show()
def on_search_result(self, users):
if hasattr(self, 'search_dialog') and self.search_dialog.isVisible():
self.search_dialog.update_search_result(users)
def on_nearby_result(self, users):
if hasattr(self, 'search_dialog') and self.search_dialog.isVisible():
self.search_dialog.update_nearby_result(users)
def start_receive_thread(self):
def receive():
buffer = ""
while self.running:
try:
chunk = self.sock.recv(4096).decode()
if not chunk:
break
buffer += chunk
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
self.process_message(json.loads(line))
except:
break
self.sig_connection_lost.emit()
threading.Thread(target=receive, daemon=True).start()
def process_message(self, msg):
typ = msg.get('type')
if typ == 'init_data':
self.friends = msg['friends']
self.groups = msg['groups']
self.online_users = msg['online_users']
self.sig_update_session_list.emit()
elif typ == 'private':
self.sig_display_message.emit('private', msg['from'], msg['data'], msg.get('time', time.time()), msg.get('msg_id'), False, '')
elif typ == 'group':
self.sig_display_message.emit('group', f"{msg['from']} @ {msg['group_id']}", msg['data'], msg.get('time', time.time()), msg.get('msg_id'), False, msg['group_id'])
elif typ == 'offline_private':
self.sig_display_message.emit('private', msg['from'], msg['content'], msg['time'], msg.get('msg_id'), True, '')
elif typ == 'offline_group':
self.sig_display_message.emit('group', f"{msg['from']} (离线)", msg['content'], msg['time'], msg.get('msg_id'), True, msg['group_id'])
elif typ == 'revoke_notify':
self.revoke_message_ui(msg['msg_id'])
elif typ == 'online_users':
self.online_users = msg['users']
self.sig_update_session_list.emit()
elif typ == 'friends_list':
self.friends = msg['friends']
self.sig_update_session_list.emit()
elif typ == 'groups_list':
self.groups = msg['groups']
self.sig_update_session_list.emit()
elif typ == 'friend_added':
self.sig_friend_added.emit(msg['friend'])
elif typ == 'friend_deleted':
self.sig_friend_deleted.emit(msg['friend'])
elif typ == 'new_friend_request':
self.sig_new_friend_request.emit(msg['from'], msg['message'])
elif typ == 'invited_to_group':
self.sig_invited_to_group.emit(msg['group_id'], msg['group_name'], msg['inviter'])
elif typ == 'group_created': # 新增处理:创建群组成功
new_group = {"group_id": msg['group_id'], "group_name": msg['group_name']}
self.groups.append(new_group)
self.sig_update_session_list.emit()
self.sig_display_system.emit(f"群组 '{msg['group_name']}' 创建成功,ID: {msg['group_id']}")
elif typ == 'file_download_info':
save_dir = QFileDialog.getExistingDirectory(self, "选择保存目录")
if save_dir:
threading.Thread(target=download_file, args=(msg['file_id'], save_dir), daemon=True).start()
elif typ == 'search_result':
self.sig_search_result.emit(msg['users'])
elif typ == 'nearby_result':
self.sig_nearby_result.emit(msg['users'])
elif typ == 'error':
self.sig_display_system.emit(f"错误: {msg['data']}")
elif typ == 'info':
self.sig_display_system.emit(msg['data'])
@pyqtSlot(str, str, str, float, str, bool, str)
def _display_message(self, chat_type, sender, content, timestamp, msg_id, offline, extra):
current_match = False
if chat_type == 'private' and self.current_chat_type == 'private' and self.current_chat == sender:
current_match = True
elif chat_type == 'group' and self.current_chat_type == 'group' and self.current_chat == extra:
current_match = True
if not current_match:
return
time_str = datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") if timestamp else datetime.now().strftime("%H:%M:%S")
offline_tag = " <span style='color:#f90;'>(离线)</span>" if offline else ""
html = f"""
<div id="msg_{msg_id}" style="margin:5px;">
<b>{sender}</b>{offline_tag} <span style="color:#999; font-size:11px;">{time_str}</span><br>
<span style="background:#f0f0f0; padding:5px; border-radius:8px; display:inline-block;">{content}</span>
</div>
"""
self.msg_display.append(html)
@pyqtSlot(str)
def _display_system_msg(self, text):
self.msg_display.append(f"<div style='margin:5px; color:#f90; text-align:center;'>{text}</div>")
@pyqtSlot()
def _update_session_list(self):
self.session_list.clear()
for friend in self.friends:
self.session_list.addItem(f"👤 {friend}")
for group in self.groups:
self.session_list.addItem(f"👥 {group['group_name']} ({group['group_id']})")
@pyqtSlot()
def _on_connection_lost(self):
self.running = False
QMessageBox.warning(self, "提示", "与服务器断开连接")
self.close()
@pyqtSlot(str)
def _on_friend_added(self, friend):
QMessageBox.information(self, "好友", f"{friend} 已成为你的好友")
self.request_friends_list()
@pyqtSlot(str)
def _on_friend_deleted(self, friend):
QMessageBox.information(self, "好友", f"你已不是 {friend} 的好友")
self.request_friends_list()
@pyqtSlot(str, str)
def _on_new_friend_request(self, from_user, message):
reply = QMessageBox.question(self, "好友申请", f"{from_user} 请求添加好友\n留言:{message}\n是否同意?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.send_json({"type": "handle_friend_request", "from": from_user, "accept": True})
else:
self.send_json({"type": "handle_friend_request", "from": from_user, "accept": False})
@pyqtSlot(str, str, str)
def _on_invited_to_group(self, group_id, group_name, inviter):
reply = QMessageBox.question(self, "群邀请", f"{inviter} 邀请你加入群 {group_name}\n是否接受?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.request_groups_list()
self.sig_display_system.emit(f"你加入了群 {group_name}")
def send_message(self):
content = self.input_box.text().strip()
if not content:
return
self.input_box.clear()
msg_id = str(int(time.time()*1000)) + self.user_id
if self.current_chat_type == 'private':
target = self.current_chat
self.send_json({"type": "private", "to": target, "data": content, "msg_id": msg_id})
self._display_message('private', self.nickname, content, time.time(), msg_id, False, '')
self.sent_msg_ids[msg_id] = (time.time(), content, 'private', target)
elif self.current_chat_type == 'group':
group_id = self.current_chat
self.send_json({"type": "group", "group_id": group_id, "data": content, "msg_id": msg_id})
self._display_message('group', self.nickname, content, time.time(), msg_id, False, group_id)
self.sent_msg_ids[msg_id] = (time.time(), content, 'group', group_id)
def send_file(self):
if self.current_chat_type == 'private':
target = self.current_chat
elif self.current_chat_type == 'group':
group_id = self.current_chat
else:
QMessageBox.warning(self, "提示", "请先选择一个好友或群组")
return
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件")
if not file_path:
return
file_id = hashlib.md5(f"{self.user_id}{time.time()}{file_path}".encode()).hexdigest()
def upload_and_notify():
if upload_file(file_path, file_id, self.user_id):
file_name = os.path.basename(file_path)
content = f"[文件] {file_name} (file_id:{file_id})"
msg_id = str(int(time.time()*1000)) + self.user_id
if self.current_chat_type == 'private':
self.send_json({"type": "private", "to": target, "data": content, "msg_id": msg_id, "file_id": file_id})
self._display_message('private', self.nickname, content, time.time(), msg_id, False, '')
else:
self.send_json({"type": "group", "group_id": group_id, "data": content, "msg_id": msg_id, "file_id": file_id})
self._display_message('group', self.nickname, content, time.time(), msg_id, False, group_id)
else:
QMessageBox.warning(self, "错误", "文件上传失败")
threading.Thread(target=upload_and_notify, daemon=True).start()
def revoke_message(self, msg_id):
self.send_json({"type": "revoke", "msg_id": msg_id})
def revoke_message_ui(self, msg_id):
js = f"""
var elem = document.getElementById('msg_{msg_id}');
if(elem) elem.innerHTML = '<div style="color:#999; text-align:center;">消息已撤回</div>';
"""
self.msg_display.page().runJavaScript(js)
def show_msg_menu(self, pos):
msg_id, ok = QInputDialog.getText(self, "撤回消息", "请输入消息ID")
if ok and msg_id:
if msg_id in self.sent_msg_ids:
send_time, _, _, _ = self.sent_msg_ids[msg_id]
if time.time() - send_time <= 120:
self.revoke_message(msg_id)
else:
QMessageBox.warning(self, "提示", "只能撤回2分钟内的消息")
else:
QMessageBox.warning(self, "提示", "无法撤回他人消息")
def add_friend_dialog(self):
uid, ok = QInputDialog.getText(self, "添加好友", "请输入对方ID:")
if ok and uid:
if uid == self.user_id:
QMessageBox.warning(self, "提示", "不能添加自己")
return
message, ok2 = QInputDialog.getText(self, "添加好友", "请输入验证信息:")
if ok2:
self.send_json({"type": "friend_request", "to": uid, "message": message})
self.sig_display_system.emit(f"已向 {uid} 发送好友申请")
def create_group_dialog(self):
group_name, ok = QInputDialog.getText(self, "创建群组", "请输入群名称:")
if ok and group_name:
self.send_json({"type": "create_group", "group_name": group_name})
def invite_member_dialog(self):
if not self.groups:
QMessageBox.warning(self, "提示", "没有群组")
return
group_names = [g['group_name'] for g in self.groups]
group_name, ok = QInputDialog.getItem(self, "邀请成员", "选择群组:", group_names, 0, False)
if not ok or not group_name:
return
selected = next(g for g in self.groups if g['group_name'] == group_name)
group_id = selected['group_id']
target, ok2 = QInputDialog.getText(self, "邀请成员", "请输入用户ID:")
if ok2 and target:
self.send_json({"type": "invite_to_group", "group_id": group_id, "user": target, "group_name": group_name})
self.sig_display_system.emit(f"已邀请 {target}")
def send_json(self, data):
if self.sock:
try:
self.sock.send((json.dumps(data) + '\n').encode())
except:
pass
def request_online_users(self):
self.send_json({"type": "get_online_users"})
def request_friends_list(self):
self.send_json({"type": "get_friends"})
def request_groups_list(self):
self.send_json({"type": "get_groups"})
def on_session_changed(self, item):
text = item.text()
if text.startswith("👤 "):
self.current_chat_type = 'private'
self.current_chat = text[2:]
self.chat_title.setText(f"与 {self.current_chat} 私聊")
elif text.startswith("👥 "):
parts = text.split('(')
if len(parts) >= 2:
group_id = parts[1].rstrip(')')
self.current_chat_type = 'group'
self.current_chat = group_id
group_name = parts[0][2:].strip()
self.chat_title.setText(f"群聊: {group_name} (ID: {group_id})")
def closeEvent(self, event):
self.running = False
if self.sock:
self.sock.close()
event.accept()
# ---------- 主程序 ----------
def main():
app = QApplication(sys.argv)
while True:
login = LoginDialog()
if login.exec_() != QDialog.Accepted:
sys.exit(0)
result = login.get_result()
if result and result[0] == 'login':
_, user_id, profile, sock = result
win = ChatWindow(user_id, profile, sock)
win.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
|
免费评分
-
| 参与人数 1 | 吾爱币 +7 |
热心值 +1 |
收起
理由
|
苏紫方璇
| + 7 |
+ 1 |
欢迎分析讨论交流,吾爱破解论坛有你更精彩! |
查看全部评分
|