2026-07-16 13:03:58 +08:00
|
|
|
|
"""fmessage 系统消息 XML 解析器。
|
|
|
|
|
|
|
|
|
|
|
|
解析微信 4.x Linux 的 fmessage sysmsg XML,提取好友申请信息。
|
|
|
|
|
|
无状态函数,便于单测。
|
|
|
|
|
|
|
|
|
|
|
|
XML 格式(推断,需环境验证):
|
|
|
|
|
|
<sysmsg type="verifyUser" ...>
|
|
|
|
|
|
<Link ...>
|
|
|
|
|
|
<UserName>xxx@stranger</UserName>
|
|
|
|
|
|
<NickName>申请人昵称</NickName>
|
|
|
|
|
|
<Content>验证消息内容</Content>
|
|
|
|
|
|
<Scene>...</Scene>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</sysmsg>
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import xml.etree.ElementTree as ET
|
2026-07-17 18:10:31 +08:00
|
|
|
|
from typing import Optional
|
2026-07-16 13:03:58 +08:00
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
from woc_bridge.models.contact import FriendRequestInfo
|
2026-07-16 13:03:58 +08:00
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("woc-bridge")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
# 可能的好友申请 XML 类型标记(大小写不敏感)
|
|
|
|
|
|
_VERIFYUSER_TYPES = {"verifyuser", "verify_user", "verification"}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 13:03:58 +08:00
|
|
|
|
def parse_friend_request(
|
|
|
|
|
|
content: str, create_time: int, msg_local_id: int
|
|
|
|
|
|
) -> Optional[FriendRequestInfo]:
|
|
|
|
|
|
"""解析 fmessage 系统消息 XML,提取好友申请信息。
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
兼容多种实际格式:
|
|
|
|
|
|
- <sysmsg type="verifyUser"><Link><UserName>...</UserName>...</Link></sysmsg>
|
|
|
|
|
|
- <sysmsg type="verifyUser"><verifyuser><username>...</username>...</verifyuser></sysmsg>
|
|
|
|
|
|
- <verifyuser>...</verifyuser> 根节点
|
|
|
|
|
|
- 标签大小写/命名空间不一致
|
|
|
|
|
|
|
2026-07-16 13:03:58 +08:00
|
|
|
|
Args:
|
|
|
|
|
|
content: message_content 原始文本(XML)
|
|
|
|
|
|
create_time: 消息时间戳
|
|
|
|
|
|
msg_local_id: 消息 local_id
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
FriendRequestInfo 或 None(解析失败 / 非 verifyUser 类型)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not content or not content.strip():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
root = ET.fromstring(content)
|
|
|
|
|
|
except ET.ParseError:
|
|
|
|
|
|
# 部分消息可能不是合法 XML(如纯文本通知),跳过
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
# 1. 判断是否为好友申请类型
|
|
|
|
|
|
if not _is_verify_user_root(root):
|
2026-07-16 13:03:58 +08:00
|
|
|
|
return None
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
# 2. 提取字段:优先 Link 子树,其次 verifyuser/verification 子树
|
2026-07-16 13:03:58 +08:00
|
|
|
|
link = root.find(".//Link")
|
2026-07-17 18:10:31 +08:00
|
|
|
|
if link is not None:
|
|
|
|
|
|
stranger_wxid = _text(link, "UserName")
|
|
|
|
|
|
nickname = _text(link, "NickName")
|
|
|
|
|
|
verify_message = _text(link, "Content")
|
|
|
|
|
|
scene = _text(link, "Scene")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 无 Link 节点时从任意子节点按候选标签名搜索
|
|
|
|
|
|
stranger_wxid = _find_text(root, ["UserName", "username", "v1", "ticket"])
|
|
|
|
|
|
nickname = _find_text(root, ["NickName", "nickname", "nick"])
|
|
|
|
|
|
verify_message = _find_text(root, ["Content", "content", "msg", "verifyContent"])
|
|
|
|
|
|
scene = _find_text(root, ["Scene", "scene", "sceneid", "scene_id"])
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 从 ticket/encrypt username 中反推带 @stranger 的 wxid
|
2026-07-16 13:03:58 +08:00
|
|
|
|
if not stranger_wxid:
|
|
|
|
|
|
return None
|
2026-07-17 18:10:31 +08:00
|
|
|
|
if "@" not in stranger_wxid:
|
|
|
|
|
|
# 微信 ticket 常见为 wxid_xxx@stranger 或加密串,统一补后缀
|
|
|
|
|
|
# 若已含 @ 则保留原样(如 @stranger / @openim)
|
|
|
|
|
|
stranger_wxid = f"{stranger_wxid}@stranger"
|
2026-07-16 13:03:58 +08:00
|
|
|
|
|
|
|
|
|
|
return FriendRequestInfo(
|
|
|
|
|
|
stranger_wxid=stranger_wxid,
|
|
|
|
|
|
nickname=nickname,
|
|
|
|
|
|
verify_message=verify_message,
|
|
|
|
|
|
scene=scene,
|
|
|
|
|
|
raw_xml=content,
|
|
|
|
|
|
create_time=create_time,
|
|
|
|
|
|
msg_local_id=msg_local_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
def _is_verify_user_root(root: ET.Element) -> bool:
|
|
|
|
|
|
"""判断 XML 根节点是否表示好友申请。"""
|
|
|
|
|
|
tag = root.tag.split("}")[-1] if root.tag else ""
|
|
|
|
|
|
if tag.lower() in _VERIFYUSER_TYPES:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if tag.lower() == "sysmsg":
|
|
|
|
|
|
msg_type = (root.get("type") or "").lower()
|
|
|
|
|
|
if msg_type in _VERIFYUSER_TYPES:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 13:03:58 +08:00
|
|
|
|
def _text(parent: ET.Element, tag: str) -> str:
|
2026-07-17 18:10:31 +08:00
|
|
|
|
"""安全提取直接子元素文本(None 安全,去掉命名空间)。"""
|
|
|
|
|
|
tag_lower = tag.lower()
|
|
|
|
|
|
for child in parent:
|
|
|
|
|
|
child_tag = child.tag.split("}")[-1] if child.tag else ""
|
|
|
|
|
|
if child_tag.lower() == tag_lower:
|
|
|
|
|
|
return child.text.strip() if child.text else ""
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _find_child_recursive(parent: ET.Element, tag: str) -> Optional[ET.Element]:
|
|
|
|
|
|
"""按本地标签名递归查找后代元素(忽略命名空间)。"""
|
|
|
|
|
|
tag_lower = tag.lower()
|
|
|
|
|
|
for elem in parent.iter():
|
|
|
|
|
|
elem_tag = elem.tag.split("}")[-1] if elem.tag else ""
|
|
|
|
|
|
if elem_tag.lower() == tag_lower:
|
|
|
|
|
|
return elem
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _find_text(parent: ET.Element, tags: list[str]) -> str:
|
|
|
|
|
|
"""按候选标签名列表顺序递归查找,返回首个非空文本。"""
|
|
|
|
|
|
for tag in tags:
|
|
|
|
|
|
el = _find_child_recursive(parent, tag)
|
|
|
|
|
|
if el is not None and el.text:
|
|
|
|
|
|
return el.text.strip()
|
|
|
|
|
|
return ""
|