WechatOnCloud/bridge/woc_bridge/models/contact.py
Kris 31953eedf4 feat: 新增好友申请自动通过功能
- 新增好友申请XML解析器,支持解析微信4.x Linux的fmessage系统消息
- 新增好友申请自动通过规则引擎,支持黑白名单、关键词、场景过滤
- 新增xdotool驱动的好友申请UI自动化流程
- 新增后台好友申请监听器,支持增量轮询与状态统计
- 新增配套API接口,支持查询申请列表、手动/自动通过、配置管理
- 新增多分辨率UI定位配置,适配1280x720和1920x1080分辨率
- 新增配置项支持自动通过功能的开关与规则配置
2026-07-16 13:03:58 +08:00

228 lines
7.7 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.

"""联系人域模型:联系人 / 联系人列表 / 群聊列表 / 群成员。"""
from __future__ import annotations
import asyncio
import enum
from typing import TYPE_CHECKING, Optional
from pydantic import BaseModel, Field
if TYPE_CHECKING:
from woc_bridge.messaging.friend_watcher import FriendRequestInfo
# ---------------------------------------------------------------------------
# 联系人接口
# ---------------------------------------------------------------------------
class Contact(BaseModel):
"""联系人。"""
wxid: str
nickname: str = ""
remark: str = ""
avatar_url: str = ""
type: str = Field(
default="person",
description="person / group / official / system",
)
# 扩展字段:从 contact.db 的 contact 表读取
alias: Optional[str] = Field(default=None, description="微信号/别名")
encrypt_username: Optional[str] = Field(default=None, description="加密用户名")
quan_pin: Optional[str] = Field(default=None, description="全拼")
pin_yin_initial: Optional[str] = Field(default=None, description="拼音首字母")
big_head_url: Optional[str] = Field(default=None, description="高清头像 URL")
small_head_url: Optional[str] = Field(default=None, description="缩略头像 URL")
description: Optional[str] = Field(default=None, description="个性签名/描述")
local_type: Optional[int] = Field(default=None, description="联系人类型标记")
verify_flag: Optional[int] = Field(default=None, description="认证标记")
delete_flag: Optional[int] = Field(default=None, description="删除标记")
chat_room_type: Optional[int] = Field(default=None, description="群类型标记")
class ContactsResponse(BaseModel):
"""联系人列表响应。"""
contacts: list[Contact] = Field(default_factory=list)
total: int = 0
class GroupsResponse(BaseModel):
"""群聊列表响应。"""
groups: list[Contact] = Field(default_factory=list)
total: int = 0
# ---------------------------------------------------------------------------
# 群成员接口
# ---------------------------------------------------------------------------
class GroupMember(BaseModel):
"""群成员。"""
wxid: str
nickname: str = ""
display_name: str = ""
is_admin: bool = False
class GroupMembersResponse(BaseModel):
"""群成员列表响应。"""
group_wxid: str
members: list[GroupMember] = Field(default_factory=list)
total: int = 0
# ---------------------------------------------------------------------------
# 修改好友备注experimental
# ---------------------------------------------------------------------------
class SetRemarkRequest(BaseModel):
"""修改好友备注请求experimental"""
remark: str = Field(description="新备注名")
display_name: Optional[str] = Field(
default=None,
description="会话显示名(备注/昵称)用于搜索框定位;"
"不传则从 contact.db 解析(修改前的 remark 优先,其次 nickname回退 wxid",
)
class SetRemarkResponse(BaseModel):
"""修改好友备注响应。"""
success: bool = Field(default=False)
error: Optional[str] = Field(default=None, description="失败时的错误描述")
# ---------------------------------------------------------------------------
# 添加好友experimental
# ---------------------------------------------------------------------------
class AddFriendRequest(BaseModel):
"""添加好友请求experimental"""
keyword: str = Field(description="搜索关键词wxid/手机号/微信号)")
message: str = Field(default="", description="可选好友验证消息")
class AddFriendResponse(BaseModel):
"""添加好友响应。"""
success: bool = Field(default=False)
error: Optional[str] = Field(default=None, description="失败时的错误描述")
# ---------------------------------------------------------------------------
# 好友申请自动通过
# ---------------------------------------------------------------------------
class FriendRequestItem(BaseModel):
"""好友申请条目API 响应用)。"""
stranger_wxid: str
nickname: str = ""
verify_message: str = ""
scene: str = ""
create_time: int
class FriendRequestsResponse(BaseModel):
"""好友申请列表响应。"""
requests: list[FriendRequestItem] = Field(default_factory=list)
total: int = 0
class AcceptFriendRequest(BaseModel):
"""手动通过好友申请请求。"""
stranger_wxid: str = Field(description="申请人 wxid含 @stranger 后缀)")
nickname: Optional[str] = Field(default=None, description="申请人昵称UI 定位用)")
class AcceptFriendResponse(BaseModel):
"""通过好友申请响应。"""
success: bool = Field(default=False)
error: Optional[str] = Field(default=None)
class AutoAcceptStatus(BaseModel):
"""自动通过运行状态。"""
running: bool
enabled: bool
processed_count: int = 0
accepted_count: int = 0
rejected_count: int = 0
last_processed_time: Optional[int] = None
class AcceptRuleConfig(BaseModel):
"""自动通过规则配置。"""
enabled: bool = Field(default=False, description="全局开关")
accept_all: bool = Field(default=False, description="通过所有申请(忽略以下规则)")
whitelist_wxids: list[str] = Field(default_factory=list, description="白名单 wxid 列表")
whitelist_nicknames: list[str] = Field(default_factory=list, description="白名单昵称列表(精确匹配)")
keywords: list[str] = Field(default_factory=list, description="验证消息关键词列表(子串匹配)")
blacklist_wxids: list[str] = Field(default_factory=list, description="黑名单 wxid 列表")
blacklist_nicknames: list[str] = Field(default_factory=list, description="黑名单昵称列表")
allow_scenes: list[str] = Field(default_factory=list, description="允许的场景(空=不限制)")
class AcceptDecision(enum.Enum):
"""规则匹配决策结果。"""
ACCEPT = "accept"
REJECT = "reject"
SKIP = "skip"
class AcceptRuleEngine:
"""好友申请自动通过规则引擎。
"黑名单 → 场景 → 白名单 → 关键词 → SKIP"优先级决策,
配置热更新受 asyncio.Lock 保护。
"""
def __init__(self, config: AcceptRuleConfig) -> None:
self._config = config
self._lock = asyncio.Lock()
async def get_config(self) -> AcceptRuleConfig:
"""获取当前规则配置(供 API 查询)。"""
async with self._lock:
return self._config
async def evaluate(self, req: "FriendRequestInfo") -> AcceptDecision:
async with self._lock:
cfg = self._config
if not cfg.enabled:
return AcceptDecision.SKIP
if cfg.accept_all:
return AcceptDecision.ACCEPT
# 黑名单优先
if req.stranger_wxid in cfg.blacklist_wxids:
return AcceptDecision.REJECT
if req.nickname in cfg.blacklist_nicknames:
return AcceptDecision.REJECT
# 场景过滤
if cfg.allow_scenes and req.scene not in cfg.allow_scenes:
return AcceptDecision.SKIP
# 白名单
if req.stranger_wxid in cfg.whitelist_wxids:
return AcceptDecision.ACCEPT
if req.nickname in cfg.whitelist_nicknames:
return AcceptDecision.ACCEPT
# 关键词
if cfg.keywords:
for kw in cfg.keywords:
if kw in req.verify_message:
return AcceptDecision.ACCEPT
return AcceptDecision.SKIP
async def update_config(self, config: AcceptRuleConfig) -> None:
async with self._lock:
self._config = config