diff --git a/admin/dashboard/blueprints/friend_circle.py b/admin/dashboard/blueprints/friend_circle.py index ddacf6f..f849d8a 100644 --- a/admin/dashboard/blueprints/friend_circle.py +++ b/admin/dashboard/blueprints/friend_circle.py @@ -2,7 +2,8 @@ import asyncio import xml.etree.ElementTree as ET from datetime import datetime -from flask import Blueprint, current_app, jsonify, render_template, request +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 @@ -15,6 +16,41 @@ def _run_client_coro(server, coro, timeout=90): 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 @@ -158,14 +194,17 @@ def _normalize_like(like): return { "author_wxid": ( _safe_text(like.get("UserName")) + or _safe_text(like.get("Username")) or _safe_text(like.get("username")) or _safe_text(like.get("Wxid")) ), "author_name": ( _safe_text(like.get("NickName")) + or _safe_text(like.get("Nickname")) or _safe_text(like.get("nickname")) or _safe_text(like.get("DisplayName")) - ) + ), + "create_time": _safe_text(like.get("CreateTime")) or _safe_text(like.get("createTime")) } @@ -238,6 +277,8 @@ def _normalize_post(node, server): comment_nodes.extend(_extract_list(node.get(key))) for key in ("LikeList", "Likes", "likeList", "likes"): like_nodes.extend(_extract_list(node.get(key))) + for key in ("LikeUserList", "likeUserList"): + like_nodes.extend(_extract_list(node.get(key))) return { "id": object_id, @@ -246,6 +287,9 @@ def _normalize_post(node, server): "author_avatar": server.contact_manager.get_head_image(author_wxid), "content": content, "timestamp": timestamp, + "like_flag": _safe_text(node.get("LikeFlag")), + "is_liked": _safe_text(node.get("LikeFlag")) in ("1", "true", "True"), + "like_count": _safe_text(node.get("LikeCount")), "location": xml_data.get("location", {}), "media": [item for item in media if item.get("url") or item.get("thumb")], "comments": [item for item in (_normalize_comment(x) for x in comment_nodes) if item.get("content") or item.get("author_wxid")], @@ -445,3 +489,14 @@ def friend_circle_comment(): 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) diff --git a/admin/dashboard/templates/friend_circle.html b/admin/dashboard/templates/friend_circle.html index 08070b9..579169c 100644 --- a/admin/dashboard/templates/friend_circle.html +++ b/admin/dashboard/templates/friend_circle.html @@ -83,8 +83,19 @@