Files
wechatauto-replica/wechatauto/msgs/base.py
T

286 lines
8.9 KiB
Python

from wechatauto import uia
from wechatauto.ui.component import (
Menu,
SelectContactWnd
)
from wechatauto.utils import uilock
from wechatauto.param import WxParam, WxResponse, PROJECT_NAME
from abc import ABC, abstractmethod
from typing import (
Dict,
List,
Union,
Any,
TYPE_CHECKING,
Iterator,
Tuple
)
from hashlib import md5
if TYPE_CHECKING:
from wechatauto.ui.chatbox import ChatBox
def truncate_string(s: str, n: int = 8) -> str:
s = s.replace('\n', '').strip()
return s if len(s) <= n else s[:n] + '...'
class Message:
"""消息对象基类
该类不会直接实例化,而是作为所有消息类型的基类提供
常用的工具方法。实际的属性均由子类在 ``__init__`` 中
动态注入。
"""
_EXCLUDE_FIELDS = {"control", "parent", "root"}
# region --- 迭代/映射相关 -------------------------------------------------
def _iter_public_items(self) -> Iterator[Tuple[str, Any]]:
"""遍历当前消息可公开的字段"""
if not hasattr(self, "__dict__"):
return
seen = set()
for key, value in self.__dict__.items():
if key.startswith("_") or key in self._EXCLUDE_FIELDS:
continue
if key == "hash" and not WxParam.MESSAGE_HASH:
continue
seen.add(key)
yield key, value
# 补充类级字段(如 type/attr),保证 to_dict/match 可用
for key in ('type', 'attr'):
if key in seen:
continue
if any(key in cls.__dict__ for cls in type(self).__mro__):
yield key, getattr(self, key)
def __iter__(self) -> Iterator[str]:
for key, _ in self._iter_public_items():
yield key
def __len__(self) -> int:
return sum(1 for _ in self._iter_public_items())
def __getitem__(self, item: str) -> Any:
for key, value in self._iter_public_items():
if key == item:
return value
raise KeyError(item)
def __contains__(self, key: object) -> bool:
if not isinstance(key, str):
return False
return any(field == key for field, _ in self._iter_public_items())
# endregion ----------------------------------------------------------------
# region --- 字段访问 -------------------------------------------------------
def keys(self) -> Tuple[str, ...]:
return tuple(key for key, _ in self._iter_public_items())
def values(self) -> Tuple[Any, ...]:
return tuple(value for _, value in self._iter_public_items())
def items(self) -> Tuple[Tuple[str, Any], ...]:
return tuple(self._iter_public_items())
def get(self, key: str, default: Any = None) -> Any:
for field, value in self._iter_public_items():
if field == key:
return value
return default
def to_dict(self) -> Dict[str, Any]:
return dict(self._iter_public_items())
def copy(self) -> Dict[str, Any]:
return self.to_dict().copy()
# endregion ----------------------------------------------------------------
# region --- 状态判断 -------------------------------------------------------
def match(self, **conditions: Any) -> bool:
"""判断当前消息是否同时满足给定的字段条件"""
data = self.to_dict()
return all(data.get(key) == value for key, value in conditions.items())
@property
def is_self(self) -> bool:
return getattr(self, "attr", None) == "self"
@property
def is_friend(self) -> bool:
return getattr(self, "attr", None) == "friend"
@property
def is_system(self) -> bool:
return getattr(self, "attr", None) == "system"
# endregion ----------------------------------------------------------------
# region --- 魔术方法 -------------------------------------------------------
def __str__(self) -> str:
content = getattr(self, "content", None)
if content is None:
return super().__str__()
return str(content)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Message):
return NotImplemented
self_id = getattr(self, "id", None)
other_id = getattr(other, "id", None)
if self_id is not None and other_id is not None:
return self_id == other_id
if WxParam.MESSAGE_HASH:
return getattr(self, "hash", None) == getattr(other, "hash", None)
return self is other
def __hash__(self) -> int:
msg_id = getattr(self, "id", None)
if msg_id is not None:
return hash(msg_id)
if WxParam.MESSAGE_HASH:
return hash(getattr(self, "hash", None))
return super().__hash__()
# endregion ----------------------------------------------------------------
class BaseMessage(Message, ABC):
type: str = 'base'
attr: str = 'base'
control: uia.Control
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
self.parent = parent
self.control = control
self.direction = additonal_attr.get('direction', None)
self.distince = additonal_attr.get('direction_distence', None)
self.root = parent.root
self.id = self.control.runtimeid
self.content = self.control.Name
rect = self.control.BoundingRectangle
self.hash_text = f'({rect.height()},{rect.width()}){self.content}'
self.hash = md5(self.hash_text.encode()).hexdigest()
def __repr__(self):
cls_name = self.__class__.__name__
content = truncate_string(self.content)
return f"<{PROJECT_NAME} - {cls_name}({content}) at {hex(id(self))}>"
def roll_into_view(self):
if not self.exists():
return WxResponse.failure('消息目标控件不存在,无法滚动至显示窗口')
if uia.RollIntoView(
self.parent.msgbox,
self.control
) == 'not exist':
return WxResponse.failure('消息目标控件不存在,无法滚动至显示窗口')
return WxResponse.success('成功')
def exists(self):
if self.control.Exists(0) and self.control.BoundingRectangle.height() > 0:
return True
return False
class HumanMessage(BaseMessage, ABC):
attr = 'human'
def __init__(
self,
control: uia.Control,
parent: "ChatBox",
additonal_attr: Dict[str, Any] = {}
):
super().__init__(control, parent, additonal_attr)
@abstractmethod
def _click(self, x, y, right=False): ...
@abstractmethod
def _bias(self): ...
def click(self):
self._click(right=False, x=self._bias * 2, y=WxParam.DEFAULT_MESSAGE_YBIAS)
def right_click(self):
self._click(right=True, x=self._bias, y=WxParam.DEFAULT_MESSAGE_YBIAS)
@uilock
def select_option(self, option: str, timeout=2) -> WxResponse:
if not self.exists():
return WxResponse.failure('消息对象已失效')
self._click(right=True, x=self._bias * 2, y=WxParam.DEFAULT_MESSAGE_YBIAS)
if menu := Menu(self, timeout):
return menu.select(option)
else:
return WxResponse.failure('操作失败')
@uilock
def forward(
self,
targets: Union[List[str], str],
timeout: int = 3,
interval: float = 0.1
) -> WxResponse:
"""转发消息
Args:
targets (Union[List[str], str]): 目标用户列表
timeout (int, optional): 超时时间,单位为秒,若为None则不启用超时设置
interval (float): 选择联系人时间间隔
Returns:
WxResponse: 调用结果
"""
if not self.exists():
return WxResponse.failure('消息对象已失效')
if not self.select_option('转发...', timeout=timeout):
return WxResponse.failure('当前消息无法转发')
select_wnd = SelectContactWnd(self)
return select_wnd.send(targets, interval=interval)
@uilock
def quote(
self, text: str,
at: Union[List[str], str] = None,
timeout: int = 3
) -> WxResponse:
"""引用消息
Args:
text (str): 引用内容
at (List[str], optional): @用户列表
timeout (int, optional): 超时时间,单位为秒,若为None则不启用超时设置
Returns:
WxResponse: 调用结果
"""
if not self.exists():
return WxResponse.failure('消息对象已失效')
if not self.select_option('引用', timeout=timeout):
return WxResponse.failure('当前消息无法引用')
if at:
self.parent.input_at(at)
return self.parent.send_text(text)