#!/usr/bin/env python3 """ twitter_web.py — Twitter 监控账号管理 Web 界面。 单文件,零依赖,只用到 Python 标准库。 """ import json import os import sys import re from datetime import datetime, timezone from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) WATCHLIST_PATH = os.path.join(SCRIPT_DIR, "twitter_watchlist.json") STATE_PATH = os.path.join(SCRIPT_DIR, "twitter_state.json") PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8010 def extract_username(value: str) -> str: value = value.strip().rstrip("/") if value.startswith("@"): return value[1:] for pattern in [r"(?:twitter\.com|x\.com)/(\w+)(?:/|$)", r"/(\w+)$"]: m = re.search(pattern, value) if m: return m.group(1) if re.match(r"^\w+$", value): return value raise ValueError(f"无法提取用户名: {value}") def load_json(path): if os.path.exists(path): with open(path) as f: return json.load(f) return {} def save_json(path, data): with open(path, "w") as f: json.dump(data, f, indent=2, ensure_ascii=False) def get_watchlist(): return load_json(WATCHLIST_PATH).get("users", []) def save_watchlist(users): save_json(WATCHLIST_PATH, {"users": users}) def get_state(): return load_json(STATE_PATH) HTML = """ Twitter 监控管理

🐦 Twitter 账号监控

管理 twitterapi.io 监控账号 · 增删改查

""" class Handler(BaseHTTPRequestHandler): def log_message(self, format, *args): pass # silent def _send(self, code, body, content_type="application/json"): body = body.encode() if isinstance(body, str) else json.dumps(body, ensure_ascii=False).encode() self.send_response(code) self.send_header("Content-Type", content_type + "; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body) def _json(self, code, data): self._send(code, data) def _error(self, code, msg): self._json(code, {"error": msg}) def do_OPTIONS(self): self.send_response(204) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() def do_GET(self): path = urlparse(self.path).path if path == "/" or path == "/index.html": self._send(200, HTML, "text/html") return if path.startswith("/api/accounts"): username = path[len("/api/accounts"):].strip("/") if username: # GET /api/accounts/ — single account users = get_watchlist() state = get_state() for u in users: if u["username"].lower() == username.lower(): entry = dict(u) entry["last_check"] = state.get(u["username"], {}).get("last_check") self._json(200, entry) return self._error(404, "账号不存在") return # GET /api/accounts — list all users = get_watchlist() state = get_state() accounts = [] for u in users: entry = dict(u) sc = state.get(u["username"], {}) ts = sc.get("last_check") if ts: try: ts = datetime.fromisoformat(ts).strftime("%m-%d %H:%M") except Exception: pass else: ts = "从未" entry["last_check"] = ts accounts.append(entry) self._json(200, {"accounts": accounts}) else: self._error(404, "Not Found") def do_POST(self): path = urlparse(self.path).path if path != "/api/accounts": self._error(404, "Not Found") return length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) if length else {} url = body.get("url", "").strip() if not url: self._error(400, "缺少 url 参数") return try: username = extract_username(url) except ValueError: self._error(400, "无法从输入中提取用户名,请输入 Twitter/X 链接或 @用户名") return users = get_watchlist() if any(u["username"].lower() == username.lower() for u in users): self._error(409, f"@{username} 已在监控列表中") return display_name = body.get("display_name", "").strip() or username users.append({ "username": username, "display_name": display_name, "added_at": datetime.now(timezone.utc).isoformat(), }) save_watchlist(users) self._json(201, {"message": f"✅ 已添加 @{username}", "username": username}) def do_PUT(self): path = urlparse(self.path).path username = path[len("/api/accounts"):].strip("/") if not username: self._error(400, "缺少用户名") return length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) if length else {} display_name = body.get("display_name", "").strip() users = get_watchlist() for u in users: if u["username"].lower() == username.lower(): if display_name: u["display_name"] = display_name save_watchlist(users) self._json(200, {"message": f"✅ @{username} 已更新"}) return self._error(404, "账号不存在") def do_DELETE(self): path = urlparse(self.path).path username = path[len("/api/accounts"):].strip("/") if not username: self._error(400, "缺少用户名") return users = get_watchlist() before = len(users) users = [u for u in users if u["username"].lower() != username.lower()] if len(users) < before: save_watchlist(users) self._json(200, {"message": f"🗑 已移除 @{username}"}) else: self._error(404, "账号不存在") def main(): print(f"🐦 Twitter 监控管理: http://0.0.0.0:{PORT}") server = HTTPServer(("0.0.0.0", PORT), Handler) try: server.serve_forever() except KeyboardInterrupt: print("\n已停止") server.server_close() if __name__ == "__main__": main()