吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1644|回复: 8
收起左侧

[Python 原创] 聊天工具半成品源码

[复制链接]
清水飞云 发表于 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("&#128206;")
        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"&#128100; {friend}")
        for group in self.groups:
            self.session_list.addItem(f"&#128101; {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("&#128100; "):
            self.current_chat_type = 'private'
            self.current_chat = text[2:]
            self.chat_title.setText(f"与 {self.current_chat} 私聊")
        elif text.startswith("&#128101; "):
            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 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

kenxy 发表于 2026-6-10 11:32
聊天内容有加密传输吗
wushishen 发表于 2026-6-10 15:50
Nic残云 发表于 2026-6-10 16:14
树根 发表于 2026-6-10 16:21
看不懂做什么的
wg521125 发表于 2026-6-10 22:10
ai世代,你这个估计最多五元,甚至更少,
XTDR12 发表于 2026-6-11 08:11
现在服务器老贵了,可以本地部署就好玩啦
tonywenzi 发表于 2026-6-18 14:34
厉害了老铁!!!python P一切
杰克布莱尼 发表于 2026-7-1 21:22
大佬,你1月份发的施工日志(工作日志)能分享下代码吗?很好用,但是施工日志能自定义导出范围有些麻烦,我想修改下。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-10 19:29

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表