From eb2e84038676ce87d9aceec8f1a49bd51b838951 Mon Sep 17 00:00:00 2001 From: 035966-L3 <2814139320@qq.com> Date: Sun, 21 Dec 2025 22:24:00 +0800 Subject: [PATCH 1/8] v4.3.2 --- LTS.py | 72 +++++++++++++++++++++++++++++++------------------------ README.md | 8 +++---- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/LTS.py b/LTS.py index 90ca3ac..33cca5d 100644 --- a/LTS.py +++ b/LTS.py @@ -30,7 +30,7 @@ import time # 程序版本 -VERSION = "v4.3.0" +VERSION = "v4.3.2" # 用于客户端解析协议 1.2 RESULTS = \ @@ -56,7 +56,7 @@ - 启动阶段的成功提示 蓝色 (blue): - [发给您的],[您发送的] 标签的颜色 -- help 指令显示的帮助消息中的第二段(可用的命令列表) +- help 指令显示的帮助消息中的第三段和第六段 白色 (white): - 普通消息和加入提示的文本 - 启动阶段的参数输入提示 @@ -68,7 +68,7 @@ - 启动阶段中上面没有提到的所有文本 青色 (cyan): - 所有除普通消息和加入提示以外的消息的文本 -- help 指令显示的帮助消息中的第一段和第三段(其余补充信息) +- help 指令显示的帮助消息中的其余段落 - 程序关闭时的「再见!」文本 (注:洋红色 (magenta) 目前没有使用过) @@ -157,20 +157,20 @@ # 用于 dashboard 指令中列出参数列表 CONFIG_LIST = \ """ -参数名称 当前值 修改示例 描述 +参数名称 当前值 修改示例 描述 -ban.ip <1> ["8.8.8.8"] IP 黑名单 -ban.words <2> ["a", "b"] 屏蔽词列表 +ban.ip <1> ["8.8.8.8"] IP 黑名单 +ban.words <2> ["a", "b"] 屏蔽词列表 -gate.enter_hint <3> "Hi there!\\n" 进入提示 -gate.enter_check {!s:<12}True 加入是否需要人工放行 +gate.enter_hint <3> "Hi there!\\n" 进入提示 +gate.enter_check {!s:<12}True 加入是否需要人工放行 -message.allow_private {!s:<12}False 是否允许私聊 -message.max_length {:<12}256 最大消息长度(字符个数) +message.allow_private {!s:<12}False 是否允许私聊 +message.max_length {:<12}256 最大消息长度(字符个数) -file.allow_any {!s:<12}False 是否允许发送文件 -file.allow_private {!s:<12}False 是否允许发送私有文件 -file.max_size {:<12}16384 最大文件大小(字节数) +file.allow_any {!s:<12}False 是否允许发送文件 +file.allow_private {!s:<12}False 是否允许发送私有文件 +file.max_size {:<12}16384 最大文件大小(字节数) 为了防止尖括号处的内容写不下,此处单独列出: <1>: @@ -583,7 +583,7 @@ def process(message): # 防止干扰用户后续的终端使用 prints("\033[0m\033[1;36m再见!\033[0m") EXIT_FLAG = True - exit() + return if message['type'] == "SERVER.CONFIG.CHANGE": # 服务端参数变更 (协议 3.4.2) announce(message['operator']) prints("配置项 {} 变更为:".format(message['key']) + str(message['value']), "cyan") @@ -601,7 +601,7 @@ def process(message): prints("聊天室服务端已经关闭。", "cyan") prints("\033[0m\033[1;36m再见!\033[0m") EXIT_FLAG = True - exit() + return # 从 my_socket 读取数据,每次 128 KiB,读完为止 def read(): @@ -1357,7 +1357,7 @@ def do_exit(arg=None): if users[i]['status'] in ["Pending", "Online", "Admin", "Root"]: send_queue.put(json.dumps({'to': i, 'content': {'type': 'SERVER.STOP.ANNOUNCE'}})) # 协议 3.2.1 EXIT_FLAG = True - exit() + return def do_help(arg=None): print() @@ -1400,7 +1400,7 @@ def thread_gate(): while True: time.sleep(0.1) if EXIT_FLAG: - exit() + return break # 尝试开启新连接 @@ -1493,7 +1493,7 @@ def thread_process(): while True: time.sleep(0.1) if EXIT_FLAG: - exit() + return break while not receive_queue.empty(): message = json.loads(receive_queue.get()) @@ -1527,7 +1527,7 @@ def thread_receive(): while True: time.sleep(0.1) if EXIT_FLAG: - exit() + return break for i in range(len(users)): if users[i]['status'] in ["Online", "Admin", "Root"]: @@ -1561,7 +1561,7 @@ def thread_send(): while True: time.sleep(0.1) if EXIT_FLAG: - exit() + return break while not send_queue.empty(): message = json.loads(send_queue.get()) @@ -1620,7 +1620,7 @@ def thread_log(): # 与其他线程不同,先写入日志再读取程序终止信号, # 确保程序终止时没有日志残留在 log_queue 中 if EXIT_FLAG: - exit() + return break def thread_check(): @@ -1631,7 +1631,7 @@ def thread_check(): while True: time.sleep(1) # 该部分对整体性能影响较大,因此执行频率下调至 1 秒一次 if EXIT_FLAG: - exit() + return break down = [] # 先完成全部下线用户检测工作再一并广播, @@ -1660,7 +1660,7 @@ def thread_input(): while True: time.sleep(0.1) if EXIT_FLAG: - exit() + return break # 输出模式 try: @@ -1693,7 +1693,7 @@ def thread_output(): while True: time.sleep(0.1) if EXIT_FLAG: - exit() + return break read() message = get_message() @@ -1789,22 +1789,32 @@ def main(): if not check_ip(tmp_config['ip']): raise config = tmp_config + prints("配置文件 config.json 读取成功!", "yellow") + except FileNotFoundError: + prints("未找到配置文件 config.json。如果该文件存在,请尝试以管理员权限重新运行。", "yellow") + prints("下面将使用默认服务端配置启动程序。", "yellow") + config = DEFAULT_SERVER_CONFIG except: + prints("配置文件 config.json 中的配置项存在错误。", "yellow") + prints("下面将使用默认服务端配置启动程序。", "yellow") config = DEFAULT_SERVER_CONFIG os.system('') # 对 Windows 尝试开启 ANSI 转义字符(带颜色文本)支持 clear_screen() + if platform.system() == "Windows": + shortcut = 'C' + else: + shortcut = 'D' prints("欢迎使用 TouchFish 聊天室!", "yellow") prints("当前程序版本:{}".format(VERSION), "yellow") prints("5 秒后将会自动按上次的配置启动。", "yellow") - prints("按下 Ctrl + C 以指定启动配置。", "yellow") - auto_start = True + prints("按下 Ctrl + {} 以按照配置文件中的配置自动启动。".format(shortcut), "yellow") + prints("按下 Enter 以指定启动配置。", "yellow") + auto_start = False try: - for i in range(5, 0, -1): - prints("剩余 " + str(i) + " 秒...", "yellow") - time.sleep(1) - except KeyboardInterrupt: - auto_start = False + input() + except BaseException as e: + auto_start = True except: pass tmp_side = None diff --git a/README.md b/README.md index d928d7a..eafaeb8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ 3. **第一次启动服务器**: - 运行程序 - - 在 5 秒内按下 `Ctrl + C` + - 按下 `Enter` - 指定启动方式(`Server`) - 输入内网 IP 地址 - 指定可用端口 @@ -53,14 +53,14 @@ 4. **后续启动服务器**: - 运行程序 - - 等待 5 秒 + - 根据指示按下 `Ctrl + C` 或 `Ctrl + D` - 程序将自动以上次的配置启动 ### 作为客户端 1. **第一次启动程序**: - 运行程序 - - 在 5 秒内按下 `Ctrl + C` + - 按下 `Enter` - 指定启动方式(`Client`) - 输入服务器 IP 地址 - 输入服务器端口 @@ -68,7 +68,7 @@ 2. **后续启动程序**: - 运行程序 - - 等待 5 秒 + - 根据指示按下 `Ctrl + C` 或 `Ctrl + D` - 程序将自动以上次的配置启动 ## 系统要求 From ef7e5bdad8fd640fe93c188a26e9da06280b18c8 Mon Sep 17 00:00:00 2001 From: 035966-L3 <2814139320@qq.com> Date: Mon, 22 Dec 2025 23:18:20 +0800 Subject: [PATCH 2/8] v4.3.3 --- .gitignore | 2 +- LTS.py | 297 +++++++++++++++++++++++++++++++++++- README-protocol-document.md | 235 ---------------------------- README.md | 2 +- protocol.txt | 51 ------- 5 files changed, 291 insertions(+), 296 deletions(-) delete mode 100644 README-protocol-document.md delete mode 100644 protocol.txt diff --git a/.gitignore b/.gitignore index 8743b52..5dfa824 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ __pycache__ config.json -log.txt +log.ndjson TouchFishFiles diff --git a/LTS.py b/LTS.py index 33cca5d..8ef7ca1 100644 --- a/LTS.py +++ b/LTS.py @@ -4,7 +4,283 @@ -# 请务必在阅读本程序前通读所有相关文档!!! +""" +# TouchFish v4 协议文档 + +本协议分为三个部分:`Gate`,`Chat` 和 `Server`,协议均使用 NDJSON(JSON 格式,相邻两个 JSON 以换行符分隔)格式进行发送。 + +--- + +# 1 Gate + +这个部分是关于进入聊天室的请求、审核等操作的协议内容。 + +## 1.1 Request + +`{ type: "GATE.REQUEST", username: string }` + +客户端连接时向服务端发送此消息,用于申请加入。 + +- `type`: `"GATE.REQUEST"`(所有协议的 `type` 字段均为固定值,下同) +- `username`: 字符串,表示用户希望使用的用户名。(所有 `username` 字段必须非空,下同) + +## 1.2 Response + +`{ type: "GATE.RESPONSE", result: "Accepted" | "Pending review" | "IP is banned" | "Room is full" | "Duplicate usernames" | "Username consists of banned words" }` + +服务端对 `1.1 Request` 的直接响应,告知客户端其请求的处理结果。 + +- `type`: `"GATE.RESPONSE"` +- `result`: 字符串,可能取值包括:(下同) + - `"Accepted"`:立即允许加入; + - `"Pending review"`:需人工审核; + - `"IP is banned"`:当前 IP 被封禁; + - `"Room is full"`:服务端用户数已达上限; + - `"Duplicate usernames"`:用户名已被使用; + - `"Username consists of banned words"`:用户名包含违禁词。 + +## 1.3 Review Result + +`{ type: "GATE.REVIEW_RESULT", accepted: boolean, operator: { username: string, uid: number } }` + +当请求状态为 `"Pending review"` 时,管理员审核完成后,服务端向该客户端发送此消息。 + +- `type`: `"GATE.REVIEW_RESULT"` +- `accepted`: 布尔值,`true` 表示通过,`false` 表示拒绝。 +- `operator`: 审核者信息: + - `username`: 操作者用户名; + - `uid`: 操作者的用户 ID。(除特殊说明外,用户 ID 为非负整数,下同) + +## 1.4 Incorrect Protocol + +`{ type: "GATE.INCORRECT_PROTOCOL", time: time, ip: ip }` + +当出现通信不符合协议规范的连接时,服务端向日志写入记录。 + +- `type`: `"GATE.INCORRECT_PROTOCOL"` +- `time`: 表示事件发生的时间。(精确到微秒,下同) +- `ip`: 字符串,表示连接的网络地址。(格式为 `IPv4:port`,下同) + +## 1.5 Client Request + +服务端对客户端连接请求的响应。 + +### 1.5.1 Announce + +`{ type: "GATE.CLIENT_REQUEST.ANNOUNCE", username: string, uid: number, result: "Accepted" | "Pending review" | "IP is banned" | "Room is full" | "Duplicate usernames" | "Username consists of banned words" }` + +服务端向所有已连接的客户端广播该客户端的连接请求。 + +- `type`: `"GATE.CLIENT_REQUEST.ANNOUNCE"` +- `username`: 用户名。 +- `uid`: 服务端为该用户分配的用户 ID。 +- `result`: 服务端对该请求的处理结果。 + +### 1.5.2 Log + +`{ type: "GATE.CLIENT_REQUEST.LOG", time: time, ip: ip, username: string, uid: number }` + +服务端向接收到的连接请求写入日志。 + +- `type`: `"GATE.CLIENT_REQUEST.LOG"` +- `time`: 同上。 +- `ip`: 客户端 IP 地址与端口。 +- `username`: 用户名。 +- `uid`: 用户 ID。 + +## 1.6 Status Change + +关于用户状态变更事件的协议。 + +### 1.6.1 Request + +`{ type: "GATE.STATUS_CHANGE.REQUEST", status: "Rejected" | "Kicked" | "Offline" | "Pending" | "Online" | "Admin" | "Root", uid: number }` + +由管理员向服务端发起的用户状态变更请求。 + +- `type`: `"GATE.STATUS_CHANGE.REQUEST"` +- `status`: 字符串,表示目标状态,可能取值包括:(下同) + - `"Rejected"`:连接被拒绝的用户;(本协议中表示管理员拒绝加入请求) + - `"Kicked"`:被踢出聊天室的用户;(本协议中表示管理员主动踢出用户) + - `"Offline"`:主动离开聊天室的用户;(本协议中不会出现) + - `"Pending"`:等待加入审核的用户;(本协议中不会出现) + - `"Online"`:在线用户;(本协议中表示管理员通过加入请求) + - `"Admin"`:在线管理员;(本协议中不会出现) + - `"Root"`:聊天室房主。(本协议中不会出现) +- `uid`: 被操作用户的用户 ID。 + +### 1.6.2 Announce + +`{ type: "GATE.STATUS_CHANGE.ANNOUNCE", status: "Rejected" | "Kicked" | "Offline" | "Pending" | "Online" | "Admin" | "Root", uid: number }` + +当用户状态变更时,服务端进行广播。 + +- `type`: `"GATE.STATUS_CHANGE.ANNOUNCE"` +- `status`: 新状态。 +- `uid`: 被变更状态的用户 ID。 + +### 1.6.3 Log + +`{ type: "GATE.STATUS_CHANGE.LOG", time: time, status: "Rejected" | "Kicked" | "Offline" | "Pending" | "Online" | "Admin" | "Root", uid: number, operator: number }` + +服务端将用户状态变更事件写入日志。 + +- `type`: 固定为 `"GATE.STATUS_CHANGE.LOG"`。 +- `time`: 同上。 +- `status`: 新状态。(`Pending` 状态和 `Root` 状态不会出现) +- `uid`: 被操作用户的用户 ID。 +- `operator`: 操作者的用户 ID。 + +--- + +# 2 Chat + +这个部分是关于在聊天室内收发消息和文件的协议内容。 + +## 2.1 Send + +`{ type: "CHAT.SEND", filename: string, content: string, to: number | -1 | -2 }` + +客户端发送消息或文件。 + +- `type`: `"CHAT.SEND"` +- `filename`: 文件名。若发送的是普通文本消息,则为空字符串 `""`;若发送文件,则为原始文件名。(下同) +- `content`: 若为消息,则为原始文本内容;若为文件,则为文件内容的 Base64 编码字符串。(下同) +- `to`: 目标接收者,可能取值包括:(下同) + - `-2`:广播给所有在线用户(相较于普通发送有特殊提示); + - `-1`:发送给所有在线用户; + - 非负整数:私聊给拥有相应用户 ID 的用户。 + +## 2.2 Receive + +`{ type: "CHAT.RECEIVE", from: number, order: number, filename: string, content: string, to: number | -1 | -2 }` + +服务端将消息转发给目标客户端。 + +- `type`: `"CHAT.RECEIVE"` +- `from`: 发送者的用户 ID。(下同) +- `order`: 文件编号(用户区分不同的文件发送请求),可能取值包括:(下同) + - `0`:普通文本消息; + - 正整数:文件编号。 +- `filename`: 同上。 +- `content`: 同上。 +- `to`: 同上。 + +## 2.3 Log + +`{ type: "CHAT.LOG", time: time, from: number, order: number, filename: string, content: string, to: number | -1 | -2 }` + +服务端将收到的聊天记录写入日志。 + +- `type`: `"CHAT.LOG"` +- `time`: 同上。 +- `from`: 同上。 +- `order`: 同上。 +- `filename`: 同上。 +- `content`: 若为消息,则为原始文本内容;若为文件,则为空字符串 `""`。(为了防止日志文件过大,具体的文件内容会单独存储) +- `to`: 同上。 + +--- + +# 3 Server + +这个部分是关于服务端运行情况的协议内容。 + +## 3.1 Start + +`{ type: "SERVER.START", time: time, server_version: string, config: JSON }` + +服务端将启动时的启动参数写入日志。 + +- `type`: `"SERVER.START"` +- `time`: 同上。 +- `server_version`: 字符串,表示服务端程序版本。(下同) +- `config`: JSON 对象,表示启动参数。(具体格式详见代码,下同) + +## 3.2 Data + +`{ type: "SERVER.DATA", server_version: string, uid: number, config: JSON, users: [JSON, ...], chat_history: [JSON, ...] }` + +用于向新连接的客户端提供完整上下文。 + +- `type`: `"SERVER.DATA"` +- `server_version`: 同上。 +- `uid`: 表示服务端分配给该用户的用户 ID。 +- `config`: 同上。 +- `users`: 用户列表,每个元素为: + - `username`: 用户名; + - `status`: 同上。 +- `chat_history`: 历史聊天记录,每条记录包含:(不包含私聊记录和文件发送记录) + - `time`: 同上; + - `from`: 同上; + - `content`: 同上; + - `to`: 同上。 + +## 3.3 Stop + +服务端正常关闭时的协议。 + +### 3.3.1 Announce + +`{ type: "SERVER.STOP.ANNOUNCE" }` + +服务端正常关闭时,向全体客户端进行广播。 + +- `type`: `"SERVER.STOP.ANNOUNCE"` + +### 3.3.2 Log + +`{ type: "SERVER.STOP.LOG", time: time }` + +服务端正常关闭时将事件写入日志。 + +- `type`: `"SERVER.STOP.LOG"` +- `time`: 同上。 + +## 3.4 Config + +### 3.4.1 Post + +`{ type: "SERVER.CONFIG.POST", key: string, value: any }` + +管理员向服务端发送配置修改请求。 + +- `type`: `"SERVER.CONFIG.POST"` +- `key`: 配置项名称。(下同) +- `value`: 配置值。(下同) + +### 3.4.2 Change + +`{ type: "SERVER.CONFIG.CHANGE", key: string, value: any, operator: number }` + +服务端向客户端广播配置修改事件。 + +- `type`: `"SERVER.CONFIG.CHANGE"` +- `key`: 同上。 +- `value`: 同上。 +- `operator`: 执行修改操作的用户 ID。 + +### 3.4.3 Save + +`{ type: "SERVER.CONFIG.SAVE", time: time }` + +服务端将聊天室房主导出配置的事件写入日志。 + +- `type`: `"SERVER.CONFIG.SAVE"` +- `time`: 同上。 + +### 3.4.4 Log + +`{ type: "SERVER.CONFIG.LOG", time: time, key: string, value: any, operator: number }` + +服务端将配置修改事件写入日志。 + +- `type`: `"SERVER.CONFIG.LOG"` +- `time`: 同上。 +- `key`: 同上。 +- `value`: 同上。 +- `operator`: 执行修改操作的用户 ID。 +""" @@ -30,7 +306,7 @@ import time # 程序版本 -VERSION = "v4.3.2" +VERSION = "v4.3.3" # 用于客户端解析协议 1.2 RESULTS = \ @@ -577,6 +853,7 @@ def process(message): if message['uid'] == my_uid and message['status'] == "Kicked": # 特殊情况:自己被踢出 while blocked: pass + my_socket.close() # 关闭相应 TCP socket prints("您被踢出了聊天室。", "cyan") # 此处不能调用 dye 函数,因为需要使用 \033[0m # 来清除 ANSI 文本序列带来的显示效果, @@ -1356,7 +1633,9 @@ def do_exit(arg=None): for i in range(len(users)): if users[i]['status'] in ["Pending", "Online", "Admin", "Root"]: send_queue.put(json.dumps({'to': i, 'content': {'type': 'SERVER.STOP.ANNOUNCE'}})) # 协议 3.2.1 + server_socket.close() EXIT_FLAG = True + my_socket.close() return def do_help(arg=None): @@ -1436,7 +1715,7 @@ def thread_gate(): except: pass log_queue.put(json.dumps({'type': 'GATE.INCORRECT_PROTOCOL', 'time': time_str(), 'ip': addresstmp})) # 协议 1.4 - conntmp.close() + conntmp.close() # 关闭相应 TCP socket continue # 分配用户 ID @@ -1465,7 +1744,7 @@ def thread_gate(): if not result in ["Accepted", "Pending review"]: users[uid]['status'] = "Rejected" - users[uid]['body'].close() + users[uid]['body'].close() # 关闭相应 TCP socket continue # 设置 TCP 保活参数(下同):启用功能,5 分钟后开始探测,间隔 30 秒 if platform.system() != "Windows": @@ -1535,7 +1814,10 @@ def thread_receive(): while True: try: users[i]['body'].setblocking(False) # 再次显式设置为非阻塞模式,避免不必要的问题 - data += users[i]['body'].recv(131072).decode('utf-8') + chunk = users[i]['body'].recv(131072).decode('utf-8') + if not chunk: + raise + data += chunk except: break users[i]['buffer'] += data @@ -1571,6 +1853,7 @@ def thread_send(): try: users[message['to']]['body'].send(bytes("\n", encoding="utf-8")) except: + users[message['to']]['body'].close() # 关闭相应 TCP socket users[message['to']]['status'] = "Offline" online_count -= 1 log_queue.put(json.dumps({'type': 'GATE.STATUS_CHANGE.LOG', 'time': time_str(), 'status': 'Offline', 'uid': message['to'], 'operator': 0})) # 协议 1.6.3 @@ -1642,6 +1925,7 @@ def thread_check(): try: users[i]['body'].send(bytes("\n", encoding="utf-8")) # 发送心跳数据(单个换行符) except: + users[i]['body'].close() # 关闭相应 TCP socket users[i]['status'] = "Offline" down.append(i) online_count -= 1 @@ -1650,9 +1934,6 @@ def thread_check(): for j in range(len(users)): if users[j]['status'] in ["Online", "Admin", "Root"]: send_queue.put(json.dumps({'to': j, 'content': {'type': 'GATE.STATUS_CHANGE.ANNOUNCE', 'status': 'Offline', 'uid': i, 'operator': 0}})) # 协议 1.6.2 - # 接着服务端的 my_socket 给服务端的 - # root_socket 发送心跳数据,用于给服务端保活 - my_socket.send(bytes("\n", encoding="utf-8")) def thread_input(): global blocked diff --git a/README-protocol-document.md b/README-protocol-document.md deleted file mode 100644 index e49879b..0000000 --- a/README-protocol-document.md +++ /dev/null @@ -1,235 +0,0 @@ -# TouchFish v4 协议文档 - -本协议分为三个部分:`Gate`,`Chat` 和 `Server`,协议均使用 NDJSON(JSON 格式,相邻两个 JSON 以换行符分隔)格式进行发送。英文版和 JSON 代码块见 [protocol.txt](protocol.txt)。 - ---- - -# 1. Gate - -这个部分是关于进入聊天室的请求、审核等操作的协议内容。 - -## 1.1 Request - -客户端连接时向服务端发送此消息,用于申请加入。 - -- `type`: `"GATE.REQUEST"`(所有协议的 `type` 字段均为固定值,下同) -- `username`: 字符串,表示用户希望使用的用户名。(所有 `username` 字段必须非空,下同) - -## 1.2 Response - -服务端对 `1.1 Request` 的直接响应,告知客户端其请求的处理结果。 - -- `type`: `"GATE.RESPONSE"` -- `result`: 字符串,可能取值包括:(下同) - - `"Accepted"`:立即允许加入; - - `"Pending review"`:需人工审核; - - `"IP is banned"`:当前 IP 被封禁; - - `"Room is full"`:服务端用户数已达上限; - - `"Duplicate usernames"`:用户名已被使用; - - `"Username consists of banned words"`:用户名包含违禁词。 - -## 1.3 Review Result - -当请求状态为 `"Pending review"` 时,管理员审核完成后,服务端向该客户端发送此消息。 - -- `type`: `"GATE.REVIEW_RESULT"` -- `accepted`: 布尔值,`true` 表示通过,`false` 表示拒绝。 -- `operator`: 审核者信息: - - `username`: 操作者用户名; - - `uid`: 操作者的用户 ID。(除特殊说明外,用户 ID 为非负整数,下同) - -## 1.4 Incorrect Protocol - -当出现通信不符合协议规范的连接时,服务端向日志写入记录。 - -- `type`: `"GATE.INCORRECT_PROTOCOL"` -- `time`: 表示事件发生的时间。(精确到微秒,下同) -- `ip`: 字符串,表示连接的网络地址。(格式为 `IPv4:port`,下同) - -## 1.5 Client Request - -服务端对客户端连接请求的响应。 - -### 1.5.1 Announce - -服务端向所有已连接的客户端广播该客户端的连接请求。 - -- `type`: `"GATE.CLIENT_REQUEST.ANNOUNCE"` -- `username`: 用户名。 -- `uid`: 服务端为该用户分配的用户 ID。 -- `result`: 服务端对该请求的处理结果。 - -### 1.5.2 Log - -服务端向接收到的连接请求写入日志。 - -- `type`: `"GATE.CLIENT_REQUEST.LOG"` -- `time`: 同上。 -- `ip`: 客户端 IP 地址与端口。 -- `username`: 用户名。 -- `uid`: 用户 ID。 - -## 1.6 Status Change - -关于用户状态变更事件的协议。 - -### 1.6.1 Request - -由管理员向服务端发起的用户状态变更请求。 - -- `type`: `"GATE.STATUS_CHANGE.REQUEST"` -- `status`: 字符串,表示目标状态,可能取值包括:(下同) - - `"Rejected"`:连接被拒绝的用户;(本协议中表示管理员拒绝加入请求) - - `"Kicked"`:被踢出聊天室的用户;(本协议中表示管理员主动踢出用户) - - `"Offline"`:主动离开聊天室的用户;(本协议中不会出现) - - `"Pending"`:等待加入审核的用户;(本协议中不会出现) - - `"Online"`:在线用户;(本协议中表示管理员通过加入请求) - - `"Admin"`:在线管理员;(本协议中不会出现) - - `"Root"`:聊天室房主。(本协议中不会出现) -- `uid`: 被操作用户的用户 ID。 - -### 1.6.2 Announce - -当用户状态变更时,服务端进行广播。 - -- `type`: `"GATE.STATUS_CHANGE.ANNOUNCE"` -- `status`: 新状态。 -- `uid`: 被变更状态的用户 ID。 - -### 1.6.3 Log - -服务端将用户状态变更事件写入日志。 - -- `type`: 固定为 `"GATE.STATUS_CHANGE.LOG"`。 -- `time`: 同上。 -- `status`: 新状态。(`Pending` 状态和 `Root` 状态不会出现) -- `uid`: 被操作用户的用户 ID。 -- `operator`: 操作者的用户 ID。 - ---- - -# 2. Chat - -这个部分是关于在聊天室内收发消息和文件的协议内容。 - -## 2.1 Send - -客户端发送消息或文件。 - -- `type`: `"CHAT.SEND"` -- `filename`: 文件名。若发送的是普通文本消息,则为空字符串 `""`;若发送文件,则为原始文件名。(下同) -- `content`: 若为消息,则为原始文本内容;若为文件,则为文件内容的 Base64 编码字符串。(下同) -- `to`: 目标接收者,可能取值包括:(下同) - - `-2`:广播给所有在线用户(相较于普通发送有特殊提示); - - `-1`:发送给所有在线用户; - - 非负整数:私聊给拥有相应用户 ID 的用户。 - -## 2.2 Receive - -服务端将消息转发给目标客户端。 - -- `type`: `"CHAT.RECEIVE"` -- `from`: 发送者的用户 ID。(下同) -- `order`: 文件编号(用户区分不同的文件发送请求),可能取值包括:(下同) - - `0`:普通文本消息; - - 正整数:文件编号。 -- `filename`: 同上。 -- `content`: 同上。 -- `to`: 同上。 - -## 2.3 Log - -服务端将收到的聊天记录写入日志。 - -- `type`: `"CHAT.LOG"` -- `time`: 同上。 -- `from`: 同上。 -- `order`: 同上。 -- `filename`: 同上。 -- `content`: 若为消息,则为原始文本内容;若为文件,则为空字符串 `""`。(为了防止日志文件过大,具体的文件内容会单独存储) -- `to`: 同上。 - ---- - -# 3. Server - -这个部分是关于服务端运行情况的协议内容。 - -## 3.1 Start - -服务端将启动时的启动参数写入日志。 - -- `type`: `"SERVER.START"` -- `time`: 同上。 -- `server_version`: 字符串,表示服务端程序版本。(下同) -- `config`: JSON 对象,表示启动参数。(具体格式详见代码,下同) - -## 3.2 Data - -用于向新连接的客户端提供完整上下文。 - -- `type`: `"SERVER.DATA"` -- `server_version`: 同上。 -- `uid`: 表示服务端分配给该用户的用户 ID。 -- `config`: 同上。 -- `users`: 用户列表,每个元素为: - - `username`: 用户名; - - `status`: 同上。 -- `chat_history`: 历史聊天记录,每条记录包含:(不包含私聊记录和文件发送记录) - - `time`: 同上; - - `from`: 同上; - - `content`: 同上; - - `to`: 同上。 - -## 3.3 Stop - -服务端正常关闭时的协议。 - -### 3.3.1 Announce - -服务端正常关闭时,向全体客户端进行广播。 - -- `type`: `"SERVER.STOP.ANNOUNCE"` - -### 3.3.2 Log - -服务端正常关闭时将事件写入日志。 - -- `type`: `"SERVER.STOP.LOG"` -- `time`: 同上。 - -## 3.4 Config - -### 3.4.1 Post - -管理员向服务端发送配置修改请求。 - -- `type`: `"SERVER.CONFIG.POST"` -- `key`: 配置项名称。(下同) -- `value`: 配置值。(下同) - -### 3.4.2 Change - -服务端向客户端广播配置修改事件。 - -- `type`: `"SERVER.CONFIG.CHANGE"` -- `key`: 同上。 -- `value`: 同上。 -- `operator`: 执行修改操作的用户 ID。 - -### 3.4.3 Save - -服务端将聊天室房主导出配置的事件写入日志。 - -- `type`: `"SERVER.CONFIG.SAVE"` -- `time`: 同上。 - -### 3.4.4 Log - -服务端将配置修改事件写入日志。 - -- `type`: `"SERVER.CONFIG.LOG"` -- `time`: 变更时间戳(微秒)。 -- `key`: 同上。 -- `value`: 同上。 -- `operator`: 执行修改操作的用户 ID。 diff --git a/README.md b/README.md index eafaeb8..f738c47 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - 合并 Client 和 Server 为一个程序(`LTS.py`) - 目前仅提供命令行(GUI 版本即将跟进) -- 更换协议(见 `protocol.txt` 或 [`协议文档`](README-protocol-document.md)) +- 更换协议(见源代码开头) ## 快速开始 diff --git a/protocol.txt b/protocol.txt deleted file mode 100644 index a6797cd..0000000 --- a/protocol.txt +++ /dev/null @@ -1,51 +0,0 @@ -1 Gate - 1.1 Request - { type: "GATE.REQUEST", username: string } // string: non-empty - 1.2 Response - { type: "GATE.RESPONSE", result: "Accepted" | "Pending review" | "IP is banned" | "Room is full" | "Duplicate usernames" | "Username consists of banned words" } - 1.3 Review Result - { type: "GATE.REVIEW_RESULT", accepted: boolean, operator: { username: string, uid: number } } // number: non-negative integer - 1.4 Incorrect Protocol - { type: "GATE.INCORRECT_PROTOCOL", time: time, ip: ip } // time: rounded to microseconds, ip: IPv4 with port ID - 1.5 Client Request - 1.5.1 Announce - { type: "GATE.CLIENT_REQUEST.ANNOUNCE", username: string, uid: number, result: "Accepted" | "Pending review" | "IP is banned" | "Room is full" | "Duplicate usernames" | "Username consists of banned words" } - 1.5.2 Log - { type: "GATE.CLIENT_REQUEST.LOG", time: time, ip: ip, username: string, uid: number } - 1.6 Status Change - 1.6.1 Request - { type: "GATE.STATUS_CHANGE.REQUEST", status: "Rejected" | "Kicked" | "Offline" | "Pending" | "Online" | "Admin" | "Root", uid: number } - 1.6.2 Announce - { type: "GATE.STATUS_CHANGE.ANNOUNCE", status: "Rejected" | "Kicked" | "Offline" | "Pending" | "Online" | "Admin" | "Root", uid: number } - 1.6.3 Log - { type: "GATE.STATUS_CHANGE.LOG", time: time, status: "Rejected" | "Kicked" | "Offline" | "Pending" | "Online" | "Admin" | "Root", uid: number, operator: number } - - -2 Chat - 2.1 Send - { type: "CHAT.SEND", filename: string, content: string, to: number | -1 | -2 } // to = -2 for broadcasting, to = -1 for public messages, to = UID for private messages for specific users, filename = "" for messages, content = base64(file_content) for files - 2.2 Receive - { type: "CHAT.RECEIVE", from: number, order: number, filename: string, content: string, to: number | -1 | -2 } // order = 0 for messages - 2.3 Log - { type: "CHAT.LOG", time: time, from: number, order: number, filename: string, content: string, to: number | -1 | -2 } // content = "" for files - - -3 Server - 3.1 Start - { type: "SERVER.START", time: time, server_version: string, config: JSON } - 3.2 Data - { type: "SERVER.DATA", server_version: string, uid: number, config: JSON, users: [JSON, ...], chat_history: [JSON, ...] } // users: { username: string, status: "Rejected" | "Kicked" | "Offline" | "Pending" | "Online" | "Admin" | "Root" }, chat_history: { time: time, from: number, content: string, to: number | -1 | -2 }, public messages only - 3.3 Stop - 3.3.1 Announce - { type: "SERVER.STOP.ANNOUNCE" } - 3.3.2 Log - { type: "SERVER.STOP.LOG", time: time } - 3.4 Config - 3.4.1 Post - { type: "SERVER.CONFIG.POST", key: string, value: any } - 3.4.2 Change - { type: "SERVER.CONFIG.CHANGE", key: string, value: any, operator: number } - 3.4.3 Save - { type: "SERVER.CONFIG.SAVE", time: time } - 3.4.4 Log - { type: "SERVER.CONFIG.LOG", time: time, key: string, value: any, operator: number } From aea9ce3fa5a81a54ede9227adf83f719b534c266 Mon Sep 17 00:00:00 2001 From: 035966-L3 <139025318+035966-L3@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:54:40 +0800 Subject: [PATCH 3/8] v4.4.0 --- LTS.py | 287 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 172 insertions(+), 115 deletions(-) diff --git a/LTS.py b/LTS.py index f0fe29d..316390d 100644 --- a/LTS.py +++ b/LTS.py @@ -159,9 +159,9 @@ - `type`: `"CHAT.RECEIVE"` - `from`: 发送者的用户 ID。(下同) -- `order`: 文件编号(用户区分不同的文件发送请求),可能取值包括:(下同) - - `0`:普通文本消息; - - 正整数:文件编号。 +- `order`: 消息编号,可能取值包括:(下同) + - 正整数:普通文本消息; + - 负整数:文件编号。 - `filename`: 同上。 - `content`: 同上。 - `to`: 同上。 @@ -212,6 +212,7 @@ - `status`: 同上。 - `chat_history`: 历史聊天记录,每条记录包含:(不包含私聊记录和文件发送记录) - `time`: 同上; + - `order`:同上; - `from`: 同上; - `content`: 同上; - `to`: 同上。 @@ -306,7 +307,7 @@ import time # 程序版本 -VERSION = "v4.3.3-rc1" +VERSION = "v4.4.0" # 用于客户端解析协议 1.2 RESULTS = \ @@ -368,7 +369,7 @@ port 服务端端口 username 连接时使用的用户名 """ -DEFAULT_CLIENT_CONFIG = {"side": "Client", "ip": "127.0.0.1", "port": 8080, "username": "user"} +DEFAULT_CLIENT_CONFIG = {"side": "Client", "ip": "touchfish.xin", "port": 7001, "username": "user"} # 默认服务端配置(side 和 general.* 必须在启动时指定): """ @@ -425,7 +426,7 @@ # 客户端配置中的期望数据类型如下,此处没有单独编写代码: """ side 必须为 "Client" -ip 必须为合法 IPv4 +ip 不能为空串 port 必须在 [1, 65535] 中取值 username 不能为空串 """ @@ -460,6 +461,19 @@ # 指令列表 COMMAND_LIST = ['admin', 'ban', 'broadcast', 'config', 'dashboard', 'distribute', 'doorman', 'exit', 'help', 'kick', 'save', 'send', 'transfer', 'whisper'] +# 缩写表 +ABBREVIATION_TABLE = \ +{ + "D": "dashboard", "F": "distribute", "E": "exit", "H": "help", "S": "send", "T": "transfer", "P": "whisper", + "I+": "ban ip add", "I-": "ban ip remove", "W+": "ban words add", "W-": "ban words remove", + "B": "broadcast", "C": "config", "G+": "doorman accept", "G-": "doorman reject", "K": "kick", + "A+": "admin add", "A-": "admin remove", "V": "save", + "d": "dashboard", "f": "distribute", "e": "exit", "h": "help", "s": "send", "t": "transfer", "p": "whisper", + "i+": "ban ip add", "i-": "ban ip remove", "w+": "ban words add", "w-": "ban words remove", + "b": "broadcast", "c": "config", "g+": "doorman accept", "g-": "doorman reject", "k": "kick", + "a+": "admin add", "a-": "admin remove", "v": "save" +} + # help 指令显示的帮助消息(分为 8 段) HELP_HINT_CONTENT = \ [ @@ -496,31 +510,35 @@ """[1:-1], """ - dashboard 展示聊天室各项数据 - distribute 发送文件 - exit 退出或关闭聊天室 - help 显示本帮助文本 - send 发送多行消息 - send 发送单行消息 - transfer 向某个用户发送私有文件 - whisper 向某个用户发送多行私聊消息 - whisper 向某个用户发送单行私聊消息 - * ban ip add 封禁 IP 或 IP 段 - * ban ip remove 解除封禁 IP 或 IP 段 - * ban words add 屏蔽某个词语 - * ban words remove 解除屏蔽某个词语 - * broadcast 向全体用户广播多行消息 - * broadcast 向全体用户广播单行消息 - * config 修改聊天室配置项 - * doorman accept 通过某个用户的加入申请 - * doorman reject 拒绝某个用户的加入申请 - * kick 踢出某个用户 - ** admin add 添加管理员 - ** admin remove 移除管理员 - ** save 保存聊天室配置项信息 + [D] dashboard 展示聊天室各项数据 + [F] distribute 发送文件 + [E] exit 退出或关闭聊天室 + [H] help 显示本帮助文本 + [S] send 发送多行消息 + [S] send 发送单行消息 + [T] transfer 向某个用户发送私有文件 + [P] whisper 向某个用户发送多行私聊消息 + [P] whisper 向某个用户发送单行私聊消息 + [I+] * ban ip add 封禁 IP 或 IP 段 + [I-] * ban ip remove 解除封禁 IP 或 IP 段 + [W+] * ban words add 屏蔽某个词语 + [W-] * ban words remove 解除屏蔽某个词语 + [B] * broadcast 向全体用户广播多行消息 + [B] * broadcast 向全体用户广播单行消息 + [C] * config 修改聊天室配置项 + [G+] * doorman accept 通过某个用户的加入申请 + [G-] * doorman reject 拒绝某个用户的加入申请 + [K] * kick 踢出某个用户 + [A+] ** admin add 添加管理员 + [A-] ** admin remove 移除管理员 + [V] ** save 保存聊天室配置项信息 """, """ +缩略表示形式不区分大小写,其他字段区分大小写。 +支持用左边方括号内的内容缩略表示右边所有没有用尖括号括起来的字段。 +所有 字段可以输入 UID 或用户名均可,优先解析为 UID。 +解析用户名遇到冲突时采纳 UID 最小的合法解析结果。 标注 * 的指令只有状态为 Admin 或 Root 的用户可以使用。 标注 ** 的指令只有状态为 Root 的用户可以使用。 对于 dashboard 指令,状态为 Root 的用户可以看到所有用户的 IP 地址,其他用户不能。 @@ -597,7 +615,9 @@ 以下是服务端启用而客户端不启用的变量: file_order 目前服务端已经传送的文件个数, - 用于从 1 开始分配文件 ID,区分文件 + 用于从 -1 开始分配文件 ID (-1, -2, -3, ...),区分文件 +message_order 目前服务端已经传送的消息个数, + 用于从 1 开始分配消息 ID (1, 2, 3, ...),区分文件 server_socket 服务端向客户端暴露用于连接的 TCP socket history 用于记录聊天上下文,在新客户端建立连接时 通过协议 3.2 发送给客户端 @@ -628,6 +648,7 @@ my_username = "user" my_uid = 0 file_order = 0 +message_order = 0 my_socket = None users = [] server_socket = socket.socket() @@ -718,6 +739,20 @@ def printc(verbose, text): if verbose: print(dye(text, "black")) +# 解析用户名 +def parse_username(arg, expected_status): + try: + uid = int(arg.split()[0]) + if uid >= 0 and uid < len(users) and users[uid]['status'] in expected_status: + return arg + raise + except: + for i in range(len(users)): + if users[i]['status'] in expected_status: + if arg.startswith(users[i]['username'] + " ") or arg == users[i]['username']: + return str(i) + arg[len(users[i]['username']):] + return "" + # 检查 element 是不是合法 IP def check_ip(element): pattern = r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$' # int.int.int.int @@ -870,7 +905,7 @@ def process(message): deletions = [item for item in config[message['key'].split('.')[0]][message['key'].split('.')[1]] if not item in message['value']] prints("该配置项相比修改前增加了:{}".format(str(additions)), "cyan") prints("该配置项相比修改前移除了:{}".format(str(deletions)), "cyan") - config[message['key'].split('.')[0]][message['key'].split('.')[1]] = message['value'] + config[message['key'].split('.')[0]][message['key'].split('.')[1]] = message['value'] return if message['type'] == "SERVER.STOP.ANNOUNCE": # 服务端关闭 (协议 3.3.1) if side == "Client": # 同上 @@ -966,22 +1001,15 @@ def do_doorman(arg, verbose=True, by=-1): if len(arg) != 2: printc(verbose, "参数错误:应当给出恰好 2 个参数。") return + arg[1] = parse_username(arg[1], ["Pending"]) try: arg[1] = int(arg[1]) except: - printc(verbose, "参数错误:UID 必须是整数。") + printc(verbose, "参数错误:用户解析失败,只能对状态为 Pending 的用户操作。") return if not arg[0] in ['accept', 'reject']: printc(verbose, "参数错误:第一个参数必须是 accept 和 reject 中的某一项。") return - if arg[1] <= -1 or arg[1] >= len(users): - printc(verbose, "UID 输入错误。") - return - if users[arg[1]]['status'] != "Pending": - printc(verbose, "只能对状态为 Pending 的用户操作。") - if users[arg[1]]['status'] in ["Online", "Admin", "Root"] and arg[0] == "reject": - printc(verbose, "您似乎想要踢出该用户,请使用以下指令:kick {}".format(arg)) - return if arg[0] == "accept": if side == "Server": @@ -1028,21 +1056,14 @@ def do_kick(arg, verbose=True, by=-1): if not arg: printc(verbose, "参数错误:应当给出恰好 1 个参数。") return + arg = parse_username(arg, ["Online", "Admin"]) try: arg = int(arg) except: - printc(verbose, "参数错误:UID 必须是整数。") - return - if arg <= -1 or arg >= len(users): - printc(verbose, "UID 输入错误。") - return - if not users[arg]['status'] in ["Online", "Admin"]: - printc(verbose, "只能对状态为 Online 或 Admin 的用户操作。") - if users[arg]['status'] == "Pending": - printc(verbose, "您似乎想要拒绝该用户的加入申请,请使用以下指令:doorman reject {}".format(arg)) + printc(verbose, "参数错误:用户解析失败,只能对状态为 Online 或 Admin 的用户操作。") return if users[by]['status'] == "Admin" and users[arg]['status'] == "Admin": - printc(verbose, "状态为 Admin 的用户只能对状态为 Online 的用户操作。") + printc(verbose, "参数错误:用户解析失败,状态为 Admin 的用户只能对状态为 Online 的用户操作。") return if side == "Server": @@ -1079,18 +1100,19 @@ def do_admin(arg, verbose=True, by=-1): if not arg[0] in ['add', 'remove']: printc(verbose, "参数错误:第一个参数必须是 add 或 remove。") return + arg[1] = parse_username(arg[1], ["Online", "Admin"]) try: arg[1] = int(arg[1]) except: - printc(verbose, "参数错误:UID 必须是整数。") - return - if arg[1] <= 0 or arg[1] >= len(users): - printc(verbose, "UID 输入错误。") + if arg[0] == 'add': + printc(verbose, "参数错误:用户解析失败,只能对状态为 Online 的用户操作。") + if arg[0] == 'remove': + printc(verbose, "参数错误:用户解析失败,只能对状态为 Admin 的用户操作。") return if arg[0] == 'add': if users[arg[1]]['status'] != "Online": - printc(verbose, "只能对状态为 Online 的用户操作。") + printc(verbose, "参数错误:用户解析失败,只能对状态为 Online 的用户操作。") return users[arg[1]]['status'] = "Admin" log_queue.put(json.dumps({'type': 'GATE.STATUS_CHANGE.LOG', 'time': time_str(), 'status': 'Admin', 'uid': arg[1], 'operator': by})) # 协议 1.6.3 @@ -1100,7 +1122,7 @@ def do_admin(arg, verbose=True, by=-1): if arg[0] == 'remove': if users[arg[1]]['status'] != "Admin": - printc(verbose, "只能对状态为 Admin 的用户操作。") + printc(verbose, "参数错误:用户解析失败,只能对状态为 Admin 的用户操作。") return users[arg[1]]['status'] = "Online" log_queue.put(json.dumps({'type': 'GATE.STATUS_CHANGE.LOG', 'time': time_str(), 'status': 'Online', 'uid': arg[1], 'operator': by})) # 协议 1.6.3 @@ -1276,7 +1298,7 @@ def do_ban(arg, verbose=True, by=-1): if users[i]['status'] in ["Online", "Admin", "Root"]: send_queue.put(json.dumps({'to': i, 'content': {'type': 'SERVER.CONFIG.CHANGE', 'key': 'ban.words', 'value': config['ban']['words'], 'operator': by}})) # 协议 3.4.2 if side == "Client": - new_value = config['ban']['words'] + new_value = config['ban']['words'][:] new_value.append(arg[2]) my_socket.send(bytes(json.dumps({'type': 'SERVER.CONFIG.POST', 'key': 'ban.words', 'value': new_value}) + "\n", encoding="utf-8")) # 协议 3.4.1 printc(verbose, "操作成功。") @@ -1292,7 +1314,7 @@ def do_ban(arg, verbose=True, by=-1): if users[i]['status'] in ["Online", "Admin", "Root"]: send_queue.put(json.dumps({'to': i, 'content': {'type': 'SERVER.CONFIG.CHANGE', 'key': 'ban.words', 'value': config['ban']['words'], 'operator': by}})) # 协议 3.4.2 if side == "Client": - new_value = config['ban']['words'] + new_value = config['ban']['words'][:] new_value.remove(arg[2]) my_socket.send(bytes(json.dumps({'type': 'SERVER.CONFIG.POST', 'key': 'ban.words', 'value': new_value}) + "\n", encoding="utf-8")) # 协议 3.4.1 printc(verbose, "操作成功。") @@ -1302,6 +1324,7 @@ def do_broadcast(arg, message=None, verbose=True, by=-1): global log_queue global send_queue global my_socket + global message_order if by == -1: by = my_uid if not users[by]['status'] in ["Admin", "Root"]: @@ -1316,11 +1339,12 @@ def do_broadcast(arg, message=None, verbose=True, by=-1): message = enter() if side == "Server": - log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': 0, 'filename': "", 'content': message, 'to': -2})) # 协议 2.3 - history.append({'time': time_str(), 'from': by, 'content': message, 'to': -2}) # 公开消息,记入 history 列表 + message_order += 1 # 给该消息分配一个新的编号,从 1 开始递增(下同) + log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': message_order, 'filename': "", 'content': message, 'to': -2})) # 协议 2.3 + history.append({'time': time_str(), 'from': by, 'content': message, 'to': -2, 'order': message_order}) # 公开消息,记入 history 列表 for i in range(len(users)): if users[i]['status'] in ["Online", "Admin", "Root"]: - send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': 0, 'filename': "", 'content': message, 'to': -2}})) # 协议 2.2 + send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': message_order, 'filename': "", 'content': message, 'to': -2}})) # 协议 2.2 if side == "Client": my_socket.send(bytes(json.dumps({'type': 'CHAT.SEND', 'filename': "", 'content': message, 'to': -2}) + "\n", encoding="utf-8")) # 协议 2.1 @@ -1331,6 +1355,7 @@ def do_send(arg, message=None, verbose=True, by=-1): global log_queue global send_queue global my_socket + global message_order if by == -1: by = my_uid if message == None: # 同上,识别调用方法 @@ -1350,11 +1375,12 @@ def do_send(arg, message=None, verbose=True, by=-1): return if side == "Server": - log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': 0, 'filename': "", 'content': message, 'to': -1})) # 协议 2.3 - history.append({'time': time_str(), 'from': by, 'content': message, 'to': -1}) # 公开消息,记入 history 列表 + message_order += 1 # 同上,给该消息分配一个新的编号 + log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': message_order, 'filename': "", 'content': message, 'to': -1})) # 协议 2.3 + history.append({'time': time_str(), 'from': by, 'content': message, 'to': -1, 'order': message_order}) # 公开消息,记入 history 列表 for i in range(len(users)): if users[i]['status'] in ["Online", "Admin", "Root"]: - send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': 0, 'filename': "", 'content': message, 'to': -1}})) # 协议 2.2 + send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': message_order, 'filename': "", 'content': message, 'to': -1}})) # 协议 2.2 if side == "Client": my_socket.send(bytes(json.dumps({'type': 'CHAT.SEND', 'filename': "", 'content': message, 'to': -1}) + "\n", encoding="utf-8")) # 协议 2.1 @@ -1364,11 +1390,13 @@ def do_whisper(arg, message=None, verbose=True, by=-1): global log_queue global send_queue global my_socket + global message_order if by == -1: by = my_uid if not config['message']['allow_private']: printc(verbose, "此聊天室目前不允许发送私聊消息。") return + arg = parse_username(arg, ["Online", "Admin", "Root"]) # 分离接收方 UID 和(可能不存在的)单行消息 try: arg, message = arg.split(' ', 1) @@ -1376,13 +1404,8 @@ def do_whisper(arg, message=None, verbose=True, by=-1): pass try: arg = int(arg) - if arg <= -1 or arg >= len(users): - raise except: - printc(verbose, "UID 输入错误。") - return - if not users[arg]['status'] in ["Online", "Admin", "Root"]: - printc(verbose, "只能向状态处于 Online、Admin、Root 中的某一项的用户发送私聊消息。") + printc(verbose, "参数错误:用户解析失败,只能对状态处于 Online、Admin、Root 中的某一项的用户操作。") return if arg == by: printc(verbose, "不能向自己发送私聊消息。") @@ -1403,12 +1426,13 @@ def do_whisper(arg, message=None, verbose=True, by=-1): if side == "Server": # 非公开消息,不记入 history 列表, # 于是这里没有了 history.append 语句 - log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': 0, 'filename': "", 'content': message, 'to': arg})) # 协议 2.3 + message_order += 1 # 同上,给该消息分配一个新的编号 + log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': message_order, 'filename': "", 'content': message, 'to': arg})) # 协议 2.3 for i in range(len(users)): # 私聊消息只对收发方,状态为 Admin 的用户和 # 状态为 Root 的用户可见 if users[i]['status'] in ["Admin", "Root"] or i == by or i == arg: - send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': 0, 'filename': "", 'content': message, 'to': arg}})) # 协议 2.2 + send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': message_order, 'filename': "", 'content': message, 'to': arg}})) # 协议 2.2 if side == "Client": my_socket.send(bytes(json.dumps({'type': 'CHAT.SEND', 'filename': "", 'content': message, 'to': arg}) + "\n", encoding="utf-8")) # 协议 2.1 @@ -1448,7 +1472,7 @@ def do_distribute(arg, message=None, verbose=True, by=-1): return if side == "Server": - file_order += 1 # 给该文件分配一个新的编号,从 1 开始(下同) + file_order -= 1 # 给该文件分配一个新的编号,从 -1 开始递减(下同) # 服务端在此处接收文件(下同); # 先写入到磁盘(相当于写入日志,尽快释放内存,且减小意外断电的情况下的损失), # 然后在第四部分的 thread_send 线程中重新读取并发送 @@ -1501,37 +1525,37 @@ def do_transfer(arg, message=None, verbose=True, by=-1): global file_order if by == -1: by = my_uid - arg = arg.split(' ', 1) - if len(arg) != 2: - printc(verbose, "参数错误:应当给出恰好 2 个参数。") - return if not config['file']['allow_any']: printc(verbose, "此聊天室目前不允许发送文件。") return if not config['file']['allow_private']: printc(verbose, "此聊天室目前不允许发送私有文件。") return + arg = parse_username(arg, ["Online", "Admin", "Root"]) + # 分离接收方 UID 和(可能不存在的)文件名 try: - arg[0] = int(arg[0]) - if arg[0] <= -1 or arg[0] >= len(users): - raise + arg, filename = arg.split(' ', 1) except: - printc(verbose, "UID 输入错误。") + pass + try: + arg = int(arg) + except: + printc(verbose, "参数错误:用户解析失败,只能对状态处于 Online、Admin、Root 中的某一项的用户操作。") return - if not users[arg[0]]['status'] in ["Online", "Admin", "Root"]: + if not users[arg]['status'] in ["Online", "Admin", "Root"]: printc(verbose, "只能向状态处于 Online、Admin、Root 中的某一项的用户发送私有文件。") return - if arg[0] == by: + if arg == by: printc(verbose, "不能向自己发送私有文件。") return for word in config['ban']['words']: - if word in arg[1]: + if word in filename: printc(verbose, "发送失败:文件名中包含屏蔽词:" + word) return if not message: # 同上,跳过重复的加密操作 try: # 同上,读取文件并转换 - with open(arg[1], 'rb') as f: + with open(filename, 'rb') as f: file_data = f.read() message = base64.b64encode(file_data).decode('utf-8') except: @@ -1542,7 +1566,7 @@ def do_transfer(arg, message=None, verbose=True, by=-1): return if side == "Server": - file_order += 1 # 同上,给该文件分配一个新的编号 + file_order -= 1 # 同上,给该文件分配一个新的编号 # 同上,服务端在此处接收文件 try: # 同上,不同系统的目录格式不同 @@ -1559,16 +1583,16 @@ def do_transfer(arg, message=None, verbose=True, by=-1): tmp_filename = "TouchFishFiles\\{}.file".format(file_order) else: tmp_filename = "TouchFishFiles/{}.file".format(file_order) - log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': file_order, 'filename': arg[1], 'content': "", 'to': arg[0]})) # 协议 2.3 + log_queue.put(json.dumps({'type': 'CHAT.LOG', 'time': time_str(), 'from': by, 'order': file_order, 'filename': filename, 'content': "", 'to': arg})) # 协议 2.3 for i in range(len(users)): # 同上,先以保存文件时使用的文件名填充 content 字段; # 私有文件只对收发方,状态为 Admin 的用户和 # 状态为 Root 的用户可见 - if users[i]['status'] in ["Admin", "Root"] or i == by or i == arg[0]: - send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': file_order, 'filename': arg[1], 'content': tmp_filename, 'to': arg[0]}})) # 协议 2.2 + if users[i]['status'] in ["Admin", "Root"] or i == by or i == arg: + send_queue.put(json.dumps({'to': i, 'content': {'type': 'CHAT.RECEIVE', 'from': by, 'order': file_order, 'filename': filename, 'content': tmp_filename, 'to': arg}})) # 协议 2.2 if side == "Client": # 协议 2.1 - token = json.dumps({'type': 'CHAT.SEND', 'filename': arg[1], 'content': message, 'to': arg[0]}) + "\n" + token = json.dumps({'type': 'CHAT.SEND', 'filename': filename, 'content': message, 'to': arg}) + "\n" chunks = [token[i:i+32768] for i in range(0, len(token), 32768)] # 同上,分段发送数据 for chunk in chunks: @@ -1612,7 +1636,6 @@ def do_dashboard(arg=None): def do_save(arg=None): global log_queue if users[my_uid]['status'] != "Root": - print(users[my_uid]['status']) print("只有处于 Root 状态的用户有权执行该操作。") return try: @@ -1680,7 +1703,6 @@ def thread_gate(): time.sleep(0.1) if EXIT_FLAG: return - break # 尝试开启新连接 conntmp, addresstmp = None, None @@ -1730,7 +1752,7 @@ def thread_gate(): if online_count == config['general']['max_connections']: result = "Room is full" for user in users[:-1]: - if user['status'] in ["Online", "Admin", "Root"] and users[uid]['username'] == user['username']: + if user['status'] in ["Online", "Admin", "Root", "Pending"] and users[uid]['username'] == user['username']: result = "Duplicate usernames" for word in config['ban']['words']: if word in users[uid]['username']: @@ -1773,7 +1795,7 @@ def thread_process(): time.sleep(0.1) if EXIT_FLAG: return - break + while not receive_queue.empty(): message = json.loads(receive_queue.get()) sender, content = message['from'], message['content'] @@ -1807,7 +1829,7 @@ def thread_receive(): time.sleep(0.1) if EXIT_FLAG: return - break + for i in range(len(users)): if users[i]['status'] in ["Online", "Admin", "Root"]: data = "" @@ -1844,7 +1866,7 @@ def thread_send(): time.sleep(0.1) if EXIT_FLAG: return - break + while not send_queue.empty(): message = json.loads(send_queue.get()) if not users[message['to']]['status'] in ["Online", "Admin", "Root"]: @@ -1904,7 +1926,6 @@ def thread_log(): # 确保程序终止时没有日志残留在 log_queue 中 if EXIT_FLAG: return - break def thread_check(): global online_count @@ -1915,7 +1936,7 @@ def thread_check(): time.sleep(1) # 该部分对整体性能影响较大,因此执行频率下调至 1 秒一次 if EXIT_FLAG: return - break + down = [] # 先完成全部下线用户检测工作再一并广播, # 避免将状态变更通知(不必要地)发送给 @@ -1941,8 +1962,9 @@ def thread_input(): while True: time.sleep(0.1) if EXIT_FLAG: + print("\033[0m", end="") return - break + # 输出模式 try: input() @@ -1955,6 +1977,10 @@ def thread_input(): print("\033[8;30m", end="") blocked = False continue + for i in list(ABBREVIATION_TABLE.keys()): + if command.startswith(i + " ") or command == i: + command = ABBREVIATION_TABLE[i] + command[len(i):] + break command = command.split(' ', 1) if len(command) == 1: command = [command[0], ""] @@ -1974,8 +2000,9 @@ def thread_output(): while True: time.sleep(0.1) if EXIT_FLAG: + print("\033[0m", end="") return - break + read() message = get_message() flush() @@ -1999,6 +2026,7 @@ def main(): global my_username global my_uid global file_order + global message_order global my_socket global users global server_socket @@ -2012,8 +2040,6 @@ def main(): global receive_queue global send_queue global print_queue - - can_read_config = True # 尝试读取配置文件 (config.json), # 检查规则详见第一部分的相关注释; @@ -2069,22 +2095,30 @@ def main(): raise if not tmp_config['username']: raise - if not check_ip(tmp_config['ip']): + if not tmp_config['ip']: raise config = tmp_config - prints("配置文件 config.json 读取成功!", "yellow") + config_read_result = "OK" except FileNotFoundError: - prints("未找到配置文件 config.json。如果该文件存在,请尝试以管理员权限重新运行。", "yellow") - prints("下面将使用默认服务端配置启动程序。", "yellow") config = DEFAULT_SERVER_CONFIG + config_read_result = "Not found" except: - prints("配置文件 config.json 中的配置项存在错误。", "yellow") - prints("下面将使用默认服务端配置启动程序。", "yellow") config = DEFAULT_SERVER_CONFIG - can_read_config = False + config_read_result = "Broken" os.system('') # 对 Windows 尝试开启 ANSI 转义字符(带颜色文本)支持 clear_screen() + + if config_read_result == "OK": + prints("配置文件 config.json 读取成功!", "yellow") + if config_read_result == "Not found": + prints("未找到配置文件 config.json。如果该文件存在,请尝试以管理员权限重新运行。", "yellow") + prints("下面将使用默认服务端配置启动程序。", "yellow") + if config_read_result == "Broken": + prints("配置文件 config.json 中的配置项存在错误。", "yellow") + prints("下面将使用默认服务端配置启动程序。", "yellow") + print() + if platform.system() == "Windows": shortcut = 'C' else: @@ -2184,34 +2218,58 @@ def main(): sys.exit(1) try: - # 启动服务端 socket + # 启动服务端 socket: + # 每两步操作之间间隔 0.01 秒, + # 防止爆出 BlockingIOError server_socket = socket.socket() + time.sleep(0.01) server_socket.bind((config['general']['server_ip'], config['general']['server_port'])) + time.sleep(0.01) server_socket.listen(config['general']['max_connections']) + time.sleep(0.01) server_socket.setblocking(False) + time.sleep(0.01) users = [{"body": None, "buffer": "", "ip": None, "username": config['general']['server_username'], "status": "Root", "busy": False}] # 初始化用户列表 + time.sleep(0.01) root_socket = socket.socket() # 为服务端创建一个连接用于接收信息(不用于发送请求) + time.sleep(0.01) root_socket.connect((config['general']['server_ip'], config['general']['server_port'])) # 连接到服务端 socket + time.sleep(0.01) # 同上,调整为非阻塞模式,缓冲区大小设置为 1 MiB,改善性能 root_socket.setblocking(False) + time.sleep(0.01) root_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) + time.sleep(0.01) root_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) + time.sleep(0.01) users[0]['body'], users[0]['ip'] = server_socket.accept() # 完成连接 + time.sleep(0.01) # 同上,设置 TCP 保活参数:启用功能,5 分钟后开始探测,间隔 30 秒 if platform.system() != "Windows": users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + time.sleep(0.01) users[0]['body'].setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 300) + time.sleep(0.01) users[0]['body'].setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) + time.sleep(0.01) else: users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + time.sleep(0.01) users[0]['body'].ioctl(socket.SIO_KEEPALIVE_VALS, (1, 300000, 30000)) + time.sleep(0.01) users[0]['body'].setblocking(False) + time.sleep(0.01) users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) + time.sleep(0.01) users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) + time.sleep(0.01) my_uid = 0 + time.sleep(0.01) my_socket = root_socket + time.sleep(0.01) except Exception as e: - prints("启动时遇到错误:无法在给定的地址上启动 socket,请检查 IP 地址或更换端口。\n详细信息:" + str(e), "red") + prints("启动时遇到错误:" + str(e), "red") + prints("请检查 IP 地址或更换端口。", "red") input("\033[0m") sys.exit(1) @@ -2260,16 +2318,15 @@ def main(): # 则覆写为默认客户端配置 if config['side'] == "Server": config = DEFAULT_CLIENT_CONFIG + config['username'] += time_str()[20:26] + # 截取 "xxxx-xx-xx xx:xx:xx.xxxxxx" 中最后的 "xxxxxx" + # 当作随机的用户名后缀,形成形如 "user123456" 的用户名 tmp_ip = None if not auto_start: tmp_ip = input("\033[0m\033[1;37m服务端 IP [{}]:".format(config['ip'])) if not tmp_ip: tmp_ip = config['ip'] config['ip'] = tmp_ip - if not check_ip(tmp_ip): - prints("参数错误:输入的服务端 IP 不是有效的点分十进制格式 IPv4 地址。", "red") - input("\033[0m") - sys.exit(1) tmp_port = None if not auto_start: tmp_port = input("\033[0m\033[1;37m端口 [{}]:".format(config['port'])) @@ -2315,7 +2372,7 @@ def main(): my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) my_socket.send(bytes(json.dumps({'type': 'GATE.REQUEST', 'username': my_username}), encoding="utf-8")) # 协议 1.1 except Exception as e: - prints("连接失败:{}".format(e), "red") + prints("启动时遇到错误:{}".format(e), "red") input("\033[0m") sys.exit(1) From d7363bdc34cf8e1d4a5ea08f69888431a52aba54 Mon Sep 17 00:00:00 2001 From: 035966-L3 <139025318+035966-L3@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:16:20 +0800 Subject: [PATCH 4/8] v4.5.0 --- LTS.py | 852 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 483 insertions(+), 369 deletions(-) diff --git a/LTS.py b/LTS.py index 316390d..6e44689 100644 --- a/LTS.py +++ b/LTS.py @@ -153,7 +153,7 @@ ## 2.2 Receive -`{ type: "CHAT.RECEIVE", from: number, order: number, filename: string, content: string, to: number | -1 | -2 }` +`{ type: "CHAT.RECEIVE", from: number, order: signed number, filename: string, content: string, to: number | -1 | -2 }` 服务端将消息转发给目标客户端。 @@ -168,7 +168,7 @@ ## 2.3 Log -`{ type: "CHAT.LOG", time: time, from: number, order: number, filename: string, content: string, to: number | -1 | -2 }` +`{ type: "CHAT.LOG", time: time, from: number, order: signed number, filename: string, content: string, to: number | -1 | -2 }` 服务端将收到的聊天记录写入日志。 @@ -307,7 +307,7 @@ import time # 程序版本 -VERSION = "v4.4.0" +VERSION = "v4.5.0" # 用于客户端解析协议 1.2 RESULTS = \ @@ -348,7 +348,9 @@ - help 指令显示的帮助消息中的其余段落 - 程序关闭时的「再见!」文本 -(注:洋红色 (magenta) 目前没有使用过) +特别说明: +- shell 指令的输出文本颜色为系统默认颜色 +- 洋红色 (magenta) 目前没有使用过 """ COLORS = \ { @@ -369,6 +371,9 @@ port 服务端端口 username 连接时使用的用户名 """ +# 需要指出的是,第五部分中会给 username 字段 +# 的默认值后面加上一个随机六位数作为后缀, +# 形成形如 "user123456" 的用户名 DEFAULT_CLIENT_CONFIG = {"side": "Client", "ip": "touchfish.xin", "port": 7001, "username": "user"} # 默认服务端配置(side 和 general.* 必须在启动时指定): @@ -459,21 +464,36 @@ """[1:-1] # 指令列表 -COMMAND_LIST = ['admin', 'ban', 'broadcast', 'config', 'dashboard', 'distribute', 'doorman', 'exit', 'help', 'kick', 'save', 'send', 'transfer', 'whisper'] +COMMAND_LIST = ['admin', 'ban', 'broadcast', 'config', 'dashboard', 'distribute', 'doorman', 'evaluate', 'exit', 'flood', 'help', 'kick', 'save', 'send', 'shell', 'transfer', 'whisper'] # 缩写表 ABBREVIATION_TABLE = \ { - "D": "dashboard", "F": "distribute", "E": "exit", "H": "help", "S": "send", "T": "transfer", "P": "whisper", + "D": "dashboard", "F": "distribute", "Q": "evaluate", "E": "exit", "L": "flood", + "H": "help", "S": "send", "J": "shell", "T": "transfer", "P": "whisper", "I+": "ban ip add", "I-": "ban ip remove", "W+": "ban words add", "W-": "ban words remove", "B": "broadcast", "C": "config", "G+": "doorman accept", "G-": "doorman reject", "K": "kick", "A+": "admin add", "A-": "admin remove", "V": "save", - "d": "dashboard", "f": "distribute", "e": "exit", "h": "help", "s": "send", "t": "transfer", "p": "whisper", + "d": "dashboard", "f": "distribute", "q": "evaluate", "e": "exit", "l": "flood", + "h": "help", "s": "send", "j": "shell", "t": "transfer", "p": "whisper", "i+": "ban ip add", "i-": "ban ip remove", "w+": "ban words add", "w-": "ban words remove", "b": "broadcast", "c": "config", "g+": "doorman accept", "g-": "doorman reject", "k": "kick", "a+": "admin add", "a-": "admin remove", "v": "save" } +# flood 指令开启的简易命令行模式的进入提示 +SIMPLE_COMMAND_LINE_HINT_CONTENT = \ +""" +您已经进入简易命令行模式。 +在简易命令行模式中,您只需要执行以下三个步骤,即可完成单行公开消息的发送: + 1. 按下 Enter 进入输入模式 + 2. 直接输入想要发送的单行消息(不需要显式执行 send 指令) + 3. 再按下 Enter 返回输出模式 +本模式下发送结果不会进行显式反馈,而是根据下面的特性间接反馈: +发送成功的消息能够在输出模式中看到(带有响铃),发送失败的消息则不会。 +在任何模式下按下 Ctrl + {} 以退出简易命令行模式。 +"""[1:-1] + # help 指令显示的帮助消息(分为 8 段) HELP_HINT_CONTENT = \ [ @@ -506,16 +526,19 @@ """[1:-1], """ -聊天室内可用的指令分为以下 14 条 22 项: +聊天室内可用的指令分为以下 17 条 25 项: """[1:-1], """ [D] dashboard 展示聊天室各项数据 [F] distribute 发送文件 + [Q] evaluate 像 Python IDLE 那样计算输入数据 [E] exit 退出或关闭聊天室 + [L] flood 开启简易命令行模式 [H] help 显示本帮助文本 [S] send 发送多行消息 [S] send 发送单行消息 + [J] shell 执行 Shell 指令 [T] transfer 向某个用户发送私有文件 [P] whisper 向某个用户发送多行私聊消息 [P] whisper 向某个用户发送单行私聊消息 @@ -539,9 +562,13 @@ 支持用左边方括号内的内容缩略表示右边所有没有用尖括号括起来的字段。 所有 字段可以输入 UID 或用户名均可,优先解析为 UID。 解析用户名遇到冲突时采纳 UID 最小的合法解析结果。 +简易命令行模式允许您直接输入并发送单行消息而省略 send,但会禁用其他指令。 标注 * 的指令只有状态为 Admin 或 Root 的用户可以使用。 标注 ** 的指令只有状态为 Root 的用户可以使用。 对于 dashboard 指令,状态为 Root 的用户可以看到所有用户的 IP 地址,其他用户不能。 +对于 evaluate 指令,该指令直接使用 eval() 函数实现,其中二进制发行版的 Python 版本为 3.6。 +对于 evaluate 指令,请不要注入恶意代码(典型的有 globals(), locals() 等),否则后果自负。 +对于 shell 指令,请不要试图执行危害本程序(或您的设备)的指令(此处从略),否则后果自负。 对于 ban ip 指令,支持输入形如 a.b.c.d/e 的 IP 段,但前缀长度 (e 值) 不得小于 24。 对于 config 指令, 的格式以 dashboard 指令输出的参数名称为准。 对于 config 指令, 的格式以 dashboard 指令输出的修改示例为准。 @@ -551,7 +578,7 @@ """[1:-1], """ -你可以在 TouchFish 的官方 Github 仓库页面获取更多联机帮助: +您可以在 TouchFish 的官方 Github 仓库页面获取更多联机帮助: https://github.com/2044-space-elevator/TouchFish """[1:-1] ] @@ -594,6 +621,8 @@ 以下是在服务端和客户端都启用的变量: config 服务端参数(对于客户端,启动前存储客户端参数, 启动后存储服务端参数) +flooded True 表示通过 flood 指令开启的「简易命令行模式」, + False 表示「简易命令行模式」 blocked True 表示 HELP_HINT 第 1 段提到的「输入模式」, False 表示「输出模式」 my_username 自身连接的用户名 @@ -645,6 +674,7 @@ """ config = DEFAULT_SERVER_CONFIG blocked = False +flooded = False my_username = "user" my_uid = 0 file_order = 0 @@ -725,8 +755,9 @@ def prints(text, color_code=None): else: print_queue.put(dye(text, color_code)) -# 不受 blocked 变量控制的强制文本输出(只用于 -# dashboard 指令和 help 命令输出信息) +# 不受 blocked 变量控制的强制文本输出: +# 只用于 dashboard 指令、flood 指令(部分) +# 和 help 指令输出信息 def printf(text, color_code=None): print(dye(text, color_code)) @@ -956,7 +987,7 @@ def get_message(): # 对于用户直接调用的指令,参数传递规则如下(某些指令只出现部分参数): """ -arg 指令参数:紧跟命令后的全部文本, +arg 指令参数:紧跟指令后的全部文本, 如输入 "admin add 1" 则传入 "add 1" message 消息:固定为 None(缺省值) verbose 是否为直接调用的指令:固定为 True(缺省值) @@ -982,7 +1013,7 @@ def get_message(): # 而不是在客户端判定指令执行成功并向服务端发送请求时就修改; # 因此服务端广播任何消息时都不应该将请求发送者排除在广播对象之外 -# 对于完全不需要参数的命令 (dashboard, exit, help), +# 对于完全不需要参数的指令 (dashboard, exit, help), # 服务端不会重新调用函数(因为根本没有请求), # 参数中只有一个 arg (缺省为 None,函数中不会调用), # 用于在第四部分的 thread_input 线程中统一调用接口 @@ -1159,7 +1190,7 @@ def do_config(arg, verbose=True, by=-1): printc(verbose, r' config gate.enter_hint "Hi there!\n"') if not input("\033[0m\033[1;30m确定要继续吗?[y/N] ") in ['y', 'Y']: return - print("\033[8;30m", end="") + print("\033[8;30m", end="", flush=True) if arg[0] == "ban.ip" or arg[0] == "ban.words": printc(verbose, "请注意,本参数修改时 需要带引号并转义。") printc(verbose, "例如,将 fuck 和 shit 设置为屏蔽词:") @@ -1167,7 +1198,7 @@ def do_config(arg, verbose=True, by=-1): printc(verbose, "该操作将【清空】原有的屏蔽词列表(或 IP 黑名单),请谨慎操作!") if not input("\033[0m\033[1;30m确定要继续吗?[y/N] ") in ['y', 'Y']: return - print("\033[8;30m", end="") + print("\033[8;30m", end="", flush=True) try: if not eval("isinstance({}, {})".format(arg[1], CONFIG_TYPE_CHECK_TABLE[arg[0]])): @@ -1285,7 +1316,7 @@ def do_ban(arg, verbose=True, by=-1): printc(verbose, "^", arg[2], "$", sep="") if not input("\033[0m\033[1;30m确定要继续吗?[y/N] ") in ['y', 'Y']: return - print("\033[8;30m", end="") + print("\033[8;30m", end="", flush=True) if arg[1] == 'add': if arg[2] in config['ban']['words']: @@ -1646,11 +1677,20 @@ def do_save(arg=None): except: print("无法将参数保存到配置文件 config.json,请稍后重试。") +def do_evaluate(arg=None): + try: + print(eval(arg)) + except Exception as e: + print("计算时遇到错误:" + str(e)) + def do_exit(arg=None): global log_queue global send_queue global EXIT_FLAG - print("\033[0m\033[1;36m再见!\033[0m") # 此处不能调用 dye 函数,原因参见第二部分 process 函数中的注释 + # 此处不能调用 dye 函数,因为需要使用 \033[0m + # 来清除 ANSI 文本序列带来的显示效果, + # 防止干扰用户后续的终端使用 + print("\033[0m\033[1;36m再见!\033[0m") if side == "Server": log_queue.put(json.dumps({'type': 'SERVER.STOP.LOG', 'time': time_str()})) # 协议 3.2.2 for i in range(len(users)): @@ -1661,12 +1701,68 @@ def do_exit(arg=None): my_socket.close() return +def do_flood(arg=None): + global flooded + global blocked + global EXIT_FLAG + flooded = True + if platform.system() == "Windows": + shortcut = 'C' + else: + shortcut = 'D' + print(SIMPLE_COMMAND_LINE_HINT_CONTENT.format(shortcut)) + print("\033[8;30m", end="", flush=True) + while True: + time.sleep(0.1) + if EXIT_FLAG: + print("\033[0m", end="", flush=True) + flooded = False + return + + # 输出模式 + try: + input() + except EOFError: + printf("已经退出了简易命令行模式。", "black") + flooded = False + return + except: + pass + + # 变更为输入模式 + blocked = True + try: + message = input("\033[0m\033[1;30m> ") + except EOFError: + print() + printf("已经退出了简易命令行模式。", "black") + flooded = False + return + except: + pass + if not message: + print("\033[8;30m", end="", flush=True) + blocked = False + continue + + # 发送消息 + do_send(message, None, False, -1) + print("\033[8;30m", end="", flush=True) + + # 变更为输出模式 + blocked = False + def do_help(arg=None): print() for hint in HELP_HINT: printf(hint['content'], hint['color']) print() +def do_shell(arg=None): + print("\033[0m", end="", flush=True) # 执行前清除现有文本效果 + os.system(arg) + print("\033[8;30m", end="", flush=True) # 执行后恢复现有文本效果 + @@ -1962,7 +2058,7 @@ def thread_input(): while True: time.sleep(0.1) if EXIT_FLAG: - print("\033[0m", end="") + print("\033[0m", end="", flush=True) return # 输出模式 @@ -1970,13 +2066,19 @@ def thread_input(): input() except: pass + # 变更为输入模式 blocked = True - command = input("\033[0m\033[1;30m> ") + try: + command = input("\033[0m\033[1;30m> ") + except: + pass if not command: - print("\033[8;30m", end="") + print("\033[8;30m", end="", flush=True) blocked = False continue + + # 将缩写形式替换为完整形式 for i in list(ABBREVIATION_TABLE.keys()): if command.startswith(i + " ") or command == i: command = ABBREVIATION_TABLE[i] + command[len(i):] @@ -1985,13 +2087,18 @@ def thread_input(): if len(command) == 1: command = [command[0], ""] if not command[0] in COMMAND_LIST: - print("指令输入错误。\n\033[8;30m", end="") + print("指令输入错误。\n\033[8;30m", end="", flush=True) blocked = False continue + # 将对应指令函数加载到 now,然后执行 now 函数 now = eval("do_{}".format(command[0])) now(command[1]) - print("\033[8;30m", end="") + time.sleep(0.1) # 同上,等待 0.1 秒以规避竞态数据问题 + while flooded: # 如果命令行被 flood 函数接管,则等待 + time.sleep(1) + print("\033[8;30m", end="", flush=True) + # 变更为输出模式 blocked = False @@ -2000,7 +2107,7 @@ def thread_output(): while True: time.sleep(0.1) if EXIT_FLAG: - print("\033[0m", end="") + print("\033[0m", end="", flush=True) return read() @@ -2022,6 +2129,7 @@ def thread_output(): def main(): global config + global flooded global blocked global my_username global my_uid @@ -2043,7 +2151,7 @@ def main(): # 尝试读取配置文件 (config.json), # 检查规则详见第一部分的相关注释; - # 检查不通过则加载默认服务端配置 + # 检查不通过则加载默认客户端配置 try: with open("config.json", "r", encoding="utf-8") as f: tmp_config = json.load(f) @@ -2100,10 +2208,10 @@ def main(): config = tmp_config config_read_result = "OK" except FileNotFoundError: - config = DEFAULT_SERVER_CONFIG + config = DEFAULT_CLIENT_CONFIG config_read_result = "Not found" except: - config = DEFAULT_SERVER_CONFIG + config = DEFAULT_CLIENT_CONFIG config_read_result = "Broken" os.system('') # 对 Windows 尝试开启 ANSI 转义字符(带颜色文本)支持 @@ -2113,10 +2221,10 @@ def main(): prints("配置文件 config.json 读取成功!", "yellow") if config_read_result == "Not found": prints("未找到配置文件 config.json。如果该文件存在,请尝试以管理员权限重新运行。", "yellow") - prints("下面将使用默认服务端配置启动程序。", "yellow") + prints("下面将使用默认客户端配置启动程序。", "yellow") if config_read_result == "Broken": prints("配置文件 config.json 中的配置项存在错误。", "yellow") - prints("下面将使用默认服务端配置启动程序。", "yellow") + prints("下面将使用默认客户端配置启动程序。", "yellow") print() if platform.system() == "Windows": @@ -2127,361 +2235,367 @@ def main(): prints("当前程序版本:{}".format(VERSION), "yellow") prints("按下 Ctrl + {} 以按照配置文件中的配置自动启动。".format(shortcut), "yellow") prints("按下 Enter 以指定启动配置。", "yellow") - auto_start = False - try: - input() - except BaseException as e: - auto_start = True - except: - pass - tmp_side = None - if not auto_start: - tmp_side = input("\033[0m\033[1;37m启动类型 (Server = 服务端, Client = 客户端) [{}]:".format(config['side'])) - if not tmp_side: - tmp_side = config['side'] - if not tmp_side in ["Server", "Client"]: - prints("参数错误。", "red") - input("\033[0m") - sys.exit(1) - if tmp_side == "Server": - # 当程序以服务端启动时, - # 若 config.json 中加载到的 side 参数为 "Client", - # 则覆写为默认服务端配置 - if config['side'] == "Client": - config = DEFAULT_SERVER_CONFIG - tmp_ip = None - if not auto_start: - tmp_ip = input("\033[0m\033[1;37m服务端 IP [{}]:".format(config['general']['server_ip'])) - if not tmp_ip: - tmp_ip = config['general']['server_ip'] - config['general']['server_ip'] = tmp_ip - if not check_ip(tmp_ip): - prints("参数错误:输入的服务端 IP 不是有效的点分十进制格式 IPv4 地址。", "red") - input("\033[0m") - sys.exit(1) - tmp_port = None - if not auto_start: - tmp_port = input("\033[0m\033[1;37m端口 [{}]:".format(config['general']['server_port'])) - if not tmp_port: - tmp_port = config['general']['server_port'] + try: + auto_start = False try: - tmp_port = int(tmp_port) - if tmp_port < 1 or tmp_port > 65535: - raise + input() + except BaseException as e: + auto_start = True except: - prints("参数错误:端口号应为不大于 65535 的正整数。", "red") - input("\033[0m") - sys.exit(1) - config['general']['server_port'] = tmp_port - tmp_server_username = None - if not auto_start: - tmp_server_username = input("\033[0m\033[1;37m服务端管理员的用户名 [{}]:".format(config['general']['server_username'])) - if not tmp_server_username: - tmp_server_username = config['general']['server_username'] - config['general']['server_username'] = tmp_server_username - my_username = config['general']['server_username'] - tmp_max_connections = None + pass + tmp_side = None if not auto_start: - tmp_max_connections = input("\033[0m\033[1;37m最大在线连接数 [{}]:".format(config['general']['max_connections'])) - if not tmp_max_connections: - tmp_max_connections = config['general']['max_connections'] - try: - tmp_max_connections = int(tmp_max_connections) - if tmp_max_connections < 1 or tmp_max_connections > 128: - raise - except: - prints("参数错误:最大在线连接数应为不大于 128 的正整数。", "red") - input("\033[0m") - sys.exit(1) - config['general']['max_connections'] = tmp_max_connections - - # 创建保存文件时使用的目录(下同) - if platform.system() == "Windows": - os.system('mkdir TouchFishFiles 1>nul 2>&1') - else: - os.system('mkdir TouchFishFiles 1>/dev/null 2>&1') - try: - with open("config.json", "w", encoding="utf-8") as f: - json.dump(config, f) - prints("本次连接中输入的参数已经保存到配置文件 config.json,下次连接时将自动加载。", "yellow") - except: - prints("启动时遇到错误:配置文件 config.json 写入失败。", "red") + tmp_side = input("\033[0m\033[1;37m启动类型 (Server = 服务端, Client = 客户端) [{}]:".format(config['side'])) + if not tmp_side: + tmp_side = config['side'] + if not tmp_side in ["Server", "Client"]: + prints("参数错误。", "red") input("\033[0m") sys.exit(1) - try: - with open("log.ndjson", "a", encoding="utf-8") as f: - pass - except: - prints("启动时遇到错误:无法向日志文件 log.ndjson 写入内容。", "red") - input("\033[0m") - sys.exit(1) - - try: - # 启动服务端 socket: - # 每两步操作之间间隔 0.01 秒, - # 防止爆出 BlockingIOError - server_socket = socket.socket() - time.sleep(0.01) - server_socket.bind((config['general']['server_ip'], config['general']['server_port'])) - time.sleep(0.01) - server_socket.listen(config['general']['max_connections']) - time.sleep(0.01) - server_socket.setblocking(False) - time.sleep(0.01) - users = [{"body": None, "buffer": "", "ip": None, "username": config['general']['server_username'], "status": "Root", "busy": False}] # 初始化用户列表 - time.sleep(0.01) - root_socket = socket.socket() # 为服务端创建一个连接用于接收信息(不用于发送请求) - time.sleep(0.01) - root_socket.connect((config['general']['server_ip'], config['general']['server_port'])) # 连接到服务端 socket - time.sleep(0.01) - # 同上,调整为非阻塞模式,缓冲区大小设置为 1 MiB,改善性能 - root_socket.setblocking(False) - time.sleep(0.01) - root_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) - time.sleep(0.01) - root_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) - time.sleep(0.01) - users[0]['body'], users[0]['ip'] = server_socket.accept() # 完成连接 - time.sleep(0.01) - # 同上,设置 TCP 保活参数:启用功能,5 分钟后开始探测,间隔 30 秒 - if platform.system() != "Windows": - users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + + if tmp_side == "Server": + # 当程序以服务端启动时, + # 若 config.json 中加载到的 side 参数为 "Client", + # 则覆写为默认服务端配置 + if config['side'] == "Client": + config = DEFAULT_SERVER_CONFIG + tmp_ip = None + if not auto_start: + tmp_ip = input("\033[0m\033[1;37m服务端 IP [{}]:".format(config['general']['server_ip'])) + if not tmp_ip: + tmp_ip = config['general']['server_ip'] + config['general']['server_ip'] = tmp_ip + if not check_ip(tmp_ip): + prints("参数错误:输入的服务端 IP 不是有效的点分十进制格式 IPv4 地址。", "red") + input("\033[0m") + sys.exit(1) + tmp_port = None + if not auto_start: + tmp_port = input("\033[0m\033[1;37m端口 [{}]:".format(config['general']['server_port'])) + if not tmp_port: + tmp_port = config['general']['server_port'] + try: + tmp_port = int(tmp_port) + if tmp_port < 1 or tmp_port > 65535: + raise + except: + prints("参数错误:端口号应为不大于 65535 的正整数。", "red") + input("\033[0m") + sys.exit(1) + config['general']['server_port'] = tmp_port + tmp_server_username = None + if not auto_start: + tmp_server_username = input("\033[0m\033[1;37m服务端管理员的用户名 [{}]:".format(config['general']['server_username'])) + if not tmp_server_username: + tmp_server_username = config['general']['server_username'] + config['general']['server_username'] = tmp_server_username + my_username = config['general']['server_username'] + tmp_max_connections = None + if not auto_start: + tmp_max_connections = input("\033[0m\033[1;37m最大在线连接数 [{}]:".format(config['general']['max_connections'])) + if not tmp_max_connections: + tmp_max_connections = config['general']['max_connections'] + try: + tmp_max_connections = int(tmp_max_connections) + if tmp_max_connections < 1 or tmp_max_connections > 128: + raise + except: + prints("参数错误:最大在线连接数应为不大于 128 的正整数。", "red") + input("\033[0m") + sys.exit(1) + config['general']['max_connections'] = tmp_max_connections + + # 创建保存文件时使用的目录(下同) + if platform.system() == "Windows": + os.system('mkdir TouchFishFiles 1>nul 2>&1') + else: + os.system('mkdir TouchFishFiles 1>/dev/null 2>&1') + try: + with open("config.json", "w", encoding="utf-8") as f: + json.dump(config, f) + prints("本次连接中输入的参数已经保存到配置文件 config.json,下次连接时将自动加载。", "yellow") + except: + prints("启动时遇到错误:配置文件 config.json 写入失败。", "red") + input("\033[0m") + sys.exit(1) + try: + with open("log.ndjson", "a", encoding="utf-8") as f: + pass + except: + prints("启动时遇到错误:无法向日志文件 log.ndjson 写入内容。", "red") + input("\033[0m") + sys.exit(1) + + try: + # 启动服务端 socket: + # 每两步操作之间间隔 0.01 秒, + # 防止爆出 BlockingIOError + server_socket = socket.socket() time.sleep(0.01) - users[0]['body'].setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 300) + server_socket.bind((config['general']['server_ip'], config['general']['server_port'])) time.sleep(0.01) - users[0]['body'].setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) + server_socket.listen(config['general']['max_connections']) time.sleep(0.01) - else: - users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + server_socket.setblocking(False) time.sleep(0.01) - users[0]['body'].ioctl(socket.SIO_KEEPALIVE_VALS, (1, 300000, 30000)) + users = [{"body": None, "buffer": "", "ip": None, "username": config['general']['server_username'], "status": "Root", "busy": False}] # 初始化用户列表 time.sleep(0.01) - users[0]['body'].setblocking(False) - time.sleep(0.01) - users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) - time.sleep(0.01) - users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) - time.sleep(0.01) - my_uid = 0 - time.sleep(0.01) - my_socket = root_socket - time.sleep(0.01) - except Exception as e: - prints("启动时遇到错误:" + str(e), "red") - prints("请检查 IP 地址或更换端口。", "red") - input("\033[0m") - sys.exit(1) - - with open("./log.ndjson", "a", encoding="utf-8") as file: - file.write(json.dumps({'type': 'SERVER.START', 'time': time_str(), 'server_version': VERSION, 'config': config}) + "\n") # 协议 3.1 - - side = "Server" - prints("启动成功!", "green") - # 响铃,显示帮助文本,显示聊天室各项信息,显示加入提示 - ring() - do_help() - do_dashboard() - if config['gate']['enter_hint']: - first_line = dye("[" + time_str()[11:19] + "]", "black") - first_line += dye(" [您发送的]", "blue") - first_line += " " - first_line += dye(" [加入提示]", "red") - first_line += " " - first_line += dye("@", "black") - first_line += dye(config['general']['server_username'], "yellow") - first_line += dye(":", "black") - prints(first_line) - prints(config['gate']['enter_hint'], "white") - - THREAD_GATE = threading.Thread(target=thread_gate) - THREAD_PROCESS = threading.Thread(target=thread_process) - THREAD_RECEIVE = threading.Thread(target=thread_receive) - THREAD_SEND = threading.Thread(target=thread_send) - THREAD_LOG = threading.Thread(target=thread_log) - THREAD_CHECK = threading.Thread(target=thread_check) - THREAD_INPUT = threading.Thread(target=thread_input) - THREAD_OUTPUT = threading.Thread(target=thread_output) - - THREAD_GATE.start() - THREAD_PROCESS.start() - THREAD_RECEIVE.start() - THREAD_SEND.start() - THREAD_LOG.start() - THREAD_CHECK.start() - THREAD_INPUT.start() - THREAD_OUTPUT.start() - - if tmp_side == "Client": - # 当程序以客户端启动时, - # 若 config.json 中加载到的 side 参数为 "Client", - # 则覆写为默认客户端配置 - if config['side'] == "Server": - config = DEFAULT_CLIENT_CONFIG - config['username'] += time_str()[20:26] - # 截取 "xxxx-xx-xx xx:xx:xx.xxxxxx" 中最后的 "xxxxxx" - # 当作随机的用户名后缀,形成形如 "user123456" 的用户名 - tmp_ip = None - if not auto_start: - tmp_ip = input("\033[0m\033[1;37m服务端 IP [{}]:".format(config['ip'])) - if not tmp_ip: - tmp_ip = config['ip'] - config['ip'] = tmp_ip - tmp_port = None - if not auto_start: - tmp_port = input("\033[0m\033[1;37m端口 [{}]:".format(config['port'])) - if not tmp_port: - tmp_port = config['port'] - try: - tmp_port = int(tmp_port) - if tmp_port < 1 or tmp_port > 65535: - raise - except: - prints("参数错误:端口号应为不大于 65535 的正整数。", "red") - input("\033[0m") - sys.exit(1) - config['port'] = tmp_port - tmp_username = None - if not auto_start: - tmp_username = input("\033[0m\033[1;37m用户名 [{}]:".format(config['username'])) - if not tmp_username: - tmp_username = config['username'] - config['username'] = tmp_username - my_username = config['username'] - # 同上,创建保存文件时使用的目录 - if platform.system() == "Windows": - os.system('mkdir TouchFishFiles 1>nul 2>&1') - else: - os.system('mkdir TouchFishFiles 1>/dev/null 2>&1') - try: - with open("config.json", "w", encoding="utf-8") as f: - json.dump(config, f) - prints("本次连接中输入的参数已经保存到配置文件 config.json,下次连接时将自动加载。", "yellow") - except: - prints("启动时遇到错误:配置文件 config.json 写入失败。", "red") - input("\033[0m") - sys.exit(1) - - prints("正在连接聊天室...", "yellow") - my_socket = socket.socket() - try: - my_socket.connect((config['ip'], config['port'])) # 连接到服务端 socket - # 同上,调整为非阻塞模式,缓冲区大小设置为 1 MiB,改善性能 - my_socket.setblocking(False) - my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) - my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) - my_socket.send(bytes(json.dumps({'type': 'GATE.REQUEST', 'username': my_username}), encoding="utf-8")) # 协议 1.1 - except Exception as e: - prints("启动时遇到错误:{}".format(e), "red") - input("\033[0m") - sys.exit(1) - - # 同上,设置 TCP 保活参数:启用功能,5 分钟后开始探测,间隔 30 秒 - if platform.system() == "Windows": - my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) - my_socket.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 300000, 30000)) - else: - my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) - my_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 300) - my_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) - - # 核验协议 1.2,获取加入请求结果 - try: - message = None - time.sleep(0.5) # 与服务端「错峰」0.5 秒,期望第一次验证就成功(总用时 1.5 秒) - for i in range(10): # 设置 10 秒的「窗口期」,每秒验证一次 - time.sleep(1) - try: - read() - message = get_message() - if not message: - raise - break - except: - pass - if not message: - raise - if not message['result'] in ["Accepted", "Pending review"] + list(RESULTS.keys()): - raise - except: - prints("连接失败:对方似乎不是 v4 及以上的 TouchFish 服务端。", "red") - prints("注:也有可能是对方服务器端口被防火墙拦截,请联系服务器所有者确认,或检查本地网络及防火墙设置。", "black") - input("\033[0m") - sys.exit(1) - - if not message['result'] in ["Accepted", "Pending review"]: - prints("连接失败:{}".format(RESULTS[message['result']]), "red") - input("\033[0m") - sys.exit(1) - - if message['result'] == "Accepted": - prints("连接成功!", "green") + root_socket = socket.socket() # 为服务端创建一个连接用于接收信息(不用于发送请求) + time.sleep(0.01) + root_socket.connect((config['general']['server_ip'], config['general']['server_port'])) # 连接到服务端 socket + time.sleep(0.01) + # 同上,调整为非阻塞模式,缓冲区大小设置为 1 MiB,改善性能 + root_socket.setblocking(False) + time.sleep(0.01) + root_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) + time.sleep(0.01) + root_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) + time.sleep(0.01) + users[0]['body'], users[0]['ip'] = server_socket.accept() # 完成连接 + time.sleep(0.01) + # 同上,设置 TCP 保活参数:启用功能,5 分钟后开始探测,间隔 30 秒 + if platform.system() != "Windows": + users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + time.sleep(0.01) + users[0]['body'].setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 300) + time.sleep(0.01) + users[0]['body'].setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) + time.sleep(0.01) + else: + users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + time.sleep(0.01) + users[0]['body'].ioctl(socket.SIO_KEEPALIVE_VALS, (1, 300000, 30000)) + time.sleep(0.01) + users[0]['body'].setblocking(False) + time.sleep(0.01) + users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) + time.sleep(0.01) + users[0]['body'].setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) + time.sleep(0.01) + my_uid = 0 + time.sleep(0.01) + my_socket = root_socket + time.sleep(0.01) + except Exception as e: + prints("启动时遇到错误:" + str(e), "red") + prints("请检查 IP 地址或更换端口。", "red") + input("\033[0m") + sys.exit(1) + + with open("./log.ndjson", "a", encoding="utf-8") as file: + file.write(json.dumps({'type': 'SERVER.START', 'time': time_str(), 'server_version': VERSION, 'config': config}) + "\n") # 协议 3.1 + + side = "Server" + prints("启动成功!", "green") + # 响铃,显示帮助文本,显示聊天室各项信息,显示加入提示 ring() + do_help() + do_dashboard() + if config['gate']['enter_hint']: + first_line = dye("[" + time_str()[11:19] + "]", "black") + first_line += dye(" [您发送的]", "blue") + first_line += " " + first_line += dye(" [加入提示]", "red") + first_line += " " + first_line += dye("@", "black") + first_line += dye(config['general']['server_username'], "yellow") + first_line += dye(":", "black") + prints(first_line) + prints(config['gate']['enter_hint'], "white") + + THREAD_GATE = threading.Thread(target=thread_gate) + THREAD_PROCESS = threading.Thread(target=thread_process) + THREAD_RECEIVE = threading.Thread(target=thread_receive) + THREAD_SEND = threading.Thread(target=thread_send) + THREAD_LOG = threading.Thread(target=thread_log) + THREAD_CHECK = threading.Thread(target=thread_check) + THREAD_INPUT = threading.Thread(target=thread_input) + THREAD_OUTPUT = threading.Thread(target=thread_output) + + THREAD_GATE.start() + THREAD_PROCESS.start() + THREAD_RECEIVE.start() + THREAD_SEND.start() + THREAD_LOG.start() + THREAD_CHECK.start() + THREAD_INPUT.start() + THREAD_OUTPUT.start() - if message['result'] == "Pending review": - prints("服务端需要对连接请求进行人工审核,请等待...", "white") - while True: - try: - read() - message = get_message() - if not message: - continue - # 特殊情况:聊天室服务端已经关闭 (协议 3.3.1) - if message['type'] == "SERVER.STOP.ANNOUNCE": - prints("聊天室服务端已经关闭。", "red") - prints("连接失败。", "red") - input("\033[0m") - sys.exit(1) - # 一般情况:人工审核完成 (协议 1.3) - if not message['accepted']: - prints("服务端管理员 {} (UID = {}) 拒绝了您的连接请求。".format(message['operator']['username'], message['operator']['uid']), "red") - prints("连接失败。", "red") - input("\033[0m") - sys.exit(1) - if message['accepted']: - time.sleep(1) # 等待 1 秒,确认协议 3.2 提供的完整上下文传输完成 - prints("服务端管理员 {} (UID = {}) 通过了您的连接请求。".format(message['operator']['username'], message['operator']['uid']), "green") - prints("连接成功!", "green") - ring() - break - except: - pass - - side = "Client" - # 获取服务端通过协议 3.2 提供的完整上下文; - # 此时自己应当处于 Online 状态 - read() - first_data = get_message() - server_version = first_data['server_version'] - my_uid = first_data['uid'] - config = first_data['config'] - users = first_data['users'] - # 自行计算在线人数(包括自己) - online_count = 0 - for user in users: - if user['status'] in ["Pending", "Online", "Admin", "Root"]: - online_count += 1 - - # 显示帮助文本,显示聊天室各项信息,显示加入提示 - do_help() - do_dashboard() - for i in first_data['chat_history']: - print_message(i) - if config['gate']['enter_hint']: - first_line = dye("[" + time_str()[11:19] + "]", "black") - first_line += dye(" [加入提示]", "red") - first_line += " " - first_line += dye("@", "black") - first_line += dye(config['general']['server_username'], "yellow") - first_line += dye(":", "black") - prints(first_line) - prints(config['gate']['enter_hint'], "white") - - THREAD_INPUT = threading.Thread(target=thread_input) - THREAD_OUTPUT = threading.Thread(target=thread_output) + if tmp_side == "Client": + # 当程序以客户端启动时, + # 若 config.json 中加载到的 side 参数为 "Client", + # 则覆写为默认客户端配置 + if config['side'] == "Server" or config_read_result != "OK": + config = DEFAULT_CLIENT_CONFIG + config['username'] += time_str()[20:26] + # 截取 "xxxx-xx-xx xx:xx:xx.xxxxxx" 中最后的 "xxxxxx" + # 当作随机的用户名后缀,形成形如 "user123456" 的用户名 + tmp_ip = None + if not auto_start: + tmp_ip = input("\033[0m\033[1;37m服务端 IP [{}]:".format(config['ip'])) + if not tmp_ip: + tmp_ip = config['ip'] + config['ip'] = tmp_ip + tmp_port = None + if not auto_start: + tmp_port = input("\033[0m\033[1;37m端口 [{}]:".format(config['port'])) + if not tmp_port: + tmp_port = config['port'] + try: + tmp_port = int(tmp_port) + if tmp_port < 1 or tmp_port > 65535: + raise + except: + prints("参数错误:端口号应为不大于 65535 的正整数。", "red") + input("\033[0m") + sys.exit(1) + config['port'] = tmp_port + tmp_username = None + if not auto_start: + tmp_username = input("\033[0m\033[1;37m用户名 [{}]:".format(config['username'])) + if not tmp_username: + tmp_username = config['username'] + config['username'] = tmp_username + my_username = config['username'] + # 同上,创建保存文件时使用的目录 + if platform.system() == "Windows": + os.system('mkdir TouchFishFiles 1>nul 2>&1') + else: + os.system('mkdir TouchFishFiles 1>/dev/null 2>&1') + try: + with open("config.json", "w", encoding="utf-8") as f: + json.dump(config, f) + prints("本次连接中输入的参数已经保存到配置文件 config.json,下次连接时将自动加载。", "yellow") + except: + prints("启动时遇到错误:配置文件 config.json 写入失败。", "red") + input("\033[0m") + sys.exit(1) - THREAD_INPUT.start() - THREAD_OUTPUT.start() + prints("正在连接聊天室...", "yellow") + my_socket = socket.socket() + try: + my_socket.connect((config['ip'], config['port'])) # 连接到服务端 socket + # 同上,调整为非阻塞模式,缓冲区大小设置为 1 MiB,改善性能 + my_socket.setblocking(False) + my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) + my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1048576) + my_socket.send(bytes(json.dumps({'type': 'GATE.REQUEST', 'username': my_username}), encoding="utf-8")) # 协议 1.1 + except Exception as e: + prints("启动时遇到错误:{}".format(e), "red") + input("\033[0m") + sys.exit(1) + + # 同上,设置 TCP 保活参数:启用功能,5 分钟后开始探测,间隔 30 秒 + if platform.system() == "Windows": + my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + my_socket.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 300000, 30000)) + else: + my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) + my_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 300) + my_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) + + # 核验协议 1.2,获取加入请求结果 + try: + message = None + time.sleep(0.5) # 与服务端「错峰」0.5 秒,期望第一次验证就成功(总用时 1.5 秒) + for i in range(10): # 设置 10 秒的「窗口期」,每秒验证一次 + time.sleep(1) + try: + read() + message = get_message() + if not message: + raise + break + except: + pass + if not message: + raise + if not message['result'] in ["Accepted", "Pending review"] + list(RESULTS.keys()): + raise + except: + prints("连接失败:对方似乎不是 v4 及以上的 TouchFish 服务端。", "red") + prints("(也有可能是对方服务器端口被防火墙拦截,请联系服务器所有者确认,或检查本地网络及防火墙设置。)", "red") + input("\033[0m") + sys.exit(1) + + if not message['result'] in ["Accepted", "Pending review"]: + prints("连接失败:{}".format(RESULTS[message['result']]), "red") + input("\033[0m") + sys.exit(1) + + if message['result'] == "Accepted": + prints("连接成功!", "green") + ring() + + if message['result'] == "Pending review": + prints("服务端需要对连接请求进行人工审核,请等待...", "white") + while True: + try: + read() + message = get_message() + if not message: + continue + # 特殊情况:聊天室服务端已经关闭 (协议 3.3.1) + if message['type'] == "SERVER.STOP.ANNOUNCE": + prints("聊天室服务端已经关闭。", "red") + prints("连接失败。", "red") + input("\033[0m") + sys.exit(1) + # 一般情况:人工审核完成 (协议 1.3) + if not message['accepted']: + prints("服务端管理员 {} (UID = {}) 拒绝了您的连接请求。".format(message['operator']['username'], message['operator']['uid']), "red") + prints("连接失败。", "red") + input("\033[0m") + sys.exit(1) + if message['accepted']: + time.sleep(1) # 等待 1 秒,确认协议 3.2 提供的完整上下文传输完成 + prints("服务端管理员 {} (UID = {}) 通过了您的连接请求。".format(message['operator']['username'], message['operator']['uid']), "green") + prints("连接成功!", "green") + ring() + break + except: + pass + + side = "Client" + # 获取服务端通过协议 3.2 提供的完整上下文; + # 此时自己应当处于 Online 状态 + read() + first_data = get_message() + server_version = first_data['server_version'] + my_uid = first_data['uid'] + config = first_data['config'] + users = first_data['users'] + # 自行计算在线人数(包括自己) + online_count = 0 + for user in users: + if user['status'] in ["Pending", "Online", "Admin", "Root"]: + online_count += 1 + + # 显示帮助文本,显示聊天室各项信息,显示加入提示 + do_help() + do_dashboard() + for i in first_data['chat_history']: + print_message(i) + if config['gate']['enter_hint']: + first_line = dye("[" + time_str()[11:19] + "]", "black") + first_line += dye(" [加入提示]", "red") + first_line += " " + first_line += dye("@", "black") + first_line += dye(config['general']['server_username'], "yellow") + first_line += dye(":", "black") + prints(first_line) + prints(config['gate']['enter_hint'], "white") + + THREAD_INPUT = threading.Thread(target=thread_input) + THREAD_OUTPUT = threading.Thread(target=thread_output) + + THREAD_INPUT.start() + THREAD_OUTPUT.start() + except BaseException as e: + print() + prints("程序运行时遇到错误:" + str(e), "red") + print("\033[0m") if __name__ == "__main__": main() From 4fd301326bf245c655a64476a949987b01c10027 Mon Sep 17 00:00:00 2001 From: 035966-L3 <2814139320@qq.com> Date: Sun, 28 Dec 2025 13:51:07 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=97=A0=E7=94=A8=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LTS.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/LTS.py b/LTS.py index 6e44689..9e49c82 100644 --- a/LTS.py +++ b/LTS.py @@ -621,8 +621,6 @@ 以下是在服务端和客户端都启用的变量: config 服务端参数(对于客户端,启动前存储客户端参数, 启动后存储服务端参数) -flooded True 表示通过 flood 指令开启的「简易命令行模式」, - False 表示「简易命令行模式」 blocked True 表示 HELP_HINT 第 1 段提到的「输入模式」, False 表示「输出模式」 my_username 自身连接的用户名 @@ -674,7 +672,6 @@ """ config = DEFAULT_SERVER_CONFIG blocked = False -flooded = False my_username = "user" my_uid = 0 file_order = 0 @@ -1702,10 +1699,8 @@ def do_exit(arg=None): return def do_flood(arg=None): - global flooded global blocked global EXIT_FLAG - flooded = True if platform.system() == "Windows": shortcut = 'C' else: @@ -1716,15 +1711,13 @@ def do_flood(arg=None): time.sleep(0.1) if EXIT_FLAG: print("\033[0m", end="", flush=True) - flooded = False return # 输出模式 try: input() except EOFError: - printf("已经退出了简易命令行模式。", "black") - flooded = False + printf("您已经退出简易命令行模式。", "black") return except: pass @@ -1735,8 +1728,7 @@ def do_flood(arg=None): message = input("\033[0m\033[1;30m> ") except EOFError: print() - printf("已经退出了简易命令行模式。", "black") - flooded = False + printf("您已经退出简易命令行模式。", "black") return except: pass @@ -2094,9 +2086,6 @@ def thread_input(): # 将对应指令函数加载到 now,然后执行 now 函数 now = eval("do_{}".format(command[0])) now(command[1]) - time.sleep(0.1) # 同上,等待 0.1 秒以规避竞态数据问题 - while flooded: # 如果命令行被 flood 函数接管,则等待 - time.sleep(1) print("\033[8;30m", end="", flush=True) # 变更为输出模式 @@ -2129,7 +2118,6 @@ def thread_output(): def main(): global config - global flooded global blocked global my_username global my_uid From e7a1ca6b5961a9d56626eebbbc8d2188af68886d Mon Sep 17 00:00:00 2001 From: 035966-L3 <139025318+035966-L3@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:08:51 +0800 Subject: [PATCH 6/8] Remove duplicate log.ndjson entry from .gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7db1270..71fbd6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ __pycache__ config.json -log.ndjson TouchFishFiles -log.ndjson \ No newline at end of file +log.ndjson From f6c521ea8ab95e72cb396dc16dccc3128f8a526f Mon Sep 17 00:00:00 2001 From: 035966-L3 <2814139320@qq.com> Date: Tue, 30 Dec 2025 23:07:16 +0800 Subject: [PATCH 7/8] v4.5.1 --- LTS.py | 57 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/LTS.py b/LTS.py index 9e49c82..0f9952d 100644 --- a/LTS.py +++ b/LTS.py @@ -307,7 +307,7 @@ import time # 程序版本 -VERSION = "v4.5.0" +VERSION = "v4.5.1" # 用于客户端解析协议 1.2 RESULTS = \ @@ -1041,7 +1041,8 @@ def do_doorman(arg, verbose=True, by=-1): if arg[0] == "accept": if side == "Server": - send_queue.put(json.dumps({'to': arg[1], 'content': {'type': 'GATE.REVIEW_RESULT', 'accepted': True, 'operator': {'username': users[by]['username'], 'uid': by}}})) # 协议 1.3 + # 重要:最后再给该用户发送信息,防止出现 + # 该用户已经断开连接而状态没有更新的情况 log_queue.put(json.dumps({'type': 'GATE.STATUS_CHANGE.LOG', 'time': time_str(), 'status': 'Online', 'uid': arg[1], 'operator': by})) # 协议 1.6.3 for i in range(len(users)): if users[i]['status'] in ["Online", "Admin", "Root"]: @@ -1051,6 +1052,7 @@ def do_doorman(arg, verbose=True, by=-1): users_abstract = [] for i in range(len(users)): users_abstract.append({"username": users[i]['username'], "status": users[i]['status']}) + send_queue.put(json.dumps({'to': arg[1], 'content': {'type': 'GATE.REVIEW_RESULT', 'accepted': True, 'operator': {'username': users[by]['username'], 'uid': by}}})) # 协议 1.3 send_queue.put(json.dumps({'to': arg[1], 'content': {'type': 'SERVER.DATA', 'server_version': VERSION, 'uid': arg[1], 'config': config, 'users': users_abstract, 'chat_history': history}})) # 协议 3.2 if side == "Client": my_socket.send(bytes(json.dumps({'type': 'GATE.STATUS_CHANGE.REQUEST', 'status': 'Online', 'uid': arg[1]}) + "\n", encoding="utf-8")) # 协议 1.6.1 @@ -1846,7 +1848,15 @@ def thread_gate(): if word in users[uid]['username']: result = "Username consists of banned words" - users[uid]['body'].send(bytes(json.dumps({'type': 'GATE.RESPONSE', 'result': result}) + "\n", encoding="utf-8")) # 协议 1.2 + while True: + try: + users[uid]['body'].send(bytes(json.dumps({'type': 'GATE.RESPONSE', 'result': result}) + "\n", encoding="utf-8")) # 协议 1.2 + break + except BlockingIOError: + continue + except: + break + log_queue.put(json.dumps({'type': 'GATE.CLIENT_REQUEST.LOG', 'time': time_str(), 'ip': users[uid]['ip'], 'username': users[uid]['username'], 'uid': uid, 'result': result})) # 协议 1.5.2 for i in range(len(users)): if users[i]['status'] in ["Online", "Admin", "Root"]: @@ -1871,7 +1881,7 @@ def thread_gate(): users_abstract = [] for i in range(len(users)): users_abstract.append({"username": users[i]['username'], "status": users[i]['status']}) - users[uid]['body'].send(bytes(json.dumps({'type': 'SERVER.DATA', 'server_version': VERSION, 'uid': uid, 'config': config, 'users': users_abstract, 'chat_history': history}) + "\n", encoding="utf-8")) # 协议 3.2 + send_queue.put(json.dumps({'to': uid, 'content': {'type': 'SERVER.DATA', 'server_version': VERSION, 'uid': uid, 'config': config, 'users': users_abstract, 'chat_history': history}})) # 协议 3.2 def thread_process(): global online_count @@ -1957,7 +1967,7 @@ def thread_send(): while not send_queue.empty(): message = json.loads(send_queue.get()) - if not users[message['to']]['status'] in ["Online", "Admin", "Root"]: + if not users[message['to']]['status'] in ["Online", "Admin", "Root", "Pending"]: continue # 先发送心跳数据(单个换行符)检查客户端是否下线 try: @@ -2459,8 +2469,7 @@ def main(): prints("启动时遇到错误:配置文件 config.json 写入失败。", "red") input("\033[0m") sys.exit(1) - - prints("正在连接聊天室...", "yellow") + my_socket = socket.socket() try: my_socket.connect((config['ip'], config['port'])) # 连接到服务端 socket @@ -2486,9 +2495,14 @@ def main(): # 核验协议 1.2,获取加入请求结果 try: message = None - time.sleep(0.5) # 与服务端「错峰」0.5 秒,期望第一次验证就成功(总用时 1.5 秒) - for i in range(10): # 设置 10 秒的「窗口期」,每秒验证一次 + seconds_consumed = 0 + print(dye("正在连接聊天室... (已经等待了 {} / 10 秒)\r", "yellow").format(seconds_consumed), end="", flush=True) + for i in range(10): + # 设置 10 秒的「窗口期」,每秒验证一次 + # 与服务端「错峰」0.5 秒,期望第一次验证就成功(总用时 1 秒) time.sleep(1) + seconds_consumed += 1 + print(dye("正在连接聊天室... (已经等待了 {} / 10 秒)\r", "yellow").format(seconds_consumed), end="", flush=True) try: read() message = get_message() @@ -2498,32 +2512,44 @@ def main(): except: pass if not message: + seconds_consumed += 1 raise if not message['result'] in ["Accepted", "Pending review"] + list(RESULTS.keys()): raise except: - prints("连接失败:对方似乎不是 v4 及以上的 TouchFish 服务端。", "red") - prints("(也有可能是对方服务器端口被防火墙拦截,请联系服务器所有者确认,或检查本地网络及防火墙设置。)", "red") + print() + if seconds_consumed == 11: + prints("连接失败:连接超时。", "red") + else: + prints("连接失败:对方返回的内容不符合 TouchFish v4 协议。", "red") + prints("对方似乎不是 v4 及以上的 TouchFish 服务端。", "red") + if seconds_consumed == 11: + prints("(也有可能是对方服务器端口被防火墙拦截,请联系服务器所有者确认,或检查本地网络及防火墙设置。)", "red") input("\033[0m") sys.exit(1) if not message['result'] in ["Accepted", "Pending review"]: + print() prints("连接失败:{}".format(RESULTS[message['result']]), "red") input("\033[0m") sys.exit(1) if message['result'] == "Accepted": + print() prints("连接成功!", "green") ring() if message['result'] == "Pending review": - prints("服务端需要对连接请求进行人工审核,请等待...", "white") + print() + seconds_consumed = 0 while True: + clock_start = datetime.datetime.now().timestamp() + print(dye("服务端需要对连接请求进行人工审核,请等待... (已经等待了 {} 秒)\r", "white").format(seconds_consumed), end="", flush=True) try: read() message = get_message() if not message: - continue + raise # 特殊情况:聊天室服务端已经关闭 (协议 3.3.1) if message['type'] == "SERVER.STOP.ANNOUNCE": prints("聊天室服务端已经关闭。", "red") @@ -2532,18 +2558,23 @@ def main(): sys.exit(1) # 一般情况:人工审核完成 (协议 1.3) if not message['accepted']: + print() prints("服务端管理员 {} (UID = {}) 拒绝了您的连接请求。".format(message['operator']['username'], message['operator']['uid']), "red") prints("连接失败。", "red") input("\033[0m") sys.exit(1) if message['accepted']: time.sleep(1) # 等待 1 秒,确认协议 3.2 提供的完整上下文传输完成 + print() prints("服务端管理员 {} (UID = {}) 通过了您的连接请求。".format(message['operator']['username'], message['operator']['uid']), "green") prints("连接成功!", "green") ring() break except: pass + clock_end = datetime.datetime.now().timestamp() + seconds_consumed += 1 + time.sleep(1 - (clock_end - clock_start)) side = "Client" # 获取服务端通过协议 3.2 提供的完整上下文; From c9bd19adbd32e22f539238ae42cb0036b98b7110 Mon Sep 17 00:00:00 2001 From: 035966-L3 <139025318+035966-L3@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:54:50 +0800 Subject: [PATCH 8/8] v4.5.2 --- LTS.py | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/LTS.py b/LTS.py index 0f9952d..972b0f9 100644 --- a/LTS.py +++ b/LTS.py @@ -307,7 +307,7 @@ import time # 程序版本 -VERSION = "v4.5.1" +VERSION = "v4.5.2" # 用于客户端解析协议 1.2 RESULTS = \ @@ -636,7 +636,7 @@ 参见 HELP_HINT 第 3 段,下同) buffer my_socket 读取时模拟的缓冲区 (发送的数据都是 NDJSON,因此遇到换行符则清空) -EXIT_FLAG 默认为 False,程序终止改为 True,通知所有线程终止 +exit_flag 默认为 False,程序终止改为 True,通知所有线程终止 print_queue 用于输入模式下记录被阻塞的输出内容(每行一条), 切换到输出模式后一并输出 @@ -684,7 +684,7 @@ history = [] online_count = 1 buffer = "" -EXIT_FLAG = False +exit_flag = False log_queue = queue.Queue() receive_queue = queue.Queue() send_queue = queue.Queue() @@ -888,7 +888,7 @@ def print_message(message): def process(message): global users global online_count - global EXIT_FLAG + global exit_flag ring() # 响铃 if message['type'] == "CHAT.RECEIVE": # 收到消息 (协议 2.2) message['time'] = time_str() @@ -922,7 +922,7 @@ def process(message): # 来清除 ANSI 文本序列带来的显示效果, # 防止干扰用户后续的终端使用 prints("\033[0m\033[1;36m再见!\033[0m") - EXIT_FLAG = True + exit_flag = True return if message['type'] == "SERVER.CONFIG.CHANGE": # 服务端参数变更 (协议 3.4.2) announce(message['operator']) @@ -940,7 +940,7 @@ def process(message): announce(0) prints("聊天室服务端已经关闭。", "cyan") prints("\033[0m\033[1;36m再见!\033[0m") - EXIT_FLAG = True + exit_flag = True return # 从 my_socket 读取数据,每次 128 KiB,读完为止 @@ -1685,7 +1685,7 @@ def do_evaluate(arg=None): def do_exit(arg=None): global log_queue global send_queue - global EXIT_FLAG + global exit_flag # 此处不能调用 dye 函数,因为需要使用 \033[0m # 来清除 ANSI 文本序列带来的显示效果, # 防止干扰用户后续的终端使用 @@ -1696,13 +1696,13 @@ def do_exit(arg=None): if users[i]['status'] in ["Pending", "Online", "Admin", "Root"]: send_queue.put(json.dumps({'to': i, 'content': {'type': 'SERVER.STOP.ANNOUNCE'}})) # 协议 3.2.1 server_socket.close() - EXIT_FLAG = True + exit_flag = True my_socket.close() return def do_flood(arg=None): global blocked - global EXIT_FLAG + global exit_flag if platform.system() == "Windows": shortcut = 'C' else: @@ -1711,7 +1711,7 @@ def do_flood(arg=None): print("\033[8;30m", end="", flush=True) while True: time.sleep(0.1) - if EXIT_FLAG: + if exit_flag: print("\033[0m", end="", flush=True) return @@ -1782,7 +1782,7 @@ def do_shell(arg=None): # 所有线程均使用 while True 的无限循环, # 每轮开始前暂停 0.1 秒防止 CPU 占用过高, -# 且均受程序终止信号 EXIT_FLAG 的调控。 +# 且均受程序终止信号 exit_flag 的调控。 def thread_gate(): global online_count @@ -1791,7 +1791,7 @@ def thread_gate(): global users while True: time.sleep(0.1) - if EXIT_FLAG: + if exit_flag: return # 尝试开启新连接 @@ -1891,7 +1891,7 @@ def thread_process(): global users while True: time.sleep(0.1) - if EXIT_FLAG: + if exit_flag: return while not receive_queue.empty(): @@ -1925,7 +1925,7 @@ def thread_receive(): global users while True: time.sleep(0.1) - if EXIT_FLAG: + if exit_flag: return for i in range(len(users)): @@ -1962,7 +1962,7 @@ def thread_send(): global users while True: time.sleep(0.1) - if EXIT_FLAG: + if exit_flag: return while not send_queue.empty(): @@ -2022,7 +2022,7 @@ def thread_log(): file.write(log_queue.get() + "\n") # 与其他线程不同,先写入日志再读取程序终止信号, # 确保程序终止时没有日志残留在 log_queue 中 - if EXIT_FLAG: + if exit_flag: return def thread_check(): @@ -2032,7 +2032,7 @@ def thread_check(): global users while True: time.sleep(1) # 该部分对整体性能影响较大,因此执行频率下调至 1 秒一次 - if EXIT_FLAG: + if exit_flag: return down = [] @@ -2056,10 +2056,10 @@ def thread_check(): def thread_input(): global blocked - global EXIT_FLAG + global exit_flag while True: time.sleep(0.1) - if EXIT_FLAG: + if exit_flag: print("\033[0m", end="", flush=True) return @@ -2102,10 +2102,10 @@ def thread_input(): blocked = False def thread_output(): - global EXIT_FLAG + global exit_flag while True: time.sleep(0.1) - if EXIT_FLAG: + if exit_flag: print("\033[0m", end="", flush=True) return @@ -2141,7 +2141,7 @@ def main(): global history global online_count global buffer - global EXIT_FLAG + global exit_flag global log_queue global receive_queue global send_queue @@ -2215,6 +2215,12 @@ def main(): os.system('') # 对 Windows 尝试开启 ANSI 转义字符(带颜色文本)支持 clear_screen() + prints("祝大家 2026 年新年快乐!", "magenta") + prints("我们准备了一些新年彩蛋,详情请见:", "magenta") + prints("https://github.com/ILoveScratch2/TouchFish-Astra/releases/tag/v2.1.0", "magenta") + prints("这段文本只在此版本 (v4.5.2) 中出现。", "magenta") + print() + if config_read_result == "OK": prints("配置文件 config.json 读取成功!", "yellow") if config_read_result == "Not found":