feat: Initialize wxauto WeChat automation project with job extraction tools
- Add wxauto package with WeChat UI automation and message handling capabilities - Implement job_extractor.py for automated job posting extraction from WeChat groups - Add job_extractor_gui.py providing graphical interface for job extraction tool - Create comprehensive documentation in Chinese covering GUI usage, multi-group support, and quick start guides - Add build configuration files (build_exe.py, build_exe.spec) for packaging as standalone executable - Include utility scripts for WeChat interaction (auto_send_msg.py, get_history.py, receive_file_transfer.py) - Add project configuration files (pyproject.toml, setup.cfg, requirements.txt) - Include test files (test_api.py, test_com_fix.py) for API and compatibility validation - Add Apache 2.0 LICENSE and comprehensive README documentation - Configure .gitignore to exclude build artifacts, logs, and temporary files
This commit is contained in:
8
wxauto/ui/__init__.py
Normal file
8
wxauto/ui/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .base import BaseUIWnd, BaseUISubWnd
|
||||
from . import (
|
||||
chatbox,
|
||||
component,
|
||||
main,
|
||||
navigationbox,
|
||||
sessionbox
|
||||
)
|
||||
56
wxauto/ui/base.py
Normal file
56
wxauto/ui/base.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from wxauto import uiautomation as uia
|
||||
from wxauto.param import PROJECT_NAME
|
||||
from wxauto.logger import wxlog
|
||||
from abc import ABC, abstractmethod
|
||||
import win32gui
|
||||
from typing import Union
|
||||
import time
|
||||
|
||||
class BaseUIWnd(ABC):
|
||||
_ui_cls_name: str = None
|
||||
_ui_name: str = None
|
||||
control: uia.Control
|
||||
|
||||
@abstractmethod
|
||||
def _lang(self, text: str):pass
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{PROJECT_NAME} - {self.__class__.__name__} at {hex(id(self))}>"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.control == other.control
|
||||
|
||||
def __bool__(self):
|
||||
return self.exists()
|
||||
|
||||
def _show(self):
|
||||
if hasattr(self, 'HWND'):
|
||||
win32gui.ShowWindow(self.HWND, 1)
|
||||
win32gui.SetWindowPos(self.HWND, -1, 0, 0, 0, 0, 3)
|
||||
win32gui.SetWindowPos(self.HWND, -2, 0, 0, 0, 0, 3)
|
||||
self.control.SwitchToThisWindow()
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.control.SendKeys('{Esc}')
|
||||
except:
|
||||
pass
|
||||
|
||||
def exists(self, wait=0):
|
||||
try:
|
||||
result = self.control.Exists(wait)
|
||||
return result
|
||||
except:
|
||||
return False
|
||||
|
||||
class BaseUISubWnd(BaseUIWnd):
|
||||
root: BaseUIWnd
|
||||
parent: None
|
||||
|
||||
def _lang(self, text: str):
|
||||
if getattr(self, 'parent'):
|
||||
return self.parent._lang(text)
|
||||
else:
|
||||
return self.root._lang(text)
|
||||
|
||||
|
||||
455
wxauto/ui/chatbox.py
Normal file
455
wxauto/ui/chatbox.py
Normal file
@@ -0,0 +1,455 @@
|
||||
from .base import BaseUISubWnd
|
||||
from wxauto.ui.component import (
|
||||
CMenuWnd,
|
||||
)
|
||||
from wxauto.param import (
|
||||
WxParam,
|
||||
WxResponse,
|
||||
)
|
||||
from wxauto.languages import *
|
||||
from wxauto.utils import (
|
||||
SetClipboardText,
|
||||
SetClipboardFiles,
|
||||
GetAllWindowExs,
|
||||
)
|
||||
from wxauto.msgs import parse_msg
|
||||
from wxauto import uiautomation as uia
|
||||
from wxauto.logger import wxlog
|
||||
from wxauto.uiautomation import Control
|
||||
from wxauto.utils.tools import roll_into_view
|
||||
import time
|
||||
import os
|
||||
|
||||
USED_MSG_IDS = {}
|
||||
|
||||
class ChatBox(BaseUISubWnd):
|
||||
def __init__(self, control: uia.Control, parent):
|
||||
self.control: Control = control
|
||||
self.root = parent
|
||||
self.parent = parent # `wx` or `chat`
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
self.msgbox = self.control.ListControl(Name=self._lang("消息"))
|
||||
# if not self.msgbox.Exists(0):
|
||||
# return
|
||||
self.editbox = self.control.EditControl()
|
||||
self.sendbtn = self.control.ButtonControl(Name=self._lang('发送'))
|
||||
self.tools = self.control.PaneControl().ToolBarControl()
|
||||
# self.id = self.msgbox.runtimeid
|
||||
self._empty = False # 用于记录是否为完全没有聊天记录的窗口,因为这种窗口之前有不会触发新消息判断的问题
|
||||
if (cid := self.id) and cid not in USED_MSG_IDS:
|
||||
USED_MSG_IDS[self.id] = tuple((i.runtimeid for i in self.msgbox.GetChildren()))
|
||||
if not USED_MSG_IDS[cid]:
|
||||
self._empty = True
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return WECHAT_CHAT_BOX.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def _update_used_msg_ids(self):
|
||||
USED_MSG_IDS[self.id] = tuple((i.runtimeid for i in self.msgbox.GetChildren()))
|
||||
|
||||
|
||||
def _open_chat_more_info(self):
|
||||
for chatinfo_control, depth in uia.WalkControl(self.control):
|
||||
if chatinfo_control.Name == self._lang('聊天信息'):
|
||||
chatinfo_control.Click()
|
||||
break
|
||||
else:
|
||||
return WxResponse.failure('未找到聊天信息按钮')
|
||||
return ChatRoomDetailWnd(self)
|
||||
|
||||
|
||||
def _activate_editbox(self):
|
||||
if not self.editbox.HasKeyboardFocus:
|
||||
self.editbox.MiddleClick()
|
||||
|
||||
@property
|
||||
def who(self):
|
||||
if hasattr(self, '_who'):
|
||||
return self._who
|
||||
self._who = self.editbox.Name
|
||||
return self._who
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
if self.msgbox.Exists(0):
|
||||
return self.msgbox.runtimeid
|
||||
return None
|
||||
|
||||
@property
|
||||
def used_msg_ids(self):
|
||||
return USED_MSG_IDS[self.id]
|
||||
|
||||
def get_info(self):
|
||||
chat_info = {}
|
||||
walk = uia.WalkControl(self.control)
|
||||
for chat_name_control, depth in walk:
|
||||
if isinstance(chat_name_control, uia.TextControl):
|
||||
break
|
||||
if (
|
||||
not isinstance(chat_name_control, uia.TextControl)
|
||||
or depth < 8
|
||||
):
|
||||
return {}
|
||||
|
||||
# chat_name_control = self.control.GetProgenyControl(11)
|
||||
chat_name_control_list = chat_name_control.GetParentControl().GetChildren()
|
||||
chat_name_control_count = len(chat_name_control_list)
|
||||
|
||||
if chat_name_control_count == 1:
|
||||
if self.control.ButtonControl(Name='公众号主页', searchDepth=9).Exists(0):
|
||||
chat_info['chat_type'] = 'official'
|
||||
else:
|
||||
chat_info['chat_type'] = 'friend'
|
||||
chat_info['chat_name'] = chat_name_control.Name
|
||||
elif chat_name_control_count >= 2:
|
||||
try:
|
||||
second_text = chat_name_control_list[1].Name
|
||||
if second_text.startswith('@'):
|
||||
chat_info['company'] = second_text
|
||||
chat_info['chat_type'] = 'service'
|
||||
chat_info['chat_name'] = chat_name_control.Name
|
||||
else:
|
||||
chat_info['group_member_count'] =\
|
||||
int(second_text.replace('(', '').replace(')', ''))
|
||||
chat_info['chat_type'] = 'group'
|
||||
chat_info['chat_name'] =\
|
||||
chat_name_control.Name.replace(second_text, '')
|
||||
except:
|
||||
chat_info['chat_type'] = 'friend'
|
||||
chat_info['chat_name'] = chat_name_control.Name
|
||||
|
||||
ori_chat_name_control =\
|
||||
chat_name_control.GetParentControl().\
|
||||
GetParentControl().TextControl(searchDepth=1)
|
||||
if ori_chat_name_control.Exists(0):
|
||||
chat_info['chat_remark'] = chat_info['chat_name']
|
||||
chat_info['chat_name'] = ori_chat_name_control.Name
|
||||
self._info = chat_info
|
||||
return chat_info
|
||||
|
||||
|
||||
def input_at(self, at_list):
|
||||
self._show()
|
||||
if isinstance(at_list, str):
|
||||
at_list = [at_list]
|
||||
self._activate_editbox()
|
||||
for friend in at_list:
|
||||
self.editbox.SendKeys('@'+friend.replace(' ', ''))
|
||||
atmenu = AtMenu(self)
|
||||
atmenu.select(friend)
|
||||
|
||||
|
||||
def clear_edit(self):
|
||||
self._show()
|
||||
self.editbox.Click()
|
||||
self.editbox.SendKeys('{Ctrl}a', waitTime=0)
|
||||
self.editbox.SendKeys('{DELETE}')
|
||||
|
||||
|
||||
def send_text(self, content: str):
|
||||
self._show()
|
||||
t0 = time.time()
|
||||
while True:
|
||||
if time.time() - t0 > 10:
|
||||
return WxResponse.failure(f'Timeout --> {self.who} - {content}')
|
||||
SetClipboardText(content)
|
||||
self._activate_editbox()
|
||||
self.editbox.SendKeys('{Ctrl}v')
|
||||
if self.editbox.GetValuePattern().Value.replace('', '').strip():
|
||||
break
|
||||
self.editbox.SendKeys('{Ctrl}v')
|
||||
if self.editbox.GetValuePattern().Value.replace('', '').strip():
|
||||
break
|
||||
self.editbox.RightClick()
|
||||
menu = CMenuWnd(self)
|
||||
menu.select('粘贴')
|
||||
if self.editbox.GetValuePattern().Value.replace('', '').strip():
|
||||
break
|
||||
t0 = time.time()
|
||||
while self.editbox.GetValuePattern().Value:
|
||||
if time.time() - t0 > 10:
|
||||
return WxResponse.failure(f'Timeout --> {self.who} - {content}')
|
||||
self._activate_editbox()
|
||||
|
||||
self.sendbtn.Click()
|
||||
if not self.editbox.GetValuePattern().Value:
|
||||
return WxResponse.success(f"success")
|
||||
elif not self.editbox.GetValuePattern().Value.replace('', '').strip():
|
||||
return self.send_text(content)
|
||||
|
||||
def send_msg(self, content: str, clear: bool=True, at=None):
|
||||
if not content and not at:
|
||||
return WxResponse.failure(f"参数 `content` 和 `at` 不能同时为空")
|
||||
|
||||
if clear:
|
||||
self.clear_edit()
|
||||
if at:
|
||||
self.input_at(at)
|
||||
|
||||
return self.send_text(content)
|
||||
|
||||
|
||||
def send_file(self, file_path):
|
||||
self._show()
|
||||
if isinstance(file_path, str):
|
||||
file_path = [file_path]
|
||||
file_path = [os.path.abspath(f) for f in file_path]
|
||||
SetClipboardFiles(file_path)
|
||||
self._activate_editbox()
|
||||
self.editbox.SendKeys('{Ctrl}v')
|
||||
self.sendbtn.Click()
|
||||
if self.editbox.GetValuePattern().Value:
|
||||
return WxResponse.fail("发送失败,请重试")
|
||||
return WxResponse.success()
|
||||
|
||||
def load_more(self, interval=0.3):
|
||||
self._show()
|
||||
msg_len = len(self.msgbox.GetChildren())
|
||||
loadmore = self.msgbox.GetChildren()[0]
|
||||
loadmore_top = loadmore.BoundingRectangle.top
|
||||
while True:
|
||||
if len(self.msgbox.GetChildren()) > msg_len:
|
||||
isload = True
|
||||
break
|
||||
else:
|
||||
msg_len = len(self.msgbox.GetChildren())
|
||||
self.msgbox.WheelUp(wheelTimes=10)
|
||||
time.sleep(interval)
|
||||
if self.msgbox.GetChildren()[0].BoundingRectangle.top == loadmore_top\
|
||||
and len(self.msgbox.GetChildren()) == msg_len:
|
||||
isload = False
|
||||
break
|
||||
else:
|
||||
loadmore_top = self.msgbox.GetChildren()[0].BoundingRectangle.top
|
||||
|
||||
self.msgbox.WheelUp(wheelTimes=1, waitTime=0.1)
|
||||
if isload:
|
||||
return WxResponse.success()
|
||||
else:
|
||||
return WxResponse.failure("没有更多消息了")
|
||||
|
||||
|
||||
def get_msgs(self):
|
||||
if self.msgbox.Exists(0):
|
||||
return [
|
||||
parse_msg(msg_control, self)
|
||||
for msg_control
|
||||
in self.msgbox.GetChildren()
|
||||
if msg_control.ControlTypeName == 'ListItemControl'
|
||||
]
|
||||
return []
|
||||
|
||||
def get_new_msgs(self):
|
||||
if not self.msgbox.Exists(0):
|
||||
return []
|
||||
msg_controls = self.msgbox.GetChildren()
|
||||
now_msg_ids = tuple((i.runtimeid for i in msg_controls))
|
||||
if not now_msg_ids: # 当前没有消息id
|
||||
return []
|
||||
if self._empty and self.used_msg_ids:
|
||||
self._empty = False
|
||||
if not self._empty and (
|
||||
(not self.used_msg_ids and now_msg_ids) # 没有使用过的消息id,但当前有消息id
|
||||
or now_msg_ids[-1] == self.used_msg_ids[-1] # 当前最后一条消息id和上次一样
|
||||
or not set(now_msg_ids)&set(self.used_msg_ids) # 当前消息id和上次没有交集
|
||||
):
|
||||
# wxlog.debug('没有新消息')
|
||||
return []
|
||||
|
||||
used_msg_ids_set = set(self.used_msg_ids)
|
||||
last_one_msgid = max(
|
||||
(x for x in now_msg_ids if x in used_msg_ids_set),
|
||||
key=self.used_msg_ids.index, default=None
|
||||
)
|
||||
new1 = [x for x in now_msg_ids if x not in used_msg_ids_set]
|
||||
new2 = now_msg_ids[now_msg_ids.index(last_one_msgid) + 1 :]\
|
||||
if last_one_msgid is not None else []
|
||||
new = [i for i in new1 if i in new2] if new2 else new1
|
||||
USED_MSG_IDS[self.id] = tuple(self.used_msg_ids + tuple(new))[-100:]
|
||||
new_controls = [i for i in msg_controls if i.runtimeid in new]
|
||||
self.msgbox.MiddleClick()
|
||||
return [
|
||||
parse_msg(msg_control, self)
|
||||
for msg_control
|
||||
in new_controls
|
||||
if msg_control.ControlTypeName == 'ListItemControl'
|
||||
]
|
||||
def get_msg_by_id(self, msg_id: str):
|
||||
if not self.msgbox.Exists(0):
|
||||
return []
|
||||
msg_controls = self.msgbox.GetChildren()
|
||||
if control_list := [i for i in msg_controls if i.runtimeid == msg_id]:
|
||||
return parse_msg(control_list[0], self)
|
||||
|
||||
def _get_tail_after_nth_match(self, msgs, last_msg, n):
|
||||
matches = [
|
||||
i for i, msg in reversed(list(enumerate(msgs)))
|
||||
if msg.content == last_msg
|
||||
]
|
||||
if len(matches) >= n:
|
||||
wxlog.debug(f'匹配到基准消息:{last_msg}')
|
||||
else:
|
||||
split_last_msg = last_msg.split(':')
|
||||
nickname = split_last_msg[0]
|
||||
content = ''.join(split_last_msg[1:])
|
||||
matches = [
|
||||
i for i, msg in reversed(list(enumerate(msgs)))
|
||||
if msg.content == content
|
||||
and msg.sender_remark == nickname
|
||||
]
|
||||
if len(matches) >= n:
|
||||
wxlog.debug(f'匹配到基准消息:<{nickname}> {content}')
|
||||
else:
|
||||
wxlog.debug(f"未匹配到基准消息,以最后一条消息为基准:{msgs[-1].content}")
|
||||
matches = [
|
||||
i for i, msg in reversed(list(enumerate(msgs)))
|
||||
if msg.attr in ('self', 'friend')
|
||||
]
|
||||
try:
|
||||
index = matches[n - 1]
|
||||
return msgs[index:]
|
||||
except IndexError:
|
||||
wxlog.debug(f"未匹配到第{n}条消息,返回空列表")
|
||||
return []
|
||||
|
||||
def get_next_new_msgs(self, count=None, last_msg=None):
|
||||
# 1. 消息列表不存在,则返回空列表
|
||||
if not self.msgbox.Exists(0):
|
||||
wxlog.debug('消息列表不存在,返回空列表')
|
||||
return []
|
||||
|
||||
# 2. 判断是否有新消息按钮,有的话点一下
|
||||
load_new_button = self.control.ButtonControl(RegexName=self._lang('re_新消息按钮'))
|
||||
if load_new_button.Exists(0):
|
||||
self._show()
|
||||
wxlog.debug('检测到新消息按钮,点击加载新消息')
|
||||
load_new_button.Click()
|
||||
time.sleep(0.5)
|
||||
msg_controls = self.msgbox.GetChildren()
|
||||
USED_MSG_IDS[self.id] = tuple((i.runtimeid for i in msg_controls))
|
||||
msgs = [
|
||||
parse_msg(msg_control, self)
|
||||
for msg_control
|
||||
in msg_controls
|
||||
if msg_control.ControlTypeName == 'ListItemControl'
|
||||
]
|
||||
|
||||
# 3. 如果有“以下是新消息”标志,则直接返回该标志下的所有消息即可
|
||||
index = next((
|
||||
i for i, msg in enumerate(msgs)
|
||||
if self._lang('以下为新消息') == msg.content
|
||||
), None)
|
||||
if index is not None:
|
||||
wxlog.debug('获取以下是新消息下的所有消息')
|
||||
return msgs[index:]
|
||||
|
||||
# 4. 根据会话列表传入的消息数量和最后一条新消息内容来判断新消息
|
||||
if count and last_msg:
|
||||
# index = next((
|
||||
# i for i, msg in enumerate(msgs[::-1])
|
||||
# if last_msg == msg.content
|
||||
# ), None)
|
||||
|
||||
# if index is not None:
|
||||
wxlog.debug(f'获取{count}条新消息,基准消息内容为:{last_msg}')
|
||||
return self._get_tail_after_nth_match(msgs, last_msg, count)
|
||||
|
||||
def get_group_members(self):
|
||||
self._show()
|
||||
roominfoWnd = self._open_chat_more_info()
|
||||
return roominfoWnd.get_group_members()
|
||||
|
||||
|
||||
class ChatRoomDetailWnd(BaseUISubWnd):
|
||||
_ui_cls_name: str = 'SessionChatRoomDetailWnd'
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.root = parent.root
|
||||
self.control = self.root.control.Control(ClassName=self._ui_cls_name, searchDepth=1)
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return CHATROOM_DETAIL_WINDOW.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def _edit(self, key, value):
|
||||
wxlog.debug(f'修改{key}为`{value}`')
|
||||
btn = self.control.TextControl(Name=key).GetParentControl().ButtonControl(Name=key)
|
||||
if btn.Exists(0):
|
||||
roll_into_view(self.control, btn)
|
||||
btn.Click()
|
||||
else:
|
||||
wxlog.debug(f'当前非群聊,无法修改{key}')
|
||||
return WxResponse.failure(f'Not a group chat, cannot modify `{key}`')
|
||||
while True:
|
||||
edit_hwnd_list = [
|
||||
i[0]
|
||||
for i in GetAllWindowExs(self.control.NativeWindowHandle)
|
||||
if i[1] == 'EditWnd'
|
||||
]
|
||||
if edit_hwnd_list:
|
||||
edit_hwnd = edit_hwnd_list[0]
|
||||
break
|
||||
btn.Click()
|
||||
edit_win32 = uia.Win32(edit_hwnd)
|
||||
edit_win32.shortcut_select_all()
|
||||
edit_win32.send_keys_shortcut('{DELETE}')
|
||||
edit_win32.input(value)
|
||||
edit_win32.send_keys_shortcut('{ENTER}')
|
||||
return WxResponse.success()
|
||||
|
||||
def get_group_members(self, control=False):
|
||||
"""获取群成员"""
|
||||
more = self.control.ButtonControl(Name=self._lang('查看更多'), searchDepth=8)
|
||||
if more.Exists(0.5):
|
||||
more.Click()
|
||||
members = [i for i in self.control.ListControl(Name=self._lang('聊天成员')).GetChildren()]
|
||||
while members[-1].Name in [self._lang('添加'), self._lang('移出')]:
|
||||
members = members[:-1]
|
||||
if control:
|
||||
return members
|
||||
member_names = [i.Name for i in members]
|
||||
self.close()
|
||||
return member_names
|
||||
|
||||
class GroupMemberElement:
|
||||
def __init__(self, control, parent) -> None:
|
||||
self.control = control
|
||||
self.parent = parent
|
||||
self.root = self.parent.root
|
||||
self.nickname = self.control.Name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<wxauto Group Member Element at {hex(id(self))}>"
|
||||
|
||||
|
||||
class AtMenu(BaseUISubWnd):
|
||||
_ui_cls_name = 'ChatContactMenu'
|
||||
|
||||
def __init__(self, parent):
|
||||
self.root = parent.root
|
||||
self.control = parent.parent.control.PaneControl(ClassName='ChatContactMenu')
|
||||
# self.control.Exists(1)
|
||||
|
||||
def clear(self, friend):
|
||||
if self.exists():
|
||||
self.control.SendKeys('{ESC}')
|
||||
for _ in range(len(friend)+1):
|
||||
self.root.chatbox.editbox.SendKeys('{BACK}')
|
||||
|
||||
def select(self, friend):
|
||||
friend_ = friend.replace(' ', '')
|
||||
if self.exists():
|
||||
ateles = self.control.ListControl().GetChildren()
|
||||
if len(ateles) == 1:
|
||||
ateles[0].Click()
|
||||
else:
|
||||
atele = self.control.ListItemControl(Name=friend)
|
||||
if atele.Exists(0):
|
||||
roll_into_view(self.control, atele)
|
||||
atele.Click()
|
||||
else:
|
||||
self.clear(friend_)
|
||||
else:
|
||||
self.clear(friend_)
|
||||
407
wxauto/ui/component.py
Normal file
407
wxauto/ui/component.py
Normal file
@@ -0,0 +1,407 @@
|
||||
from wxauto.utils import (
|
||||
FindWindow,
|
||||
SetClipboardText,
|
||||
ReadClipboardData,
|
||||
GetAllWindows,
|
||||
GetWindowRect,
|
||||
capture
|
||||
)
|
||||
from wxauto.param import (
|
||||
WxParam,
|
||||
WxResponse,
|
||||
)
|
||||
from wxauto.languages import *
|
||||
from .base import (
|
||||
BaseUISubWnd
|
||||
)
|
||||
from wxauto.logger import wxlog
|
||||
from wxauto.uiautomation import (
|
||||
ControlFromHandle,
|
||||
)
|
||||
from wxauto.utils.tools import (
|
||||
get_file_dir,
|
||||
roll_into_view,
|
||||
)
|
||||
from PIL import Image
|
||||
from wxauto import uiautomation as uia
|
||||
import traceback
|
||||
import shutil
|
||||
import time
|
||||
|
||||
class EditBox:
|
||||
...
|
||||
|
||||
class SelectContactWnd(BaseUISubWnd):
|
||||
"""选择联系人窗口"""
|
||||
_ui_cls_name = 'SelectContactWnd'
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.root = parent.root
|
||||
hwnd = FindWindow(self._ui_cls_name, timeout=1)
|
||||
if hwnd:
|
||||
self.control = ControlFromHandle(hwnd)
|
||||
else:
|
||||
self.control = parent.root.control.PaneControl(ClassName=self._ui_cls_name, searchDepth=1)
|
||||
|
||||
self.editbox = self.control.EditControl()
|
||||
|
||||
def send(self, target):
|
||||
if isinstance(target, str):
|
||||
SetClipboardText(target)
|
||||
while not self.editbox.HasKeyboardFocus:
|
||||
self.editbox.Click()
|
||||
time.sleep(0.1)
|
||||
self.editbox.SendKeys('{Ctrl}a')
|
||||
self.editbox.SendKeys('{Ctrl}v')
|
||||
checkbox = self.control.ListControl().CheckBoxControl()
|
||||
if checkbox.Exists(1):
|
||||
checkbox.Click()
|
||||
self.control.ButtonControl(Name='发送').Click()
|
||||
return WxResponse.success()
|
||||
else:
|
||||
self.control.SendKeys('{Esc}')
|
||||
wxlog.debug(f'未找到好友:{target}')
|
||||
return WxResponse.failure(f'未找到好友:{target}')
|
||||
|
||||
elif isinstance(target, list):
|
||||
n = 0
|
||||
fail = []
|
||||
multiselect = self.control.ButtonControl(Name='多选')
|
||||
if multiselect.Exists(0):
|
||||
multiselect.Click()
|
||||
for i in target:
|
||||
SetClipboardText(i)
|
||||
while not self.editbox.HasKeyboardFocus:
|
||||
self.editbox.Click()
|
||||
time.sleep(0.1)
|
||||
self.editbox.SendKeys('{Ctrl}a')
|
||||
self.editbox.SendKeys('{Ctrl}v')
|
||||
checkbox = self.control.ListControl().CheckBoxControl()
|
||||
if checkbox.Exists(1):
|
||||
checkbox.Click()
|
||||
n += 1
|
||||
else:
|
||||
fail.append(i)
|
||||
wxlog.debug(f"未找到转发对象:{i}")
|
||||
if n > 0:
|
||||
self.control.ButtonControl(RegexName='分别发送(\d+)').Click()
|
||||
if n == len(target):
|
||||
return WxResponse.success()
|
||||
else:
|
||||
return WxResponse.success('存在未转发成功名单', data=fail)
|
||||
else:
|
||||
self.control.SendKeys('{Esc}')
|
||||
wxlog.debug(f'所有好友均未未找到:{target}')
|
||||
return WxResponse.failure(f'所有好友均未未找到:{target}')
|
||||
|
||||
class CMenuWnd(BaseUISubWnd):
|
||||
_ui_cls_name = 'CMenuWnd'
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.root = parent.root
|
||||
if menulist := [i for i in GetAllWindows() if 'CMenuWnd' in i]:
|
||||
self.control = uia.ControlFromHandle(menulist[0][0])
|
||||
else:
|
||||
self.control = self.root.control.MenuControl(ClassName=self._ui_cls_name)
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return MENU_OPTIONS.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
@property
|
||||
def option_controls(self):
|
||||
return self.control.ListControl().GetChildren()
|
||||
|
||||
@property
|
||||
def option_names(self):
|
||||
return [c.Name for c in self.option_controls]
|
||||
|
||||
def select(self, item):
|
||||
if not self.exists(0):
|
||||
return WxResponse.failure('菜单窗口不存在')
|
||||
if isinstance(item, int):
|
||||
self.option_controls[item].Click()
|
||||
return WxResponse.success()
|
||||
|
||||
item = self._lang(item)
|
||||
for c in self.option_controls:
|
||||
if c.Name == item:
|
||||
c.Click()
|
||||
return WxResponse.success()
|
||||
if self.exists(0):
|
||||
self.close()
|
||||
return WxResponse.failure(f'未找到选项:{item}')
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.control.SendKeys('{ESC}')
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
class NetErrInfoTipsBarWnd(BaseUISubWnd):
|
||||
_ui_cls_name = 'NetErrInfoTipsBarWnd'
|
||||
|
||||
def __init__(self, parent):
|
||||
self.control = parent.root.control.PaneControl(ClassName=self._ui_cls_name)
|
||||
|
||||
def __bool__(self):
|
||||
return self.exists(0)
|
||||
|
||||
class WeChatImage(BaseUISubWnd):
|
||||
_ui_cls_name = 'ImagePreviewWnd'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.hwnd = FindWindow(classname=self._ui_cls_name)
|
||||
if self.hwnd:
|
||||
self.control = ControlFromHandle(self.hwnd)
|
||||
self.type = 'image'
|
||||
if self.control.PaneControl(ClassName='ImagePreviewLayerWnd').Exists(0):
|
||||
self.type = 'video'
|
||||
MainControl1 = [i for i in self.control.GetChildren() if not i.ClassName][0]
|
||||
self.ToolsBox, self.PhotoBox = MainControl1.GetChildren()
|
||||
|
||||
# tools按钮
|
||||
self.t_previous = self.ToolsBox.ButtonControl(Name=self._lang('上一张'))
|
||||
self.t_next = self.ToolsBox.ButtonControl(Name=self._lang('下一张'))
|
||||
self.t_zoom = self.ToolsBox.ButtonControl(Name=self._lang('放大'))
|
||||
self.t_translate = self.ToolsBox.ButtonControl(Name=self._lang('翻译'))
|
||||
self.t_ocr = self.ToolsBox.ButtonControl(Name=self._lang('提取文字'))
|
||||
self.t_save = self.control.ButtonControl(Name=self._lang('另存为...'))
|
||||
self.t_qrcode = self.ToolsBox.ButtonControl(Name=self._lang('识别图中二维码'))
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return IMAGE_WINDOW.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def ocr(self, wait=10):
|
||||
result = ''
|
||||
ctrls = self.PhotoBox.GetChildren()
|
||||
if len(ctrls) == 2:
|
||||
self.t_ocr.Click()
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < wait:
|
||||
ctrls = self.PhotoBox.GetChildren()
|
||||
if len(ctrls) == 3:
|
||||
TranslateControl = ctrls[-1]
|
||||
result = TranslateControl.TextControl().Name
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
self.t_ocr.Click()
|
||||
time.sleep(0.1)
|
||||
return result
|
||||
|
||||
def save(self, dir_path=None, timeout=10):
|
||||
"""保存图片/视频
|
||||
|
||||
Args:
|
||||
dir_path (str): 绝对路径,包括文件名和后缀,例如:"D:/Images/微信图片_xxxxxx.png"
|
||||
timeout (int, optional): 保存超时时间,默认10秒
|
||||
|
||||
Returns:
|
||||
str: 文件保存路径,即savepath
|
||||
"""
|
||||
if dir_path is None:
|
||||
dir_path = WxParam.DEFAULT_SAVE_PATH
|
||||
suffix = 'png' if self.type == 'image' else 'mp4'
|
||||
filename = f"wxauto_{self.type}_{time.strftime('%Y%m%d%H%M%S')}.{suffix}"
|
||||
filepath = get_file_dir(dir_path) / filename
|
||||
t0 = time.time()
|
||||
|
||||
SetClipboardText('')
|
||||
while True:
|
||||
if time.time() - t0 > timeout:
|
||||
if self.control.Exists(0):
|
||||
self.control.SendKeys('{Esc}')
|
||||
raise TimeoutError('下载超时')
|
||||
try:
|
||||
self.control.ButtonControl(Name=self._lang('更多')).Click()
|
||||
menu = self.control.MenuControl(ClassName='CMenuWnd')
|
||||
menu.MenuItemControl(Name=self._lang('复制')).Click()
|
||||
clipboard_data = ReadClipboardData()
|
||||
path = clipboard_data['15'][0]
|
||||
wxlog.debug(f"读取到图片/视频路径:{path}")
|
||||
break
|
||||
except:
|
||||
wxlog.debug(traceback.format_exc())
|
||||
time.sleep(0.1)
|
||||
shutil.copyfile(path, filepath)
|
||||
SetClipboardText('')
|
||||
if self.control.Exists(0):
|
||||
wxlog.debug("关闭图片窗口")
|
||||
self.control.SendKeys('{Esc}')
|
||||
return filepath
|
||||
|
||||
class NewFriendElement:
|
||||
def __init__(self, control, parent):
|
||||
self.parent = parent
|
||||
self.root = parent.root
|
||||
self.control = control
|
||||
self.name = self.control.Name
|
||||
self.msg = self.control.GetFirstChildControl().PaneControl(SearchDepth=1).GetChildren()[-1].TextControl().Name
|
||||
self.NewFriendsBox = self.root.chatbox.control.ListControl(Name='新的朋友').GetParentControl()
|
||||
self.status = self.control.GetFirstChildControl().GetChildren()[-1]
|
||||
self.acceptable = isinstance(self.status, uia.ButtonControl)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<wxauto New Friends Element at {hex(id(self))} ({self.name}: {self.msg})>"
|
||||
|
||||
def delete(self):
|
||||
wxlog.info(f'删除好友请求: {self.name}')
|
||||
roll_into_view(self.NewFriendsBox, self.control)
|
||||
self.control.RightClick()
|
||||
menu = CMenuWnd(self.root)
|
||||
menu.select('删除')
|
||||
|
||||
def reply(self, text):
|
||||
wxlog.debug(f'回复好友请求: {self.name}')
|
||||
roll_into_view(self.NewFriendsBox, self.control)
|
||||
self.control.Click()
|
||||
self.root.ChatBox.ButtonControl(Name='回复').Click()
|
||||
edit = self.root.ChatBox.EditControl()
|
||||
edit.Click()
|
||||
edit.SendKeys('{Ctrl}a')
|
||||
SetClipboardText(text)
|
||||
edit.SendKeys('{Ctrl}v')
|
||||
time.sleep(0.1)
|
||||
self.root.ChatBox.ButtonControl(Name='发送').Click()
|
||||
dialog = self.root.UiaAPI.PaneControl(ClassName='WeUIDialog')
|
||||
while edit.Exists(0):
|
||||
if dialog.Exists(0):
|
||||
systext = dialog.TextControl().Name
|
||||
wxlog.debug(f'系统提示: {systext}')
|
||||
dialog.SendKeys('{Esc}')
|
||||
self.root.ChatBox.ButtonControl(Name='').Click()
|
||||
return WxResponse.failure(msg=systext)
|
||||
time.sleep(0.1)
|
||||
self.root.ChatBox.ButtonControl(Name='').Click()
|
||||
return WxResponse.success()
|
||||
|
||||
def accept(self, remark=None, tags=None, permission='朋友圈'):
|
||||
"""接受好友请求
|
||||
|
||||
Args:
|
||||
remark (str, optional): 备注名
|
||||
tags (list, optional): 标签列表
|
||||
permission (str, optional): 朋友圈权限, 可选值:'朋友圈', '仅聊天'
|
||||
"""
|
||||
if not self.acceptable:
|
||||
wxlog.debug(f"当前好友状态无法接受好友请求:{self.name}")
|
||||
return
|
||||
wxlog.debug(f"接受好友请求:{self.name} 备注:{remark} 标签:{tags}")
|
||||
self.root._show()
|
||||
roll_into_view(self.NewFriendsBox, self.status)
|
||||
self.status.Click()
|
||||
NewFriendsWnd = self.root.control.WindowControl(ClassName='WeUIDialog')
|
||||
tipscontrol = NewFriendsWnd.TextControl(Name="你的联系人较多,添加新的朋友时需选择权限")
|
||||
|
||||
permission_sns = NewFriendsWnd.CheckBoxControl(Name='聊天、朋友圈、微信运动等')
|
||||
permission_chat = NewFriendsWnd.CheckBoxControl(Name='仅聊天')
|
||||
if tipscontrol.Exists(0.5):
|
||||
permission_sns = tipscontrol.GetParentControl().GetParentControl().TextControl(Name='朋友圈')
|
||||
permission_chat = tipscontrol.GetParentControl().GetParentControl().TextControl(Name='仅聊天')
|
||||
|
||||
if remark:
|
||||
remarkedit = NewFriendsWnd.TextControl(Name='备注名').GetParentControl().EditControl()
|
||||
remarkedit.Click()
|
||||
remarkedit.SendKeys('{Ctrl}a')
|
||||
remarkedit.SendKeys(remark)
|
||||
|
||||
if tags:
|
||||
tagedit = NewFriendsWnd.TextControl(Name='标签').GetParentControl().EditControl()
|
||||
for tag in tags:
|
||||
tagedit.Click()
|
||||
tagedit.SendKeys(tag)
|
||||
NewFriendsWnd.PaneControl(ClassName='DropdownWindow').TextControl().Click()
|
||||
|
||||
if permission == '朋友圈':
|
||||
permission_sns.Click()
|
||||
elif permission == '仅聊天':
|
||||
permission_chat.Click()
|
||||
|
||||
NewFriendsWnd.ButtonControl(Name='确定').Click()
|
||||
|
||||
class WeChatLoginWnd(BaseUISubWnd):
|
||||
_ui_cls_name = 'WeChatLoginWndForPC'
|
||||
|
||||
def __init__(self):
|
||||
self.hwnd = FindWindow(classname=self._ui_cls_name)
|
||||
if self.hwnd:
|
||||
self.control = ControlFromHandle(self.hwnd)
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return WECHAT_LOGINWND.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def clear_hint(self) -> bool:
|
||||
self._show()
|
||||
hint_dialog = self.control.PaneControl(Name=self._lang('提示'))
|
||||
if hint_dialog.Exists(0):
|
||||
dialog_button = self.control.ButtonControl(Name=self._lang('确定'))
|
||||
dialog_button.Click()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def login(self) -> bool:
|
||||
self._show()
|
||||
enter = self.control.ButtonControl(Name=self._lang('进入微信'))
|
||||
if enter.Exists(0):
|
||||
enter.Click()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_qrcode(self) -> Image.Image:
|
||||
self._show()
|
||||
qrcode = self.control.ButtonControl(Name=self._lang('二维码'))
|
||||
if qrcode.Exists(0):
|
||||
window_rect = GetWindowRect(self.hwnd)
|
||||
win_left, win_top, win_right, win_bottom = window_rect
|
||||
|
||||
bbox = win_left + 62, win_top + 88, win_left + 218, win_top + 245
|
||||
return capture(self.hwnd, bbox)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class WeChatBrowser(BaseUISubWnd):
|
||||
_ui_cls_name = 'Chrome_WidgetWin_0'
|
||||
_ui_name = '微信'
|
||||
|
||||
def __init__(self):
|
||||
self.hwnd = FindWindow(classname=self._ui_cls_name, name=self._ui_name)
|
||||
if self.hwnd:
|
||||
self.control = ControlFromHandle(self.hwnd)
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return WECHAT_BROWSER.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def get_url(self) -> str:
|
||||
self._show()
|
||||
tab = self.control.TabItemControl()
|
||||
if tab.Exists():
|
||||
tab.RightClick()
|
||||
copy_link_item = uia.MenuItemControl(Name=self._lang('复制链接'))
|
||||
if copy_link_item.Exists():
|
||||
copy_link_item.Click()
|
||||
clipboard_data = ReadClipboardData()
|
||||
url = (clipboard_data.get('13') or
|
||||
clipboard_data.get('1') or
|
||||
None)
|
||||
SetClipboardText('')
|
||||
return url
|
||||
else:
|
||||
wxlog.debug(f'找不到复制链接菜单项')
|
||||
else:
|
||||
wxlog.debug(f'找不到标签页')
|
||||
|
||||
def close(self):
|
||||
close_button = self.control.ButtonControl(Name=self._lang('关闭'), foundIndex=1)
|
||||
if close_button.Exists():
|
||||
close_button.Click()
|
||||
close_button = self.control.ButtonControl(Name=self._lang('关闭'), foundIndex=2)
|
||||
if close_button.Exists():
|
||||
close_button.Click()
|
||||
close_button = self.control.ButtonControl(Name=self._lang('关闭'), foundIndex=3)
|
||||
if close_button.Exists():
|
||||
close_button.Click()
|
||||
265
wxauto/ui/main.py
Normal file
265
wxauto/ui/main.py
Normal file
@@ -0,0 +1,265 @@
|
||||
from .base import (
|
||||
BaseUIWnd,
|
||||
BaseUISubWnd
|
||||
)
|
||||
from wxauto import uiautomation as uia
|
||||
from wxauto.param import WxResponse
|
||||
from .chatbox import ChatBox
|
||||
from .sessionbox import SessionBox
|
||||
from .navigationbox import NavigationBox
|
||||
from wxauto.param import (
|
||||
WxParam,
|
||||
WxResponse,
|
||||
PROJECT_NAME
|
||||
)
|
||||
from wxauto.languages import *
|
||||
from wxauto.utils import (
|
||||
GetAllWindows,
|
||||
FindWindow,
|
||||
)
|
||||
from wxauto.logger import wxlog
|
||||
from typing import (
|
||||
Union,
|
||||
List,
|
||||
)
|
||||
|
||||
class WeChatSubWnd(BaseUISubWnd):
|
||||
_ui_cls_name: str = 'ChatWnd'
|
||||
chatbox: ChatBox = None
|
||||
nickname: str = ''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: Union[str, int],
|
||||
parent: 'WeChatMainWnd',
|
||||
timeout: int = 3
|
||||
):
|
||||
self.root = self
|
||||
self.parent = parent
|
||||
if isinstance(key, str):
|
||||
hwnd = FindWindow(classname=self._ui_cls_name, name=key, timeout=timeout)
|
||||
else:
|
||||
hwnd = key
|
||||
self.control = uia.ControlFromHandle(hwnd)
|
||||
if self.control is not None:
|
||||
chatbox_control = self.control.PaneControl(ClassName='', searchDepth=1)
|
||||
self.chatbox = ChatBox(chatbox_control, self)
|
||||
self.nickname = self.control.Name
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>'
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
if not hasattr(self, '_pid'):
|
||||
self._pid = self.control.ProcessId
|
||||
return self._pid
|
||||
|
||||
def _get_chatbox(
|
||||
self,
|
||||
nickname: str=None,
|
||||
exact: bool=False
|
||||
) -> ChatBox:
|
||||
return self.chatbox
|
||||
|
||||
def chat_info(self):
|
||||
return self.chatbox.get_info()
|
||||
|
||||
def load_more_message(self, interval=0.3) -> WxResponse:
|
||||
return self.chatbox.load_more(interval)
|
||||
|
||||
def send_msg(
|
||||
self,
|
||||
msg: str,
|
||||
who: str=None,
|
||||
clear: bool=True,
|
||||
at: Union[str, List[str]]=None,
|
||||
exact: bool=False,
|
||||
) -> WxResponse:
|
||||
chatbox = self._get_chatbox(who, exact)
|
||||
if not chatbox:
|
||||
return WxResponse.failure(f'未找到会话: {who}')
|
||||
return chatbox.send_msg(msg, clear, at)
|
||||
|
||||
def send_files(
|
||||
self,
|
||||
filepath,
|
||||
who=None,
|
||||
exact=False
|
||||
) -> WxResponse:
|
||||
chatbox = self._get_chatbox(who, exact)
|
||||
if not chatbox:
|
||||
return WxResponse.failure(f'未找到会话: {who}')
|
||||
return chatbox.send_file(filepath)
|
||||
|
||||
def get_group_members(
|
||||
self,
|
||||
who: str=None,
|
||||
exact: bool=False
|
||||
) -> List[str]:
|
||||
chatbox = self._get_chatbox(who, exact)
|
||||
if not chatbox:
|
||||
return WxResponse.failure(f'未找到会话: {who}')
|
||||
return chatbox.get_group_members()
|
||||
|
||||
def get_msgs(self):
|
||||
chatbox = self._get_chatbox()
|
||||
if chatbox:
|
||||
return chatbox.get_msgs()
|
||||
return []
|
||||
|
||||
def get_new_msgs(self):
|
||||
return self._get_chatbox().get_new_msgs()
|
||||
|
||||
def get_msg_by_id(self, msg_id: str):
|
||||
return self._get_chatbox().get_msg_by_id(msg_id)
|
||||
|
||||
|
||||
version_error_msg = """
|
||||
错误:未找到可用的微信窗口
|
||||
|
||||
当前版本仅适用于3.9版本客户端,如果您当前客户端版本为4.x,请在官网下载3.9版本客户端
|
||||
下载链接:https://pc.weixin.qq.com
|
||||
"""
|
||||
|
||||
|
||||
class WeChatMainWnd(WeChatSubWnd):
|
||||
_ui_cls_name: str = 'WeChatMainWndForPC'
|
||||
_ui_name: str = '微信'
|
||||
|
||||
def __init__(self, hwnd: int = None):
|
||||
self.root = self
|
||||
self.parent = self
|
||||
if hwnd:
|
||||
self._setup_ui(hwnd)
|
||||
else:
|
||||
hwnd = FindWindow(classname=self._ui_cls_name)
|
||||
if not hwnd:
|
||||
raise Exception(version_error_msg)
|
||||
self._setup_ui(hwnd)
|
||||
|
||||
print(f'初始化成功,获取到已登录窗口:{self.nickname}')
|
||||
|
||||
def _setup_ui(self, hwnd: int):
|
||||
self.HWND = hwnd
|
||||
self.control = uia.ControlFromHandle(hwnd)
|
||||
MainControl1 = [i for i in self.control.GetChildren() if not i.ClassName][0]
|
||||
MainControl2 = MainControl1.GetFirstChildControl()
|
||||
navigation_control, sessionbox_control, chatbox_control = MainControl2.GetChildren()
|
||||
self.navigation = NavigationBox(navigation_control, self)
|
||||
self.sessionbox = SessionBox(sessionbox_control, self)
|
||||
self.chatbox = ChatBox(chatbox_control, self)
|
||||
self.nickname = self.navigation.my_icon.Name
|
||||
self.NavigationBox = self.navigation.control
|
||||
self.SessionBox = self.sessionbox.control
|
||||
self.ChatBox = self.chatbox.control
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>'
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return WECHAT_MAIN.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def _get_chatbox(
|
||||
self,
|
||||
nickname: str=None,
|
||||
exact: bool=False
|
||||
) -> ChatBox:
|
||||
if nickname and (chatbox := WeChatSubWnd(nickname, self, timeout=0)).control:
|
||||
return chatbox.chatbox
|
||||
else:
|
||||
if nickname:
|
||||
switch_result = self.sessionbox.switch_chat(keywords=nickname, exact=exact)
|
||||
if not switch_result:
|
||||
return None
|
||||
if self.chatbox.msgbox.Exists(0.5):
|
||||
return self.chatbox
|
||||
|
||||
def switch_chat(
|
||||
self,
|
||||
keywords: str,
|
||||
exact: bool = False,
|
||||
force: bool = False,
|
||||
force_wait: Union[float, int] = 0.5
|
||||
):
|
||||
return self.sessionbox.switch_chat(keywords, exact, force, force_wait)
|
||||
|
||||
def get_all_sub_wnds(self):
|
||||
sub_wxs = [i for i in GetAllWindows() if i[1] == WeChatSubWnd._ui_cls_name]
|
||||
return [WeChatSubWnd(i[0], self) for i in sub_wxs]
|
||||
|
||||
def get_sub_wnd(self, who: str, timeout: int=0):
|
||||
if hwnd := FindWindow(classname=WeChatSubWnd._ui_cls_name, name=who, timeout=timeout):
|
||||
return WeChatSubWnd(hwnd, self)
|
||||
|
||||
def open_separate_window(self, keywords: str) -> WeChatSubWnd:
|
||||
if subwin := self.get_sub_wnd(keywords):
|
||||
wxlog.debug(f"{keywords} 获取到已存在的子窗口: {subwin}")
|
||||
return subwin
|
||||
self._show()
|
||||
if nickname := self.sessionbox.switch_chat(keywords):
|
||||
wxlog.debug(f"{keywords} 切换到聊天窗口: {nickname}")
|
||||
if subwin := self.get_sub_wnd(nickname):
|
||||
wxlog.debug(f"{nickname} 获取到已存在的子窗口: {subwin}")
|
||||
return subwin
|
||||
else:
|
||||
keywords = nickname
|
||||
if result := self.sessionbox.open_separate_window(keywords):
|
||||
find_nickname = result['data'].get('nickname', keywords)
|
||||
return WeChatSubWnd(find_nickname, self)
|
||||
|
||||
def _get_next_new_message(self, filter_mute: bool=False):
|
||||
def get_new_message(session):
|
||||
last_content = session.content
|
||||
new_count = session.new_count
|
||||
chat_name = session.name
|
||||
session.click()
|
||||
return self.chatbox.get_next_new_msgs(new_count, last_content)
|
||||
|
||||
def get_new_session(filter_mute):
|
||||
sessions = self.sessionbox.get_session()
|
||||
if sessions[0].name == self._lang('折叠的群聊'):
|
||||
self.navigation.chat_icon.DoubleClick()
|
||||
sessions = self.sessionbox.get_session()
|
||||
new_sessions = [
|
||||
i for i in sessions
|
||||
if i.isnew
|
||||
and i.name != self._lang('折叠的群聊')
|
||||
]
|
||||
if filter_mute:
|
||||
new_sessions = [i for i in new_sessions if i.ismute == False]
|
||||
return new_sessions
|
||||
|
||||
if new_msgs := self.chatbox.get_new_msgs():
|
||||
wxlog.debug("获取当前页面新消息")
|
||||
return new_msgs
|
||||
elif new_sessions := get_new_session(filter_mute):
|
||||
wxlog.debug("当前会话列表获取新消息")
|
||||
return get_new_message(new_sessions[0])
|
||||
else:
|
||||
self.sessionbox.go_top()
|
||||
if new_sessions := get_new_session(filter_mute):
|
||||
wxlog.debug("当前会话列表获取新消息")
|
||||
return get_new_message(new_sessions[0])
|
||||
else:
|
||||
self.navigation.chat_icon.DoubleClick()
|
||||
if new_sessions := get_new_session(filter_mute):
|
||||
wxlog.debug("翻页会话列表获取新消息")
|
||||
return get_new_message(new_sessions[0])
|
||||
else:
|
||||
wxlog.debug("没有新消息")
|
||||
return []
|
||||
|
||||
def get_next_new_message(self, filter_mute: bool=False):
|
||||
if filter_mute and not self.navigation.has_new_message():
|
||||
return {}
|
||||
new_msgs = self._get_next_new_message(filter_mute)
|
||||
if new_msgs:
|
||||
chat_info = self.chatbox.get_info()
|
||||
return {
|
||||
'chat_name': chat_info['chat_name'],
|
||||
'chat_type': chat_info['chat_type'],
|
||||
'msg': new_msgs
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
55
wxauto/ui/navigationbox.py
Normal file
55
wxauto/ui/navigationbox.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
from wxauto.param import (
|
||||
WxParam,
|
||||
)
|
||||
from wxauto.languages import *
|
||||
from wxauto.uiautomation import Control
|
||||
|
||||
class NavigationBox:
|
||||
def __init__(self, control, parent):
|
||||
self.control: Control = control
|
||||
self.root = parent.root
|
||||
self.parent = parent
|
||||
self.top_control = control.GetTopLevelControl()
|
||||
self.init()
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return WECHAT_NAVIGATION_BOX.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def init(self):
|
||||
self.my_icon = self.control.ButtonControl()
|
||||
self.chat_icon = self.control.ButtonControl(Name=self._lang('聊天'))
|
||||
self.contact_icon = self.control.ButtonControl(Name=self._lang('通讯录'))
|
||||
self.favorites_icon = self.control.ButtonControl(Name=self._lang('收藏'))
|
||||
self.files_icon = self.control.ButtonControl(Name=self._lang('聊天文件'))
|
||||
self.moments_icon = self.control.ButtonControl(Name=self._lang('朋友圈'))
|
||||
self.browser_icon = self.control.ButtonControl(Name=self._lang('搜一搜'))
|
||||
self.video_icon = self.control.ButtonControl(Name=self._lang('视频号'))
|
||||
self.stories_icon = self.control.ButtonControl(Name=self._lang('看一看'))
|
||||
self.mini_program_icon = self.control.ButtonControl(Name=self._lang('小程序面板'))
|
||||
self.phone_icon = self.control.ButtonControl(Name=self._lang('手机'))
|
||||
self.settings_icon = self.control.ButtonControl(Name=self._lang('设置及其他'))
|
||||
|
||||
def switch_to_chat_page(self):
|
||||
self.chat_icon.Click()
|
||||
|
||||
def switch_to_contact_page(self):
|
||||
self.contact_icon.Click()
|
||||
|
||||
def switch_to_favorites_page(self):
|
||||
self.favorites_icon.Click()
|
||||
|
||||
def switch_to_files_page(self):
|
||||
self.files_icon.Click()
|
||||
|
||||
def switch_to_browser_page(self):
|
||||
self.browser_icon.Click()
|
||||
|
||||
# 是否有新消息
|
||||
def has_new_message(self):
|
||||
from wxauto.utils.win32 import capture
|
||||
|
||||
rect = self.chat_icon.BoundingRectangle
|
||||
bbox = rect.left, rect.top, rect.right, rect.bottom
|
||||
img = capture(self.root.HWND, bbox)
|
||||
return any(p[0] > p[1] and p[0] > p[2] for p in img.getdata())
|
||||
265
wxauto/ui/sessionbox.py
Normal file
265
wxauto/ui/sessionbox.py
Normal file
@@ -0,0 +1,265 @@
|
||||
from __future__ import annotations
|
||||
from wxauto.ui.component import (
|
||||
CMenuWnd
|
||||
)
|
||||
from wxauto.param import (
|
||||
WxParam,
|
||||
WxResponse,
|
||||
)
|
||||
from wxauto.languages import *
|
||||
from wxauto.utils import (
|
||||
SetClipboardText,
|
||||
)
|
||||
from wxauto.logger import wxlog
|
||||
from wxauto.uiautomation import Control
|
||||
from wxauto.utils.tools import roll_into_view
|
||||
from typing import (
|
||||
List,
|
||||
Union
|
||||
)
|
||||
import time
|
||||
import re
|
||||
|
||||
|
||||
class SessionBox:
|
||||
def __init__(self, control, parent):
|
||||
self.control: Control = control
|
||||
self.root = parent.root
|
||||
self.parent = parent
|
||||
self.top_control = control.GetTopLevelControl()
|
||||
self.init()
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return WECHAT_SESSION_BOX.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE)
|
||||
|
||||
def init(self):
|
||||
self.searchbox = self.control.EditControl(Name=self._lang('搜索'))
|
||||
self.session_list =\
|
||||
self.control.ListControl(Name=self._lang('会话'), searchDepth=7)
|
||||
self.archived_session_list =\
|
||||
self.control.ListControl(Name=self._lang('折叠的群聊'), searchDepth=7)
|
||||
|
||||
def get_session(self) -> List[SessionElement]:
|
||||
if self.session_list.Exists(0):
|
||||
return [
|
||||
SessionElement(i, self)
|
||||
for i in self.session_list.GetChildren()
|
||||
if i.Name != self._lang('折叠置顶聊天')
|
||||
and not re.match(self._lang('re_置顶聊天'), i.Name)
|
||||
]
|
||||
elif self.archived_session_list.Exists(0):
|
||||
return [SessionElement(i, self) for i in self.archived_session_list.GetChildren()]
|
||||
else:
|
||||
return []
|
||||
|
||||
def roll_up(self, n: int=5):
|
||||
self.control.MiddleClick()
|
||||
self.control.WheelUp(wheelTimes=n)
|
||||
|
||||
def roll_down(self, n: int=5):
|
||||
self.control.MiddleClick()
|
||||
self.control.WheelDown(wheelTimes=n)
|
||||
|
||||
def switch_chat(
|
||||
self,
|
||||
keywords: str,
|
||||
exact: bool = False,
|
||||
force: bool = False,
|
||||
force_wait: Union[float, int] = 0.5
|
||||
):
|
||||
wxlog.debug(f"切换聊天窗口: {keywords}, {exact}, {force}, {force_wait}")
|
||||
self.root._show()
|
||||
sessions = self.get_session()
|
||||
for session in sessions:
|
||||
if (
|
||||
keywords == session.name
|
||||
and session.control.BoundingRectangle.height()
|
||||
):
|
||||
session.switch()
|
||||
return keywords
|
||||
|
||||
self.searchbox.RightClick()
|
||||
SetClipboardText(keywords)
|
||||
menu = CMenuWnd(self)
|
||||
menu.select('粘贴')
|
||||
|
||||
search_result = self.control.ListControl(RegexName='.*?IDS_FAV_SEARCH_RESULT.*?')
|
||||
|
||||
if force:
|
||||
time.sleep(force_wait)
|
||||
self.searchbox.SendKeys('{ENTER}')
|
||||
return ''
|
||||
|
||||
t0 = time.time()
|
||||
while time.time() -t0 < WxParam.SEARCH_CHAT_TIMEOUT:
|
||||
results = []
|
||||
search_result_items = search_result.GetChildren()
|
||||
highlight_who = re.sub(r'(\s+)', r'</em>\1<em>', keywords)
|
||||
for search_result_item in search_result_items:
|
||||
item_name = search_result_item.Name
|
||||
if (
|
||||
search_result_item.ControlTypeName == 'PaneControl'
|
||||
and search_result_item.TextControl(Name='聊天记录').Exists(0)
|
||||
) or item_name == f'搜索 {keywords}':
|
||||
break
|
||||
elif (
|
||||
search_result_item.ControlTypeName == 'ListItemControl'
|
||||
and search_result_item.TextControl(Name=f"微信号: <em>{keywords}</em>").Exists(0)
|
||||
):
|
||||
wxlog.debug(f"{keywords} 匹配到微信号:{item_name}")
|
||||
search_result_item.Click()
|
||||
return item_name
|
||||
elif (
|
||||
search_result_item.ControlTypeName == 'ListItemControl'
|
||||
and search_result_item.TextControl(Name=f"昵称: <em>{highlight_who}</em>").Exists(0)
|
||||
):
|
||||
wxlog.debug(f"{keywords} 匹配到昵称:{item_name}")
|
||||
search_result_item.Click()
|
||||
return item_name
|
||||
elif (
|
||||
search_result_item.ControlTypeName == 'ListItemControl'
|
||||
and search_result_item.TextControl(Name=f"群聊名称: <em>{highlight_who}</em>").Exists(0)
|
||||
):
|
||||
wxlog.debug(f"{keywords} 匹配到群聊名称:{item_name}")
|
||||
search_result_item.Click()
|
||||
return item_name
|
||||
elif (
|
||||
search_result_item.ControlTypeName == 'ListItemControl'
|
||||
and keywords == item_name
|
||||
):
|
||||
wxlog.debug(f"{keywords} 完整匹配")
|
||||
search_result_item.Click()
|
||||
return keywords
|
||||
elif (
|
||||
search_result_item.ControlTypeName == 'ListItemControl'
|
||||
and keywords in item_name
|
||||
):
|
||||
results.append(search_result_item)
|
||||
|
||||
if exact:
|
||||
wxlog.debug(f"{keywords} 未精准匹配,返回None")
|
||||
if search_result.Exists(0):
|
||||
search_result.SendKeys('{Esc}')
|
||||
return None
|
||||
if results:
|
||||
wxlog.debug(f"{keywords} 匹配到多个结果,返回第一个")
|
||||
results[0].Click()
|
||||
return results[0].Name
|
||||
|
||||
if search_result.Exists(0):
|
||||
search_result.SendKeys('{Esc}')
|
||||
|
||||
|
||||
def open_separate_window(self, name: str):
|
||||
wxlog.debug(f"打开独立窗口: {name}")
|
||||
sessions = self.get_session()
|
||||
for session in sessions:
|
||||
if session.name == name:
|
||||
wxlog.debug(f"找到会话: {name}")
|
||||
while session.control.BoundingRectangle.height():
|
||||
try:
|
||||
session.click()
|
||||
session.double_click()
|
||||
except:
|
||||
pass
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
return WxResponse.success(data={'nickname': name})
|
||||
wxlog.debug(f"未找到会话: {name}")
|
||||
return WxResponse.failure('未找到会话')
|
||||
|
||||
def go_top(self):
|
||||
wxlog.debug("回到会话列表顶部")
|
||||
if self.archived_session_list.Exists(0):
|
||||
self.control.ButtonControl(Name=self._lang('返回')).Click()
|
||||
time.sleep(0.3)
|
||||
first_session_name = self.session_list.GetChildren()[0].Name
|
||||
while True:
|
||||
self.control.WheelUp(wheelTimes=3)
|
||||
time.sleep(0.1)
|
||||
if self.session_list.GetChildren()[0].Name == first_session_name:
|
||||
break
|
||||
else:
|
||||
first_session_name = self.session_list.GetChildren()[0].Name
|
||||
|
||||
|
||||
class SessionElement:
|
||||
def __init__(
|
||||
self,
|
||||
control: Control,
|
||||
parent: SessionBox
|
||||
):
|
||||
self.root = parent.root
|
||||
self.parent = parent
|
||||
self.control = control
|
||||
info_controls = [i for i in self.control.GetProgenyControl(3).GetChildren() if i.ControlTypeName=='TextControl']
|
||||
self.name = info_controls[0].Name
|
||||
self.time = info_controls[-1].Name
|
||||
self.content = (
|
||||
temp_control.Name
|
||||
if (temp_control := control.GetProgenyControl(4, -1, control_type='TextControl'))
|
||||
else None
|
||||
)
|
||||
self.ismute = (
|
||||
True
|
||||
if control.GetProgenyControl(4, 1, control_type='PaneControl')
|
||||
else False
|
||||
)
|
||||
self.isnew = (new_tag_control := control.GetProgenyControl(2, 2)) is not None
|
||||
self.new_count = 0
|
||||
if self.isnew:
|
||||
if new_tag_name := (new_tag_control.Name):
|
||||
try:
|
||||
self.new_count = int(new_tag_name)
|
||||
self.ismute = False
|
||||
except ValueError:
|
||||
self.new_count = 999
|
||||
else:
|
||||
new_text = re.findall(self._lang('re_条数'), str(self.content))
|
||||
if new_text:
|
||||
try:
|
||||
self.new_count = int(re.findall('\d+', new_text[0])[0])
|
||||
except ValueError:
|
||||
self.new_count = 999
|
||||
self.content = self.content[len(new_text[0])+1:]
|
||||
else:
|
||||
self.new_count = 1
|
||||
|
||||
|
||||
self.info = {
|
||||
'name': self.name,
|
||||
'time': self.time,
|
||||
'content': self.content,
|
||||
'isnew': self.isnew,
|
||||
'new_count': self.new_count,
|
||||
'ismute': self.ismute
|
||||
}
|
||||
|
||||
def _lang(self, text: str) -> str:
|
||||
return self.parent._lang(text)
|
||||
|
||||
def roll_into_view(self):
|
||||
self.root._show()
|
||||
roll_into_view(self.control.GetParentControl(), self.control)
|
||||
|
||||
|
||||
def _click(self, right: bool=False, double: bool=False):
|
||||
self.roll_into_view()
|
||||
if right:
|
||||
self.control.RightClick()
|
||||
elif double:
|
||||
self.control.DoubleClick()
|
||||
else:
|
||||
self.control.Click()
|
||||
|
||||
def click(self):
|
||||
self._click()
|
||||
|
||||
def right_click(self):
|
||||
self._click(right=True)
|
||||
|
||||
def double_click(self):
|
||||
self._click(double=True)
|
||||
|
||||
def switch(self):
|
||||
self.click()
|
||||
Reference in New Issue
Block a user