只用表情 XML 里的 md5 + len/totallen 作为发送参数。
不再用图片文件大小、文件名等回退逻辑去“猜参数”。 发送接口不再“先返回发送中”,而是直接调用 SendEmoji 并等待结果: 成功才返回 表情发送成功 失败直接返回具体错误(不会再假成功)
This commit is contained in:
@@ -148,27 +148,24 @@ def _extract_emoji_meta(attachment_url: str, image_path: str):
|
|||||||
md5 = ""
|
md5 = ""
|
||||||
total_length = 0
|
total_length = 0
|
||||||
|
|
||||||
# 优先按 XML 结构解析,避免纯正则误命中其他字段。
|
# 只接受 XML 中的参数,不做文件名或文件大小回退,避免参数污染。
|
||||||
if text.startswith("<"):
|
if not text.startswith("<"):
|
||||||
try:
|
return "", 0
|
||||||
root = ET.fromstring(text)
|
try:
|
||||||
emoji_node = root.find(".//emoji")
|
root = ET.fromstring(text)
|
||||||
if emoji_node is not None:
|
emoji_node = root.find(".//emoji")
|
||||||
md5 = _safe_text(emoji_node.attrib.get("md5", "")).strip().lower()
|
if emoji_node is None:
|
||||||
for key in ("totallen", "total_len", "totalLen", "len"):
|
return "", 0
|
||||||
value = _safe_text(emoji_node.attrib.get(key, "")).strip()
|
md5 = _safe_text(emoji_node.attrib.get("md5", "")).strip().lower()
|
||||||
if value.isdigit():
|
for key in ("totallen", "total_len", "totalLen", "len"):
|
||||||
total_length = int(value)
|
value = _safe_text(emoji_node.attrib.get(key, "")).strip()
|
||||||
break
|
if value.isdigit():
|
||||||
except Exception:
|
total_length = int(value)
|
||||||
pass
|
break
|
||||||
|
except Exception:
|
||||||
if not md5:
|
|
||||||
md5_match = _EMOJI_MD5_RE.search(text)
|
md5_match = _EMOJI_MD5_RE.search(text)
|
||||||
if md5_match:
|
if md5_match:
|
||||||
md5 = md5_match.group(1).lower()
|
md5 = md5_match.group(1).lower()
|
||||||
|
|
||||||
if total_length <= 0:
|
|
||||||
len_match = _EMOJI_TOTALLEN_RE.search(text)
|
len_match = _EMOJI_TOTALLEN_RE.search(text)
|
||||||
if len_match:
|
if len_match:
|
||||||
try:
|
try:
|
||||||
@@ -176,12 +173,6 @@ def _extract_emoji_meta(attachment_url: str, image_path: str):
|
|||||||
except Exception:
|
except Exception:
|
||||||
total_length = 0
|
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
|
return md5, total_length
|
||||||
|
|
||||||
|
|
||||||
@@ -684,12 +675,18 @@ def api_send_message():
|
|||||||
return jsonify({'success': False, 'message': '表情参数格式错误'})
|
return jsonify({'success': False, 'message': '表情参数格式错误'})
|
||||||
md5 = _safe_text(content.get('md5')).strip().lower()
|
md5 = _safe_text(content.get('md5')).strip().lower()
|
||||||
total_length = int(content.get('total_length') or 0)
|
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 或长度'})
|
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({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '消息发送中'
|
'message': '表情发送成功'
|
||||||
})
|
})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -286,6 +286,12 @@ def get_hourly_message_trend():
|
|||||||
@login_required
|
@login_required
|
||||||
def message_media_proxy():
|
def message_media_proxy():
|
||||||
target_url = request.args.get('url', '').strip()
|
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:
|
try:
|
||||||
return _proxy_remote_media(target_url)
|
return _proxy_remote_media(target_url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1221,6 +1221,11 @@
|
|||||||
},
|
},
|
||||||
getChatMediaUrl(url) {
|
getChatMediaUrl(url) {
|
||||||
if (!url) return '';
|
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://')) {
|
if (url.startsWith('blob:') || url.startsWith('data:') || url.startsWith('/static/') || url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -1232,6 +1237,23 @@
|
|||||||
}
|
}
|
||||||
return `/api/messages/media_proxy?url=${encodeURIComponent(url)}`;
|
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 = {}) {
|
createLocalChatMessage(payload = {}) {
|
||||||
return {
|
return {
|
||||||
messageId: `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
messageId: `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
|||||||
@@ -571,13 +571,13 @@ class MessageStorageDB(BaseDBOperator):
|
|||||||
return self.get_pending_media_messages(minutes_ago, limit)
|
return self.get_pending_media_messages(minutes_ago, limit)
|
||||||
|
|
||||||
def get_recent_emoji_assets(self, limit: int = 200) -> List[Dict]:
|
def get_recent_emoji_assets(self, limit: int = 200) -> List[Dict]:
|
||||||
"""获取已下载落盘的表情资源记录(用于通讯录聊天面板的表情库)"""
|
"""获取近期表情消息记录(用于提取 md5 + len 发送参数)"""
|
||||||
sql = """
|
sql = """
|
||||||
SELECT message_id, group_id, sender, timestamp, message_type, attachment_url, image_path
|
SELECT message_id, group_id, sender, timestamp, message_type, attachment_url, image_path
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE message_type IN ('47', '1048625', '1090519089')
|
WHERE message_type IN ('47', '1048625', '1090519089')
|
||||||
AND image_path IS NOT NULL
|
AND attachment_url IS NOT NULL
|
||||||
AND image_path <> ''
|
AND attachment_url <> ''
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT %s
|
LIMIT %s
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user