import asyncio import xml.etree.ElementTree as ET from datetime import datetime import requests from flask import Blueprint, Response, current_app, jsonify, render_template, request, stream_with_context from loguru import logger from .auth import login_required friend_circle_bp = Blueprint("friend_circle", __name__) def _run_client_coro(server, coro, timeout=90): future = asyncio.run_coroutine_threadsafe(coro, server.robot.ipad_loop) return future.result(timeout=timeout) def _proxy_remote_media(target_url: str) -> Response: if not target_url: return Response("missing url", status=400) headers = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/123.0.0.0 Safari/537.36" ), "Referer": "http://weixin.qq.com/" } range_header = request.headers.get("Range") if range_header: headers["Range"] = range_header upstream = requests.get(target_url, headers=headers, stream=True, timeout=30) response_headers = {} for key in ("Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", "Cache-Control", "ETag", "Last-Modified"): value = upstream.headers.get(key) if value: response_headers[key] = value if "Accept-Ranges" not in response_headers: response_headers["Accept-Ranges"] = "bytes" return Response( stream_with_context(upstream.iter_content(chunk_size=64 * 1024)), status=upstream.status_code, headers=response_headers, direct_passthrough=True ) def _safe_text(value, default=""): if value is None: return default if isinstance(value, dict): if "buffer" in value: return _safe_text(value.get("buffer"), default) if "string" in value: return _safe_text(value.get("string"), default) if "text" in value: return _safe_text(value.get("text"), default) if isinstance(value, (int, float)): return str(value) if isinstance(value, str): return value return default def _walk_nodes(payload): if isinstance(payload, dict): yield payload for value in payload.values(): yield from _walk_nodes(value) elif isinstance(payload, list): for item in payload: yield from _walk_nodes(item) def _extract_list(values): if isinstance(values, list): return values if isinstance(values, dict): for key in ("list", "List", "items", "Items"): if isinstance(values.get(key), list): return values.get(key) return [] def _find_timeline_xml(value): if isinstance(value, str): return value if "= 50: break return jsonify({"success": True, "data": result}) @friend_circle_bp.route("/api/friend_circle/list") @login_required def friend_circle_list(): server = current_app.dashboard_server towxid = request.args.get("towxid", "").strip() max_id = int(request.args.get("max_id", 0)) first_page_md5 = request.args.get("first_page_md5", "").strip() try: if towxid: payload = _run_client_coro( server, server.client.get_friend_circle_detail(towxid=towxid, max_id=max_id, first_page_md5=first_page_md5) ) else: payload = _run_client_coro( server, server.client.get_friend_circle_list(max_id=max_id, first_page_md5=first_page_md5) ) posts = _extract_posts(payload, server) return jsonify({"success": True, "data": posts, "raw": payload}) except Exception as exc: logger.error(f"获取朋友圈列表失败: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @friend_circle_bp.route("/api/friend_circle/detail") @login_required def friend_circle_detail(): server = current_app.dashboard_server object_id = request.args.get("id", "").strip() towxid = request.args.get("towxid", "").strip() try: payload = _run_client_coro( server, server.client.get_friend_circle_id_detail(object_id=object_id, towxid=towxid) ) posts = _extract_posts(payload, server) detail = posts[0] if posts else None return jsonify({"success": True, "data": detail, "raw": payload}) except Exception as exc: logger.error(f"获取朋友圈详情失败: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @friend_circle_bp.route("/api/friend_circle/publish", methods=["POST"]) @login_required def friend_circle_publish(): server = current_app.dashboard_server data = request.get_json(silent=True) or {} content = (data.get("content") or "").strip() blacklist = (data.get("blacklist") or "").strip() with_user_list = (data.get("with_user_list") or "").strip() images = data.get("images") or [] if not content and not images: return jsonify({"success": False, "message": "内容和图片不能同时为空"}), 400 try: media_items = [] for image in images: upload_data = _run_client_coro( server, server.client.upload_friend_circle_media(image) ) urls = _extract_list(upload_data.get("Urls")) thumbs = _extract_list(upload_data.get("ThumbUrls")) media_items.append({ "id": _safe_text(upload_data.get("Id")) or _safe_text(upload_data.get("MediaId")), "url": _safe_text(urls[0].get("Url")) if urls else "", "thumb": _safe_text(thumbs[0].get("Url")) if thumbs else (_safe_text(urls[0].get("Url")) if urls else ""), "md5": _safe_text(upload_data.get("Md5")), "total_size": upload_data.get("TotalSize", 0), "width": upload_data.get("Width", 0), "height": upload_data.get("Height", 0) }) payload = _run_client_coro( server, server.client.publish_friend_circle( content=content, media_items=media_items, blacklist=blacklist, with_user_list=with_user_list ) ) return jsonify({"success": True, "data": payload}) except Exception as exc: logger.error(f"发布朋友圈失败: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @friend_circle_bp.route("/api/friend_circle/like", methods=["POST"]) @login_required def friend_circle_like(): server = current_app.dashboard_server data = request.get_json(silent=True) or {} object_id = str(data.get("id") or "").strip() cancel_like = bool(data.get("cancel")) if not object_id: return jsonify({"success": False, "message": "缺少朋友圈ID"}), 400 try: if cancel_like: payload = _run_client_coro( server, server.client.friend_circle_operation(object_id=object_id, type=5) ) else: payload = _run_client_coro( server, server.client.friend_circle_comment(object_id=object_id, type=1) ) return jsonify({"success": True, "data": payload}) except Exception as exc: logger.error(f"朋友圈点赞操作失败: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @friend_circle_bp.route("/api/friend_circle/comment", methods=["POST"]) @login_required def friend_circle_comment(): server = current_app.dashboard_server data = request.get_json(silent=True) or {} object_id = str(data.get("id") or "").strip() content = (data.get("content") or "").strip() reply_comment_id = int(data.get("reply_comment_id") or 0) if not object_id or not content: return jsonify({"success": False, "message": "评论目标和内容不能为空"}), 400 try: payload = _run_client_coro( server, server.client.friend_circle_comment( object_id=object_id, content=content, type=2, reply_comment_id=reply_comment_id ) ) return jsonify({"success": True, "data": payload}) except Exception as exc: logger.error(f"朋友圈评论失败: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @friend_circle_bp.route("/api/friend_circle/media_proxy") @login_required def friend_circle_media_proxy(): target_url = request.args.get("url", "").strip() try: return _proxy_remote_media(target_url) except Exception as exc: logger.error(f"朋友圈媒体代理失败: {exc}") return Response(f"proxy failed: {exc}", status=502)