diff --git a/clients/wcfautopy/MANIFEST.in b/clients/wcfautopy/MANIFEST.in new file mode 100644 index 0000000..e228aeb --- /dev/null +++ b/clients/wcfautopy/MANIFEST.in @@ -0,0 +1,2 @@ +include wcferry/*.dll +include wcferry/*.exe diff --git a/clients/wcfautopy/README.MD b/clients/wcfautopy/README.MD new file mode 100644 index 0000000..1b716a6 --- /dev/null +++ b/clients/wcfautopy/README.MD @@ -0,0 +1,163 @@ +# WeChatFerry wcfautopy 客户端(基于python客户端进行修改) +[![PyPi](https://img.shields.io/pypi/v/wcferry.svg)](https://pypi.python.org/pypi/wcferry) [![Downloads](https://static.pepy.tech/badge/wcferry)](https://pypi.python.org/pypi/wcferry) [![Documentation Status](https://readthedocs.org/projects/wechatferry/badge/?version=latest)](https://wechatferry.readthedocs.io/zh/latest/?badge=latest) + +|[📖 文档](https://wechatferry.readthedocs.io/)|[📺 视频教程](https://mp.weixin.qq.com/s/APdjGyZ2hllXxyG_sNCfXQ)|[🙋 FAQ](https://mp.weixin.qq.com/s/vAGpn1C9stI8Xzt1hUJhLA)| +|:-:|:-:|:-:| + +🤖示例机器人框架:[WeChatRobot](https://github.com/lich0821/WeChatRobot)。 + +## 快速开始 +```sh +pip install --upgrade wcferry +``` + +### Demo: +```py +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +from time import sleep +from wcferry import Wcf, WxMsg, Register + +logging.basicConfig(level='DEBUG', format="%(asctime)s %(message)s") +LOG = logging.getLogger("Demo") + + + +def main(): + receiver = Register() + + @receiver.message_register(isDivision=True, isGroup=True, isPyq=False) + def process_msg(bot: Wcf, msg: WxMsg): + """ + 同步消息函数装饰器 + """ + LOG.info(f"收到消息: {msg}") + + sleep(5) # 等微信加载好,以免信息显示异常 + LOG.info(f"已经登录: {True if bot.is_login() else False}") + LOG.info(f"wxid: {bot.get_self_wxid()}") + + # bot.disable_recv_msg() # 当需要停止接收消息时调用 + sleep(5) + ret = bot.send_text("Hello world.", "filehelper") + LOG.info(f"send_text: {ret}") + + sleep(5) + # 需要确保图片路径正确,建议使用绝对路径(使用双斜杠\\) + ret = bot.send_image("https://raw.githubusercontent.com/lich0821/WeChatFerry/master/assets/QR.jpeg", "filehelper") + LOG.info(f"send_image: {ret}") + + sleep(5) + # 需要确保文件路径正确,建议使用绝对路径(使用双斜杠\\) + ret = bot.send_file("https://raw.githubusercontent.com/lich0821/WeChatFerry/master/README.MD", "filehelper") + LOG.info(f"send_file: {ret}") + + sleep(5) + LOG.info(f"Message types:\n{bot.get_msg_types()}") + LOG.info(f"Contacts:\n{bot.get_contacts()}") + + sleep(5) + LOG.info(f"DBs:\n{bot.get_dbs()}") + LOG.info(f"Tables:\n{bot.get_tables('db')}") + LOG.info(f"Results:\n{bot.query_sql('MicroMsg.db', 'SELECT * FROM Contact LIMIT 1;')}") + + # 需要真正的 V3、V4 信息 + # bot.accept_new_friend("v3", "v4") + + # 添加群成员,填写正确的群 ID 和成员 wxid + # ret = bot.add_chatroom_members("chatroom id", "wxid1,wxid2,wxid3,...") + # LOG.info(f"add_chatroom_members: {ret}") + + # 删除群成员,填写正确的群 ID 和成员 wxid + # ret = bot.del_chatroom_members("chatroom id", "wxid1,wxid2,wxid3,...") + # LOG.info(f"add_chatroom_members: {ret}") + + sleep(5) + bot.refresh_pyq(0) # 刷新朋友圈第一页 + # bot.refresh_pyq(id) # 从 id 开始刷新朋友圈 + + @receiver.async_message_register() + async def async_process_msg(bot: Wcf, msg: WxMsg): + """ + 异步消息函数装饰器 + """ + print(msg) + + # 开始接受消息 + receiver.run() + + +if __name__ == "__main__": + main() + + +``` + +|![碲矿](https://raw.githubusercontent.com/lich0821/WeChatFerry/master/assets/TEQuant.jpg)|![赞赏](https://raw.githubusercontent.com/lich0821/WeChatFerry/master/assets/QR.jpeg)| +|:-:|:-:| +|后台回复 `WeChatFerry` 加群交流|如果你觉得有用| + +## 一起开发 +### 配置环境 +```sh +# 创建虚拟环境 +python -m venv .env +# 激活虚拟环境 +source .env/Scripts/activate +# 升级 pip +pip install --upgrade pip +# 安装依赖包 +pip install grpcio-tools pynng +``` + +### 重新生成 PB 文件 +```sh +# CMD +cd clients\wcfautopy\wcferry +python -m grpc_tools.protoc --python_out=. --proto_path=..\..\..\WeChatFerry\rpc\proto\ wcf.proto + +# GitBash +cd clients/wcfautopy/wcferry +python -m grpc_tools.protoc --python_out=. --proto_path=../../../WeChatFerry/rpc/proto/ wcf.proto +``` + +## 版本更新 +### 39.0.3.0 (2023.09.28) +* 修复登录账号昵称超长报错问题 + +
点击查看更多 + +版本号:`w.x.y.z`。 + +其中: +* `w` 是微信的大版本号,如 `37` (3.7.a.a), `38` (3.8.a.a), `39` (3.9.a.a) +* `x` 是适配的微信的小版本号,从 0 开始 +* `y` 是 `WeChatFerry` 的版本,从 0 开始 +* `z` 是各客户端的版本,从 0 开始 + +功能: + +* 检查登录状态 +* 获取登录账号的 wxid +* 获取消息类型 +* 获取所有联系人 +* 获取所有好友 +* 获取数据库 +* 获取某数据库下的表 +* 获取用户信息 +* 发送文本消息(可 @) +* 发送图片(wcfautopy 客户端支持网络路径) +* 发送文件(wcfautopy 客户端支持网络路径) +* 允许接收消息 +* 停止接收消息 +* 执行 SQL 查询 +* 接受好友申请 +* 添加群成员 +* 删除群成员 +* 解密图片 +* 获取朋友圈消息 +* 某功能(Breaking Change) + +
diff --git a/clients/wcfautopy/demo.py b/clients/wcfautopy/demo.py new file mode 100644 index 0000000..0ace02c --- /dev/null +++ b/clients/wcfautopy/demo.py @@ -0,0 +1,78 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +from time import sleep +from wcferry import Wcf, WxMsg, Register + +logging.basicConfig(level='DEBUG', format="%(asctime)s %(message)s") +LOG = logging.getLogger("Demo") + + + +def main(): + receiver = Register() + + @receiver.message_register(isDivision=True, isGroup=True, isPyq=False) + def process_msg(bot: Wcf, msg: WxMsg): + """ + 同步消息函数装饰器 + """ + LOG.info(f"收到消息: {msg}") + + sleep(5) # 等微信加载好,以免信息显示异常 + LOG.info(f"已经登录: {True if bot.is_login() else False}") + LOG.info(f"wxid: {bot.get_self_wxid()}") + + # bot.disable_recv_msg() # 当需要停止接收消息时调用 + sleep(5) + ret = bot.send_text("Hello world.", "filehelper") + LOG.info(f"send_text: {ret}") + + sleep(5) + # 需要确保图片路径正确,建议使用绝对路径(使用双斜杠\\) + ret = bot.send_image("https://raw.githubusercontent.com/lich0821/WeChatFerry/master/assets/QR.jpeg", "filehelper") + LOG.info(f"send_image: {ret}") + + sleep(5) + # 需要确保文件路径正确,建议使用绝对路径(使用双斜杠\\) + ret = bot.send_file("https://raw.githubusercontent.com/lich0821/WeChatFerry/master/README.MD", "filehelper") + LOG.info(f"send_file: {ret}") + + sleep(5) + LOG.info(f"Message types:\n{bot.get_msg_types()}") + LOG.info(f"Contacts:\n{bot.get_contacts()}") + + sleep(5) + LOG.info(f"DBs:\n{bot.get_dbs()}") + LOG.info(f"Tables:\n{bot.get_tables('db')}") + LOG.info(f"Results:\n{bot.query_sql('MicroMsg.db', 'SELECT * FROM Contact LIMIT 1;')}") + + # 需要真正的 V3、V4 信息 + # bot.accept_new_friend("v3", "v4") + + # 添加群成员,填写正确的群 ID 和成员 wxid + # ret = bot.add_chatroom_members("chatroom id", "wxid1,wxid2,wxid3,...") + # LOG.info(f"add_chatroom_members: {ret}") + + # 删除群成员,填写正确的群 ID 和成员 wxid + # ret = bot.del_chatroom_members("chatroom id", "wxid1,wxid2,wxid3,...") + # LOG.info(f"add_chatroom_members: {ret}") + + sleep(5) + bot.refresh_pyq(0) # 刷新朋友圈第一页 + # bot.refresh_pyq(id) # 从 id 开始刷新朋友圈 + + @receiver.async_message_register() + async def async_process_msg(bot: Wcf, msg: WxMsg): + """ + 异步消息函数装饰器 + """ + print(msg) + + # 开始接受消息 + receiver.run() + + +if __name__ == "__main__": + main() diff --git a/clients/wcfautopy/roomdata.proto b/clients/wcfautopy/roomdata.proto new file mode 100644 index 0000000..22f307e --- /dev/null +++ b/clients/wcfautopy/roomdata.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +package com.iamteer.wcf; + +message RoomData { + + message RoomMember { + string wxid = 1; + string name = 2; + int32 state = 3; + } + + repeated RoomMember members = 1; + + int32 field_2 = 2; + int32 field_3 = 3; + int32 field_4 = 4; + int32 room_capacity = 5; + int32 field_6 = 6; + int64 field_7 = 7; + int64 field_8 = 8; +} diff --git a/clients/wcfautopy/setup.py b/clients/wcfautopy/setup.py new file mode 100644 index 0000000..2f1fa75 --- /dev/null +++ b/clients/wcfautopy/setup.py @@ -0,0 +1,45 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + + +from __future__ import print_function +from setuptools import setup, find_packages + +import wcferry + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + + +setup( + name="wcferry", + version=wcferry.__version__, + author="Changhua", + author_email="lichanghua0821@gmail.com", + description="一个玩微信的工具", + long_description=long_description, + long_description_content_type="text/markdown", + license="MIT", + url="https://github.com/lich0821/WeChatFerry", + python_requires=">=3.8", + packages=find_packages(), + include_package_data=True, + install_requires=[ + "setuptools", + "grpcio-tools", + "pynng", + "requests", + ], + classifiers=[ + "Environment :: Win32 (MS Windows)", + "Intended Audience :: Developers", + "Intended Audience :: Customer Service", + "Topic :: Communications :: Chat", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + ], + project_urls={ + "Documentation": "https://wechatferry.readthedocs.io/zh/latest/index.html", + "GitHub": "https://github.com/lich0821/WeChatFerry/", + }, +) diff --git a/clients/wcfautopy/wcferry/__init__.py b/clients/wcfautopy/wcferry/__init__.py new file mode 100644 index 0000000..f5d4343 --- /dev/null +++ b/clients/wcfautopy/wcferry/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from wcferry.client import Wcf, __version__ +from wcferry.wxmsg import WxMsg +from wcferry.auto_res import Register + diff --git a/clients/wcfautopy/wcferry/auto_res/__init__.py b/clients/wcfautopy/wcferry/auto_res/__init__.py new file mode 100644 index 0000000..320befe --- /dev/null +++ b/clients/wcfautopy/wcferry/auto_res/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from wcferry.auto_res.bot import Register +from wcferry.auto_res.core import load_function + + +Register = load_function(Register) + diff --git a/clients/wcfautopy/wcferry/auto_res/bot.py b/clients/wcfautopy/wcferry/auto_res/bot.py new file mode 100644 index 0000000..e30aae7 --- /dev/null +++ b/clients/wcfautopy/wcferry/auto_res/bot.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +from typing import Callable, Any +from wcferry.event import Event +from abc import abstractmethod +from wcferry.client import Wcf +import logging + + +class Register(Event): + def __init__(self, debug=True, **kwargs): + super(Register, self).__init__() + logging.basicConfig(level='DEBUG', format="%(asctime)s %(message)s") + self._LOG = logging.getLogger("Demo") + self._LOG.info("开始初始化...") + # 默认连接本地服务 + self._wcf = Wcf(debug=debug, **kwargs) + + @abstractmethod + def _process_msg(self, wcf: Wcf): + """ + 有消息的时候,通知分发器分发消息 + :param wcf: Wcf + :return: None + """ + raise NotImplementedError + + @abstractmethod + def _register(self, + func: Callable[[Any], Any]): + """ + 消息处理工厂, 所有消息处理函数都会汇总在这里提交给事件分发器 + :param func: 被装饰的待处理消息函数 + :return: func + """ + raise NotImplementedError + + @abstractmethod + def _processing_async_func(self, + isGroup: bool, + isDivision: bool, + isPyq: bool): + """ + 异步函数消息处理函数, 用来接受非协程函数 + 参数: + :param isGroup 对消息进行限制, 当为True时, 只接受群消息, 当为False时, 只接受私聊消息, + 注意! 仅当isDivision为 True时, isGroup参数生效 + :param isPyq 是否接受朋友圈消息 + :param isDivision 是否对消息分组 + """ + raise NotImplementedError + + @abstractmethod + def _processing_universal_func(self, + isGroup: bool, + isDivision: bool, + isPyq: bool): + """ + 同步函数消息处理函数, 用来接受非协程函数 + 参数: + :param isGroup 对消息进行限制, 当为True时, 只接受群消息, 当为False时, 只接受私聊消息, + 注意! 仅当isDivision为 True时, isGroup参数生效 + :param isPyq 是否接受朋友圈消息 + :param isDivision 是否对消息分组 + """ + raise NotImplementedError + + @abstractmethod + def message_register(self, + isGroup: bool = False, + isDivision: bool = False, + isPyq: bool = False): + """ + 外部可访问接口 + 消息处理函数注册器, 用来接受同步函数 + 参数: + :param isGroup 对消息进行限制, 当为True时, 只接受群消息, 当为False时, 只接受私聊消息, + 注意! 仅当isDivision为 True时, isGroup参数生效 + :param isPyq 是否接受朋友圈消息 + :param isDivision 是否对消息分组 + """ + raise NotImplementedError + + @abstractmethod + def async_message_register(self, + isGroup: bool = False, + isDivision: bool = False, + isPyq: bool = False): + """ + 外部可访问接口 + 消息处理函数注册器, 用来接受异步函数 + 参数: + :param isGroup 对消息进行限制, 当为True时, 只接受群消息, 当为False时, 只接受私聊消息, + 注意! 仅当isDivision为 True时, isGroup参数生效 + :param isPyq 是否接受朋友圈消息 + :param isDivision 是否对消息分组 + """ + raise NotImplementedError + + @abstractmethod + def run(self, *args, **kwargs): + """ + 启动程序, 开始接受消息 + """ + raise NotImplementedError + + @abstractmethod + def stop_receiving(self): + """ + 停止接受消息 + """ + raise NotImplementedError diff --git a/clients/wcfautopy/wcferry/auto_res/core.py b/clients/wcfautopy/wcferry/auto_res/core.py new file mode 100644 index 0000000..e883964 --- /dev/null +++ b/clients/wcfautopy/wcferry/auto_res/core.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +import asyncio +import functools +import queue +import traceback +from threading import Thread +from typing import Callable, Any +from wcferry.client import Wcf +from wcferry.wxmsg import WxMsg + + +def load_function(cls): + cls._process_msg = _process_msg + cls._register = _register + cls._processing_async_func = _processing_async_func + cls._processing_universal_func = _processing_universal_func + cls.message_register = message_register + cls.async_message_register = async_message_register + cls.run = run + cls.stop_receiving = stop_receiving + return cls + + +def _process_msg(self, wcf: Wcf): + """有消息的时候,通知分发器分发消息""" + while wcf.is_receiving_msg(): + try: + msg = wcf.get_msg() + self._message = msg + self._run_func() + except queue.Empty: + pass + + +def _register(self, + func: Callable[[Any], Any]): + self._add_callback(func, self._wcf) + # 此处必须返回被装饰函数原函数, 否则丢失被装饰函数信息 + return func + + +def _processing_async_func(self, + isGroup: bool, + isDivision: bool, + isPyq: bool,): + def _async_func(func): + + @functools.wraps(func) + @self._register + async def __async_func(bot: Wcf, message: WxMsg): + try: + # 判断被装饰函数是否为协程函数, 本函数要求是协程函数 + if not asyncio.iscoroutinefunction(func): raise ValueError( + f'这里应使用协程函数, 而被装饰函数-> ({func.__name__}) <-是非协程函数') + if message.is_pyq() and isPyq: + return await func(bot, message) + if not isDivision: + return await func(bot, message) + if message.from_group() and isGroup: + return await func(bot, message) + if not message.from_group() and not isGroup: + return await func(bot, message) + except: + traceback.print_exc() + return __async_func + return _async_func + + +def _processing_universal_func(self, + isGroup: bool, + isDivision: bool, + isPyq: bool, ): + def _universal_func(func): + + @functools.wraps(func) + @self._register + def universal_func(bot: Wcf, message: WxMsg): + try: + # 判断被装饰函数是否为协程函数, 本函数要求是协程函数 + if asyncio.iscoroutinefunction(func): raise ValueError( + f'这里应使用非协程函数, 而被装饰函数-> ({func.__name__}) <-协程函数') + if message.is_pyq() and isPyq: + return func(bot, message) + if not isDivision: + return func(bot, message) + if message.from_group() and isGroup: + return func(bot, message) + if not message.from_group() and not isGroup: + return func(bot, message) + except: + traceback.print_exc() + return None + return universal_func + return _universal_func + + +def message_register(self, + isGroup: bool = False, + isDivision: bool = False, + isPyq: bool = False): + return self._processing_universal_func(isGroup, isDivision, isPyq) + + +def async_message_register(self, + isGroup: bool = False, + isDivision: bool = False, + isPyq: bool = False): + return self._processing_async_func(isGroup, isDivision, isPyq) + + +def run(self, *args, **kwargs): + self._wcf.enable_receiving_msg(*args, pyq=True, **kwargs) + Thread(target=self._process_msg, name="GetMessage", args=(self._wcf,), daemon=True).start() + self._LOG.debug("开始接受消息") + self._wcf.keep_running() + +def stop_receiving(self): + return self._wcf.disable_recv_msg() diff --git a/clients/wcfautopy/wcferry/client.py b/clients/wcfautopy/wcferry/client.py new file mode 100644 index 0000000..9136f47 --- /dev/null +++ b/clients/wcfautopy/wcferry/client.py @@ -0,0 +1,694 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +__version__ = "39.0.3.0" + +import atexit +import base64 +import logging +import mimetypes +import os +import re +import sys +from queue import Queue +from threading import Thread +from time import sleep +from typing import Callable, Dict, List, Optional + +import pynng +import requests +from google.protobuf import json_format +from wcferry import wcf_pb2 +from wcferry.roomdata_pb2 import RoomData +from wcferry.wxmsg import WxMsg + + +def _retry(): + def decorator(func): + """ Retry the function """ + def wrapper(*args, **kwargs): + def logerror(e): + func_name = re.findall(r"func: (.*?)\n", str(args[1]))[-1] + logging.getLogger("WCF").error(f"Call {func_name} failed: {e}") + + try: + ret = func(*args, **kwargs) + except pynng.Timeout as _: # 如果超时,重试 + try: + ret = func(*args, **kwargs) + except Exception as e: + logerror(e) + ret = wcf_pb2.Response() + except Exception as e: # 其他异常,退出 + logerror(e) + sys.exit(-1) + + return ret + return wrapper + return decorator + + +class Wcf(): + """WeChatFerry, 一个玩微信的工具。 + + Args: + host (str): `wcferry` RPC 服务器地址,默认本地启动;也可以指定地址连接远程服务 + port (int): `wcferry` RPC 服务器端口,默认为 10086,接收消息会占用 `port+1` 端口 + debug (bool): 是否开启调试模式(仅本地启动有效) + + Attributes: + contacts (list): 联系人缓存,调用 `get_contacts` 后更新 + self_wxid (str): 登录账号 wxid + """ + + def __init__(self, host: str = None, port: int = 10086, debug: bool = True) -> None: + self._local_mode = False + self._is_running = False + self._is_receiving_msg = False + self._wcf_root = os.path.abspath(os.path.dirname(__file__)) + self._dl_path = f"{self._wcf_root}/.dl" + os.makedirs(self._dl_path, exist_ok=True) + self.LOG = logging.getLogger("WCF") + self.LOG.info(f"wcferry version: {__version__}") + self.port = port + self.host = host + if host is None: + self._local_mode = True + self.host = "127.0.0.1" + cmd = fr'"{self._wcf_root}\wcf.exe" start {self.port} {"debug" if debug else ""}' + if os.system(cmd) != 0: + self.LOG.error("初始化失败!") + os._exit(-1) + + self.cmd_url = f"tcp://{self.host}:{self.port}" + + # 连接 RPC + self.cmd_socket = pynng.Pair1() # Client --> Server,发送消息 + self.cmd_socket.send_timeout = 2000 # 发送 2 秒超时 + self.cmd_socket.recv_timeout = 2000 # 接收 2 秒超时 + try: + self.cmd_socket.dial(self.cmd_url, block=True) + except Exception as e: + self.LOG.error(f"连接失败: {e}") + os._exit(-2) + + self.msg_socket = pynng.Pair1() # Server --> Client,接收消息 + self.msg_socket.send_timeout = 2000 # 发送 2 秒超时 + self.msg_socket.recv_timeout = 2000 # 接收 2 秒超时 + self.msg_url = self.cmd_url.replace(str(self.port), str(self.port + 1)) + + atexit.register(self.cleanup) # 退出的时候停止消息接收,防止资源占用 + while not self.is_login(): # 等待微信登录成功 + sleep(1) + + self._is_running = True + self.contacts = [] + self.msgQ = Queue() + self._SQL_TYPES = {1: int, 2: float, 3: lambda x: x.decode("utf-8"), 4: bytes, 5: lambda x: None} + self.self_wxid = self.get_self_wxid() + + def __del__(self) -> None: + self.cleanup() + + def cleanup(self) -> None: + """关闭连接,回收资源""" + if not self._is_running: + return + + self.disable_recv_msg() + self.cmd_socket.close() + + if self._local_mode: + cmd = fr'"{self._wcf_root}\wcf.exe" stop' + if os.system(cmd) != 0: + self.LOG.error("退出失败!") + return + self._is_running = False + + def keep_running(self): + """阻塞进程,让 RPC 一直维持连接""" + try: + while True: + sleep(1) + except Exception as e: + self.cleanup() + + @_retry() + def _send_request(self, req: wcf_pb2.Request) -> wcf_pb2.Response: + data = req.SerializeToString() + self.cmd_socket.send(data) + rsp = wcf_pb2.Response() + rsp.ParseFromString(self.cmd_socket.recv_msg().bytes) + return rsp + + def is_receiving_msg(self) -> bool: + """是否已启动接收消息功能""" + return self._is_receiving_msg + + def is_login(self) -> bool: + """是否已经登录""" + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_IS_LOGIN # FUNC_IS_LOGIN + rsp = self._send_request(req) + + return rsp.status == 1 + + def get_self_wxid(self) -> str: + """获取登录账户的 wxid""" + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_GET_SELF_WXID # FUNC_GET_SELF_WXID + rsp = self._send_request(req) + + return rsp.str + + def get_msg_types(self) -> Dict: + """获取所有消息类型""" + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_GET_MSG_TYPES # FUNC_GET_MSG_TYPES + rsp = self._send_request(req) + types = json_format.MessageToDict(rsp.types).get("types", {}) + types = {int(k): v for k, v in types.items()} + + return dict(sorted(dict(types).items())) + + def get_contacts(self) -> List[Dict]: + """获取完整通讯录""" + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_GET_CONTACTS # FUNC_GET_CONTACTS + rsp = self._send_request(req) + contacts = json_format.MessageToDict(rsp.contacts).get("contacts", []) + + self.contacts.clear() + for cnt in contacts: + gender = cnt.get("gender", "") + if gender == 1: + gender = "男" + elif gender == 2: + gender = "女" + else: + gender = "" + contact = { + "wxid": cnt.get("wxid", ""), + "code": cnt.get("code", ""), + "remark": cnt.get("remark", ""), + "name": cnt.get("name", ""), + "country": cnt.get("country", ""), + "province": cnt.get("province", ""), + "city": cnt.get("city", ""), + "gender": gender} + self.contacts.append(contact) + return self.contacts + + def get_dbs(self) -> List[str]: + """获取所有数据库""" + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_GET_DB_NAMES # FUNC_GET_DB_NAMES + rsp = self._send_request(req) + dbs = json_format.MessageToDict(rsp.dbs).get("names", []) + + return dbs + + def get_tables(self, db: str) -> List[Dict]: + """获取 db 中所有表 + + Args: + db (str): 数据库名(可通过 `get_dbs` 查询) + + Returns: + List[Dict]: `db` 下的所有表名及对应建表语句 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_GET_DB_TABLES # FUNC_GET_DB_TABLES + req.str = db + rsp = self._send_request(req) + tables = json_format.MessageToDict(rsp.tables).get("tables", []) + + return tables + + def get_user_info(self) -> Dict: + """获取登录账号个人信息""" + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_GET_USER_INFO # FUNC_GET_USER_INFO + rsp = self._send_request(req) + ui = json_format.MessageToDict(rsp.ui) + + return ui + + def send_text(self, msg: str, receiver: str, aters: Optional[str] = "") -> int: + """发送文本消息 + + Args: + msg (str): 要发送的消息,换行使用 `\\\\n` (单杠);如果 @ 人的话,需要带上跟 `aters` 里数量相同的 @ + receiver (str): 消息接收人,wxid 或者 roomid + aters (str): 要 @ 的 wxid,多个用逗号分隔;`@所有人` 只需要 `notify@all` + + Returns: + int: 0 为成功,其他失败 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_SEND_TXT # FUNC_SEND_TXT + req.txt.msg = msg + req.txt.receiver = receiver + if aters: + req.txt.aters = aters + rsp = self._send_request(req) + return rsp.status + + def _download_file(self, url: str) -> str: + path = None + if not self._local_mode: + self.LOG.error(f"只有本地模式才支持网络路径!") + return path + + try: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36', } + rsp = requests.get(url, headers=headers, stream=True, timeout=60) + rsp.raw.decode_content = True + + # 提取文件名 + fname = os.path.basename(url) + ct = rsp.headers["content-type"] + ext = mimetypes.guess_extension(ct) + if ext: + if ext not in fname: + fname = fname + ext + else: + fname = fname.split(ext)[0] + ext + + # 保存文件,用完后删除 + with open(f"{self._dl_path}/{fname}", "wb") as of: + of.write(rsp.content) + + path = os.path.normpath(f"{self._dl_path}/{fname}") + except Exception as e: + self.LOG.error(f"网络资源下载失败: {e}") + + return path + + def _process_path(self, path) -> str: + """处理路径,如果是网络路径则下载文件 + """ + if path.startswith("http"): + path = self._download_file(path) + if not path: + return -102 # 下载失败 + elif not os.path.exists(path): + self.LOG.error(f"图片或者文件不存在,请检查路径: {path}") + return -101 # 文件不存在 + + return path + + def send_image(self, path: str, receiver: str) -> int: + """发送图片,非线程安全 + + Args: + path (str): 图片路径,如:`C:/Projs/WeChatRobot/TEQuant.jpeg` 或 `https://raw.githubusercontent.com/lich0821/WeChatFerry/master/assets/TEQuant.jpg` + receiver (str): 消息接收人,wxid 或者 roomid + + Returns: + int: 0 为成功,其他失败 + """ + path = self._process_path(path) + if isinstance(path, int): + return path + + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_SEND_IMG # FUNC_SEND_IMG + req.file.path = path + req.file.receiver = receiver + rsp = self._send_request(req) + return rsp.status + + def send_file(self, path: str, receiver: str) -> int: + """发送文件,非线程安全 + + Args: + path (str): 本地文件路径,如:`C:/Projs/WeChatRobot/README.MD` 或 `https://raw.githubusercontent.com/lich0821/WeChatFerry/master/README.MD` + receiver (str): 消息接收人,wxid 或者 roomid + + Returns: + int: 0 为成功,其他失败 + """ + path = self._process_path(path) + if isinstance(path, int): + return path + + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_SEND_FILE # FUNC_SEND_FILE + req.file.path = path + req.file.receiver = receiver + rsp = self._send_request(req) + return rsp.status + + def send_xml(self, receiver: str, xml: str, type: int, path: str = None) -> int: + """发送 XML + + Args: + receiver (str): 消息接收人,wxid 或者 roomid + xml (str): xml 内容 + type (int): xml 类型,如:0x21 为小程序 + path (str): 封面图片路径 + + Returns: + int: 0 为成功,其他失败 + """ + raise Exception("Not implemented, yet") + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_SEND_XML # FUNC_SEND_XML + req.xml.receiver = receiver + req.xml.content = xml + req.xml.type = type + if path: + req.xml.path = path + rsp = self._send_request(req) + return rsp.status + + def send_emotion(self, path: str, receiver: str) -> int: + """发送表情 + + Args: + path (str): 本地表情路径,如:`C:/Projs/WeChatRobot/emo.gif` + receiver (str): 消息接收人,wxid 或者 roomid + + Returns: + int: 0 为成功,其他失败 + """ + raise Exception("Not implemented, yet") + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_SEND_EMOTION # FUNC_SEND_EMOTION + req.file.path = path + req.file.receiver = receiver + rsp = self._send_request(req) + return rsp.status + + def get_msg(self, block=True) -> WxMsg: + """从消息队列中获取消息 + + Args: + block (bool): 是否阻塞,默认阻塞 + + Returns: + WxMsg: 微信消息 + + Raises: + Empty: 如果阻塞并且超时,抛出空异常,需要用户自行捕获 + """ + return self.msgQ.get(block, timeout=1) + + def enable_receiving_msg(self, pyq=False) -> bool: + """允许接收消息,成功后通过 `get_msg` 读取消息""" + def listening_msg(): + rsp = wcf_pb2.Response() + self.msg_socket.dial(self.msg_url, block=True) + while self._is_receiving_msg: + try: + rsp.ParseFromString(self.msg_socket.recv_msg().bytes) + except Exception as e: + pass + else: + self.msgQ.put(WxMsg(rsp.wxmsg)) + + # 退出前关闭通信通道 + self.msg_socket.close() + + if self._is_receiving_msg: + return True + + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_ENABLE_RECV_TXT # FUNC_ENABLE_RECV_TXT + req.flag = pyq + rsp = self._send_request(req) + if rsp.status != 0: + return False + + self._is_receiving_msg = True + # 阻塞,把控制权交给用户 + # self.listening_msg(callback) + + # 不阻塞,启动一个新的线程来接收消息 + Thread(target=listening_msg, name="GetMessage", daemon=True).start() + + return True + + def enable_recv_msg(self, callback: Callable[[WxMsg], None] = None) -> bool: + """(不建议使用)设置接收消息回调,消息量大时可能会丢失消息 + + .. deprecated:: 3.7.0.30.13 + """ + def listening_msg(): + rsp = wcf_pb2.Response() + self.msg_socket.dial(self.msg_url, block=True) + while self._is_receiving_msg: + try: + rsp.ParseFromString(self.msg_socket.recv_msg().bytes) + except Exception as e: + pass + else: + callback(WxMsg(rsp.wxmsg)) + # 退出前关闭通信通道 + self.msg_socket.close() + + if self._is_receiving_msg: + return True + + if callback is None: + return False + + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_ENABLE_RECV_TXT # FUNC_ENABLE_RECV_TXT + rsp = self._send_request(req) + if rsp.status != 0: + return False + + self._is_receiving_msg = True + # 阻塞,把控制权交给用户 + # listening_msg() + + # 不阻塞,启动一个新的线程来接收消息 + Thread(target=listening_msg, name="GetMessage", daemon=True).start() + + return True + + def disable_recv_msg(self) -> int: + """停止接收消息""" + if not self._is_receiving_msg: + return 0 + + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_DISABLE_RECV_TXT # FUNC_DISABLE_RECV_TXT + rsp = self._send_request(req) + self._is_receiving_msg = False + + return rsp.status + + def query_sql(self, db: str, sql: str) -> List[Dict]: + """执行 SQL,如果数据量大注意分页,以免 OOM + + Args: + db (str): 要查询的数据库 + sql (str): 要执行的 SQL + + Returns: + List[Dict]: 查询结果 + """ + result = [] + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_EXEC_DB_QUERY # FUNC_EXEC_DB_QUERY + req.query.db = db + req.query.sql = sql + rsp = self._send_request(req) + rows = json_format.MessageToDict(rsp.rows).get("rows", []) + for r in rows: + row = {} + for f in r["fields"]: + c = base64.b64decode(f.get("content", "")) + row[f["column"]] = self._SQL_TYPES[f["type"]](c) + result.append(row) + return result + + def accept_new_friend(self, v3: str, v4: str, scene: int = 30) -> int: + """通过好友申请 + + Args: + v3 (str): 加密用户名 (好友申请消息里 v3 开头的字符串) + v4 (str): Ticket (好友申请消息里 v4 开头的字符串) + scene: 申请方式 (好友申请消息里的 scene); 为了兼容旧接口,默认为扫码添加 (30) + + Returns: + int: 1 为成功,其他失败 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_ACCEPT_FRIEND # FUNC_ACCEPT_FRIEND + req.v.v3 = v3 + req.v.v4 = v4 + req.v.scene = scene + rsp = self._send_request(req) + return rsp.status + + def get_friends(self) -> List[Dict]: + """获取好友列表""" + not_friends = { + "fmessage": "朋友推荐消息", + "medianote": "语音记事本", + "floatbottle": "漂流瓶", + "filehelper": "文件传输助手", + "newsapp": "新闻", + } + friends = [] + for cnt in self.get_contacts(): + if (cnt["wxid"].endswith("@chatroom") or # 群聊 + cnt["wxid"].startswith("gh_") or # 公众号 + cnt["wxid"] in not_friends.keys() # 其他杂号 + ): + continue + friends.append(cnt) + + return friends + + def receive_transfer(self, wxid: str, transferid: str, transactionid: str) -> int: + """接收转账 + + Args: + wxid (str): 转账消息里的发送人 wxid + transferid (str): 转账消息里的 transferid + transactionid (str): 转账消息里的 transactionid + + Returns: + int: 1 为成功,其他失败 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_RECV_TRANSFER # FUNC_RECV_TRANSFER + req.tf.wxid = wxid + req.tf.tfid = transferid + req.tf.taid = transactionid + rsp = self._send_request(req) + return rsp.status + + def refresh_pyq(self, id: int = 0) -> int: + """刷新朋友圈 + + Args: + id (int): 开始 id,0 为最新页 + + Returns: + int: 1 为成功,其他失败 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_REFRESH_PYQ # FUNC_REFRESH_PYQ + req.ui64 = id + rsp = self._send_request(req) + return rsp.status + + def decrypt_image(self, src: str, dst: str) -> bool: + """解密图片: + + Args: + src (str): 加密的图片路径 + dst (str): 解密的图片路径 + + Returns: + bool: 是否成功 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_DECRYPT_IMAGE # FUNC_DECRYPT_IMAGE + req.dec.src = src + req.dec.dst = dst + rsp = self._send_request(req) + return rsp.status == 1 + + def add_chatroom_members(self, roomid: str, wxids: str) -> int: + """添加群成员 + + Args: + roomid (str): 待加群的 id + wxids (str): 要加到群里的 wxid,多个用逗号分隔 + + Returns: + int: 1 为成功,其他失败 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_ADD_ROOM_MEMBERS # FUNC_ADD_ROOM_MEMBERS + req.m.roomid = roomid + req.m.wxids = wxids + rsp = self._send_request(req) + return rsp.status + + def del_chatroom_members(self, roomid: str, wxids: str) -> int: + """删除群成员 + + Args: + roomid (str): 群的 id + wxids (str): 要删除成员的 wxid,多个用逗号分隔 + + Returns: + int: 1 为成功,其他失败 + """ + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_DEL_ROOM_MEMBERS # FUNC_DEL_ROOM_MEMBERS + req.m.roomid = roomid + req.m.wxids = wxids.replace(" ", "") + rsp = self._send_request(req) + return rsp.status + + def get_chatroom_members(self, roomid: str) -> Dict: + """获取群成员 + + Args: + roomid (str): 群的 id + + Returns: + Dict: 群成员列表: {wxid1: 昵称1, wxid2: 昵称2, ...} + """ + members = {} + contacts = self.query_sql("MicroMsg.db", "SELECT UserName, NickName FROM Contact;") + contacts = {contact["UserName"]: contact["NickName"]for contact in contacts} + crs = self.query_sql("MicroMsg.db", f"SELECT RoomData FROM ChatRoom WHERE ChatRoomName = '{roomid}';") + if not crs: + return members + + bs = crs[0].get("RoomData") + if not bs: + return members + + crd = RoomData() + crd.ParseFromString(bs) + if not bs: + return members + + for member in crd.members: + members[member.wxid] = member.name if member.name else contacts.get(member.wxid, "") + + return members + + def get_alias_in_chatroom(self, wxid: str, roomid: str) -> str: + """获取群名片 + + Args: + wxid (str): wxid + roomid (str): 群的 id + + Returns: + str: 群名片 + """ + nickname = self.query_sql("MicroMsg.db", f"SELECT NickName FROM Contact WHERE UserName = '{wxid}';") + if not nickname: + return "" + + nickname = nickname[0].get("NickName", "") + + crs = self.query_sql("MicroMsg.db", f"SELECT RoomData FROM ChatRoom WHERE ChatRoomName = '{roomid}';") + if not crs: + return "" + + bs = crs[0].get("RoomData") + if not bs: + return "" + + crd = RoomData() + crd.ParseFromString(bs) + for member in crd.members: + if member.wxid == wxid: + return member.name if member.name else nickname + + return "" diff --git a/clients/wcfautopy/wcferry/event/__init__.py b/clients/wcfautopy/wcferry/event/__init__.py new file mode 100644 index 0000000..104d964 --- /dev/null +++ b/clients/wcfautopy/wcferry/event/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from wcferry.event.event import Event +from wcferry.event.core import load_function + +Event = load_function(Event) + + + + + diff --git a/clients/wcfautopy/wcferry/event/core.py b/clients/wcfautopy/wcferry/event/core.py new file mode 100644 index 0000000..7254525 --- /dev/null +++ b/clients/wcfautopy/wcferry/event/core.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import traceback +import asyncio +from threading import Thread + + +def load_function(cls): + cls._add_callback = _add_callback + cls._run_func = _run_func + return cls + + +def _add_callback(self, func, bot): + """ + 消息处理函数加载器 + :param func: 消息处理函数 + :param args: 消息处理函数参数 + :param kwargs: 消息处理函数参数 + """ + if func in self._message_callback_func_list: return + self._message_callback_func_list.append(func) + self._message_callback_func[func] = bot + + + +def _run_func(self): + """ + 消息分发器, 将消息发送给所有消息处理函数 + """ + try: + async_func = [] + universal_func = [] + for ele in self._message_callback_func: + if asyncio.iscoroutinefunction(ele): + async_func.append(ele) + else: + universal_func.append(ele) + + # 同步函数运行器 + def run_universal_func(): + for fn in universal_func: + fn(self._message_callback_func[fn], self._message) + + if len(universal_func) != 0: Thread(target=run_universal_func).start() + if len(async_func) == 0: return + + # 异步函数运行器 + async def _run_callback(): + tasks = [asyncio.create_task(func(self._message_callback_func[func], self._message)) + for func in async_func] + await asyncio.wait(tasks) + + self._loop.run_until_complete(_run_callback()) + except: + traceback.print_exc() + + + diff --git a/clients/wcfautopy/wcferry/event/event.py b/clients/wcfautopy/wcferry/event/event.py new file mode 100644 index 0000000..fadb03f --- /dev/null +++ b/clients/wcfautopy/wcferry/event/event.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from abc import abstractmethod +import asyncio +import logging + + +class Event(object): + _message_callback_func = {} + _message_callback_func_list = [] + _loop = asyncio.get_event_loop() + + def __init__(self): + super(Event, self).__init__() + self._message = None + self._logger: logging = logging.getLogger() + + @abstractmethod + def _add_callback(self, func, *args, **kwargs): + """ + 消息处理函数加载器 + :param func: 消息处理函数 + :param args: 消息处理函数参数 + :param kwargs: 消息处理函数参数 + """ + raise NotImplementedError + + @abstractmethod + def _run_func(self): + """ + 消息分发器, 将消息发送给所有消息处理函数 + """ + raise NotImplementedError diff --git a/clients/wcfautopy/wcferry/roomdata_pb2.py b/clients/wcfautopy/wcferry/roomdata_pb2.py new file mode 100644 index 0000000..23ab48a --- /dev/null +++ b/clients/wcfautopy/wcferry/roomdata_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: roomdata.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eroomdata.proto\x12\x0f\x63om.iamteer.wcf\"\xf7\x01\n\x08RoomData\x12\x35\n\x07members\x18\x01 \x03(\x0b\x32$.com.iamteer.wcf.RoomData.RoomMember\x12\x0f\n\x07\x66ield_2\x18\x02 \x01(\x05\x12\x0f\n\x07\x66ield_3\x18\x03 \x01(\x05\x12\x0f\n\x07\x66ield_4\x18\x04 \x01(\x05\x12\x15\n\rroom_capacity\x18\x05 \x01(\x05\x12\x0f\n\x07\x66ield_6\x18\x06 \x01(\x05\x12\x0f\n\x07\x66ield_7\x18\x07 \x01(\x03\x12\x0f\n\x07\x66ield_8\x18\x08 \x01(\x03\x1a\x37\n\nRoomMember\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\x05\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'roomdata_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _ROOMDATA._serialized_start=36 + _ROOMDATA._serialized_end=283 + _ROOMDATA_ROOMMEMBER._serialized_start=228 + _ROOMDATA_ROOMMEMBER._serialized_end=283 +# @@protoc_insertion_point(module_scope) diff --git a/clients/wcfautopy/wcferry/wcf_pb2.py b/clients/wcfautopy/wcferry/wcf_pb2.py new file mode 100644 index 0000000..ba142c9 --- /dev/null +++ b/clients/wcfautopy/wcferry/wcf_pb2.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: wcf.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\twcf.proto\x12\x03wcf\"\xe8\x02\n\x07Request\x12\x1c\n\x04\x66unc\x18\x01 \x01(\x0e\x32\x0e.wcf.Functions\x12\x1b\n\x05\x65mpty\x18\x02 \x01(\x0b\x32\n.wcf.EmptyH\x00\x12\r\n\x03str\x18\x03 \x01(\tH\x00\x12\x1b\n\x03txt\x18\x04 \x01(\x0b\x32\x0c.wcf.TextMsgH\x00\x12\x1c\n\x04\x66ile\x18\x05 \x01(\x0b\x32\x0c.wcf.PathMsgH\x00\x12\x1d\n\x05query\x18\x06 \x01(\x0b\x32\x0c.wcf.DbQueryH\x00\x12\x1e\n\x01v\x18\x07 \x01(\x0b\x32\x11.wcf.VerificationH\x00\x12\x1c\n\x01m\x18\x08 \x01(\x0b\x32\x0f.wcf.AddMembersH\x00\x12\x1a\n\x03xml\x18\t \x01(\x0b\x32\x0b.wcf.XmlMsgH\x00\x12\x1b\n\x03\x64\x65\x63\x18\n \x01(\x0b\x32\x0c.wcf.DecPathH\x00\x12\x1b\n\x02tf\x18\x0b \x01(\x0b\x32\r.wcf.TransferH\x00\x12\x0e\n\x04ui64\x18\x0c \x01(\x04H\x00\x12\x0e\n\x04\x66lag\x18\r \x01(\x08H\x00\x42\x05\n\x03msg\"\xab\x02\n\x08Response\x12\x1c\n\x04\x66unc\x18\x01 \x01(\x0e\x32\x0e.wcf.Functions\x12\x10\n\x06status\x18\x02 \x01(\x05H\x00\x12\r\n\x03str\x18\x03 \x01(\tH\x00\x12\x1b\n\x05wxmsg\x18\x04 \x01(\x0b\x32\n.wcf.WxMsgH\x00\x12\x1e\n\x05types\x18\x05 \x01(\x0b\x32\r.wcf.MsgTypesH\x00\x12$\n\x08\x63ontacts\x18\x06 \x01(\x0b\x32\x10.wcf.RpcContactsH\x00\x12\x1b\n\x03\x64\x62s\x18\x07 \x01(\x0b\x32\x0c.wcf.DbNamesH\x00\x12\x1f\n\x06tables\x18\x08 \x01(\x0b\x32\r.wcf.DbTablesH\x00\x12\x1b\n\x04rows\x18\t \x01(\x0b\x32\x0b.wcf.DbRowsH\x00\x12\x1b\n\x02ui\x18\n \x01(\x0b\x32\r.wcf.UserInfoH\x00\x42\x05\n\x03msg\"\x07\n\x05\x45mpty\"\xba\x01\n\x05WxMsg\x12\x0f\n\x07is_self\x18\x01 \x01(\x08\x12\x10\n\x08is_group\x18\x02 \x01(\x08\x12\n\n\x02id\x18\x03 \x01(\x04\x12\x0c\n\x04type\x18\x04 \x01(\r\x12\n\n\x02ts\x18\x05 \x01(\r\x12\x0e\n\x06roomid\x18\x06 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\x0e\n\x06sender\x18\x08 \x01(\t\x12\x0c\n\x04sign\x18\t \x01(\t\x12\r\n\x05thumb\x18\n \x01(\t\x12\r\n\x05\x65xtra\x18\x0b \x01(\t\x12\x0b\n\x03xml\x18\x0c \x01(\t\"7\n\x07TextMsg\x12\x0b\n\x03msg\x18\x01 \x01(\t\x12\x10\n\x08receiver\x18\x02 \x01(\t\x12\r\n\x05\x61ters\x18\x03 \x01(\t\")\n\x07PathMsg\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x10\n\x08receiver\x18\x02 \x01(\t\"G\n\x06XmlMsg\x12\x10\n\x08receiver\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\x05\"a\n\x08MsgTypes\x12\'\n\x05types\x18\x01 \x03(\x0b\x32\x18.wcf.MsgTypes.TypesEntry\x1a,\n\nTypesEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\nRpcContact\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0e\n\x06remark\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0f\n\x07\x63ountry\x18\x05 \x01(\t\x12\x10\n\x08province\x18\x06 \x01(\t\x12\x0c\n\x04\x63ity\x18\x07 \x01(\t\x12\x0e\n\x06gender\x18\x08 \x01(\x05\"0\n\x0bRpcContacts\x12!\n\x08\x63ontacts\x18\x01 \x03(\x0b\x32\x0f.wcf.RpcContact\"\x18\n\x07\x44\x62Names\x12\r\n\x05names\x18\x01 \x03(\t\"$\n\x07\x44\x62Table\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03sql\x18\x02 \x01(\t\"(\n\x08\x44\x62Tables\x12\x1c\n\x06tables\x18\x01 \x03(\x0b\x32\x0c.wcf.DbTable\"\"\n\x07\x44\x62Query\x12\n\n\x02\x64\x62\x18\x01 \x01(\t\x12\x0b\n\x03sql\x18\x02 \x01(\t\"8\n\x07\x44\x62\x46ield\x12\x0c\n\x04type\x18\x01 \x01(\x05\x12\x0e\n\x06\x63olumn\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\x0c\"%\n\x05\x44\x62Row\x12\x1c\n\x06\x66ields\x18\x01 \x03(\x0b\x32\x0c.wcf.DbField\"\"\n\x06\x44\x62Rows\x12\x18\n\x04rows\x18\x01 \x03(\x0b\x32\n.wcf.DbRow\"5\n\x0cVerification\x12\n\n\x02v3\x18\x01 \x01(\t\x12\n\n\x02v4\x18\x02 \x01(\t\x12\r\n\x05scene\x18\x03 \x01(\x05\"+\n\nAddMembers\x12\x0e\n\x06roomid\x18\x01 \x01(\t\x12\r\n\x05wxids\x18\x02 \x01(\t\"D\n\x08UserInfo\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06mobile\x18\x03 \x01(\t\x12\x0c\n\x04home\x18\x04 \x01(\t\"#\n\x07\x44\x65\x63Path\x12\x0b\n\x03src\x18\x01 \x01(\t\x12\x0b\n\x03\x64st\x18\x02 \x01(\t\"4\n\x08Transfer\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04tfid\x18\x02 \x01(\t\x12\x0c\n\x04taid\x18\x03 \x01(\t*\x84\x04\n\tFunctions\x12\x11\n\rFUNC_RESERVED\x10\x00\x12\x11\n\rFUNC_IS_LOGIN\x10\x01\x12\x16\n\x12\x46UNC_GET_SELF_WXID\x10\x10\x12\x16\n\x12\x46UNC_GET_MSG_TYPES\x10\x11\x12\x15\n\x11\x46UNC_GET_CONTACTS\x10\x12\x12\x15\n\x11\x46UNC_GET_DB_NAMES\x10\x13\x12\x16\n\x12\x46UNC_GET_DB_TABLES\x10\x14\x12\x16\n\x12\x46UNC_GET_USER_INFO\x10\x15\x12\x11\n\rFUNC_SEND_TXT\x10 \x12\x11\n\rFUNC_SEND_IMG\x10!\x12\x12\n\x0e\x46UNC_SEND_FILE\x10\"\x12\x11\n\rFUNC_SEND_XML\x10#\x12\x15\n\x11\x46UNC_SEND_EMOTION\x10$\x12\x18\n\x14\x46UNC_ENABLE_RECV_TXT\x10\x30\x12\x19\n\x15\x46UNC_DISABLE_RECV_TXT\x10@\x12\x16\n\x12\x46UNC_EXEC_DB_QUERY\x10P\x12\x16\n\x12\x46UNC_ACCEPT_FRIEND\x10Q\x12\x16\n\x12\x46UNC_RECV_TRANSFER\x10R\x12\x14\n\x10\x46UNC_REFRESH_PYQ\x10S\x12\x16\n\x12\x46UNC_DECRYPT_IMAGE\x10`\x12\x19\n\x15\x46UNC_ADD_ROOM_MEMBERS\x10p\x12\x19\n\x15\x46UNC_DEL_ROOM_MEMBERS\x10qB\r\n\x0b\x63om.iamteerb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'wcf_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\013com.iamteer' + _MSGTYPES_TYPESENTRY._options = None + _MSGTYPES_TYPESENTRY._serialized_options = b'8\001' + _FUNCTIONS._serialized_start=1878 + _FUNCTIONS._serialized_end=2394 + _REQUEST._serialized_start=19 + _REQUEST._serialized_end=379 + _RESPONSE._serialized_start=382 + _RESPONSE._serialized_end=681 + _EMPTY._serialized_start=683 + _EMPTY._serialized_end=690 + _WXMSG._serialized_start=693 + _WXMSG._serialized_end=879 + _TEXTMSG._serialized_start=881 + _TEXTMSG._serialized_end=936 + _PATHMSG._serialized_start=938 + _PATHMSG._serialized_end=979 + _XMLMSG._serialized_start=981 + _XMLMSG._serialized_end=1052 + _MSGTYPES._serialized_start=1054 + _MSGTYPES._serialized_end=1151 + _MSGTYPES_TYPESENTRY._serialized_start=1107 + _MSGTYPES_TYPESENTRY._serialized_end=1151 + _RPCCONTACT._serialized_start=1154 + _RPCCONTACT._serialized_end=1289 + _RPCCONTACTS._serialized_start=1291 + _RPCCONTACTS._serialized_end=1339 + _DBNAMES._serialized_start=1341 + _DBNAMES._serialized_end=1365 + _DBTABLE._serialized_start=1367 + _DBTABLE._serialized_end=1403 + _DBTABLES._serialized_start=1405 + _DBTABLES._serialized_end=1445 + _DBQUERY._serialized_start=1447 + _DBQUERY._serialized_end=1481 + _DBFIELD._serialized_start=1483 + _DBFIELD._serialized_end=1539 + _DBROW._serialized_start=1541 + _DBROW._serialized_end=1578 + _DBROWS._serialized_start=1580 + _DBROWS._serialized_end=1614 + _VERIFICATION._serialized_start=1616 + _VERIFICATION._serialized_end=1669 + _ADDMEMBERS._serialized_start=1671 + _ADDMEMBERS._serialized_end=1714 + _USERINFO._serialized_start=1716 + _USERINFO._serialized_end=1784 + _DECPATH._serialized_start=1786 + _DECPATH._serialized_end=1821 + _TRANSFER._serialized_start=1823 + _TRANSFER._serialized_end=1875 +# @@protoc_insertion_point(module_scope) diff --git a/clients/wcfautopy/wcferry/wxmsg.py b/clients/wcfautopy/wcferry/wxmsg.py new file mode 100644 index 0000000..122ec7c --- /dev/null +++ b/clients/wcfautopy/wcferry/wxmsg.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +import re +from datetime import datetime +import time +from wcferry import wcf_pb2 + + +class WxMsg(dict): + """微信消息 + + Attributes: + type (int): 消息类型,可通过 `get_msg_types` 获取 + id (str): 消息 id + xml (str): 消息 xml 部分 + sender (str): 消息发送人 + roomid (str): (仅群消息有)群 id + content (str): 消息内容 + thumb (str): 视频或图片消息的缩略图路径 + extra (str): 视频或图片消息的路径 + """ + + def __init__(self, msg: wcf_pb2.WxMsg) -> None: + super(WxMsg, self).__init__() + self._is_self = msg.is_self + self._is_group = msg.is_group + self._type = msg.type + self._id = msg.id + self._ts = msg.ts + self._sign = msg.sign + self._xml = msg.xml + self._sender = msg.sender + self._roomid = msg.roomid + self._content = msg.content + self._thumb = msg.thumb + self._extra = msg.extra + self.__data = {'isSelf': True if self._is_self else False, + 'isGroup': True if self._is_group else False, + 'isPyq': True if self._type == 0 else False, + 'data': { + 'type': self._type, + 'content': self._content, + 'sender': self._sender, + 'msgid': self._id, + 'roomid': self._roomid if self._roomid else None, + 'xml': self._xml, + 'thumb': self._thumb if self._thumb else None, + 'extra': self._extra if self._extra else None, + 'time': int(time.time() * 1000), + }, 'revokmsgid': None, 'isRevokeMsg': False, } + self.__revokmsg_p() + + def __revokmsg_p(self): + rmsg = self.__data['data']['content'] + rev_type = re.findall('", rmsg) + if len(rev_type) == 0 or len(rev_w) == 0: return + if rev_type[0] == 'revokemsg' and rev_w[0] == '你撤回了一条消息': + self.__data['data']['content'] = rev_w[0] + self.__data['isRevokeMsg'] = True + self.__data['revokmsgid'] = re.findall('(.*?)', rmsg)[0] + + def __str__(self) -> str: + return repr(self.__data) + + def __repr__(self) -> str: + return repr(self.__data) + + def __getitem__(self, key): + return self.__data[key] + + def __getattr__(self, item): + if item in ['content', 'sender', 'roomid', 'xml', 'thumb', 'extra', 'type']: + return self.__data['data'][item] + if item == 'id': + return self.__data['data']['msgid'] + if item == 'ts': + return self._ts + if item == 'sign': + return self._sign + + def __setitem__(self, key, value): + self.__data[key] = value + + def is_image(self) -> bool: + """是否是图片""" + return self.type == 3 and ('imgdatahash' in self.__data['data']['content']) + + def is_voice(self) -> bool: + """是否是语音""" + return self.type == 34 and ('voicemsg' in self.__data['data']['content']) + + def is_video(self) -> bool: + """是否是视频""" + return self.type == 43 and ('videomsg' in self.__data['data']['content']) + + def is_pyq(self) -> bool: + return self.type == 0 + + def from_self(self) -> bool: + """是否自己发的消息""" + return self._is_self == 1 + + def from_group(self) -> bool: + """是否群聊消息""" + return self._is_group + + def is_at(self, wxid) -> bool: + """是否被 @:群消息,在 @ 名单里,并且不是 @ 所有人""" + if not self.from_group(): + return False # 只有群消息才能 @ + + if not re.findall(f".*({wxid}).*", self.xml): + return False # 不在 @ 清单里 + + if re.findall(r"@(?:所有人|all|All)", self.content): + return False # 排除 @ 所有人 + + return True + + def is_text(self) -> bool: + """是否文本消息""" + return self.type == 1