WechatOnCloud/bridge/woc_bridge/messaging/friend_parser.py
Kris c45282f094 feat: 新增好友自动通过、UI自动化能力与多分辨率适配
- 新增登录状态守卫后台任务
- 新增好友申请自动通过规则引擎
- 新增多分辨率UI配置与模板资源
- 新增消息拉取复合游标支持
- 优化发送队列与UI自动化逻辑
- 新增批量发送日志与错误处理
- 优化Docker镜像构建与ptrace初始化
- 新增联系人名称缓存预热
2026-07-17 18:10:31 +08:00

136 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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
from typing import Optional
from woc_bridge.models.contact import FriendRequestInfo
logger = logging.getLogger("woc-bridge")
# 可能的好友申请 XML 类型标记(大小写不敏感)
_VERIFYUSER_TYPES = {"verifyuser", "verify_user", "verification"}
def parse_friend_request(
content: str, create_time: int, msg_local_id: int
) -> Optional[FriendRequestInfo]:
"""解析 fmessage 系统消息 XML提取好友申请信息。
兼容多种实际格式:
- <sysmsg type="verifyUser"><Link><UserName>...</UserName>...</Link></sysmsg>
- <sysmsg type="verifyUser"><verifyuser><username>...</username>...</verifyuser></sysmsg>
- <verifyuser>...</verifyuser> 根节点
- 标签大小写/命名空间不一致
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
# 1. 判断是否为好友申请类型
if not _is_verify_user_root(root):
return None
# 2. 提取字段:优先 Link 子树,其次 verifyuser/verification 子树
link = root.find(".//Link")
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
if not stranger_wxid:
return None
if "@" not in stranger_wxid:
# 微信 ticket 常见为 wxid_xxx@stranger 或加密串,统一补后缀
# 若已含 @ 则保留原样(如 @stranger / @openim
stranger_wxid = f"{stranger_wxid}@stranger"
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,
)
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
def _text(parent: ET.Element, tag: str) -> str:
"""安全提取直接子元素文本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 ""