diff --git a/bsp_monitor/twitter_web.py b/bsp_monitor/twitter_web.py new file mode 100644 index 0000000..cbdec57 --- /dev/null +++ b/bsp_monitor/twitter_web.py @@ -0,0 +1,380 @@ +#!/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() diff --git a/web/templates/chan_tv.html b/web/templates/chan_tv.html index 50a5171..a8a2993 100644 --- a/web/templates/chan_tv.html +++ b/web/templates/chan_tv.html @@ -1,6 +1,15 @@ + + + + 缠论 Chart — TradingView diff --git a/web/templates/index.html b/web/templates/index.html index fe8cca4..4f4ddf0 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1,6 +1,15 @@ + + + + 缠论分析系统