#!/usr/bin/env python3 """ pz-bansync —— PZ 服务器封禁列表与 banserver API 双向同步 在 PZ 服务器本机运行 """ import json import logging import socket import sqlite3 import ssl import struct import sys import time import urllib.request from pathlib import Path # ==================== 配置区 ==================== API_BASE = "https://banserver.zuckertech.cn" # ban联服器 API 地址 API_USERNAME = "your_api_username" # ban联服器 API 用户名 API_PASSWORD = "your_api_password" # ban联服器 API 密码 RCON_HOST = "127.0.0.1" RCON_PORT = 27015 RCON_PASS = "your_rcon_password" # RCON 密码 PZ_DB = "your_pz_server Server.db" # PZ 服务器数据库路径 SYNC_INTERVAL = 300 # 同步间隔(秒) LOG_FILE = Path("your_log_file.log") # ============ 同步模式 ============ # "full" —— 完全同步(默认): # banserver 新增 → PZ 加封禁 # banserver 移除 → PZ 解封 # 结果:两边完全一致 # # "add_only" —— 只增不减: # 只把 banserver 的新封禁加到 PZ # 不处理 PZ 里"banserver 已经没有"的记录 # 结果:PZ 的封禁列表只会增加,不会减少 # # 建议:首次接入用 "add_only" 观察一段时间,确认无误后再改成 "full" SYNC_MODE = "full" # ================================================== log = logging.getLogger("pz-bansync") # ---------------- RCON(Source RCON 协议 + 响应排空) ---------------- class RconClient: SERVERDATA_AUTH = 3 SERVERDATA_AUTH_RESPONSE = 2 SERVERDATA_EXECCOMMAND = 2 SERVERDATA_RESPONSE_VALUE = 0 def __init__(self, host, port, password): self.host, self.port, self.password = host, port, password self.sock = None self.req_id = 0 def _next_id(self): self.req_id += 1 return self.req_id def _send(self, pkt_id, pkt_type, body): body_b = body.encode("utf-8") payload = struct.pack(" {resp.strip()!r}") pz_update_reason(PZ_DB, sid, api_bans.get(sid, "")) except Exception as e: log.error(f"ban {sid} 失败: {e}") # 移除:仅 full 模式执行 if SYNC_MODE == "full": for sid in sorted(to_unban): try: resp = rcon.command(f"unbanid {sid}") log.info(f"[-] unban {sid} -> {resp.strip()!r}") except Exception as e: log.error(f"unban {sid} 失败: {e}") elif to_unban: log.info(f"(add_only 模式:{len(to_unban)} 条待移除记录已忽略)") def main(): LOG_FILE.parent.mkdir(parents=True, exist_ok=True) root = logging.getLogger() root.setLevel(logging.INFO) fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") fh = logging.FileHandler(LOG_FILE, encoding="utf-8") fh.setFormatter(fmt) root.addHandler(fh) sh = logging.StreamHandler(sys.stdout) sh.setFormatter(fmt) root.addHandler(sh) # 校验模式 global SYNC_MODE SYNC_MODE = SYNC_MODE.strip().lower() if SYNC_MODE not in ("full", "add_only"): log.error(f"SYNC_MODE 值非法: {SYNC_MODE!r},可选 'full' 或 'add_only',已回退为 'full'") SYNC_MODE = "full" log.info("=" * 60) log.info("pz-bansync 启动") log.info(" API: %s", API_BASE) log.info(" RCON: %s:%d", RCON_HOST, RCON_PORT) log.info(" DB: %s", PZ_DB) log.info(" 间隔: %d 秒", SYNC_INTERVAL) log.info(" 同步模式: %s", "full(完全同步)" if SYNC_MODE == "full" else "add_only(只增不减)") log.info("=" * 60) while True: try: token = api_login() except Exception as e: log.error(f"API 登录失败: {e}") time.sleep(SYNC_INTERVAL) continue rcon = RconClient(RCON_HOST, RCON_PORT, RCON_PASS) try: rcon.connect() log.info("RCON connected") except Exception as e: log.error(f"RCON 连接失败: {e}") time.sleep(SYNC_INTERVAL) continue try: sync_once(rcon, token) except Exception: log.exception("同步异常") finally: rcon.close() time.sleep(SYNC_INTERVAL) if __name__ == "__main__": main()