diff --git a/.gitignore b/.gitignore index 8d9bcf1..05bea95 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,8 @@ main.spec build/ config.ini ntchat/wc/*.pyd +ntchat/wc/*.dat wheelhouse/ -setup_conf.py \ No newline at end of file +setup_conf.py +upload.bat +download/ \ No newline at end of file diff --git a/README.md b/README.md index a38a16f..3d7f449 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

NtChat

- release + release License

@@ -14,7 +14,14 @@ - 支持好友和群管理 ## 支持的微信版本下载 -- [WeChatSetup3.6.0.18.exe](https://webcdn.m.qq.com/spcmgr/download/WeChat3.6.0.18.exe) +- 下载 [WeChatSetup3.6.0.18.exe](https://github.com/tom-snow/wechat-windows-versions/releases/download/v3.6.0.18/WeChatSetup-3.6.0.18.exe) + +## 帮助文档 +- 查看 [常见问题](docs/FAQ.md) +- 查看 [常用示例](examples) +- 查看 [NtChatHttp接口示例](fastapi_example) +- 加入群聊 [PyXCGUI&NtChat交流群](https://jq.qq.com/?_wv=1027&k=oIXzbTbI) +- 查看 [PyXCGUI项目](https://github.com/smallevilbeast/pyxcgui) ## 安装 @@ -38,7 +45,7 @@ import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 等待登录 wechat.wait_login() @@ -63,7 +70,7 @@ import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 等待登录 wechat.wait_login() @@ -97,7 +104,7 @@ import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 注册消息回调 @@ -120,6 +127,14 @@ except KeyboardInterrupt: sys.exit() ``` +## 使用fastapi框架实现的web api接口 + +通过fastapi的swagger在线文档可以很方便的管理NtChat接口 + +[查看fastapi_example例子](./fastapi_example) + +![vfazT0.jpg](https://s1.ax1x.com/2022/08/29/vfazT0.jpg) + ## 使用pyxcgui界面库实现的简单例子 @@ -131,7 +146,7 @@ except KeyboardInterrupt: # -*- coding: utf8 -*- import xcgui import ntchat -from xcgui import XApp, XWindow +from xcgui import XApp, XWindow, RunUiThread class NtChatWindow(XWindow): @@ -155,7 +170,9 @@ class NtChatWindow(XWindow): def on_btn_open_clicked(self, sender, _): self.wechat_instance = ntchat.WeChat() - self.wechat_instance.open() + self.wechat_instance.open(smart=True) + + # 监听所有通知消息 self.wechat_instance.on(ntchat.MT_ALL, self.on_recv_message) def on_btn_send_clicked(self, sender, _): @@ -167,6 +184,7 @@ class NtChatWindow(XWindow): else: self.wechat_instance.send_text(self.edit_wxid.getText(), self.edit_content.getText()) + @RunUiThread() def on_recv_message(self, wechat, message): text = self.edit_log.getText() text += "\n" diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..bde7be8 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,102 @@ +## 1. WeChatVersionNotMatchError异常 +如果出现`ntchat.exception.WeChatVersionNotMatchError`异常, 请确认是否安装github上指定的微信版本,如果确认已经安装,还是报错,可以在代码中添加以下代码,跳过微信版本检测 +```python +import ntchat +ntchat.set_wechat_exe_path(wechat_version='3.6.0.18') +``` +如果还是无法正常使用,但确认已经安装过了3.6.0.18版本可以如下设置 +```python +import ntchat + +# wechat_exe_path设置成自己3.6.0.18版本的微信的安装路径 +ntchat.set_wechat_exe_path( + wechat_exe_path=r"C:\Program Files (x86)\Tencent\WeChat\WeChat.exe", + wechat_version="3.6.0.18") +``` + +也可以使用注册表修复这个问题,将下面内容保存成WeChatFix.reg, 并双击运行, 如果安装时有修改安装路径,需要修改下面的InstallPath为自己设定的安装路径 +```editorconfig +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\SOFTWARE\Tencent\WeChat] +"Version"=dword:63060012 +"InstallPath"="C:\Program Files (x86)\Tencent\WeChat" +``` + +## 2. `ImportError: cannot import name 'wcprobe' from 'ntchat.wc'` + +出现在这个错误的原因是因为你在github下载的源码目录中运行程序,因为wcprobe是根据python版本自动编译生成的,所以源码目录中没有这个文件. + +你需要将要`运行的例子文件移动到非源码目录下`,再去运行或打包 + +## 3. 如何多开 + +新建多个ntchat.WeChat实例,然后调用open方法: +```python +import ntchat + +# 多开3个微信 +for i in range(3): + wechat = ntchat.WeChat() + wechat.open(smart=False) +``` +更完善的多实例管理查看[fastapi_example例子](./fastapi_example) + +## 4. 如何监听输出所有的消息 +```python +# 注册监听所有消息回调 +@wechat.msg_register(ntchat.MT_ALL) +def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): + print("########################") + print(message) +``` +完全例子查看[examples/msg_register_all.py](../examples/msg_register_all.py) + +## 5. 如何关闭NtChat的日志 + +`os.environ['NTCHAT_LOG'] = "ERROR"` 要在`import ntchat`前执行 +```python +# -*- coding: utf-8 -*- +import os +import sys +import time +os.environ['NTCHAT_LOG'] = "ERROR" + +import ntchat +``` + +## 6. 如何正常的关闭Cmd窗口 + +先使用`pip install pywin32` 安装pywin32模块, 然后在代码中添加以下代码, 完整例子查看[examples/cmd_close_event.py](../examples/cmd_close_event.py) +```python +import sys +import ntchat +import win32api + +def on_exit(sig, func=None): + ntchat.exit_() + sys.exit() + + +# 当关闭cmd窗口时 +win32api.SetConsoleCtrlHandler(on_exit, True) +``` + + +## 7. pyinstaller打包exe +使用pyinstaller打包NtChat项目,需要添加`--collect-data=ntchat`选项 + +打包成单个exe程序 +```bash +pyinstaller -F --collect-data=ntchat main.py +``` + +将所有的依赖文件打包到一个目录中 +```bash +pyinstaller -y --collect-data=ntchat main.py +``` + +打包fastapi_example示例,需要添加`--paths=. --collect-data=ntchat` +```bash +pyinstaller -F --paths=. --collect-data=ntchat main.py +``` \ No newline at end of file diff --git a/examples/auto_accept_friend_request.py b/examples/auto_accept_friend_request.py index 0d29212..676883f 100644 --- a/examples/auto_accept_friend_request.py +++ b/examples/auto_accept_friend_request.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- import sys +import time import ntchat import xml.dom.minidom wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 注册消息回调 @@ -21,12 +22,18 @@ def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): scene = dom.documentElement.getAttribute("scene") # 自动同意好友申请 - wechat_instance.accept_friend_request(encryptusername, ticket, int(scene)) + ret = wechat_instance.accept_friend_request(encryptusername, ticket, int(scene)) + if ret: + # 通过后向他发条消息 + wechat_instance.send_text(to_wxid=ret["userName"], content="你好!!!!!") + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 try: while True: - pass + time.sleep(0.5) except KeyboardInterrupt: ntchat.exit_() sys.exit() + diff --git a/examples/bomber.py b/examples/bomber.py new file mode 100644 index 0000000..d1f8ee9 --- /dev/null +++ b/examples/bomber.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +import os +os.environ['NTCHAT_LOG'] = "ERROR" + +import time +import ntchat + +wechat = ntchat.WeChat() +wechat.open(smart=True) + +print("正在登录微信") +wechat.wait_login() + +peer_wxid = None + +while True: + contact_remark = input("请输入想发送的联系人备注: ") + contacts = wechat.search_contacts(remark=contact_remark) + if not contacts: + print(f"没有搜索到备注是{contact_remark}的联系人") + else: + print(f"搜索到{len(contacts)}个联系人: ") + print("0. 重新选择") + for i, contact in enumerate(contacts): + print(f"{i+1}. 昵称: {contact['nickname']}, 备注: {contact['remark']}") + seq = int(input("输入上面编号进行选择: ")) + if seq != 0: + peer_wxid = contacts[seq-1]["wxid"] + break + +content = input("请输入发送的内容: ") +number = int(input("请输入发送的次数: ")) + +for i in range(1, number+1): + time.sleep(0.1) + print("正在发送第%d遍" % i) + wechat.send_text(to_wxid=peer_wxid, content=content) + + +ntchat.exit_() + + + diff --git a/examples/close_log.py b/examples/close_log.py new file mode 100644 index 0000000..e809e97 --- /dev/null +++ b/examples/close_log.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +import os +import sys +import time +os.environ['NTCHAT_LOG'] = "ERROR" + +import ntchat + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + + +# 注册消息回调 +@wechat.msg_register(ntchat.MT_RECV_TEXT_MSG) +def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + self_wxid = wechat_instance.get_login_info()["wxid"] + room_wxid = data["room_wxid"] + + # 判断消息不是自己发的并且不是群消息时,回复对方 + if from_wxid != self_wxid and not room_wxid: + wechat_instance.send_text(to_wxid=from_wxid, content=f"你发送的消息是: {data['msg']}") + + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +try: + while True: + time.sleep(0.5) +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() diff --git a/examples/cmd_close_event.py b/examples/cmd_close_event.py new file mode 100644 index 0000000..14d8d28 --- /dev/null +++ b/examples/cmd_close_event.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat +try: + import win32api +except ImportError: + print("Error: this example require pywin32, use `pip install pywin32` install") + sys.exit() + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + + +# 注册消息回调 +@wechat.msg_register(ntchat.MT_RECV_TEXT_MSG) +def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + self_wxid = wechat_instance.get_login_info()["wxid"] + + # 判断消息不是自己发的,并回复对方 + if from_wxid != self_wxid: + wechat_instance.send_text(to_wxid=from_wxid, content=f"你发送的消息是: {data['msg']}") + + +def exit_application(): + ntchat.exit_() + sys.exit() + + +def on_exit(sig, func=None): + exit_application() + + +# 当关闭cmd窗口时 +win32api.SetConsoleCtrlHandler(on_exit, True) + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +try: + while True: + time.sleep(0.5) +# 当Ctrl+C结束程序时 +except KeyboardInterrupt: + exit_application() diff --git a/examples/echo_bot_image.py b/examples/echo_bot_image.py new file mode 100644 index 0000000..5cc6961 --- /dev/null +++ b/examples/echo_bot_image.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + + +# 注册消息回调 +@wechat.msg_register(ntchat.MT_RECV_PICTURE_MSG) +def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + self_wxid = wechat_instance.get_login_info()["wxid"] + room_wxid = data["room_wxid"] + + # 判断消息不是自己发的并且不是群消息时,回复对方 + if from_wxid != self_wxid and not room_wxid: + time.sleep(3) + wechat_instance.send_image(to_wxid=from_wxid, file_path=data["image"]) + + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +try: + while True: + time.sleep(0.5) +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() diff --git a/examples/echo_bot_msg_register.py b/examples/echo_bot_msg_register.py index 685a703..84bf82b 100644 --- a/examples/echo_bot_msg_register.py +++ b/examples/echo_bot_msg_register.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- import sys +import time import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 注册消息回调 @@ -14,15 +15,17 @@ def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): data = message["data"] from_wxid = data["from_wxid"] self_wxid = wechat_instance.get_login_info()["wxid"] + room_wxid = data["room_wxid"] - # 判断消息不是自己发的,并回复对方 - if from_wxid != self_wxid: + # 判断消息不是自己发的并且不是群消息时,回复对方 + if from_wxid != self_wxid and not room_wxid: wechat_instance.send_text(to_wxid=from_wxid, content=f"你发送的消息是: {data['msg']}") +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 try: while True: - pass + time.sleep(0.5) except KeyboardInterrupt: ntchat.exit_() sys.exit() diff --git a/examples/echo_bot_on.py b/examples/echo_bot_on.py index 980e43d..32ccf54 100644 --- a/examples/echo_bot_on.py +++ b/examples/echo_bot_on.py @@ -1,29 +1,32 @@ # -*- coding: utf-8 -*- import sys +import time import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): data = message["data"] from_wxid = data["from_wxid"] self_wxid = wechat_instance.get_login_info()["wxid"] + room_wxid = data["room_wxid"] - # 判断消息不是自己发的,并回复对方 - if from_wxid != self_wxid: + # 判断消息不是自己发的并且不是群消息时,回复对方 + if from_wxid != self_wxid and not room_wxid: wechat_instance.send_text(to_wxid=from_wxid, content=f"你发送的消息是: {data['msg']}") # 监听接收文本消息 wechat.on(ntchat.MT_RECV_TEXT_MSG, on_recv_text_msg) +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 try: while True: - pass + time.sleep(0.5) except KeyboardInterrupt: ntchat.exit_() sys.exit() diff --git a/examples/get_contacts.py b/examples/get_contacts.py index 9c17c69..7a6e1c1 100644 --- a/examples/get_contacts.py +++ b/examples/get_contacts.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- import sys +import time import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 等待登录 wechat.wait_login() @@ -17,9 +18,11 @@ print(contacts) +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 try: while True: - pass + time.sleep(0.5) except KeyboardInterrupt: ntchat.exit_() sys.exit() + diff --git a/examples/get_publics.py b/examples/get_publics.py new file mode 100644 index 0000000..1a99452 --- /dev/null +++ b/examples/get_publics.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat + + +def version_tuple(v): + return tuple(map(int, (v.split(".")))) + + +if version_tuple(ntchat.__version__) < version_tuple('0.1.8'): + print("error: ntchat version required 0.1.8, use `pip install -U ntchat` to upgrade") + sys.exit() + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + +# 等待登录 +wechat.wait_login() + +# 获取群列表并输出 +rooms = wechat.get_publics() + +print("公众号列表: ") +print(rooms) + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +try: + while True: + time.sleep(0.5) +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() diff --git a/examples/get_rooms.py b/examples/get_rooms.py index 986bc2c..f187f7c 100644 --- a/examples/get_rooms.py +++ b/examples/get_rooms.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- import sys +import time import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 等待登录 wechat.wait_login() @@ -17,9 +18,11 @@ print(rooms) +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 try: while True: - pass + time.sleep(0.5) except KeyboardInterrupt: ntchat.exit_() sys.exit() + diff --git a/examples/msg_register_all.py b/examples/msg_register_all.py new file mode 100644 index 0000000..d357acb --- /dev/null +++ b/examples/msg_register_all.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + + +# 注册监听所有消息回调 +@wechat.msg_register(ntchat.MT_ALL) +def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): + print("########################") + print(message) + + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +try: + while True: + time.sleep(0.5) +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() diff --git a/examples/multi_open.py b/examples/multi_open.py index a9ed2e7..e79d41a 100644 --- a/examples/multi_open.py +++ b/examples/multi_open.py @@ -5,5 +5,5 @@ # 多开3个微信 for i in range(3): wechat = ntchat.WeChat() - wechat.open() + wechat.open(smart=False) diff --git a/examples/quit_event.py b/examples/quit_event.py new file mode 100644 index 0000000..d607b76 --- /dev/null +++ b/examples/quit_event.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat + + +def version_tuple(v): + return tuple(map(int, (v.split(".")))) + + +if version_tuple(ntchat.__version__) < version_tuple('0.1.4'): + print("error: ntchat version required 0.1.4, use `pip install -U ntchat` to upgrade") + sys.exit() + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + +global_quit_flag = False + + +# 微信进程关闭通知 +@wechat.msg_register(ntchat.MT_RECV_WECHAT_QUIT_MSG) +def on_wechat_quit(wechat_instace): + print("###################") + global global_quit_flag + global_quit_flag = True + + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +while True: + if global_quit_flag: + break + time.sleep(0.5) + +ntchat.exit_() +sys.exit() diff --git a/examples/resources/send_text_ui.jpg b/examples/resources/send_text_ui.jpg deleted file mode 100644 index 8ee3ac2..0000000 Binary files a/examples/resources/send_text_ui.jpg and /dev/null differ diff --git a/examples/schedule_send_text.py b/examples/schedule_send_text.py new file mode 100644 index 0000000..08efe94 --- /dev/null +++ b/examples/schedule_send_text.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat +from datetime import datetime +try: + import schedule +except ImportError: + print("Error: this example require schedule module, use `pip install schedule` install") + sys.exit() + +# 创建微信实例 +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + + +# 发送文本消息任务 +def send_text_job(): + if not wechat.login_status: + return + human_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + wechat.send_text(to_wxid="filehelper", content=f"[NtChat] {human_time}") + + +# 设置调度的参数,这里是每5秒执行一次 +schedule.every(5).seconds.do(send_text_job) + + +''' +# 每小时执行 +schedule.every().hour.do(job) + +# 每天12:25执行 +schedule.every().day.at("12:25").do(job) + +# 每2到5分钟时执行 +schedule.every(5).to(10).minutes.do(job) + +# 每星期4的19:15执行 +schedule.every().thursday.at("19:15").do(job) + +# 每第17分钟时就执行 +schedule.every().minute.at(":17").do(job) +''' + +try: + while True: + schedule.run_pending() + time.sleep(1) +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() + + + + diff --git a/examples/search_contacts.py b/examples/search_contacts.py new file mode 100644 index 0000000..2987536 --- /dev/null +++ b/examples/search_contacts.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat + + +def version_tuple(v): + return tuple(map(int, (v.split(".")))) + + +if version_tuple(ntchat.__version__) < version_tuple('0.1.7'): + print("error: ntchat version required 0.1.7, use `pip install -U ntchat` to upgrade") + sys.exit() + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + +# 等待登录 +wechat.wait_login() + +# 根据wxid模糊查询查询联系人 +contacts = wechat.search_contacts(wxid="wxid_") +print(contacts) + +# 根据微信号模糊查询联系人 +# contacts = wechat.search_contacts(account="") + + +# 根据昵称模糊查询联系人, 如昵称包含`小`的联系人 +contacts = wechat.search_contacts(nickname="小") +print(contacts) + +# 根据备注查询联系人 +contacts = wechat.search_contacts(remark="备注") +print(contacts) + + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +try: + while True: + time.sleep(0.5) +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() diff --git a/examples/send_room_at_msg.py b/examples/send_room_at_msg.py new file mode 100644 index 0000000..cc1478b --- /dev/null +++ b/examples/send_room_at_msg.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat + +wechat = ntchat.WeChat() + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + +# 等待登录 +wechat.wait_login() + +''' +test,你好{$@},你好{$@}.早上好 + +发送内容中{$@}占位符说明: + +文本消息的content的内容中设置占位字符串 {$@},这些字符的位置就是最终的@符号所在的位置 +假设这两个被@的微信号的群昵称分别为aa,bb +则实际发送的内容为 "test,你好@ aa,你好@ bb.早上好"(占位符被替换了) + +占位字符串的数量必须和at_list中的微信数量相等. +''' + +# 下面是@两个人的发送例子,room_wxid, at_list需要自己替换 +wechat.send_room_at_msg(to_wxid="xxxxxx@chatroom", + content="测试, 你好{$@},你好{$@}", + at_list=['wxid_xxxxxxxx', 'wxid_xxxxxxxxx']) + + +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 +try: + while True: + time.sleep(0.5) +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() + + + + diff --git a/examples/send_text.py b/examples/send_text.py index b6932e3..536aa3c 100644 --- a/examples/send_text.py +++ b/examples/send_text.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- import sys +import time import ntchat wechat = ntchat.WeChat() # 打开pc微信, smart: 是否管理已经登录的微信 -wechat.open(smart=False) +wechat.open(smart=True) # 等待登录 wechat.wait_login() @@ -13,12 +14,14 @@ # 向文件助手发送一条消息 wechat.send_text(to_wxid="filehelper", content="hello, filehelper") +# 以下是为了让程序不结束,如果有用于PyQt等有主循环消息的框架,可以去除下面代码 try: while True: - pass + time.sleep(0.5) except KeyboardInterrupt: ntchat.exit_() sys.exit() + diff --git a/examples/send_text_ui.py b/examples/send_text_ui.py index 64543c8..59f1aad 100644 --- a/examples/send_text_ui.py +++ b/examples/send_text_ui.py @@ -1,6 +1,6 @@ import xcgui import ntchat -from xcgui import XApp, XWindow +from xcgui import XApp, XWindow, RunUiThread class NtChatWindow(XWindow): @@ -24,7 +24,9 @@ def __init__(self): def on_btn_open_clicked(self, sender, _): self.wechat_instance = ntchat.WeChat() - self.wechat_instance.open() + self.wechat_instance.open(smart=True) + + # 监听所有通知消息 self.wechat_instance.on(ntchat.MT_ALL, self.on_recv_message) def on_btn_send_clicked(self, sender, _): @@ -36,6 +38,7 @@ def on_btn_send_clicked(self, sender, _): else: self.wechat_instance.send_text(self.edit_wxid.getText(), self.edit_content.getText()) + @RunUiThread() def on_recv_message(self, wechat, message): text = self.edit_log.getText() text += "\n" diff --git a/examples/show_login_qrcode.py b/examples/show_login_qrcode.py new file mode 100644 index 0000000..49018ba --- /dev/null +++ b/examples/show_login_qrcode.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +import sys +import time +import ntchat + + +def version_tuple(v): + return tuple(map(int, (v.split(".")))) + + +if version_tuple(ntchat.__version__) < version_tuple('0.1.15'): + print("error: ntchat version required 0.1.15, use `pip install -U ntchat` to upgrade") + sys.exit() + +wechat = ntchat.WeChat() + +# 打开一个新的微信,并显示二维码界面 +wechat.open(smart=False, show_login_qrcode=True) diff --git a/examples/test.py b/examples/test.py deleted file mode 100644 index b15c3a0..0000000 --- a/examples/test.py +++ /dev/null @@ -1,8 +0,0 @@ -import xml.dom.minidom - -content = "" -dom = xml.dom.minidom.parseString(content) -encryptusername = dom.documentElement.getAttribute("encryptusername") -ticket = dom.documentElement.getAttribute("ticket") -scene = dom.documentElement.getAttribute("scene") -print(dom.documentElement.getAttribute("encryptusername")) \ No newline at end of file diff --git a/examples/transmit.py b/examples/transmit.py new file mode 100644 index 0000000..9062f84 --- /dev/null +++ b/examples/transmit.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +from email import message +from email.mime import image +import sys +import os.path +import time +import ntchat +import re + +# 聊天记录通知 +MT_RECV_CHAT_RECORDS_MSG = 11061 + +wechat = ntchat.WeChat() + +# 要监听的wxids,可以通过获取contact接口获取wxid,也可以开启后从debug信息中看出来 +from_wxids = ["xxxxx", "xxxxxx"] +# 要转发的目标群 +target_wxids = ["xxxxxxxx@chatroom"] +# 检查文件等待时长,单位s +wait_limit = 10 + +# 打开pc微信, smart: 是否管理已经登录的微信 +wechat.open(smart=True) + + +@wechat.msg_register(ntchat.MT_RECV_TEXT_MSG) +def on_recv_text_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + self_wxid = wechat_instance.get_login_info()["wxid"] + + # 判断消息不是自己发的,且来自于想要转发的用户列表,并发给target用户 + if from_wxid != self_wxid and from_wxid in from_wxids: + for target_wxid in target_wxids: + wechat_instance.send_text(to_wxid=target_wxid, + content=f"{data['msg']}") + + +# 等待file_path的文件被下载,超过等待次数后返回true +def wait_for_file(file_path) -> bool: + cnt = 0 + while not os.path.exists(file_path): + time.sleep(1) + cnt = cnt + 1 + if cnt > wait_limit: + print( + f"wait for {wait_limit} second, but file cannot be downloaded, forgive." + ) + return False + return True + + +@wechat.msg_register(ntchat.MT_RECV_IMAGE_MSG) +def on_recv_img_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + img_path = data["image"] + # img_path = "D:\\a.png" + self_wxid = wechat_instance.get_login_info()["wxid"] + + # 判断消息不是自己发的,且来自于想要转发的用户列表,并发给target用户 + if from_wxid != self_wxid and from_wxid in from_wxids: + if wait_for_file(file_path=img_path): + for target_wxid in target_wxids: + wechat_instance.send_image(to_wxid=target_wxid, file_path=img_path) + + +@wechat.msg_register(ntchat.MT_RECV_FILE_MSG) +def on_recv_img_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + file_path = data["file"] + self_wxid = wechat_instance.get_login_info()["wxid"] + + # 判断消息不是自己发的,且来自于想要转发的用户列表,并发给target用户 + if from_wxid != self_wxid and from_wxid in from_wxids: + if wait_for_file(file_path=file_path): + for target_wxid in target_wxids: + wechat_instance.send_file(to_wxid=target_wxid, file_path=file_path) + + +def update_wxid_in_xml(xml, from_wxid, target_wxid): + patten = re.compile(from_wxid) + return patten.sub(target_wxid, xml) + + +@wechat.msg_register(MT_RECV_CHAT_RECORDS_MSG) +def on_recv_chat_record_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + raw_msg = data["raw_msg"] + self_wxid = wechat_instance.get_login_info()["wxid"] + xml = update_wxid_in_xml(raw_msg, from_wxid, self_wxid) + # 判断消息不是自己发的,且来自于想要转发的用户列表,并发给target用户 + if from_wxid != self_wxid and from_wxid in from_wxids: + for target_wxid in target_wxids: + wechat_instance.send_xml(to_wxid=target_wxid, xml=xml) + + +@wechat.msg_register(ntchat.MT_RECV_LINK_MSG) +def on_recv_link_msg(wechat_instance: ntchat.WeChat, message): + data = message["data"] + from_wxid = data["from_wxid"] + raw_msg = data["raw_msg"] + # xml中的fromusername改成自己的wxid 再发 + self_wxid = wechat_instance.get_login_info()["wxid"] + xml = update_wxid_in_xml(raw_msg, from_wxid, self_wxid) + # 判断消息不是自己发的,且来自于想要转发的用户列表,并发给target用户 + if from_wxid != self_wxid and from_wxid in from_wxids: + for target_wxid in target_wxids: + wechat_instance.send_xml(to_wxid=target_wxid, xml=xml) + + +try: + while True: + pass +except KeyboardInterrupt: + ntchat.exit_() + sys.exit() \ No newline at end of file diff --git a/fastapi_example/README.md b/fastapi_example/README.md new file mode 100644 index 0000000..ad6c5a1 --- /dev/null +++ b/fastapi_example/README.md @@ -0,0 +1,26 @@ +## NtChat fastapi完整示例 + +通过fastapi的swagger在线文档可以很方便的管理NtChat接口 +![vfazT0.jpg](https://s1.ax1x.com/2022/08/29/vfazT0.jpg) + +## 安装依赖 +```bash +pip install -r requirements.txt +``` + +## 运行例子 +```bash +python main.py +``` + +## 访问api在线文档 +[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) + + +## 如何调用 + +可以使用requests库去访问接口 + +/client/create 是创建一个微信的实例,返回guid,标识实例的id, 后面所有的接口都要用到 + +/client/open 是打开并管理上微信实例 \ No newline at end of file diff --git a/fastapi_example/__init__.py b/fastapi_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi_example/down.py b/fastapi_example/down.py new file mode 100644 index 0000000..b00582d --- /dev/null +++ b/fastapi_example/down.py @@ -0,0 +1,24 @@ +import os.path +import requests +from xdg import get_download_dir +from models import SendMediaReqModel +from ntchat.utils import generate_guid + + +def new_download_file(): + while True: + path = os.path.join(get_download_dir(), generate_guid("temp")) + if not os.path.isfile(path): + return path + + +def get_local_path(model: SendMediaReqModel): + if os.path.isfile(model.file_path): + return model.file_path + if not model.url: + return None + data = requests.get(model.url).content + temp_file = new_download_file() + with open(temp_file, 'wb') as fp: + fp.write(data) + return temp_file diff --git a/fastapi_example/exception.py b/fastapi_example/exception.py new file mode 100644 index 0000000..d80b757 --- /dev/null +++ b/fastapi_example/exception.py @@ -0,0 +1,9 @@ +class ClientNotExists(Exception): + guid = "" + + def __init__(self, guid): + self.guid = guid + + +class MediaNotExistsError(Exception): + pass diff --git a/fastapi_example/main.py b/fastapi_example/main.py new file mode 100644 index 0000000..5bcff80 --- /dev/null +++ b/fastapi_example/main.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +import uvicorn +import threading +from functools import wraps +from fastapi import FastAPI +from mgr import ClientManager +from down import get_local_path +from exception import MediaNotExistsError, ClientNotExists +import models +import ntchat + + +def response_json(status=0, data=None, msg=""): + return { + "status": status, + "data": {} if data is None else data, + "msg": msg + } + + +class catch_exception: + def __call__(self, f): + @wraps(f) + async def wrapper(*args, **kwargs): + try: + return await f(*args, **kwargs) + except ntchat.WeChatNotLoginError: + return response_json(msg="wechat instance not login") + except ntchat.WeChatBindError: + return response_json(msg="wechat bind error") + except ntchat.WeChatVersionNotMatchError: + return response_json(msg="wechat version not match, install require wechat version") + except MediaNotExistsError: + return response_json(msg="file_path or url error") + except ClientNotExists as e: + return response_json(msg="client not exists, guid: %s" % e.guid) + except Exception as e: + return response_json(msg=str(e)) + + return wrapper + + +client_mgr = ClientManager() +app = FastAPI(title="NtChat fastapi完整示例", + description="NtChat项目地址: https://github.com/smallevilbeast/ntchat") + + +@app.post("/client/create", summary="创建实例", tags=["Client"], + response_model=models.ResponseModel) +@catch_exception() +async def client_create(): + guid = client_mgr.create_client() + return response_json(1, {"guid": guid}) + + +@app.post("/client/open", summary="打开微信", tags=["Client"], + response_model=models.ResponseModel) +@catch_exception() +async def client_open(model: models.ClientOpenReqModel): + client = client_mgr.get_client(model.guid) + ret = client.open(model.smart, model.show_login_qrcode) + + # 当show_login_qrcode=True时, 打开微信时会显示二维码界面 + if model.show_login_qrcode: + client.qrcode_event = threading.Event() + client.qrcode_event.wait(timeout=10) + return response_json(1 if ret else 0, {'qrcode': client.qrcode}) + + +@app.post("/global/set_callback_url", summary="设置接收通知地址", tags=["Global"], + response_model=models.ResponseModel) +@catch_exception() +async def client_set_callback_url(model: models.CallbackUrlReqModel): + client_mgr.callback_url = model.callback_url + return response_json(1) + + +@app.post("/user/get_profile", summary="获取自己的信息", tags=["User"], + response_model=models.ResponseModel) +@catch_exception() +async def user_get_profile(model: models.ClientReqModel): + data = client_mgr.get_client(model.guid).get_self_info() + return response_json(1, data) + + +@app.post("/contact/get_contacts", summary="获取联系人列表", tags=["Contact"], + response_model=models.ResponseModel) +@catch_exception() +async def get_contacts(model: models.ClientReqModel): + data = client_mgr.get_client(model.guid).get_contacts() + return response_json(1, data) + + +@app.post("/contact/get_contact_detail", summary="获取指定联系人详细信息", tags=["Contact"], + response_model=models.ResponseModel) +@catch_exception() +async def get_contact_detail(model: models.ContactDetailReqModel): + data = client_mgr.get_client(model.guid).get_contact_detail(model.wxid) + return response_json(1, data) + + +@app.post("/contact/modify_remark", summary="修改联系人备注", tags=["Contact"], response_model=models.ResponseModel) +@catch_exception() +async def send_gif(model: models.ModifyFriendRemarkReqModel): + data = client_mgr.get_client(model.guid).modify_friend_remark(model.wxid, model.remark) + return response_json(1, data) + + +@app.post("/room/get_rooms", summary="获取群列表", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def get_rooms(model: models.ClientReqModel): + data = client_mgr.get_client(model.guid).get_rooms() + return response_json(1, data) + + +@app.post("/room/get_name_name", summary="获取群名称", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def get_rooms(model: models.GetRoomNameReqModel): + name = client_mgr.get_client(model.guid).get_room_name(model.room_wxid) + data = { + "name": name + } + return response_json(1, data) + + +@app.post("/room/get_room_members", summary="获取群成员列表", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def get_room_members(model: models.GetRoomMembersReqModel): + data = client_mgr.get_client(model.guid).get_room_members(model.room_wxid) + return response_json(1, data) + + +@app.post("/room/create_room", summary="创建群", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def create_room(model: models.CreateRoomReqModel): + ret = client_mgr.get_client(model.guid).create_room(model.member_list) + return response_json(1 if ret else 0) + + +@app.post("/room/add_room_member", summary="添加好友入群", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def add_room_member(model: models.RoomMembersReqModel): + data = client_mgr.get_client(model.guid).add_room_member(model.room_wxid, model.member_list) + return response_json(1, data) + + +@app.post("/room/invite_room_member", summary="邀请好友入群", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def invite_room_member(model: models.RoomMembersReqModel): + data = client_mgr.get_client(model.guid).invite_room_member(model.room_wxid, model.member_list) + return response_json(1, data) + + +@app.post("/room/del_room_member", summary="删除群成员", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def del_room_member(model: models.RoomMembersReqModel): + data = client_mgr.get_client(model.guid).del_room_member(model.room_wxid, model.member_list) + return response_json(1, data) + + +@app.post("/room/add_room_friend", summary="添加群成员为好友", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def add_room_friend(model: models.AddRoomFriendReqModel): + data = client_mgr.get_client(model.guid).add_room_friend(model.room_wxid, + model.wxid, + model.verify) + return response_json(1, data) + + +@app.post("/room/modify_name", summary="修改群名", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def add_room_friend(model: models.ModifyRoomNameReqModel): + data = client_mgr.get_client(model.guid).modify_room_name(model.room_wxid, + model.name) + return response_json(1, data) + + +@app.post("/room/quit_room", summary="退出群", tags=["Room"], + response_model=models.ResponseModel) +@catch_exception() +async def quit_room(model: models.RoomReqModel): + data = client_mgr.get_client(model.guid).quit_room(model.room_wxid) + return response_json(1, data) + + +@app.post("/msg/send_text", summary="发送文本消息", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def msg_send_text(model: models.SendTextReqModel): + ret = client_mgr.get_client(model.guid).send_text(model.to_wxid, model.content) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_room_at", summary="发送群@消息", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_room_at(model: models.SendRoomAtReqModel): + ret = client_mgr.get_client(model.guid).send_room_at_msg(model.to_wxid, + model.content, + model.at_list) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_card", summary="发送名片", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_card(model: models.SendCardReqModel): + ret = client_mgr.get_client(model.guid).send_card(model.to_wxid, + model.card_wxid) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_link_card", summary="发送链接卡片消息", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_link_card(model: models.SendLinkCardReqModel): + ret = client_mgr.get_client(model.guid).send_link_card(model.to_wxid, + model.title, + model.desc, + model.url, + model.image_url) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_image", summary="发送图片", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_image(model: models.SendMediaReqModel): + file_path = get_local_path(model) + if file_path is None: + raise MediaNotExistsError() + ret = client_mgr.get_client(model.guid).send_image(model.to_wxid, file_path) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_file", summary="发送文件", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_file(model: models.SendMediaReqModel): + file_path = get_local_path(model) + if file_path is None: + raise MediaNotExistsError() + ret = client_mgr.get_client(model.guid).send_file(model.to_wxid, file_path) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_video", summary="发送视频", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_video(model: models.SendMediaReqModel): + file_path = get_local_path(model) + if file_path is None: + raise MediaNotExistsError() + ret = client_mgr.get_client(model.guid).send_video(model.to_wxid, file_path) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_gif", summary="发送GIF", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_gif(model: models.SendMediaReqModel): + file_path = get_local_path(model) + if file_path is None: + raise MediaNotExistsError() + ret = client_mgr.get_client(model.guid).send_gif(model.to_wxid, file_path) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_xml", summary="发送XML原始消息", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_gif(model: models.SendXmlReqModel): + ret = client_mgr.get_client(model.guid).send_xml(model.to_wxid, model.xml) + return response_json(1 if ret else 0) + + +@app.post("/msg/send_pat", summary="发送拍一拍", tags=["Msg"], response_model=models.ResponseModel) +@catch_exception() +async def send_gif(model: models.SendPatReqModel): + data = client_mgr.get_client(model.guid).send_pat(model.room_wxid, model.patted_wxid) + return response_json(1, data) + + +if __name__ == '__main__': + uvicorn.run(app=app, host='0.0.0.0', port=8000) diff --git a/fastapi_example/mgr.py b/fastapi_example/mgr.py new file mode 100644 index 0000000..61f8e04 --- /dev/null +++ b/fastapi_example/mgr.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +import ntchat +import threading +import requests +from typing import Dict, Union +from ntchat.utils.singleton import Singleton +from utils import generate_guid +from exception import ClientNotExists + + +class ClientWeChat(ntchat.WeChat): + guid: str = "" + qrcode_event: threading.Event = None + qrcode: str = "" + + +class ClientManager(metaclass=Singleton): + __client_map: Dict[str, ntchat.WeChat] = {} + callback_url: str = "" + + def new_guid(self): + """ + 生成新的guid + """ + while True: + guid = generate_guid("wechat") + if guid not in self.__client_map: + return guid + + def create_client(self): + guid = self.new_guid() + wechat = ClientWeChat() + wechat.guid = guid + self.__client_map[guid] = wechat + + # 注册回调 + wechat.on(ntchat.MT_ALL, self.__on_callback) + wechat.on(ntchat.MT_RECV_WECHAT_QUIT_MSG, self.__on_quit_callback) + return guid + + def get_client(self, guid: str) -> ClientWeChat: + client = self.__client_map.get(guid, None) + if client is None: + raise ClientNotExists(guid) + return client + + def remove_client(self, guid): + if guid in self.__client_map: + del self.__client_map[guid] + + def __on_callback(self, wechat: ClientWeChat, message: dict): + + # 通知二维码显示 + msg_type = message['type'] + if msg_type == ntchat.MT_RECV_LOGIN_QRCODE_MSG and wechat.qrcode_event: + wechat.qrcode = message["data"]["code"] + wechat.qrcode_event.set() + + if not self.callback_url: + return + + client_message = { + "guid": wechat.guid, + "message": message + } + requests.post(self.callback_url, json=client_message) + + def __on_quit_callback(self, wechat): + self.__on_callback(wechat, {"type": ntchat.MT_RECV_WECHAT_QUIT_MSG, "data": {}}) diff --git a/fastapi_example/models.py b/fastapi_example/models.py new file mode 100644 index 0000000..1028c51 --- /dev/null +++ b/fastapi_example/models.py @@ -0,0 +1,157 @@ +from typing import Optional, List, Any, Union, Dict +from pydantic import BaseModel + + +class ClientReqModel(BaseModel): + guid: str + + +class ResponseModel(BaseModel): + status: int + msg: Optional[str] = "" + data: Optional[Any] = None + + +class ClientOpenReqModel(ClientReqModel): + smart: Optional[bool] = True + show_login_qrcode: Optional[bool] = False + + +class CallbackUrlReqModel(BaseModel): + callback_url: Optional[str] = "" + + +class UserProfileModel(BaseModel): + wxid: str + nickname: str + account: str + avatar: str + + +class ContactModel(BaseModel): + account: str + avatar: str + city: str + country: str + nickname: str + province: str + remark: str + sex: int + wxid: str + + +class ContactDetailReqModel(ClientReqModel): + wxid: str + + +class ContactDetailModel(BaseModel): + account: str + avatar: str + city: str + country: str + nickname: str + province: str + remark: str + sex: int + wxid: str + signature: str + small_avatar: str + sns_pic: str + source_type: int + status: int + v1: str + v2: str + + +class AcceptFriendReqModel(ClientReqModel): + encryptusername: str + ticket: str + scene: int + + +class RoomModel(BaseModel): + wxid: str + nickname: str + avatar: str + is_manager: int + manager_wxid: str + total_member: int + member_list: List[str] + + +class RoomMemberModel(ContactModel): + display_name: str + + +class GetRoomMembersReqModel(ClientReqModel): + room_wxid: str + + +class GetRoomNameReqModel(ClientReqModel): + room_wxid: str + + +class CreateRoomReqModel(ClientReqModel): + member_list: List[str] + + +class RoomMembersReqModel(CreateRoomReqModel): + room_wxid: str + + +class AddRoomFriendReqModel(ClientReqModel): + room_wxid: str + wxid: str + verify: str + + +class RoomReqModel(ClientReqModel): + room_wxid: str + + +class ModifyRoomNameReqModel(RoomReqModel): + name: str + + +class SendMsgReqModel(ClientReqModel): + to_wxid: str + + +class SendTextReqModel(SendMsgReqModel): + content: str + + +class SendRoomAtReqModel(SendTextReqModel): + at_list: List[str] + + +class SendCardReqModel(SendMsgReqModel): + card_wxid: str + + +class SendLinkCardReqModel(SendMsgReqModel): + title: str + desc: str + url: str + image_url: str + + +class SendMediaReqModel(SendMsgReqModel): + file_path: Optional[str] = "" + url: Optional[str] = "" + + +class SendXmlReqModel(SendMsgReqModel): + xml: str + + +class SendPatReqModel(ClientReqModel): + room_wxid: str + patted_wxid: str + + +class ModifyFriendRemarkReqModel(ClientReqModel): + wxid: str + remark: str + + diff --git a/fastapi_example/requirements.txt b/fastapi_example/requirements.txt new file mode 100644 index 0000000..15cb0c7 --- /dev/null +++ b/fastapi_example/requirements.txt @@ -0,0 +1,4 @@ +ntchat +fastapi +requests +uvicorn \ No newline at end of file diff --git a/fastapi_example/utils.py b/fastapi_example/utils.py new file mode 100644 index 0000000..bac1664 --- /dev/null +++ b/fastapi_example/utils.py @@ -0,0 +1,6 @@ +import uuid +import time + + +def generate_guid(prefix=''): + return str(uuid.uuid3(uuid.NAMESPACE_URL, prefix + str(time.time()))) diff --git a/fastapi_example/xdg.py b/fastapi_example/xdg.py new file mode 100644 index 0000000..3243b8d --- /dev/null +++ b/fastapi_example/xdg.py @@ -0,0 +1,15 @@ +import os +import sys +import os.path + + +def get_exec_dir(): + return os.path.dirname(sys.argv[0]) + + +def get_download_dir(): + user_dir = os.path.join(get_exec_dir(), 'download') + user_dir = os.path.abspath(user_dir) + if not os.path.isdir(user_dir): + os.makedirs(user_dir) + return user_dir diff --git a/ntchat/__init__.py b/ntchat/__init__.py index 437b2cb..d24e420 100644 --- a/ntchat/__init__.py +++ b/ntchat/__init__.py @@ -1,9 +1,24 @@ from .conf import VERSION from .core.wechat import WeChat from .wc import wcprobe -from .const.wx_type import * +from .const.notify_type import * from .exception import * +from . import conf __version__ = VERSION -exit_ = wcprobe.exit + +def set_wechat_exe_path(wechat_exe_path=None, wechat_version=None): + """ + 自定义微信路径 + """ + conf.DEFAULT_WECHAT_EXE_PATH = wechat_exe_path + conf.DEFAULT_WECHAT_VERSION = wechat_version + + +def get_install_wechat_version(): + return wcprobe.get_install_wechat_version() + + +def exit_(): + wcprobe.exit() diff --git a/ntchat/conf/__init__.py b/ntchat/conf/__init__.py index 8d76f25..1e3ba15 100644 --- a/ntchat/conf/__init__.py +++ b/ntchat/conf/__init__.py @@ -1 +1,9 @@ -VERSION = '0.1.1' +VERSION = '0.1.16' + +LOG_LEVEL = "DEBUG" +LOG_KEY = 'NTCHAT_LOG' +LOG_FILE_KEY = 'NTCHAT_LOG_FILE' + +DEFAULT_WECHAT_EXE_PATH = None +DEFAULT_WECHAT_VERSION = None + diff --git a/ntchat/const/notify_type.py b/ntchat/const/notify_type.py new file mode 100644 index 0000000..e1d017f --- /dev/null +++ b/ntchat/const/notify_type.py @@ -0,0 +1,86 @@ +# 用于接收所有的通知消息 +MT_ALL = 11000 + +# 微信进程退出通知 +MT_RECV_WECHAT_QUIT_MSG = 11001 + +# 第个通知消息,此时已经托管上微信 +MT_READY_MSG = 11024 + +# 登录二维码通知 +MT_RECV_LOGIN_QRCODE_MSG = 11087 + +# 用户登录成功的通知 +MT_USER_LOGIN_MSG = 11025 + +# 用户注销或退出微信的通知 +MT_USER_LOGOUT_MSG = 11026 + +# 文本消息通知 +MT_RECV_TEXT_MSG = 11046 + +# 图片消息通知 +MT_RECV_IMAGE_MSG = 11047 +MT_RECV_PICTURE_MSG = 11047 + +# 语音消息通知 +MT_RECV_VOICE_MSG = 11048 + +# 新好友请求通知 +MT_RECV_FRIEND_MSG = 11049 + +# 好友分享名片通知 +MT_RECV_CARD_MSG = 11050 + +# 视频消息通知 +MT_RECV_VIDEO_MSG = 11051 + +# 表情消息通知 +MT_RECV_EMOJI_MSG = 11052 + +# 位置消息通知 +MT_RECV_LOCATION_MSG = 11053 + +# 链接卡片消息通知 +MT_RECV_LINK_MSG = 11054 + +# 文件消息通知 +MT_RECV_FILE_MSG = 11055 + +# 小程序消息通知 +MT_RECV_MINIAPP_MSG = 11056 + +# 二维码支付通知 +MT_RECV_WCPAY_MSG = 11057 + +# 系统消息通知 +MT_RECV_SYSTEM_MSG = 11058 + +# 撤回消息通知 +MT_RECV_REVOKE_MSG = 11059 + +# 未知消息通知 +MT_RECV_OTHER_MSG = 11060 + +# 未知应用消息通知 +MT_RECV_OTHER_APP_MSG = 11061 + +# 群成员新增通知 +MT_ROOM_ADD_MEMBER_NOTIFY_MSG = 11098 + +# 群成员删除通知 +MT_ROOM_DEL_MEMBER_NOTIFY_MSG = 11099 + +# 通过接口创建群聊的通知 +MT_ROOM_CREATE_NOTIFY_MSG = 11100 + +# 退群或被踢通知 +MT_ROOM_DEL_NOTIFY_MSG = 11101 + +# 联系人新增通知 +MT_CONTACT_ADD_NOITFY_MSG = 11102 + +# 联系人删除通知 +MT_CONTACT_DEL_NOTIFY_MSG = 11103 + + diff --git a/ntchat/const/send_type.py b/ntchat/const/send_type.py new file mode 100644 index 0000000..5fc5793 --- /dev/null +++ b/ntchat/const/send_type.py @@ -0,0 +1,83 @@ +# 获取自己的帐号信息 +MT_GET_SELF_MSG = 11028 + +# 获取所有的联系人 +MT_GET_CONTACTS_MSG = 11030 + +# 获取所有的群 +MT_GET_ROOMS_MSG = 11031 + +# 获取公众号列表 +MT_GET_PUBLICS_MSG = 11033 + +# 获取指定的群成员 +MT_GET_ROOM_MEMBERS_MSG = 11032 + +# 获取指定联系人的详细信息 +MT_GET_CONTACT_DETAIL_MSG = 11029 + +# 获取指定群的详细信息 +MT_GET_ROOM_DETAIL_MSG = 11125 + +# 发送文本消息 +MT_SEND_TEXT_MSG = 11036 + +# 发送群@消息 +MT_SEND_ROOM_AT_MSG = 11037 + +# 发送名片消息 +MT_SEND_CARD_MSG = 11038 + +# 发送链接卡片消息 +MT_SEND_LINK_MSG = 11039 + +# 发送图片消息 +MT_SEND_IMAGE_MSG = 11040 + +# 发送文件消息 +MT_SEND_FILE_MSG = 11041 + +# 发送视频消息 +MT_SEND_VIDEO_MSG = 11042 + +# 发送gif消息 +MT_SEND_GIF_MSG = 11043 + +# 发送xml消息 +MT_SEND_XML_MSG = 11113 + +# 修改好友备注 +MT_MODIFY_FRIEND_REMARK = 11063 + +# 接受新好友请求 +MT_ACCEPT_FRIEND_MSG = 11065 + +# 创建群 +MT_CREATE_ROOM_MSG = 11068 + +# 添加好友进群 +MT_ADD_TO_ROOM_MSG = 11069 + +# 邀请好友进群 +MT_INVITE_TO_ROOM_MSG = 11070 + +# 移除群成员 +MT_DEL_ROOM_MEMBER_MSG = 11071 + +# 修改群名 +MT_MOD_ROOM_NAME_MSG = 11072 + +# 修改群公告 +MT_MOD_ROOM_NOTICE_MSG = 11073 + +# 退出群 +MT_QUIT_DEL_ROOM_MSG = 11077 + +# 添加群成员为好友 +MT_ADD_FRIEND_MSG = 11062 + +# 数据库查询 +MT_SQL_QUERY_MSG = 11027 + +# 拍一拍 +MT_SEND_PAT_MSG = 11250 diff --git a/ntchat/const/wx_type.py b/ntchat/const/wx_type.py deleted file mode 100644 index 538ddab..0000000 --- a/ntchat/const/wx_type.py +++ /dev/null @@ -1,50 +0,0 @@ -MT_ALL = 11000 -MT_READY_MSG = 11024 -MT_USER_LOGIN_MSG = 11025 -MT_USER_LOGOUT_MSG = 11026 -MT_GET_SELF_MSG = 11028 -MT_GET_CONTACTS_MSG = 11030 -MT_GET_ROOMS_MSG = 11031 -MT_GET_ROOM_MEMBERS_MSG = 11032 -MT_GET_CONTACT_DETAIL_MSG = 11034 - -# 发送消息 -MT_SEND_TEXT_MSG = 11036 -MT_SEND_ROOM_AT_MSG = 11037 -MT_SEND_CARD_MSG = 11038 -MT_SEND_LINK_MSG = 11039 -MT_SEND_IMAGE_MSG = 11040 -MT_SEND_FILE_MSG = 11041 -MT_SEND_VIDEO_MSG = 11042 -MT_SEND_GIF_MSG = 11043 - -# 接收消息类 -MT_RECV_TEXT_MSG = 11046 -MT_RECV_PICTURE_MSG = 11047 -MT_RECV_VOICE_MSG = 11048 -MT_RECV_FRIEND_MSG = 11049 -MT_RECV_CARD_MSG = 11050 -MT_RECV_VIDEO_MSG = 11051 -MT_RECV_EMOJI_MSG = 11052 -MT_RECV_LOCATION_MSG = 11053 -MT_RECV_LINK_MSG = 11054 -MT_RECV_FILE_MSG = 11055 -MT_RECV_MINIAPP_MSG = 11056 -MT_RECV_WCPAY_MSG = 11057 -MT_RECV_SYSTEM_MSG = 11058 -MT_RECV_REVOKE_MSG = 11059 -MT_RECV_OTHER_MSG = 11060 -MT_RECV_OTHER_APP_MSG = 11061 - -# 好友操作 -MT_ACCEPT_FRIEND_MSG = 11065 - -# 群操作类 -MT_CREATE_ROOM_MSG = 11068 -MT_INVITE_TO_ROOM_MSG = 11069 -MT_INVITE_TO_ROOM_REQ_MSG = 11070 -MT_DEL_ROOM_MEMBER_MSG = 11071 -MT_MOD_ROOM_NAME_MSG = 11072 -MT_MOD_ROOM_NOTICE_MSG = 11073 -MT_QUIT_DEL_ROOM_MSG = 11077 - diff --git a/ntchat/core/mgr.py b/ntchat/core/mgr.py index cc4cab5..baacf1e 100644 --- a/ntchat/core/mgr.py +++ b/ntchat/core/mgr.py @@ -1,11 +1,12 @@ import json import os.path -from ntchat.wc import wcprobe -from ntchat.utils.xdg import get_helper_file -from ntchat.exception import WeChatVersionNotMatchError, WeChatBindError +from ntchat.wc import wcprobe, SUPPORT_VERSIONS +from ntchat.utils.xdg import get_helper_file, is_support_version, has_helper_file +from ntchat.exception import WeChatVersionNotMatchError, WeChatBindError, WeChatRuntimeError from ntchat.utils.singleton import Singleton -from ntchat.const import wx_type +from ntchat.const import notify_type from ntchat.utils.logger import get_logger +from ntchat import conf log = get_logger("WeChatManager") @@ -14,8 +15,8 @@ class WeChatMgr(metaclass=Singleton): __instance_list = [] __instance_map = {} - def __init__(self, wechat_exe_path=None, wechat_version=None): - self.set_wechat_exe_path(wechat_exe_path, wechat_version) + def __init__(self): + self.set_wechat_exe_path(conf.DEFAULT_WECHAT_EXE_PATH, conf.DEFAULT_WECHAT_VERSION) # init callbacks wcprobe.init_callback(self.__on_accept, self.__on_recv, self.__on_close) @@ -30,9 +31,16 @@ def set_wechat_exe_path(self, wechat_exe_path=None, wechat_version=None): else: version = wechat_version + if not is_support_version(version): + raise WeChatVersionNotMatchError(f"ntchat support wechat versions: {','.join(SUPPORT_VERSIONS)}") + + if not has_helper_file(): + raise WeChatRuntimeError('When using pyinstaller to package exe, you need to add the ' + '`--collect-data=ntchat` parameter') + helper_file = get_helper_file(version) if not os.path.exists(helper_file): - raise WeChatVersionNotMatchError() + raise WeChatRuntimeError("missing core files") log.info("initialize wechat, version: %s", version) @@ -48,8 +56,7 @@ def __bind_wechat(self, client_id, pid): if client_id not in self.__instance_map: for instance in self.__instance_list: if instance.pid == pid: - instance.client_id = client_id - instance.status = True + instance.bind_client_id(client_id) self.__instance_map[client_id] = instance bind_instance = instance break @@ -62,7 +69,7 @@ def __on_accept(self, client_id): def __on_recv(self, client_id, data): message = json.loads(data) - if message["type"] == wx_type.MT_READY_MSG: + if message["type"] == notify_type.MT_READY_MSG: self.__bind_wechat(client_id, message["data"]["pid"]) else: self.__instance_map[client_id].on_recv(message) @@ -70,5 +77,4 @@ def __on_recv(self, client_id, data): def __on_close(self, client_id): log.debug("close client_id: %d", client_id) if client_id in self.__instance_map: - self.__instance_map[client_id].login_status = False - self.__instance_map[client_id].status = False + self.__instance_map[client_id].on_close() diff --git a/ntchat/core/wechat.py b/ntchat/core/wechat.py index 921d71a..49df2c2 100644 --- a/ntchat/core/wechat.py +++ b/ntchat/core/wechat.py @@ -1,7 +1,7 @@ import pyee import json from ntchat.core.mgr import WeChatMgr -from ntchat.const import wx_type +from ntchat.const import notify_type, send_type from threading import Event from ntchat.wc import wcprobe from ntchat.utils import generate_guid @@ -41,6 +41,17 @@ def get_response_data(self): return self.__response_message["data"] +class RaiseExceptionFunc: + def __init__(self, func): + self.func = func + + def __call__(self, *args, **kwargs): + try: + self.func(*args, **kwargs) + except Exception as e: + log.error('callback error, in function `%s`, error: %s', self.func.__name__, e) + + class WeChat: client_id: int = 0 pid: int = 0 @@ -51,33 +62,47 @@ def __init__(self): WeChatMgr().append_instance(self) self.__wait_login_event = Event() self.__req_data_cache = {} - self.__msg_event_emitter = pyee.EventEmitter() + self.event_emitter = pyee.EventEmitter() self.__login_info = {} def on(self, msg_type, f): - return self.__msg_event_emitter.on(str(msg_type), f) - - def msg_register(self, msg_type: Union[int, List[int], Tuple[int]]): if not (isinstance(msg_type, list) or isinstance(msg_type, tuple)): msg_type = [msg_type] + for event in msg_type: + self.event_emitter.on(str(event), RaiseExceptionFunc(f)) + def msg_register(self, msg_type: Union[int, List[int], Tuple[int]]): def wrapper(f): wraps(f) - for event in msg_type: - self.on(event, f) + self.on(msg_type, f) return f return wrapper + def on_close(self): + self.login_status = False + self.status = False + self.event_emitter.emit(str(notify_type.MT_RECV_WECHAT_QUIT_MSG), self) + + message = { + "type": notify_type.MT_RECV_WECHAT_QUIT_MSG, + "data": {} + } + self.event_emitter.emit(str(notify_type.MT_ALL), self, message) + + def bind_client_id(self, client_id): + self.status = True + self.client_id = client_id + def on_recv(self, message): log.debug("on recv message: %s", message) msg_type = message["type"] extend = message.get("extend", None) - if msg_type == wx_type.MT_USER_LOGIN_MSG: + if msg_type == notify_type.MT_USER_LOGIN_MSG: self.login_status = True self.__wait_login_event.set() self.__login_info = message.get("data", {}) log.info("login success, wxid: %s, nickname: %s", self.__login_info["wxid"], self.__login_info["nickname"]) - elif msg_type == wx_type.MT_USER_LOGOUT_MSG: + elif msg_type == notify_type.MT_USER_LOGOUT_MSG: self.login_status = False log.info("logout, pid: %d", self.pid) @@ -86,14 +111,17 @@ def on_recv(self, message): req_data.on_response(message) del self.__req_data_cache[extend] else: - self.__msg_event_emitter.emit(str(msg_type), self, message) - self.__msg_event_emitter.emit(str(wx_type.MT_ALL), self, message) + self.event_emitter.emit(str(msg_type), self, message) + self.event_emitter.emit(str(notify_type.MT_ALL), self, message) def wait_login(self, timeout=None): log.info("wait login...") self.__wait_login_event.wait(timeout) - def open(self, smart=False): + def open(self, smart=False, show_login_qrcode=False): + if show_login_qrcode: + wcprobe.show_login_qrcode() + self.pid = wcprobe.open(smart) log.info("open wechat pid: %d", self.pid) return self.pid != 0 @@ -137,6 +165,16 @@ def __new_extend(self): def __repr__(self): return f"WeChatInstance(pid: {self.pid}, client_id: {self.client_id})" + def sql_query(self, sql: str, db: int): + """ + 数据库查询 + """ + data = { + "sql": sql, + "db": db + } + return self.__send_sync(send_type.MT_SQL_QUERY_MSG, data) + def get_login_info(self): """ 获取登录信息 @@ -147,25 +185,87 @@ def get_self_info(self): """ 获取自己个人信息跟登录信息类似 """ - return self.__send_sync(wx_type.MT_GET_SELF_MSG) + return self.__send_sync(send_type.MT_GET_SELF_MSG) def get_contacts(self): """ 获取联系人列表 """ - return self.__send_sync(wx_type.MT_GET_CONTACTS_MSG) + return self.__send_sync(send_type.MT_GET_CONTACTS_MSG) + + def get_publics(self): + """ + 获取关注公众号列表 + """ + return self.__send_sync(send_type.MT_GET_PUBLICS_MSG) def get_contact_detail(self, wxid): + """ + 获取联系人详细信息 + """ data = { "wxid": wxid } - return self.__send_sync(wx_type.MT_GET_CONTACT_DETAIL_MSG, data) + return self.__send_sync(send_type.MT_GET_CONTACT_DETAIL_MSG, data) + + def search_contacts(self, + wxid: Union[None, str] = None, + account: Union[None, str] = None, + nickname: Union[None, str] = None, + remark: Union[None, str] = None, + fuzzy_search: bool = True): + """ + 根据wxid、微信号、昵称和备注模糊搜索联系人 + """ + conds = {} + if wxid: + conds["username"] = wxid + if account: + conds["alias"] = account + if nickname: + conds["nickname"] = nickname + if remark: + conds["remark"] = remark + if not conds: + return [] + + cond_pairs = [] + tag = '%' if fuzzy_search else '' + for k, v in conds.items(): + cond_pairs.append(f"{k} like '{tag}{v}{tag}'") + + cond_str = " or ".join(cond_pairs) + sql = f"select username from contact where {cond_str}" + message = self.sql_query(sql, 1) + if not message: + return [] + + result = message["result"] + if not result: + return [] + + contacts = [] + for wxid_list in result: + if len(wxid_list) > 0: + wxid = wxid_list[0] + contact = self.get_contact_detail(wxid) + contacts.append(contact) + return contacts def get_rooms(self): """ 获取群列表 """ - return self.__send_sync(wx_type.MT_GET_ROOMS_MSG) + return self.__send_sync(send_type.MT_GET_ROOMS_MSG) + + def get_room_detail(self, room_wxid): + """ + 获取指定群详细信息 + """ + data = { + "room_wxid": room_wxid + } + return self.__send_sync(send_type.MT_GET_ROOM_DETAIL_MSG, data) def get_room_members(self, room_wxid: str): """ @@ -174,7 +274,7 @@ def get_room_members(self, room_wxid: str): data = { "room_wxid": room_wxid } - return self.__send_sync(wx_type.MT_GET_ROOM_MEMBERS_MSG, data) + return self.__send_sync(send_type.MT_GET_ROOM_MEMBERS_MSG, data) def send_text(self, to_wxid: str, content: str): """ @@ -184,7 +284,7 @@ def send_text(self, to_wxid: str, content: str): "to_wxid": to_wxid, "content": content } - return self.__send(wx_type.MT_SEND_TEXT_MSG, data) + return self.__send(send_type.MT_SEND_TEXT_MSG, data) def send_room_at_msg(self, to_wxid: str, content: str, at_list: List[str]): """ @@ -195,7 +295,7 @@ def send_room_at_msg(self, to_wxid: str, content: str, at_list: List[str]): 'content': content, 'at_list': at_list } - return self.__send(wx_type.MT_SEND_ROOM_AT_MSG, data) + return self.__send(send_type.MT_SEND_ROOM_AT_MSG, data) def send_card(self, to_wxid: str, card_wxid: str): """ @@ -205,7 +305,7 @@ def send_card(self, to_wxid: str, card_wxid: str): 'to_wxid': to_wxid, 'card_wxid': card_wxid } - return self.__send(wx_type.MT_SEND_CARD_MSG, data) + return self.__send(send_type.MT_SEND_CARD_MSG, data) def send_link_card(self, to_wxid: str, title: str, desc: str, url: str, image_url: str): """ @@ -218,7 +318,7 @@ def send_link_card(self, to_wxid: str, title: str, desc: str, url: str, image_ur 'url': url, 'image_url': image_url } - return self.__send(wx_type.MT_SEND_LINK_MSG, data) + return self.__send(send_type.MT_SEND_LINK_MSG, data) def send_image(self, to_wxid: str, file_path: str): """ @@ -228,7 +328,7 @@ def send_image(self, to_wxid: str, file_path: str): 'to_wxid': to_wxid, 'file': file_path } - return self.__send(wx_type.MT_SEND_IMAGE_MSG, data) + return self.__send(send_type.MT_SEND_IMAGE_MSG, data) def send_file(self, to_wxid: str, file_path: str): """ @@ -238,7 +338,7 @@ def send_file(self, to_wxid: str, file_path: str): 'to_wxid': to_wxid, 'file': file_path } - return self.__send(wx_type.MT_SEND_FILE_MSG, data) + return self.__send(send_type.MT_SEND_FILE_MSG, data) # def send_video(self, to_wxid: str, file_path: str): @@ -249,7 +349,7 @@ def send_video(self, to_wxid: str, file_path: str): 'to_wxid': to_wxid, 'file': file_path } - return self.__send(wx_type.MT_SEND_VIDEO_MSG, data) + return self.__send(send_type.MT_SEND_VIDEO_MSG, data) def send_gif(self, to_wxid, file): """ @@ -259,7 +359,28 @@ def send_gif(self, to_wxid, file): 'to_wxid': to_wxid, 'file': file } - return self.__send(wx_type.MT_SEND_GIF_MSG, data) + return self.__send(send_type.MT_SEND_GIF_MSG, data) + + def send_xml(self, to_wxid, xml, app_type=5): + """ + 发送xml消息 + """ + data = { + "to_wxid": to_wxid, + "xml": xml, + "app_type": app_type + } + return self.__send(send_type.MT_SEND_XML_MSG, data) + + def send_pat(self, room_wxid: str, patted_wxid: str): + """ + 发送拍一拍 + """ + data = { + "room_wxid": room_wxid, + "patted_wxid": patted_wxid + } + return self.__send_sync(send_type.MT_SEND_PAT_MSG, data) def accept_friend_request(self, encryptusername: str, ticket: str, scene: int): """ @@ -270,13 +391,13 @@ def accept_friend_request(self, encryptusername: str, ticket: str, scene: int): "ticket": ticket, "scene": scene } - return self.__send_sync(wx_type.MT_ACCEPT_FRIEND_MSG, data) + return self.__send_sync(send_type.MT_ACCEPT_FRIEND_MSG, data) def create_room(self, member_list: List[str]): """ 创建群 """ - return self.__send(wx_type.MT_CREATE_ROOM_MSG, member_list) + return self.__send(send_type.MT_CREATE_ROOM_MSG, member_list) def add_room_member(self, room_wxid: str, member_list: List[str]): """ @@ -286,7 +407,7 @@ def add_room_member(self, room_wxid: str, member_list: List[str]): "room_wxid": room_wxid, "member_list": member_list } - return self.__send_sync(wx_type.MT_INVITE_TO_ROOM_MSG, data) + return self.__send_sync(send_type.MT_ADD_TO_ROOM_MSG, data) def invite_room_member(self, room_wxid: str, member_list: List[str]): """ @@ -296,7 +417,7 @@ def invite_room_member(self, room_wxid: str, member_list: List[str]): "room_wxid": room_wxid, "member_list": member_list } - return self.__send_sync(wx_type.MT_INVITE_TO_ROOM_REQ_MSG, data) + return self.__send_sync(send_type.MT_INVITE_TO_ROOM_MSG, data) def del_room_member(self, room_wxid: str, member_list: List[str]): """ @@ -306,7 +427,7 @@ def del_room_member(self, room_wxid: str, member_list: List[str]): "room_wxid": room_wxid, "member_list": member_list } - return self.__send_sync(wx_type.MT_DEL_ROOM_MEMBER_MSG, data) + return self.__send_sync(send_type.MT_DEL_ROOM_MEMBER_MSG, data) def modify_room_name(self, room_wxid: str, name: str): """ @@ -316,7 +437,7 @@ def modify_room_name(self, room_wxid: str, name: str): "room_wxid": room_wxid, "name": name } - return self.__send_sync(wx_type.MT_MOD_ROOM_NAME_MSG, data) + return self.__send_sync(send_type.MT_MOD_ROOM_NAME_MSG, data) def modify_room_notice(self, room_wxid: str, notice: str): """ @@ -326,6 +447,45 @@ def modify_room_notice(self, room_wxid: str, notice: str): "room_wxid": room_wxid, "notice": notice } - return self.__send_sync(wx_type.MT_MOD_ROOM_NOTICE_MSG, data) + return self.__send_sync(send_type.MT_MOD_ROOM_NOTICE_MSG, data) + def add_room_friend(self, room_wxid: str, wxid: str, verify: str): + """ + 添加群成员为好友 + """ + data = { + "room_wxid": room_wxid, + "wxid": wxid, + "source_type": 14, + "remark": verify + } + return self.__send_sync(send_type.MT_ADD_FRIEND_MSG, data) + + def quit_room(self, room_wxid: str): + """ + 退出群 + """ + data = { + "room_wxid": room_wxid + } + return self.__send(send_type.MT_QUIT_DEL_ROOM_MSG, data) + def modify_friend_remark(self, wxid: str, remark: str): + """ + 修改好友备注 + """ + data = { + "wxid": wxid, + "remark": remark + } + return self.__send_sync(send_type.MT_MODIFY_FRIEND_REMARK, data) + + def get_room_name(self, room_wxid: str) -> str: + """ + 获取群名 + """ + sql = f"select nickname from contact where username='{room_wxid}'" + result = self.sql_query(sql, 1)["result"] + if result: + return result[0][0] + return '' diff --git a/ntchat/exception/__init__.py b/ntchat/exception/__init__.py index 1f65fd6..c15f237 100644 --- a/ntchat/exception/__init__.py +++ b/ntchat/exception/__init__.py @@ -8,3 +8,7 @@ class WeChatBindError(Exception): class WeChatNotLoginError(Exception): pass + + +class WeChatRuntimeError(Exception): + pass diff --git a/ntchat/utils/logger.py b/ntchat/utils/logger.py index 09d5477..35d8227 100644 --- a/ntchat/utils/logger.py +++ b/ntchat/utils/logger.py @@ -1,60 +1,25 @@ -import logging import os -import configparser -from datetime import datetime -from .xdg import get_log_dir, get_exec_dir - -NTCHAT_LOG_KEY = 'NTCHAT_LOG' -NTCHAT_LOG_FILE_KEY = 'NTCHAT_LOG_FILE' - - -config_file = os.path.join(get_exec_dir(), "config.ini") -CONFIG_DEBUG_LEVEL = '' - -if os.path.exists(config_file): - config = configparser.ConfigParser() - config.read(config_file) - CONFIG_DEBUG_LEVEL = config.get('Config', 'LogLevel', fallback=CONFIG_DEBUG_LEVEL) +import logging +from .. import conf def get_logger(name: str) -> logging.Logger: """ configured Loggers """ - NTCHAT_LOG = os.environ.get(NTCHAT_LOG_KEY, 'DEBUG') + log_level = os.environ.get(conf.LOG_KEY, conf.LOG_LEVEL) log_formatter = logging.Formatter( fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - if CONFIG_DEBUG_LEVEL: - NTCHAT_LOG = CONFIG_DEBUG_LEVEL - # create logger and set level to debug logger = logging.getLogger(name) logger.handlers = [] - logger.setLevel(NTCHAT_LOG) + logger.setLevel(log_level) logger.propagate = False - # create file handler and set level to debug - if NTCHAT_LOG_FILE_KEY in os.environ: - filepath = os.environ[NTCHAT_LOG_FILE_KEY] - else: - base_dir = get_log_dir() - if not os.path.exists(base_dir): - os.mkdir(base_dir) - - time_now = datetime.now() - time_format = '%Y-%m-%d-%H-%M' - - filepath = f'{base_dir}/log-{time_now.strftime(time_format)}.txt' - - file_handler = logging.FileHandler(filepath, 'a', encoding='utf-8') - file_handler.setLevel(NTCHAT_LOG) - file_handler.setFormatter(log_formatter) - logger.addHandler(file_handler) - # create console handler and set level to info console_handler = logging.StreamHandler() - console_handler.setLevel(NTCHAT_LOG) + console_handler.setLevel(log_level) console_handler.setFormatter(log_formatter) logger.addHandler(console_handler) diff --git a/ntchat/utils/xdg.py b/ntchat/utils/xdg.py index 339972e..d0836a7 100644 --- a/ntchat/utils/xdg.py +++ b/ntchat/utils/xdg.py @@ -1,6 +1,7 @@ import os import sys import os.path +from ntchat.wc import SUPPORT_VERSIONS def get_exec_dir(): @@ -23,12 +24,16 @@ def get_wc_dir(): def get_helper_file(version): - return os.path.join(get_wc_dir(), f"helper_{version}.pyd") + return os.path.join(get_wc_dir(), f"helper_{version}.dat") -def get_support_download_url(): - return 'https://webcdn.m.qq.com/spcmgr/download/WeChat3.6.0.18.exe' +def has_helper_file(): + for name in os.listdir(get_wc_dir()): + if name.startswith("helper_"): + return True + return False -if __name__ == '__main__': - print(get_helper_file('3.6.0.18')) +def is_support_version(version): + return version in SUPPORT_VERSIONS + diff --git a/ntchat/wc/__init__.py b/ntchat/wc/__init__.py index e69de29..3f6d525 100644 --- a/ntchat/wc/__init__.py +++ b/ntchat/wc/__init__.py @@ -0,0 +1,3 @@ +SUPPORT_VERSIONS = [ + '3.6.0.18' +] diff --git a/setup.py b/setup.py index b5b7aa3..10a6b87 100644 --- a/setup.py +++ b/setup.py @@ -194,7 +194,7 @@ def add_prefix(l, prefix): setup( name='ntchat', - version='0.1.1', + version='0.1.16', description='About Conversational RPA SDK for Chatbot Makers', long_description="", long_description_content_type='text/markdown', @@ -211,7 +211,7 @@ def add_prefix(l, prefix): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10' ], - package_data={"": ["py.typed", "*.pyi", "helper*.pyd"]}, + package_data={"": ["py.typed", "*.pyi", "helper*.dat"]}, include_package_data=False, packages=find_packages(include=['ntchat', 'ntchat.*']), keywords='wechat ntchat pywechat rebot',