From 177bdd1c61c5e90d841cdcd774c37e01a9850f45 Mon Sep 17 00:00:00 2001 From: Changhua Date: Sun, 16 Oct 2022 16:50:22 +0800 Subject: [PATCH] Add python cilent --- python/README.MD | 26 +++++++++ python/demo.py | 49 ++++++++++++++++ python/wcf/client.py | 131 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 python/README.MD create mode 100644 python/demo.py create mode 100644 python/wcf/client.py diff --git a/python/README.MD b/python/README.MD new file mode 100644 index 0000000..39576d1 --- /dev/null +++ b/python/README.MD @@ -0,0 +1,26 @@ +# WeChatFerry Python 客户端 +代码于 `Python3.7` 环境开发。 + +## 配置环境 +```sh +# 创建虚拟环境 +python -m venv .env +# 激活虚拟环境 +source .env/Scripts/activate +# 升级 pip +pip install --upgrade pip +# 安装依赖包 +pip install grpcio grpcio-tools +``` + +## 运行 +```sh +# 启动客户端 +python demo.py +``` + +## 重新生成 gRPC 文件 +```sh +cd wcf +python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I=../ wcf.proto +``` diff --git a/python/demo.py b/python/demo.py new file mode 100644 index 0000000..6310bbe --- /dev/null +++ b/python/demo.py @@ -0,0 +1,49 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import signal +from time import sleep + +from wcf.client import Wcf + + +def main(): + logging.info("Start demo...") + wcf = Wcf() + + def handler(sig, frame): + wcf.cleanup() + exit(0) + + signal.signal(signal.SIGINT, handler) + sleep(1) # Slow down + print(f"Is Login: {True if wcf.is_login() else False}") + print(f"SelfWxid: {wcf.get_self_wxid()}") + + sleep(1) + wcf.enable_recv_msg(print) + # wcf.disable_recv_msg() # Call anytime when you don't want to receive messages + + ret = wcf.send_text("Hello world.", "filehelper") + print(f"send_text: {ret}") + + ret = wcf.send_image("TEQuant.jpeg", "filehelper") + print(f"send_image: {ret}") + + print(f"Message types:\n{wcf.get_msg_types()}") + print(f"Contacts:\n{wcf.get_contacts()}") + + print(f"DBs:\n{wcf.get_dbs()}") + print(f"Tables:\n{wcf.get_tables('db')}") + print(f"Results:\n{wcf.query_sql('MicroMsg.db', 'SELECT * FROM Contact LIMIT 1;')}") + + # wcf.accept_new_friend("v3", "v4") # 需要真正的 V3、V4 信息 + + # Keep running to receive messages + wcf.keep_running() + + +if __name__ == "__main__": + logging.basicConfig(level='DEBUG') + main() diff --git a/python/wcf/client.py b/python/wcf/client.py new file mode 100644 index 0000000..0dffa2b --- /dev/null +++ b/python/wcf/client.py @@ -0,0 +1,131 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +import atexit +import ctypes +import logging +import os +import sys +from threading import Thread +from time import sleep +from typing import Any, Callable, Optional + +import grpc + +WCF_ROOT = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, WCF_ROOT) +import wcf_pb2 # noqa +import wcf_pb2_grpc # noqa + + +class Wcf(): + def __init__(self, host_port: str = "localhost:10086") -> None: + self._enable_recv_msg = False + self.LOG = logging.getLogger("WCF") + self._sdk = ctypes.cdll.LoadLibrary(f"{WCF_ROOT}/sdk.dll") + if self._sdk.WxInitSDK() != 0: + self.LOG.error("初始化失败!") + return + + self._channel = grpc.insecure_channel(host_port) + self._stub = wcf_pb2_grpc.WcfStub(self._channel) + atexit.register(self.disable_recv_msg) # 退出的时候停止消息接收,防止内存泄露 + self._is_running = True + + def __del__(self) -> None: + self.cleanup() + + def cleanup(self) -> None: + if not self._is_running: + return + + self.disable_recv_msg() + self._channel.close() + self._sdk.WxDestroySDK() + handle = self._sdk._handle + del self._sdk + ctypes.windll.kernel32.FreeLibrary(handle) + self._is_running = False + + def keep_running(self): + try: + while True: + sleep(1) + except Exception as e: + self.cleanup() + + def is_login(self) -> int: + rsp = self._stub.RpcIsLogin(wcf_pb2.Empty()) + return rsp.status + + def get_self_wxid(self) -> str: + rsp = self._stub.RpcGetSelfWxid(wcf_pb2.Empty()) + return rsp.str + + def _rpc_get_message(self, func): + rsps = self._stub.RpcEnableRecvMsg(wcf_pb2.Empty()) + try: + for rsp in rsps: + func(rsp) + except Exception as e: + self.LOG.error(f"RpcEnableRecvMsg: {e}") + finally: + self.disable_recv_msg() + + def enable_recv_msg(self, callback: Callable[..., Any] = None) -> bool: + if self._enable_recv_msg: + return True + + if callback is None: + return False + + self._enable_recv_msg = True + # 阻塞,把控制权交给用户 + # self._rpc_get_message(callback) + + # 不阻塞,启动一个新的线程来接收消息 + Thread(target=self._rpc_get_message, name="GetMessage", args=(callback,), daemon=True).start() + + return True + + def disable_recv_msg(self) -> int: + if not self._enable_recv_msg: + return -1 + + rsp = self._stub.RpcDisableRecvMsg(wcf_pb2.Empty()) + if rsp.status == 0: + self._enable_recv_msg = False + + return rsp.status + + def send_text(self, msg: str, receiver: str, aters: Optional[str] = "") -> int: + rsp = self._stub.RpcSendTextMsg(wcf_pb2.TextMsg(msg=msg, receiver=receiver, aters=aters)) + return rsp.status + + def send_image(self, path: str, receiver: str) -> int: + rsp = self._stub.RpcSendImageMsg(wcf_pb2.ImageMsg(path=path, receiver=receiver)) + return rsp.status + + def get_msg_types(self) -> wcf_pb2.MsgTypes: + rsp = self._stub.RpcGetMsgTypes(wcf_pb2.Empty()) + return rsp + + def get_contacts(self) -> wcf_pb2.Contacts: + rsp = self._stub.RpcGetContacts(wcf_pb2.Empty()) + return rsp + + def get_dbs(self) -> wcf_pb2.DbNames: + rsp = self._stub.RpcGetDbNames(wcf_pb2.Empty()) + return rsp + + def get_tables(self, db: str) -> wcf_pb2.DbTables: + rsp = self._stub.RpcGetDbTables(wcf_pb2.String(str=db)) + return rsp + + def query_sql(self, db: str, sql: str) -> wcf_pb2.DbRows: + rsp = self._stub.RpcExecDbQuery(wcf_pb2.DbQuery(db=db, sql=sql)) + return rsp + + def accept_new_friend(self, v3: str, v4: str) -> int: + rsp = self._stub.RpcAcceptNewFriend(wcf_pb2.Verification(v3=v3, v4=v4)) + return rsp.status