From ee5c1ebadc970e75ecce938a6d87f8748ad574c1 Mon Sep 17 00:00:00 2001 From: liuwei Date: Wed, 15 Apr 2026 11:36:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AA=E7=94=A8=E8=A1=A8=E6=83=85=20XML=20?= =?UTF-8?q?=E9=87=8C=E7=9A=84=20md5=20+=20len/totallen=20=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E5=8F=91=E9=80=81=E5=8F=82=E6=95=B0=E3=80=82=20?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E7=94=A8=E5=9B=BE=E7=89=87=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E3=80=81=E6=96=87=E4=BB=B6=E5=90=8D=E7=AD=89?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E9=80=BB=E8=BE=91=E5=8E=BB=E2=80=9C=E7=8C=9C?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E2=80=9D=E3=80=82=20=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8D=E5=86=8D=E2=80=9C=E5=85=88=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=8F=91=E9=80=81=E4=B8=AD=E2=80=9D=EF=BC=8C=E8=80=8C?= =?UTF-8?q?=E6=98=AF=E7=9B=B4=E6=8E=A5=E8=B0=83=E7=94=A8=20SendEmoji=20?= =?UTF-8?q?=E5=B9=B6=E7=AD=89=E5=BE=85=E7=BB=93=E6=9E=9C=EF=BC=9A=20?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E6=89=8D=E8=BF=94=E5=9B=9E=20=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E5=8F=91=E9=80=81=E6=88=90=E5=8A=9F=20=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=9B=B4=E6=8E=A5=E8=BF=94=E5=9B=9E=E5=85=B7=E4=BD=93?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=88=E4=B8=8D=E4=BC=9A=E5=86=8D=E5=81=87?= =?UTF-8?q?=E6=88=90=E5=8A=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/dashboard/blueprints/contacts.py | 51 +++++++++---------- admin/dashboard/blueprints/messages.py | 6 +++ .../templates/contacts_management.html | 22 ++++++++ db/message_storage.py | 6 +-- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/admin/dashboard/blueprints/contacts.py b/admin/dashboard/blueprints/contacts.py index 093d536..9de8a32 100644 --- a/admin/dashboard/blueprints/contacts.py +++ b/admin/dashboard/blueprints/contacts.py @@ -148,27 +148,24 @@ def _extract_emoji_meta(attachment_url: str, image_path: str): md5 = "" total_length = 0 - # 优先按 XML 结构解析,避免纯正则误命中其他字段。 - if text.startswith("<"): - try: - root = ET.fromstring(text) - emoji_node = root.find(".//emoji") - if emoji_node is not None: - md5 = _safe_text(emoji_node.attrib.get("md5", "")).strip().lower() - for key in ("totallen", "total_len", "totalLen", "len"): - value = _safe_text(emoji_node.attrib.get(key, "")).strip() - if value.isdigit(): - total_length = int(value) - break - except Exception: - pass - - if not md5: + # 只接受 XML 中的参数,不做文件名或文件大小回退,避免参数污染。 + if not text.startswith("<"): + return "", 0 + try: + root = ET.fromstring(text) + emoji_node = root.find(".//emoji") + if emoji_node is None: + return "", 0 + md5 = _safe_text(emoji_node.attrib.get("md5", "")).strip().lower() + for key in ("totallen", "total_len", "totalLen", "len"): + value = _safe_text(emoji_node.attrib.get(key, "")).strip() + if value.isdigit(): + total_length = int(value) + break + except Exception: md5_match = _EMOJI_MD5_RE.search(text) if md5_match: md5 = md5_match.group(1).lower() - - if total_length <= 0: len_match = _EMOJI_TOTALLEN_RE.search(text) if len_match: try: @@ -176,12 +173,6 @@ def _extract_emoji_meta(attachment_url: str, image_path: str): except Exception: total_length = 0 - if not md5 and image_path: - filename = os.path.basename(_safe_text(image_path)) - stem = os.path.splitext(filename)[0] - if re.fullmatch(r"[0-9a-fA-F]{16,64}", stem): - md5 = stem.lower() - return md5, total_length @@ -684,12 +675,18 @@ def api_send_message(): return jsonify({'success': False, 'message': '表情参数格式错误'}) md5 = _safe_text(content.get('md5')).strip().lower() total_length = int(content.get('total_length') or 0) - if not md5 or total_length <= 0: + if not re.fullmatch(r"[0-9a-f]{16,64}", md5) or total_length <= 0: return jsonify({'success': False, 'message': '缺少表情 md5 或长度'}) - send_message_in_thread(server.client.send_emoji_message, wxid, md5, total_length) + try: + loop = get_or_create_loop() + future = asyncio.run_coroutine_threadsafe(server.client.send_emoji_message(wxid, md5, total_length), loop) + future.result(timeout=20) + except Exception as e: + logger.error(f"发送表情失败 md5={md5} len={total_length} wxid={wxid}: {e}") + return jsonify({'success': False, 'message': f'表情发送失败: {str(e)}'}), 500 return jsonify({ 'success': True, - 'message': '消息发送中' + 'message': '表情发送成功' }) else: diff --git a/admin/dashboard/blueprints/messages.py b/admin/dashboard/blueprints/messages.py index f978044..17bb812 100644 --- a/admin/dashboard/blueprints/messages.py +++ b/admin/dashboard/blueprints/messages.py @@ -286,6 +286,12 @@ def get_hourly_message_trend(): @login_required def message_media_proxy(): target_url = request.args.get('url', '').strip() + if not target_url: + return Response("missing url", status=400) + if target_url.startswith("<"): + return Response("invalid media url", status=400) + if not re.match(r"^https?://", target_url, flags=re.IGNORECASE): + return Response("unsupported media url", status=400) try: return _proxy_remote_media(target_url) except Exception as e: diff --git a/admin/dashboard/templates/contacts_management.html b/admin/dashboard/templates/contacts_management.html index 09ae659..5e3234c 100644 --- a/admin/dashboard/templates/contacts_management.html +++ b/admin/dashboard/templates/contacts_management.html @@ -1221,6 +1221,11 @@ }, getChatMediaUrl(url) { if (!url) return ''; + if (url.trim().startsWith('<')) { + const parsed = this.extractXmlMediaUrl(url); + if (!parsed) return ''; + return `/api/messages/media_proxy?url=${encodeURIComponent(parsed)}`; + } if (url.startsWith('blob:') || url.startsWith('data:') || url.startsWith('/static/') || url.startsWith('http://') || url.startsWith('https://')) { return url; } @@ -1232,6 +1237,23 @@ } return `/api/messages/media_proxy?url=${encodeURIComponent(url)}`; }, + extractXmlMediaUrl(xmlText) { + if (!xmlText) return ''; + const patterns = [ + /cdnurl\s*=\s*["']([^"']+)["']/i, + /encrypturl\s*=\s*["']([^"']+)["']/i, + /externurl\s*=\s*["']([^"']+)["']/i, + /cdnmidimgurl\s*=\s*["']([^"']+)["']/i, + /cdnthumburl\s*=\s*["']([^"']+)["']/i + ]; + for (const pattern of patterns) { + const match = pattern.exec(xmlText); + if (match && match[1]) { + return match[1].replace(/&/g, '&').trim(); + } + } + return ''; + }, createLocalChatMessage(payload = {}) { return { messageId: `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, diff --git a/db/message_storage.py b/db/message_storage.py index 9edc247..ec5f4b6 100644 --- a/db/message_storage.py +++ b/db/message_storage.py @@ -571,13 +571,13 @@ class MessageStorageDB(BaseDBOperator): return self.get_pending_media_messages(minutes_ago, limit) def get_recent_emoji_assets(self, limit: int = 200) -> List[Dict]: - """获取已下载落盘的表情资源记录(用于通讯录聊天面板的表情库)""" + """获取近期表情消息记录(用于提取 md5 + len 发送参数)""" sql = """ SELECT message_id, group_id, sender, timestamp, message_type, attachment_url, image_path FROM messages WHERE message_type IN ('47', '1048625', '1090519089') - AND image_path IS NOT NULL - AND image_path <> '' + AND attachment_url IS NOT NULL + AND attachment_url <> '' ORDER BY timestamp DESC LIMIT %s """