From d6c8d41ce0d5bdcee94befe676be7137de0b5cf0 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:08:27 +0800 Subject: [PATCH 01/13] Create readme.md --- itchat/readme.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 itchat/readme.md diff --git a/itchat/readme.md b/itchat/readme.md new file mode 100644 index 00000000..f4ccca8f --- /dev/null +++ b/itchat/readme.md @@ -0,0 +1,7 @@ +As itchat is multi-threaded, it's impossible to catch the internet errors it has produced. So I added some callbacks. + +In core.functionDict, I added the key "Error" whose value is a list, where we may store error-handling functions. + +Use '@itchat.error_register(True)' to register your error-handling function. + +这玩意多线程导致我没法捕捉错误,所以搞了个回调。用@itchat.error_register(True)来注册你的错误处理函数。 From 6ea722599ccbb9e5b9e187214bdc0effdb0e7a99 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:09:29 +0800 Subject: [PATCH 02/13] Delete __init__.py --- itchat/__init__.py | 67 ---------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 itchat/__init__.py diff --git a/itchat/__init__.py b/itchat/__init__.py deleted file mode 100644 index 256fc721..00000000 --- a/itchat/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -from . import content -from .core import Core -from .config import VERSION -from .log import set_logging - -__version__ = VERSION - -instanceList = [] - -def new_instance(): - newInstance = Core() - instanceList.append(newInstance) - return newInstance - -originInstance = new_instance() - -# I really want to use sys.modules[__name__] = originInstance -# but it makes auto-fill a real mess, so forgive me for my following ** -# actually it toke me less than 30 seconds, god bless Uganda - -# components.login -login = originInstance.login -get_QRuuid = originInstance.get_QRuuid -get_QR = originInstance.get_QR -check_login = originInstance.check_login -web_init = originInstance.web_init -show_mobile_login = originInstance.show_mobile_login -start_receiving = originInstance.start_receiving -get_msg = originInstance.get_msg -logout = originInstance.logout -# components.contact -update_chatroom = originInstance.update_chatroom -update_friend = originInstance.update_friend -get_contact = originInstance.get_contact -get_friends = originInstance.get_friends -get_chatrooms = originInstance.get_chatrooms -get_mps = originInstance.get_mps -set_alias = originInstance.set_alias -set_pinned = originInstance.set_pinned -add_friend = originInstance.add_friend -get_head_img = originInstance.get_head_img -create_chatroom = originInstance.create_chatroom -set_chatroom_name = originInstance.set_chatroom_name -delete_member_from_chatroom = originInstance.delete_member_from_chatroom -add_member_into_chatroom = originInstance.add_member_into_chatroom -# components.messages -send_raw_msg = originInstance.send_raw_msg -send_msg = originInstance.send_msg -upload_file = originInstance.upload_file -send_file = originInstance.send_file -send_image = originInstance.send_image -send_video = originInstance.send_video -send = originInstance.send -revoke = originInstance.revoke -# components.hotreload -dump_login_status = originInstance.dump_login_status -load_login_status = originInstance.load_login_status -# components.register -auto_login = originInstance.auto_login -configured_reply = originInstance.configured_reply -msg_register = originInstance.msg_register -run = originInstance.run -# other functions -search_friends = originInstance.search_friends -search_chatrooms = originInstance.search_chatrooms -search_mps = originInstance.search_mps -set_logging = set_logging From 603c078913674566dbafbb672224e53dff9128aa Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:11:56 +0800 Subject: [PATCH 03/13] Add files via upload --- itchat/__init__.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 itchat/__init__.py diff --git a/itchat/__init__.py b/itchat/__init__.py new file mode 100644 index 00000000..a7cb466b --- /dev/null +++ b/itchat/__init__.py @@ -0,0 +1,69 @@ +from . import content +from .core import Core +from .config import VERSION +from .log import set_logging + +__version__ = VERSION + +instanceList = [] + +def new_instance(): + newInstance = Core() + instanceList.append(newInstance) + return newInstance + +originInstance = new_instance() + +# I really want to use sys.modules[__name__] = originInstance +# but it makes auto-fill a real mess, so forgive me for my following ** +# actually it toke me less than 30 seconds, god bless Uganda + +# components.login +login = originInstance.login +get_QRuuid = originInstance.get_QRuuid +get_QR = originInstance.get_QR +check_login = originInstance.check_login +web_init = originInstance.web_init +show_mobile_login = originInstance.show_mobile_login +start_receiving = originInstance.start_receiving +get_msg = originInstance.get_msg +logout = originInstance.logout +# components.contact +update_chatroom = originInstance.update_chatroom +update_friend = originInstance.update_friend +get_contact = originInstance.get_contact +get_friends = originInstance.get_friends +get_chatrooms = originInstance.get_chatrooms +get_mps = originInstance.get_mps +set_alias = originInstance.set_alias +set_pinned = originInstance.set_pinned +add_friend = originInstance.add_friend +get_head_img = originInstance.get_head_img +create_chatroom = originInstance.create_chatroom +set_chatroom_name = originInstance.set_chatroom_name +delete_member_from_chatroom = originInstance.delete_member_from_chatroom +add_member_into_chatroom = originInstance.add_member_into_chatroom +# components.messages +send_raw_msg = originInstance.send_raw_msg +send_msg = originInstance.send_msg +upload_file = originInstance.upload_file +send_file = originInstance.send_file +send_image = originInstance.send_image +send_video = originInstance.send_video +send = originInstance.send +revoke = originInstance.revoke +# components.hotreload +dump_login_status = originInstance.dump_login_status +load_login_status = originInstance.load_login_status +# components.register +auto_login = originInstance.auto_login +configured_reply = originInstance.configured_reply +msg_register = originInstance.msg_register +error_register = originInstance.error_register +run = originInstance.run +# other functions +search_friends = originInstance.search_friends +search_chatrooms = originInstance.search_chatrooms +search_mps = originInstance.search_mps +set_logging = set_logging + From b83c7c703f5f7cf64462ff61a93270faad519b56 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:12:38 +0800 Subject: [PATCH 04/13] Update README.md --- README.md | 307 +----------------------------------------------------- 1 file changed, 4 insertions(+), 303 deletions(-) diff --git a/README.md b/README.md index 75767d9b..acb2d47b 100644 --- a/README.md +++ b/README.md @@ -1,306 +1,7 @@ -# itchat +As itchat is multi-threaded, it's impossible to catch the internet errors it has produced. So I added some callbacks. -[![Gitter][gitter-picture]][gitter] ![py27][py27] ![py35][py35] [English version][english-version] +In core.functionDict, I added the key "Error" whose value is a list, where we may store error-handling functions. -itchat是一个开源的微信个人号接口,使用python调用微信从未如此简单。 +Use '@itchat.error_register(True)' to register your error-handling function. -使用不到三十行的代码,你就可以完成一个能够处理所有信息的微信机器人。 - -当然,该api的使用远不止一个机器人,更多的功能等着你来发现,比如[这些][tutorial2]。 - -该接口与公众号接口[itchatmp][itchatmp]共享类似的操作方式,学习一次掌握两个工具。 - -如今微信已经成为了个人社交的很大一部分,希望这个项目能够帮助你扩展你的个人的微信号、方便自己的生活。 - -## 安装 - -可以通过本命令安装itchat: - -```python -pip install itchat -``` - -## 简单入门实例 - -有了itchat,如果你想要给文件传输助手发一条信息,只需要这样: - -```python -import itchat - -itchat.auto_login() - -itchat.send('Hello, filehelper', toUserName='filehelper') -``` - -如果你想要回复发给自己的文本消息,只需要这样: - -```python -import itchat - -@itchat.msg_register(itchat.content.TEXT) -def text_reply(msg): - return msg.text - -itchat.auto_login() -itchat.run() -``` - -一些进阶应用可以在下面的开源机器人的源码和进阶应用中看到,或者你也可以阅览[文档][document]。 - -## 试一试 - -这是一个基于这一项目的[开源小机器人][robot-source-code],百闻不如一见,有兴趣可以尝试一下。 - -由于好友数量实在增长过快,自动通过好友验证的功能演示暂时关闭。 - -![QRCode][robot-qr] - -## 截屏 - -![file-autoreply][robot-demo-file] ![login-page][robot-demo-login] - -## 进阶应用 - -### 特殊的字典使用方式 - -通过打印itchat的用户以及注册消息的参数,可以发现这些值都是字典。 - -但实际上itchat精心构造了相应的消息、用户、群聊、公众号类。 - -其所有的键值都可以通过这一方式访问: - -```python -@itchat.msg_register(TEXT) -def _(msg): - # equals to print(msg['FromUserName']) - print(msg.fromUserName) -``` - -属性名为键值首字母小写后的内容。 - -```python -author = itchat.search_friends(nickName='LittleCoder')[0] -author.send('greeting, littlecoder!') -``` - -### 各类型消息的注册 - -通过如下代码,微信已经可以就日常的各种信息进行获取与回复。 - -```python -import itchat, time -from itchat.content import * - -@itchat.msg_register([TEXT, MAP, CARD, NOTE, SHARING]) -def text_reply(msg): - msg.user.send('%s: %s' % (msg.type, msg.text)) - -@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO]) -def download_files(msg): - msg.download(msg.fileName) - typeSymbol = { - PICTURE: 'img', - VIDEO: 'vid', }.get(msg.type, 'fil') - return '@%s@%s' % (typeSymbol, msg.fileName) - -@itchat.msg_register(FRIENDS) -def add_friend(msg): - msg.user.verify() - msg.user.send('Nice to meet you!') - -@itchat.msg_register(TEXT, isGroupChat=True) -def text_reply(msg): - if msg.isAt: - msg.user.send(u'@%s\u2005I received: %s' % ( - msg.actualNickName, msg.text)) - -itchat.auto_login(True) -itchat.run(True) -``` - -### 命令行二维码 - -通过以下命令可以在登陆的时候使用命令行显示二维码: - -```python -itchat.auto_login(enableCmdQR=True) -``` - -部分系统可能字幅宽度有出入,可以通过将enableCmdQR赋值为特定的倍数进行调整: - -```python -# 如部分的linux系统,块字符的宽度为一个字符(正常应为两字符),故赋值为2 -itchat.auto_login(enableCmdQR=2) -``` - -默认控制台背景色为暗色(黑色),若背景色为浅色(白色),可以将enableCmdQR赋值为负值: - -```python -itchat.auto_login(enableCmdQR=-1) -``` - -### 退出程序后暂存登陆状态 - -通过如下命令登陆,即使程序关闭,一定时间内重新开启也可以不用重新扫码。 - -```python -itchat.auto_login(hotReload=True) -``` - -### 用户搜索 - -使用`search_friends`方法可以搜索用户,有四种搜索方式: -1. 仅获取自己的用户信息 -2. 获取特定`UserName`的用户信息 -3. 获取备注、微信号、昵称中的任何一项等于`name`键值的用户 -4. 获取备注、微信号、昵称分别等于相应键值的用户 - -其中三、四项可以一同使用,下面是示例程序: - -```python -# 获取自己的用户信息,返回自己的属性字典 -itchat.search_friends() -# 获取特定UserName的用户信息 -itchat.search_friends(userName='@abcdefg1234567') -# 获取任何一项等于name键值的用户 -itchat.search_friends(name='littlecodersh') -# 获取分别对应相应键值的用户 -itchat.search_friends(wechatAccount='littlecodersh') -# 三、四项功能可以一同使用 -itchat.search_friends(name='LittleCoder机器人', wechatAccount='littlecodersh') -``` - -关于公众号、群聊的获取与搜索在文档中有更加详细的介绍。 - -### 附件的下载与发送 - -itchat的附件下载方法存储在msg的Text键中。 - -发送的文件的文件名(图片给出的默认文件名)都存储在msg的FileName键中。 - -下载方法接受一个可用的位置参数(包括文件名),并将文件相应的存储。 - -```python -@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO]) -def download_files(msg): - msg.download(msg.fileName) - itchat.send('@%s@%s' % ( - 'img' if msg['Type'] == 'Picture' else 'fil', msg['FileName']), - msg['FromUserName']) - return '%s received' % msg['Type'] -``` - -如果你不需要下载到本地,仅想要读取二进制串进行进一步处理可以不传入参数,方法将会返回图片的二进制串。 - -```python -@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO]) -def download_files(msg): - with open(msg.fileName, 'wb') as f: - f.write(msg.download()) -``` - -### 用户多开 - -使用如下命令可以完成多开的操作: - -```python -import itchat - -newInstance = itchat.new_instance() -newInstance.auto_login(hotReload=True, statusStorageDir='newInstance.pkl') - -@newInstance.msg_register(itchat.content.TEXT) -def reply(msg): - return msg.text - -newInstance.run() -``` - -### 退出及登陆完成后调用特定方法 - -登陆完成后的方法需要赋值在`loginCallback`中。 - -而退出后的方法需要赋值在`exitCallback`中。 - -```python -import time - -import itchat - -def lc(): - print('finish login') -def ec(): - print('exit') - -itchat.auto_login(loginCallback=lc, exitCallback=ec) -time.sleep(3) -itchat.logout() -``` - -若不设置loginCallback的值,则将会自动删除二维码图片并清空命令行显示。 - -## 常见问题与解答 - -Q: 如何通过这个包将自己的微信号变为控制器? - -A: 有两种方式:发送、接受自己UserName的消息;发送接收文件传输助手(filehelper)的消息 - -Q: 为什么我发送信息的时候部分信息没有成功发出来? - -A: 有些账号是天生无法给自己的账号发送信息的,建议使用`filehelper`代替。 - -## 作者 - -[LittleCoder][littlecodersh]: 构架及维护Python2 Python3版本。 - -[tempdban][tempdban]: 协议、构架及日常维护。 - -[Chyroc][Chyroc]: 完成第一版本的Python3构架。 - -## 类似项目 - -[youfou/wxpy][youfou-wxpy]: 优秀的api包装和配套插件,微信机器人/优雅的微信个人号API - -[liuwons/wxBot][liuwons-wxBot]: 类似的基于Python的微信机器人 - -[zixia/wechaty][zixia-wechaty]: 基于Javascript(ES6)的微信个人账号机器人NodeJS框架/库 - -[sjdy521/Mojo-Weixin][Mojo-Weixin]: 使用Perl语言编写的微信客户端框架,可通过插件提供基于HTTP协议的api接口供其他语言调用 - -[HanSon/vbot][HanSon-vbot]: 基于PHP7的微信个人号机器人,通过实现匿名函数可以方便地实现各种自定义的功能 - -[yaphone/itchat4j][yaphone-itchat4j]: 用Java扩展个人微信号的能力 - -[kanjielu/jeeves][kanjielu-jeeves]: 使用springboot开发的微信机器人 - -## 问题和建议 - -如果有什么问题或者建议都可以在这个[Issue][issue#1]和我讨论 - -或者也可以在gitter上交流:[![Gitter][gitter-picture]][gitter] - -当然也可以加入我们新建的QQ群讨论:549762872, 205872856 - -[gitter-picture]: https://badges.gitter.im/littlecodersh/ItChat.svg -[gitter]: https://gitter.im/littlecodersh/ItChat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge -[py27]: https://img.shields.io/badge/python-2.7-ff69b4.svg -[py35]: https://img.shields.io/badge/python-3.5-red.svg -[english-version]: https://github.com/littlecodersh/ItChat/blob/master/README_EN.md -[itchatmp]: https://github.com/littlecodersh/itchatmp -[document]: https://itchat.readthedocs.org/zh/latest/ -[tutorial2]: http://python.jobbole.com/86532/ -[robot-source-code]: https://gist.github.com/littlecodersh/ec8ddab12364323c97d4e36459174f0d -[robot-qr]: http://7xrip4.com1.z0.glb.clouddn.com/ItChat%2FQRCode2.jpg?imageView/2/w/400/ -[robot-demo-file]: http://7xrip4.com1.z0.glb.clouddn.com/ItChat%2FScreenshots%2F%E5%BE%AE%E4%BF%A1%E8%8E%B7%E5%8F%96%E6%96%87%E4%BB%B6%E5%9B%BE%E7%89%87.png?imageView/2/w/300/ -[robot-demo-login]: http://7xrip4.com1.z0.glb.clouddn.com/ItChat%2FScreenshots%2F%E7%99%BB%E5%BD%95%E7%95%8C%E9%9D%A2%E6%88%AA%E5%9B%BE.jpg?imageView/2/w/450/ -[littlecodersh]: https://github.com/littlecodersh -[tempdban]: https://github.com/tempdban -[Chyroc]: https://github.com/Chyroc -[youfou-wxpy]: https://github.com/youfou/wxpy -[liuwons-wxBot]: https://github.com/liuwons/wxBot -[zixia-wechaty]: https://github.com/zixia/wechaty -[Mojo-Weixin]: https://github.com/sjdy521/Mojo-Weixin -[HanSon-vbot]: https://github.com/hanson/vbot -[yaphone-itchat4j]: https://github.com/yaphone/itchat4j -[kanjielu-jeeves]: https://github.com/kanjielu/jeeves -[issue#1]: https://github.com/littlecodersh/ItChat/issues/1 +这玩意多线程导致我没法捕捉错误,所以搞了个回调。用@itchat.error_register(True)来注册你的错误处理函数。 From f4aebe7db1751ed1dda76df192d881ad9f7cc969 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:13:00 +0800 Subject: [PATCH 05/13] Delete README_EN.md --- README_EN.md | 293 --------------------------------------------------- 1 file changed, 293 deletions(-) delete mode 100644 README_EN.md diff --git a/README_EN.md b/README_EN.md deleted file mode 100644 index ab6df03d..00000000 --- a/README_EN.md +++ /dev/null @@ -1,293 +0,0 @@ -# itchat - -[![Gitter][gitter-picture]][gitter] ![py27][py27] ![py35][py35] [Chinese version][chinese-version] - -itchat is an open source api for WeChat, a commonly-used Chinese social networking app. - -Accessing your personal wechat account through itchat in python has never been easier. - -A wechat robot can handle all the basic messages with only less than 30 lines of codes. - -And it's similiar to itchatmp (api for wechat massive platform), learn once and get two tools. - -Now Wechat is an important part of personal life, hopefully this repo can help you extend your personal wechat account's functionality and better user's experience with wechat. - -## Installation - -itchat can be installed with this little one-line command: - -```python -pip install itchat -``` - -## Simple uses - -With itchat, if you want to send a message to filehelper, this is how: - -```python -import itchat - -itchat.auto_login() - -itchat.send('Hello, filehelper', toUserName='filehelper') -``` - -And you only need to write this to reply personal text messages. - -```python -import itchat - -@itchat.msg_register(itchat.content.TEXT) -def text_reply(msg): - return msg.text - -itchat.auto_login() -itchat.run() -``` - -For more advanced uses you may continue on reading or browse the [document][document]. - -## Have a try - -This QRCode is a wechat account based on the framework of [demo code][robot-source-code]. Seeing is believing, so have a try:) - -![QRCode][robot-qr] - -## Screenshots - -![file-autoreply][robot-demo-file] ![login-page][robot-demo-login] - -## Advanced uses - -### Special usage of message dictionary - -You may find out that all the users and messages of itchat are dictionaries by printing them out onto the screen. - -But actually they are useful classes itchat created. - -They have useful keys and useful interfaces, like: - -```python -@itchat.msg_register(TEXT) -def _(msg): - # equals to print(msg['FromUserName']) - print(msg.fromUserName) -``` - -And like: - -```python -author = itchat.search_friends(nickName='LittleCoder')[0] -author.send('greeting, littlecoder!') -``` - -### Message register of various types - -The following is a demo of how itchat is configured to fetch and reply daily information. - -```python -import itchat, time -from itchat.content import * - -@itchat.msg_register([TEXT, MAP, CARD, NOTE, SHARING]) -def text_reply(msg): - msg.user.send('%s: %s' % (msg.type, msg.text)) - -@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO]) -def download_files(msg): - msg.download(msg.fileName) - typeSymbol = { - PICTURE: 'img', - VIDEO: 'vid', }.get(msg.type, 'fil') - return '@%s@%s' % (typeSymbol, msg.fileName) - -@itchat.msg_register(FRIENDS) -def add_friend(msg): - msg.user.verify() - msg.user.send('Nice to meet you!') - -@itchat.msg_register(TEXT, isGroupChat=True) -def text_reply(msg): - if msg.isAt: - msg.user.send(u'@%s\u2005I received: %s' % ( - msg.actualNickName, msg.text)) - -itchat.auto_login(True) -itchat.run(True) -``` - -### Command line QR Code - -You can access the QR Code in command line through using this command: - -```python -itchat.auto_login(enableCmdQR=True) -``` - -Because of width of some character differs from systems, you may adjust the enableCmdQR to fix the problem. - -```python -# for some linux system, width of block character is one instead of two, so enableCmdQR should be 2 -itchat.auto_login(enableCmdQR=2) -``` - -Default background color of command line is dark (black), if it's not, you may set enableCmdQR to be negative: - -```python -itchat.auto_login(enableCmdQR=-1) -``` - -### Hot reload - -By using the following command, you may reload the program without re-scan QRCode in some time. - -```python -itchat.auto_login(hotReload=True) -``` - -### User search - -By using `search_friends`, you have four ways to search a user: -1. Get your own user information -2. Get user information through `UserName` -3. Get user information whose remark name or wechat account or nickname matches name key of the function -4. Get user information whose remark name, wechat account and nickname match what are given to the function - -Way 3, 4 can be used together, the following is the demo program: - -```python -# get your own user information -itchat.search_friends() -# get user information of specific username -itchat.search_friends(userName='@abcdefg1234567') -# get user information of function 3 -itchat.search_friends(name='littlecodersh') -# get user information of function 4 -itchat.search_friends(wechatAccount='littlecodersh') -# combination of way 3, 4 -itchat.search_friends(name='LittleCoder机器人', wechatAccount='littlecodersh') -``` - -There is detailed information about searching and getting of massive platforms and chatrooms in document. - -### Download and send attachments - -The attachment download function of itchat is in Text key of msg - -Name of the file (default name of picture) is in FileName key of msg - -Download function accept one location value (include the file name) and store attachment accordingly. - -```python -@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO]) -def download_files(msg): - msg.download(msg.fileName) - itchat.send('@%s@%s' % ( - 'img' if msg['Type'] == 'Picture' else 'fil', msg['FileName']), - msg['FromUserName']) - return '%s received' % msg['Type'] -``` - -If you don't want a local copy of the picture, you may pass nothing to the function to get a binary string. - -```python -@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO]) -def download_files(msg): - with open(msg.fileName, 'wb') as f: - f.write(msg.download()) -``` - -### Multi instance - -You may use the following commands to open multi instance. - -```python -import itchat - -newInstance = itchat.new_instance() -newInstance.auto_login(hotReload=True, statusStorageDir='newInstance.pkl') - -@newInstance.msg_register(itchat.content.TEXT) -def reply(msg): - return msg['Text'] - -newInstance.run() -``` - -### Set callback after login and logout - -Callback of login and logout are set through `loginCallback` and `exitCallback`. - -```python -import time - -import itchat - -def lc(): - print('finish login') -def ec(): - print('exit') - -itchat.auto_login(loginCallback=lc, exitCallback=ec) -time.sleep(3) -itchat.logout() -``` - -If loginCallback is not set, qr picture will be deleted and cmd will be cleared. - -If you exit through phone, exitCallback will also be called. - -## FAQ - -Q: How to use this package to use my wechat as an monitor? - -A: There are two ways: communicate with your own account or with filehelper. - -Q: Why sometimes I can't send messages? - -A: Some account simply can't send messages to yourself, so use `filehelper` instead. - -## Author - -[LittleCoder][littlecodersh]: Structure and py2 py3 version - -[tempdban][tempdban]: Structure and daily maintainance - -[Chyroc][Chyroc]: first py3 version - -## See also - -[liuwons/wxBot][liuwons-wxBot]: A wechat robot similiar to the robot branch - -[zixia/wechaty][zixia-wechaty]: Wechat for bot in Javascript(ES6), Personal Account Robot Framework/Library - -[sjdy521/Mojo-Weixin][Mojo-Weixin]: Wechat web api in Perl, available with HTTP requests - -[yaphone/itchat4j][yaphone-itchat4j]: Extend your wechat with java - -## Comments - -If you have any problems or suggestions, feel free to put it up in this [Issue][issue#1]. - -Or you may also use [![Gitter][gitter-picture]][gitter] - -[gitter-picture]: https://badges.gitter.im/littlecodersh/ItChat.svg -[gitter]: https://gitter.im/littlecodersh/ItChat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge -[py27]: https://img.shields.io/badge/python-2.7-ff69b4.svg -[py35]: https://img.shields.io/badge/python-3.5-red.svg -[chinese-version]: https://github.com/littlecodersh/ItChat/blob/master/README.md -[document]: https://itchat.readthedocs.org/zh/latest/ -[robot-source-code]: https://gist.github.com/littlecodersh/ec8ddab12364323c97d4e36459174f0d -[robot-qr]: http://7xrip4.com1.z0.glb.clouddn.com/ItChat%2FQRCode2.jpg?imageView/2/w/400/ -[robot-demo-file]: http://7xrip4.com1.z0.glb.clouddn.com/ItChat%2FScreenshots%2F%E5%BE%AE%E4%BF%A1%E8%8E%B7%E5%8F%96%E6%96%87%E4%BB%B6%E5%9B%BE%E7%89%87.png?imageView/2/w/300/ -[robot-demo-login]: http://7xrip4.com1.z0.glb.clouddn.com/ItChat%2FScreenshots%2F%E7%99%BB%E5%BD%95%E7%95%8C%E9%9D%A2%E6%88%AA%E5%9B%BE.jpg?imageView/2/w/450/ -[fields.py-2]: https://gist.github.com/littlecodersh/9a0c5466f442d67d910f877744011705 -[fields.py-3]: https://gist.github.com/littlecodersh/e93532d5e7ddf0ec56c336499165c4dc -[littlecodersh]: https://github.com/littlecodersh -[tempdban]: https://github.com/tempdban -[Chyroc]: https://github.com/Chyroc -[liuwons-wxBot]: https://github.com/liuwons/wxBot -[zixia-wechaty]: https://github.com/zixia/wechaty -[Mojo-Weixin]: https://github.com/sjdy521/Mojo-Weixin -[yaphone-itchat4j]: https://github.com/yaphone/itchat4j -[issue#1]: https://github.com/littlecodersh/ItChat/issues/1 From a6544ee4a0b95b4cc754e0275c8a75c22d5c8aca Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:14:02 +0800 Subject: [PATCH 06/13] Delete __init__.py --- itchat/components/__init__.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 itchat/components/__init__.py diff --git a/itchat/components/__init__.py b/itchat/components/__init__.py deleted file mode 100644 index f088c173..00000000 --- a/itchat/components/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .contact import load_contact -from .hotreload import load_hotreload -from .login import load_login -from .messages import load_messages -from .register import load_register - -def load_components(core): - load_contact(core) - load_hotreload(core) - load_login(core) - load_messages(core) - load_register(core) From 0e5325df6e7e5b25c460efd8db2a639e580d044e Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:14:43 +0800 Subject: [PATCH 07/13] Delete login.py --- itchat/components/login.py | 361 ------------------------------------- 1 file changed, 361 deletions(-) delete mode 100644 itchat/components/login.py diff --git a/itchat/components/login.py b/itchat/components/login.py deleted file mode 100644 index c1fb0391..00000000 --- a/itchat/components/login.py +++ /dev/null @@ -1,361 +0,0 @@ -import os, time, re, io -import threading -import json, xml.dom.minidom -import random -import traceback, logging -try: - from httplib import BadStatusLine -except ImportError: - from http.client import BadStatusLine - -import requests -from pyqrcode import QRCode - -from .. import config, utils -from ..returnvalues import ReturnValue -from ..storage.templates import wrap_user_dict -from .contact import update_local_chatrooms, update_local_friends -from .messages import produce_msg - -logger = logging.getLogger('itchat') - -def load_login(core): - core.login = login - core.get_QRuuid = get_QRuuid - core.get_QR = get_QR - core.check_login = check_login - core.web_init = web_init - core.show_mobile_login = show_mobile_login - core.start_receiving = start_receiving - core.get_msg = get_msg - core.logout = logout - -def login(self, enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - if self.alive or self.isLogging: - logger.warning('itchat has already logged in.') - return - self.isLogging = True - while self.isLogging: - uuid = push_login(self) - if uuid: - qrStorage = io.BytesIO() - else: - logger.info('Getting uuid of QR code.') - while not self.get_QRuuid(): - time.sleep(1) - logger.info('Downloading QR code.') - qrStorage = self.get_QR(enableCmdQR=enableCmdQR, - picDir=picDir, qrCallback=qrCallback) - logger.info('Please scan the QR code to log in.') - isLoggedIn = False - while not isLoggedIn: - status = self.check_login() - if hasattr(qrCallback, '__call__'): - qrCallback(uuid=self.uuid, status=status, qrcode=qrStorage.getvalue()) - if status == '200': - isLoggedIn = True - elif status == '201': - if isLoggedIn is not None: - logger.info('Please press confirm on your phone.') - isLoggedIn = None - elif status != '408': - break - if isLoggedIn: - break - elif self.isLogging: - logger.info('Log in time out, reloading QR code.') - else: - return # log in process is stopped by user - logger.info('Loading the contact, this may take a little while.') - self.web_init() - self.show_mobile_login() - self.get_contact(True) - if hasattr(loginCallback, '__call__'): - r = loginCallback() - else: - utils.clear_screen() - if os.path.exists(picDir or config.DEFAULT_QR): - os.remove(picDir or config.DEFAULT_QR) - logger.info('Login successfully as %s' % self.storageClass.nickName) - self.start_receiving(exitCallback) - self.isLogging = False - -def push_login(core): - cookiesDict = core.s.cookies.get_dict() - if 'wxuin' in cookiesDict: - url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % ( - config.BASE_URL, cookiesDict['wxuin']) - headers = { 'User-Agent' : config.USER_AGENT } - r = core.s.get(url, headers=headers).json() - if 'uuid' in r and r.get('ret') in (0, '0'): - core.uuid = r['uuid'] - return r['uuid'] - return False - -def get_QRuuid(self): - url = '%s/jslogin' % config.BASE_URL - params = { - 'appid' : 'wx782c26e4c19acffb', - 'fun' : 'new', } - headers = { 'User-Agent' : config.USER_AGENT } - r = self.s.get(url, params=params, headers=headers) - regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' - data = re.search(regx, r.text) - if data and data.group(1) == '200': - self.uuid = data.group(2) - return self.uuid - -def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): - uuid = uuid or self.uuid - picDir = picDir or config.DEFAULT_QR - qrStorage = io.BytesIO() - qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid) - qrCode.png(qrStorage, scale=10) - if hasattr(qrCallback, '__call__'): - qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue()) - else: - with open(picDir, 'wb') as f: - f.write(qrStorage.getvalue()) - if enableCmdQR: - utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR) - else: - utils.print_qr(picDir) - return qrStorage - -def check_login(self, uuid=None): - uuid = uuid or self.uuid - url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL - localTime = int(time.time()) - params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( - uuid, int(-localTime / 1579), localTime) - headers = { 'User-Agent' : config.USER_AGENT } - r = self.s.get(url, params=params, headers=headers) - regx = r'window.code=(\d+)' - data = re.search(regx, r.text) - if data and data.group(1) == '200': - if process_login_info(self, r.text): - return '200' - else: - return '400' - elif data: - return data.group(1) - else: - return '400' - -def process_login_info(core, loginContent): - ''' when finish login (scanning qrcode) - * syncUrl and fileUploadingUrl will be fetched - * deviceid and msgid will be generated - * skey, wxsid, wxuin, pass_ticket will be fetched - ''' - regx = r'window.redirect_uri="(\S+)";' - core.loginInfo['url'] = re.search(regx, loginContent).group(1) - headers = { 'User-Agent' : config.USER_AGENT } - r = core.s.get(core.loginInfo['url'], headers=headers, allow_redirects=False) - core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind('/')] - for indexUrl, detailedUrl in ( - ("wx2.qq.com" , ("file.wx2.qq.com", "webpush.wx2.qq.com")), - ("wx8.qq.com" , ("file.wx8.qq.com", "webpush.wx8.qq.com")), - ("qq.com" , ("file.wx.qq.com", "webpush.wx.qq.com")), - ("web2.wechat.com" , ("file.web2.wechat.com", "webpush.web2.wechat.com")), - ("wechat.com" , ("file.web.wechat.com", "webpush.web.wechat.com"))): - fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl] - if indexUrl in core.loginInfo['url']: - core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \ - fileUrl, syncUrl - break - else: - core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url'] - core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] - core.loginInfo['logintime'] = int(time.time() * 1e3) - core.loginInfo['BaseRequest'] = {} - for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes: - if node.nodeName == 'skey': - core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data - elif node.nodeName == 'wxsid': - core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data - elif node.nodeName == 'wxuin': - core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data - elif node.nodeName == 'pass_ticket': - core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data - if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]): - logger.error('Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text) - core.isLogging = False - return False - return True - -def web_init(self): - url = '%s/webwxinit' % self.loginInfo['url'] - params = { - 'r': int(-time.time() / 1579), - 'pass_ticket': self.loginInfo['pass_ticket'], } - data = { 'BaseRequest': self.loginInfo['BaseRequest'], } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT, } - r = self.s.post(url, params=params, data=json.dumps(data), headers=headers) - dic = json.loads(r.content.decode('utf-8', 'replace')) - # deal with login info - utils.emoji_formatter(dic['User'], 'NickName') - self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) - self.loginInfo['User'] = wrap_user_dict(utils.struct_friend_info(dic['User'])) - self.memberList.append(self.loginInfo['User']) - self.loginInfo['SyncKey'] = dic['SyncKey'] - self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) - for item in dic['SyncKey']['List']]) - self.storageClass.userName = dic['User']['UserName'] - self.storageClass.nickName = dic['User']['NickName'] - # deal with contact list returned when init - contactList = dic.get('ContactList', []) - chatroomList, otherList = [], [] - for m in contactList: - if m['Sex'] != 0: - otherList.append(m) - elif '@@' in m['UserName']: - m['MemberList'] = [] # don't let dirty info pollute the list - chatroomList.append(m) - elif '@' in m['UserName']: - # mp will be dealt in update_local_friends as well - otherList.append(m) - if chatroomList: - update_local_chatrooms(self, chatroomList) - if otherList: - update_local_friends(self, otherList) - return dic - -def show_mobile_login(self): - url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest' : self.loginInfo['BaseRequest'], - 'Code' : 3, - 'FromUserName' : self.storageClass.userName, - 'ToUserName' : self.storageClass.userName, - 'ClientMsgId' : int(time.time()), } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT, } - r = self.s.post(url, data=json.dumps(data), headers=headers) - return ReturnValue(rawResponse=r) - -def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): - self.alive = True - def maintain_loop(): - retryCount = 0 - while self.alive: - try: - i = sync_check(self) - if i is None: - self.alive = False - elif i == '0': - pass - else: - msgList, contactList = self.get_msg() - if msgList: - msgList = produce_msg(self, msgList) - for msg in msgList: - self.msgList.put(msg) - if contactList: - chatroomList, otherList = [], [] - for contact in contactList: - if '@@' in contact['UserName']: - chatroomList.append(contact) - else: - otherList.append(contact) - chatroomMsg = update_local_chatrooms(self, chatroomList) - chatroomMsg['User'] = self.loginInfo['User'] - self.msgList.put(chatroomMsg) - update_local_friends(self, otherList) - retryCount = 0 - except requests.exceptions.ReadTimeout: - pass - except: - retryCount += 1 - logger.error(traceback.format_exc()) - if self.receivingRetryCount < retryCount: - self.alive = False - else: - time.sleep(1) - self.logout() - if hasattr(exitCallback, '__call__'): - exitCallback() - else: - logger.info('LOG OUT!') - if getReceivingFnOnly: - return maintain_loop - else: - maintainThread = threading.Thread(target=maintain_loop) - maintainThread.setDaemon(True) - maintainThread.start() - -def sync_check(self): - url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url']) - params = { - 'r' : int(time.time() * 1000), - 'skey' : self.loginInfo['skey'], - 'sid' : self.loginInfo['wxsid'], - 'uin' : self.loginInfo['wxuin'], - 'deviceid' : self.loginInfo['deviceid'], - 'synckey' : self.loginInfo['synckey'], - '_' : self.loginInfo['logintime'], } - headers = { 'User-Agent' : config.USER_AGENT } - self.loginInfo['logintime'] += 1 - try: - r = self.s.get(url, params=params, headers=headers, timeout=config.TIMEOUT) - except requests.exceptions.ConnectionError as e: - try: - if not isinstance(e.args[0].args[1], BadStatusLine): - raise - # will return a package with status '0 -' - # and value like: - # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93 - # seems like status of typing, but before I make further achievement code will remain like this - return '2' - except: - raise - r.raise_for_status() - regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' - pm = re.search(regx, r.text) - if pm is None or pm.group(1) != '0': - logger.debug('Unexpected sync check result: %s' % r.text) - return None - return pm.group(2) - -def get_msg(self): - url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['wxsid'], - self.loginInfo['skey'],self.loginInfo['pass_ticket']) - data = { - 'BaseRequest' : self.loginInfo['BaseRequest'], - 'SyncKey' : self.loginInfo['SyncKey'], - 'rr' : ~int(time.time()), } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, data=json.dumps(data), headers=headers, timeout=config.TIMEOUT) - dic = json.loads(r.content.decode('utf-8', 'replace')) - if dic['BaseResponse']['Ret'] != 0: return None, None - self.loginInfo['SyncKey'] = dic['SyncKey'] - self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) - for item in dic['SyncCheckKey']['List']]) - return dic['AddMsgList'], dic['ModContactList'] - -def logout(self): - if self.alive: - url = '%s/webwxlogout' % self.loginInfo['url'] - params = { - 'redirect' : 1, - 'type' : 1, - 'skey' : self.loginInfo['skey'], } - headers = { 'User-Agent' : config.USER_AGENT } - self.s.get(url, params=params, headers=headers) - self.alive = False - self.isLogging = False - self.s.cookies.clear() - del self.chatroomList[:] - del self.memberList[:] - del self.mpList[:] - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'logout successfully.', - 'Ret': 0, }}) From 3c0c11a3b2891de19d6b7e97e765297000390d48 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:14:51 +0800 Subject: [PATCH 08/13] Delete register.py --- itchat/components/register.py | 103 ---------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 itchat/components/register.py diff --git a/itchat/components/register.py b/itchat/components/register.py deleted file mode 100644 index 079d3191..00000000 --- a/itchat/components/register.py +++ /dev/null @@ -1,103 +0,0 @@ -import logging, traceback, sys, threading -try: - import Queue -except ImportError: - import queue as Queue - -from ..log import set_logging -from ..utils import test_connect -from ..storage import templates - -logger = logging.getLogger('itchat') - -def load_register(core): - core.auto_login = auto_login - core.configured_reply = configured_reply - core.msg_register = msg_register - core.run = run - -def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', - enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - if not test_connect(): - logger.info("You can't get access to internet or wechat domain, so exit.") - sys.exit() - self.useHotReload = hotReload - self.hotReloadDir = statusStorageDir - if hotReload: - if self.load_login_status(statusStorageDir, - loginCallback=loginCallback, exitCallback=exitCallback): - return - self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, - loginCallback=loginCallback, exitCallback=exitCallback) - self.dump_login_status(statusStorageDir) - else: - self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, - loginCallback=loginCallback, exitCallback=exitCallback) - -def configured_reply(self): - ''' determine the type of message and reply if its method is defined - however, I use a strange way to determine whether a msg is from massive platform - I haven't found a better solution here - The main problem I'm worrying about is the mismatching of new friends added on phone - If you have any good idea, pleeeease report an issue. I will be more than grateful. - ''' - try: - msg = self.msgList.get(timeout=1) - except Queue.Empty: - pass - else: - if isinstance(msg['User'], templates.User): - replyFn = self.functionDict['FriendChat'].get(msg['Type']) - elif isinstance(msg['User'], templates.MassivePlatform): - replyFn = self.functionDict['MpChat'].get(msg['Type']) - elif isinstance(msg['User'], templates.Chatroom): - replyFn = self.functionDict['GroupChat'].get(msg['Type']) - if replyFn is None: - r = None - else: - try: - r = replyFn(msg) - if r is not None: - self.send(r, msg.get('FromUserName')) - except: - logger.warning(traceback.format_exc()) - -def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False): - ''' a decorator constructor - return a specific decorator based on information given ''' - if not (isinstance(msgType, list) or isinstance(msgType, tuple)): - msgType = [msgType] - def _msg_register(fn): - for _msgType in msgType: - if isFriendChat: - self.functionDict['FriendChat'][_msgType] = fn - if isGroupChat: - self.functionDict['GroupChat'][_msgType] = fn - if isMpChat: - self.functionDict['MpChat'][_msgType] = fn - if not any((isFriendChat, isGroupChat, isMpChat)): - self.functionDict['FriendChat'][_msgType] = fn - return fn - return _msg_register - -def run(self, debug=False, blockThread=True): - logger.info('Start auto replying.') - if debug: - set_logging(loggingLevel=logging.DEBUG) - def reply_fn(): - try: - while self.alive: - self.configured_reply() - except KeyboardInterrupt: - if self.useHotReload: - self.dump_login_status() - self.alive = False - logger.debug('itchat received an ^C and exit.') - logger.info('Bye~') - if blockThread: - reply_fn() - else: - replyThread = threading.Thread(target=reply_fn) - replyThread.setDaemon(True) - replyThread.start() From 3142501f23aedb05d1e8248b8d63eeb01ffef58a Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:15:24 +0800 Subject: [PATCH 09/13] Add files via upload --- itchat/components/__init__.py | 14 ++ itchat/components/error.py | 9 + itchat/components/login.py | 367 ++++++++++++++++++++++++++++++++++ itchat/components/register.py | 104 ++++++++++ 4 files changed, 494 insertions(+) create mode 100644 itchat/components/__init__.py create mode 100644 itchat/components/error.py create mode 100644 itchat/components/login.py create mode 100644 itchat/components/register.py diff --git a/itchat/components/__init__.py b/itchat/components/__init__.py new file mode 100644 index 00000000..cb17cd84 --- /dev/null +++ b/itchat/components/__init__.py @@ -0,0 +1,14 @@ +from .contact import load_contact +from .hotreload import load_hotreload +from .login import load_login +from .messages import load_messages +from .register import load_register +from .error import load_error + +def load_components(core): + load_contact(core) + load_hotreload(core) + load_login(core) + load_messages(core) + load_register(core) + load_error(core) diff --git a/itchat/components/error.py b/itchat/components/error.py new file mode 100644 index 00000000..9053e7c0 --- /dev/null +++ b/itchat/components/error.py @@ -0,0 +1,9 @@ +def load_error(core): + core.error_register=error_register + +def error_register(self,accept_all_errors): + if not accept_all_errors: + raise NotImplementedError() + def register(fn): + self.functionDict['Error'].append(fn) + return register diff --git a/itchat/components/login.py b/itchat/components/login.py new file mode 100644 index 00000000..bc3a0371 --- /dev/null +++ b/itchat/components/login.py @@ -0,0 +1,367 @@ +import os, sys, time, re, io +import threading +import json, xml.dom.minidom +import copy, pickle, random +import traceback, logging +try: + from httplib import BadStatusLine +except ImportError: + from http.client import BadStatusLine + +import requests +from pyqrcode import QRCode + +from .. import config, utils +from ..returnvalues import ReturnValue +from ..storage.templates import wrap_user_dict +from .contact import update_local_chatrooms, update_local_friends +from .messages import produce_msg + +logger = logging.getLogger('itchat') +requests.adapters.DEFAULT_RETRIES = 10 + +def load_login(core): + core.login = login + core.get_QRuuid = get_QRuuid + core.get_QR = get_QR + core.check_login = check_login + core.web_init = web_init + core.show_mobile_login = show_mobile_login + core.start_receiving = start_receiving + core.get_msg = get_msg + core.logout = logout + +def login(self, enableCmdQR=False, picDir=None, qrCallback=None, + loginCallback=None, exitCallback=None): + if self.alive or self.isLogging: + logger.warning('itchat has already logged in.') + return + self.isLogging = True + while self.isLogging: + uuid = push_login(self) + if uuid: + qrStorage = io.BytesIO() + else: + logger.info('Getting uuid of QR code.') + while not self.get_QRuuid(): + time.sleep(1) + logger.info('Downloading QR code.') + qrStorage = self.get_QR(enableCmdQR=enableCmdQR, + picDir=picDir, qrCallback=qrCallback) + logger.info('Please scan the QR code to log in.') + isLoggedIn = False + while not isLoggedIn: + status = self.check_login() + if hasattr(qrCallback, '__call__'): + qrCallback(uuid=self.uuid, status=status, qrcode=qrStorage.getvalue()) + if status == '200': + isLoggedIn = True + elif status == '201': + if isLoggedIn is not None: + logger.info('Please press confirm on your phone.') + isLoggedIn = None + elif status != '408': + break + if isLoggedIn: + break + elif self.isLogging: + logger.info('Log in time out, reloading QR code.') + else: + return # log in process is stopped by user + logger.info('Loading the contact, this may take a little while.') + self.web_init() + self.show_mobile_login() + self.get_contact(True) + if hasattr(loginCallback, '__call__'): + r = loginCallback() + else: + utils.clear_screen() + if os.path.exists(picDir or config.DEFAULT_QR): + os.remove(picDir or config.DEFAULT_QR) + logger.info('Login successfully as %s' % self.storageClass.nickName) + self.start_receiving(exitCallback) + self.isLogging = False + +def push_login(core): + cookiesDict = core.s.cookies.get_dict() + if 'wxuin' in cookiesDict: + url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % ( + config.BASE_URL, cookiesDict['wxuin']) + headers = { 'User-Agent' : config.USER_AGENT } + r = core.s.get(url, headers=headers).json() + if 'uuid' in r and r.get('ret') in (0, '0'): + core.uuid = r['uuid'] + return r['uuid'] + return False + +def get_QRuuid(self): + url = '%s/jslogin' % config.BASE_URL + params = { + 'appid' : 'wx782c26e4c19acffb', + 'fun' : 'new', } + headers = { 'User-Agent' : config.USER_AGENT } + r = self.s.get(url, params=params, headers=headers) + regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' + data = re.search(regx, r.text) + if data and data.group(1) == '200': + self.uuid = data.group(2) + return self.uuid + +def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): + uuid = uuid or self.uuid + picDir = picDir or config.DEFAULT_QR + qrStorage = io.BytesIO() + qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid) + qrCode.png(qrStorage, scale=10) + if hasattr(qrCallback, '__call__'): + qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue()) + else: + if enableCmdQR: + utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR) + else: + with open(picDir, 'wb') as f: + f.write(qrStorage.getvalue()) + utils.print_qr(picDir) + return qrStorage + +def check_login(self, uuid=None): + uuid = uuid or self.uuid + url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL + localTime = int(time.time()) + params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( + uuid, int(-localTime / 1579), localTime) + headers = { 'User-Agent' : config.USER_AGENT } + r = self.s.get(url, params=params, headers=headers) + regx = r'window.code=(\d+)' + data = re.search(regx, r.text) + if data and data.group(1) == '200': + if process_login_info(self, r.text): + return '200' + else: + return '400' + elif data: + return data.group(1) + else: + return '400' + +def process_login_info(core, loginContent): + ''' when finish login (scanning qrcode) + * syncUrl and fileUploadingUrl will be fetched + * deviceid and msgid will be generated + * skey, wxsid, wxuin, pass_ticket will be fetched + ''' + regx = r'window.redirect_uri="(\S+)";' + core.loginInfo['url'] = re.search(regx, loginContent).group(1) + headers = { 'User-Agent' : config.USER_AGENT } + r = core.s.get(core.loginInfo['url'], headers=headers, allow_redirects=False) + core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind('/')] + for indexUrl, detailedUrl in ( + ("wx2.qq.com" , ("file.wx2.qq.com", "webpush.wx2.qq.com")), + ("wx8.qq.com" , ("file.wx8.qq.com", "webpush.wx8.qq.com")), + ("qq.com" , ("file.wx.qq.com", "webpush.wx.qq.com")), + ("web2.wechat.com" , ("file.web2.wechat.com", "webpush.web2.wechat.com")), + ("wechat.com" , ("file.web.wechat.com", "webpush.web.wechat.com"))): + fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl] + if indexUrl in core.loginInfo['url']: + core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \ + fileUrl, syncUrl + break + else: + core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url'] + core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] + core.loginInfo['BaseRequest'] = {} + for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes: + if node.nodeName == 'skey': + core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data + elif node.nodeName == 'wxsid': + core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data + elif node.nodeName == 'wxuin': + core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data + elif node.nodeName == 'pass_ticket': + core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data + if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]): + logger.error('Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text) + core.isLogging = False + return False + return True + +def web_init(self): + url = '%s/webwxinit' % self.loginInfo['url'] + params = { + 'r': int(-time.time() / 1579), + 'pass_ticket': self.loginInfo['pass_ticket'], } + data = { 'BaseRequest': self.loginInfo['BaseRequest'], } + headers = { + 'ContentType': 'application/json; charset=UTF-8', + 'User-Agent' : config.USER_AGENT, } + r = self.s.post(url, params=params, data=json.dumps(data), headers=headers) + dic = json.loads(r.content.decode('utf-8', 'replace')) + # deal with login info + utils.emoji_formatter(dic['User'], 'NickName') + self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) + self.loginInfo['User'] = wrap_user_dict(utils.struct_friend_info(dic['User'])) + self.memberList.append(self.loginInfo['User']) + self.loginInfo['SyncKey'] = dic['SyncKey'] + self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) + for item in dic['SyncKey']['List']]) + self.storageClass.userName = dic['User']['UserName'] + self.storageClass.nickName = dic['User']['NickName'] + # deal with contact list returned when init + contactList = dic.get('ContactList', []) + chatroomList, otherList = [], [] + for m in contactList: + if m['Sex'] != 0: + otherList.append(m) + elif '@@' in m['UserName']: + m['MemberList'] = [] # don't let dirty info pollute the list + chatroomList.append(m) + elif '@' in m['UserName']: + # mp will be dealt in update_local_friends as well + otherList.append(m) + if chatroomList: + update_local_chatrooms(self, chatroomList) + if otherList: + update_local_friends(self, otherList) + return dic + +def show_mobile_login(self): + url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % ( + self.loginInfo['url'], self.loginInfo['pass_ticket']) + data = { + 'BaseRequest' : self.loginInfo['BaseRequest'], + 'Code' : 3, + 'FromUserName' : self.storageClass.userName, + 'ToUserName' : self.storageClass.userName, + 'ClientMsgId' : int(time.time()), } + headers = { + 'ContentType': 'application/json; charset=UTF-8', + 'User-Agent' : config.USER_AGENT, } + r = self.s.post(url, data=json.dumps(data), headers=headers) + return ReturnValue(rawResponse=r) + +def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): + self.alive = True + def maintain_loop(): + retryCount = 0 + while self.alive: + try: + i = sync_check(self) + if i is None: + self.alive = False + elif i == '0': + pass + else: + msgList, contactList = self.get_msg() + if msgList: + msgList = produce_msg(self, msgList) + for msg in msgList: + self.msgList.put(msg) + if contactList: + chatroomList, otherList = [], [] + for contact in contactList: + if '@@' in contact['UserName']: + chatroomList.append(contact) + else: + otherList.append(contact) + chatroomMsg = update_local_chatrooms(self, chatroomList) + chatroomMsg['User'] = self.loginInfo['User'] + self.msgList.put(chatroomMsg) + update_local_friends(self, otherList) + retryCount = 0 + except requests.exceptions.ReadTimeout: + pass + except : + retryCount += 1 + err=traceback.format_exc() + for f in self.functionDict['Error']: + f('Unknown Internet Error',err) + if self.receivingRetryCount < retryCount: + #self.alive = False + self.functionDict['Error']('Retry times reached limit','still retrying') + else: + time.sleep(1) + self.logout() + if hasattr(exitCallback, '__call__'): + exitCallback() + else: + logger.info('LOG OUT!') + if getReceivingFnOnly: + return maintain_loop + else: + maintainThread = threading.Thread(target=maintain_loop) + maintainThread.setDaemon(True) + maintainThread.start() + +def sync_check(self): + url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url']) + params = { + 'r' : int(time.time() * 1000), + 'skey' : self.loginInfo['skey'], + 'sid' : self.loginInfo['wxsid'], + 'uin' : self.loginInfo['wxuin'], + 'deviceid' : self.loginInfo['deviceid'], + 'synckey' : self.loginInfo['synckey'], + '_' : int(time.time() * 1000),} + headers = { 'User-Agent' : config.USER_AGENT } + try: + r = self.s.get(url, params=params, headers=headers, timeout=config.TIMEOUT) + except requests.exceptions.ConnectionError as e: + try: + if not isinstance(e.args[0].args[1], BadStatusLine): + for f in self.functionDict['Error']: + f(e) + return + # will return a package with status '0 -' + # and value like: + # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93 + # seems like status of typing, but before I make further achievement code will remain like this + return '2' + except: + for f in self.functionDict['Error']: + f(None) + return + r.raise_for_status() + regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' + pm = re.search(regx, r.text) + if pm is None or pm.group(1) != '0': + logger.debug('Unexpected sync check result: %s' % r.text) + return None + return pm.group(2) + +def get_msg(self): + url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( + self.loginInfo['url'], self.loginInfo['wxsid'], + self.loginInfo['skey'],self.loginInfo['pass_ticket']) + data = { + 'BaseRequest' : self.loginInfo['BaseRequest'], + 'SyncKey' : self.loginInfo['SyncKey'], + 'rr' : ~int(time.time()), } + headers = { + 'ContentType': 'application/json; charset=UTF-8', + 'User-Agent' : config.USER_AGENT } + r = self.s.post(url, data=json.dumps(data), headers=headers, timeout=config.TIMEOUT) + dic = json.loads(r.content.decode('utf-8', 'replace')) + if dic['BaseResponse']['Ret'] != 0: return None, None + self.loginInfo['SyncKey'] = dic['SyncCheckKey'] + self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) + for item in dic['SyncCheckKey']['List']]) + return dic['AddMsgList'], dic['ModContactList'] + +def logout(self): + if self.alive: + url = '%s/webwxlogout' % self.loginInfo['url'] + params = { + 'redirect' : 1, + 'type' : 1, + 'skey' : self.loginInfo['skey'], } + headers = { 'User-Agent' : config.USER_AGENT } + self.s.get(url, params=params, headers=headers) + self.alive = False + self.isLogging = False + self.s.cookies.clear() + del self.chatroomList[:] + del self.memberList[:] + del self.mpList[:] + return ReturnValue({'BaseResponse': { + 'ErrMsg': 'logout successfully.', + 'Ret': 0, }}) diff --git a/itchat/components/register.py b/itchat/components/register.py new file mode 100644 index 00000000..b497fb72 --- /dev/null +++ b/itchat/components/register.py @@ -0,0 +1,104 @@ +import logging, traceback, sys, threading +try: + import Queue +except ImportError: + import queue as Queue + +from ..log import set_logging +from ..utils import test_connect +from ..storage import templates + +logger = logging.getLogger('itchat') + +def load_register(core): + core.auto_login = auto_login + core.configured_reply = configured_reply + core.msg_register = msg_register + core.run = run + +def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', + enableCmdQR=False, picDir=None, qrCallback=None, + loginCallback=None, exitCallback=None): + x=test_connect() + if x!=True: + for f in self.functionDict['Error']: + f("You can't get access to wechat. ",x) + self.useHotReload = hotReload + self.hotReloadDir = statusStorageDir + if hotReload: + if self.load_login_status(statusStorageDir, + loginCallback=loginCallback, exitCallback=exitCallback): + return + self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, + loginCallback=loginCallback, exitCallback=exitCallback) + self.dump_login_status(statusStorageDir) + else: + self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, + loginCallback=loginCallback, exitCallback=exitCallback) + +def configured_reply(self): + ''' determine the type of message and reply if its method is defined + however, I use a strange way to determine whether a msg is from massive platform + I haven't found a better solution here + The main problem I'm worrying about is the mismatching of new friends added on phone + If you have any good idea, pleeeease report an issue. I will be more than grateful. + ''' + try: + msg = self.msgList.get(timeout=1) + except Queue.Empty: + pass + else: + if isinstance(msg['User'], templates.User): + replyFn = self.functionDict['FriendChat'].get(msg['Type']) + elif isinstance(msg['User'], templates.MassivePlatform): + replyFn = self.functionDict['MpChat'].get(msg['Type']) + elif isinstance(msg['User'], templates.Chatroom): + replyFn = self.functionDict['GroupChat'].get(msg['Type']) + if replyFn is None: + r = None + else: + try: + r = replyFn(msg) + if r is not None: + self.send(r, msg.get('FromUserName')) + except: + logger.warning(traceback.format_exc()) + +def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False): + ''' a decorator constructor + return a specific decorator based on information given ''' + if not (isinstance(msgType, list) or isinstance(msgType, tuple)): + msgType = [msgType] + def _msg_register(fn): + for _msgType in msgType: + if isFriendChat: + self.functionDict['FriendChat'][_msgType] = fn + if isGroupChat: + self.functionDict['GroupChat'][_msgType] = fn + if isMpChat: + self.functionDict['MpChat'][_msgType] = fn + if not any((isFriendChat, isGroupChat, isMpChat)): + self.functionDict['FriendChat'][_msgType] = fn + return fn + return _msg_register + +def run(self, debug=False, blockThread=True): + logger.info('Start auto replying.') + if debug: + set_logging(loggingLevel=logging.DEBUG) + def reply_fn(): + try: + while self.alive: + self.configured_reply() + except KeyboardInterrupt: + if self.useHotReload: + self.dump_login_status() + self.alive = False + logger.debug('itchat received an ^C and exit.') + logger.info('Bye~') + if blockThread: + reply_fn() + else: + replyThread = threading.Thread(target=reply_fn) + replyThread.setDaemon(True) + replyThread.start() From 31d071d10155e8ab41a7a21e070716df8cde7747 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:19:40 +0800 Subject: [PATCH 10/13] Delete __init__.py --- itchat/__init__.py | 69 ---------------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 itchat/__init__.py diff --git a/itchat/__init__.py b/itchat/__init__.py deleted file mode 100644 index a7cb466b..00000000 --- a/itchat/__init__.py +++ /dev/null @@ -1,69 +0,0 @@ -from . import content -from .core import Core -from .config import VERSION -from .log import set_logging - -__version__ = VERSION - -instanceList = [] - -def new_instance(): - newInstance = Core() - instanceList.append(newInstance) - return newInstance - -originInstance = new_instance() - -# I really want to use sys.modules[__name__] = originInstance -# but it makes auto-fill a real mess, so forgive me for my following ** -# actually it toke me less than 30 seconds, god bless Uganda - -# components.login -login = originInstance.login -get_QRuuid = originInstance.get_QRuuid -get_QR = originInstance.get_QR -check_login = originInstance.check_login -web_init = originInstance.web_init -show_mobile_login = originInstance.show_mobile_login -start_receiving = originInstance.start_receiving -get_msg = originInstance.get_msg -logout = originInstance.logout -# components.contact -update_chatroom = originInstance.update_chatroom -update_friend = originInstance.update_friend -get_contact = originInstance.get_contact -get_friends = originInstance.get_friends -get_chatrooms = originInstance.get_chatrooms -get_mps = originInstance.get_mps -set_alias = originInstance.set_alias -set_pinned = originInstance.set_pinned -add_friend = originInstance.add_friend -get_head_img = originInstance.get_head_img -create_chatroom = originInstance.create_chatroom -set_chatroom_name = originInstance.set_chatroom_name -delete_member_from_chatroom = originInstance.delete_member_from_chatroom -add_member_into_chatroom = originInstance.add_member_into_chatroom -# components.messages -send_raw_msg = originInstance.send_raw_msg -send_msg = originInstance.send_msg -upload_file = originInstance.upload_file -send_file = originInstance.send_file -send_image = originInstance.send_image -send_video = originInstance.send_video -send = originInstance.send -revoke = originInstance.revoke -# components.hotreload -dump_login_status = originInstance.dump_login_status -load_login_status = originInstance.load_login_status -# components.register -auto_login = originInstance.auto_login -configured_reply = originInstance.configured_reply -msg_register = originInstance.msg_register -error_register = originInstance.error_register -run = originInstance.run -# other functions -search_friends = originInstance.search_friends -search_chatrooms = originInstance.search_chatrooms -search_mps = originInstance.search_mps -set_logging = set_logging - From c5a62eab646ee49429f6abdf4684af346adb6392 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:19:51 +0800 Subject: [PATCH 11/13] Delete core.py --- itchat/core.py | 459 ------------------------------------------------- 1 file changed, 459 deletions(-) delete mode 100644 itchat/core.py diff --git a/itchat/core.py b/itchat/core.py deleted file mode 100644 index 52d6ae4f..00000000 --- a/itchat/core.py +++ /dev/null @@ -1,459 +0,0 @@ -import requests - -from . import storage -from .components import load_components - -class Core(object): - def __init__(self): - ''' init is the only method defined in core.py - alive is value showing whether core is running - - you should call logout method to change it - - after logout, a core object can login again - storageClass only uses basic python types - - so for advanced uses, inherit it yourself - receivingRetryCount is for receiving loop retry - - it's 5 now, but actually even 1 is enough - - failing is failing - ''' - self.alive, self.isLogging = False, False - self.storageClass = storage.Storage(self) - self.memberList = self.storageClass.memberList - self.mpList = self.storageClass.mpList - self.chatroomList = self.storageClass.chatroomList - self.msgList = self.storageClass.msgList - self.loginInfo = {} - self.s = requests.Session() - self.uuid = None - self.functionDict = {'FriendChat': {}, 'GroupChat': {}, 'MpChat': {}} - self.useHotReload, self.hotReloadDir = False, 'itchat.pkl' - self.receivingRetryCount = 5 - def login(self, enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - ''' log in like web wechat does - for log in - - a QR code will be downloaded and opened - - then scanning status is logged, it paused for you confirm - - finally it logged in and show your nickName - for options - - enableCmdQR: show qrcode in command line - - integers can be used to fit strange char length - - picDir: place for storing qrcode - - qrCallback: method that should accept uuid, status, qrcode - - loginCallback: callback after successfully logged in - - if not set, screen is cleared and qrcode is deleted - - exitCallback: callback after logged out - - it contains calling of logout - for usage - ..code::python - - import itchat - itchat.login() - - it is defined in components/login.py - and of course every single move in login can be called outside - - you may scan source code to see how - - and modified according to your own demand - ''' - raise NotImplementedError() - def get_QRuuid(self): - ''' get uuid for qrcode - uuid is the symbol of qrcode - - for logging in, you need to get a uuid first - - for downloading qrcode, you need to pass uuid to it - - for checking login status, uuid is also required - if uuid has timed out, just get another - it is defined in components/login.py - ''' - raise NotImplementedError() - def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): - ''' download and show qrcode - for options - - uuid: if uuid is not set, latest uuid you fetched will be used - - enableCmdQR: show qrcode in cmd - - picDir: where to store qrcode - - qrCallback: method that should accept uuid, status, qrcode - it is defined in components/login.py - ''' - raise NotImplementedError() - def check_login(self, uuid=None): - ''' check login status - for options: - - uuid: if uuid is not set, latest uuid you fetched will be used - for return values: - - a string will be returned - - for meaning of return values - - 200: log in successfully - - 201: waiting for press confirm - - 408: uuid timed out - - 0 : unknown error - for processing: - - syncUrl and fileUrl is set - - BaseRequest is set - blocks until reaches any of above status - it is defined in components/login.py - ''' - raise NotImplementedError() - def web_init(self): - ''' get info necessary for initializing - for processing: - - own account info is set - - inviteStartCount is set - - syncKey is set - - part of contact is fetched - it is defined in components/login.py - ''' - raise NotImplementedError() - def show_mobile_login(self): - ''' show web wechat login sign - the sign is on the top of mobile phone wechat - sign will be added after sometime even without calling this function - it is defined in components/login.py - ''' - raise NotImplementedError() - def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): - ''' open a thread for heart loop and receiving messages - for options: - - exitCallback: callback after logged out - - it contains calling of logout - - getReceivingFnOnly: if True thread will not be created and started. Instead, receive fn will be returned. - for processing: - - messages: msgs are formatted and passed on to registered fns - - contact : chatrooms are updated when related info is received - it is defined in components/login.py - ''' - raise NotImplementedError() - def get_msg(self): - ''' fetch messages - for fetching - - method blocks for sometime until - - new messages are to be received - - or anytime they like - - synckey is updated with returned synccheckkey - it is defined in components/login.py - ''' - raise NotImplementedError() - def logout(self): - ''' logout - if core is now alive - logout will tell wechat backstage to logout - and core gets ready for another login - it is defined in components/login.py - ''' - raise NotImplementedError() - def update_chatroom(self, userName, detailedMember=False): - ''' update chatroom - for chatroom contact - - a chatroom contact need updating to be detailed - - detailed means members, encryid, etc - - auto updating of heart loop is a more detailed updating - - member uin will also be filled - - once called, updated info will be stored - for options - - userName: 'UserName' key of chatroom or a list of it - - detailedMember: whether to get members of contact - it is defined in components/contact.py - ''' - raise NotImplementedError() - def update_friend(self, userName): - ''' update chatroom - for friend contact - - once called, updated info will be stored - for options - - userName: 'UserName' key of a friend or a list of it - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_contact(self, update=False): - ''' fetch part of contact - for part - - all the massive platforms and friends are fetched - - if update, only starred chatrooms are fetched - for options - - update: if not set, local value will be returned - for results - - chatroomList will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_friends(self, update=False): - ''' fetch friends list - for options - - update: if not set, local value will be returned - for results - - a list of friends' info dicts will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_chatrooms(self, update=False, contactOnly=False): - ''' fetch chatrooms list - for options - - update: if not set, local value will be returned - - contactOnly: if set, only starred chatrooms will be returned - for results - - a list of chatrooms' info dicts will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_mps(self, update=False): - ''' fetch massive platforms list - for options - - update: if not set, local value will be returned - for results - - a list of platforms' info dicts will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def set_alias(self, userName, alias): - ''' set alias for a friend - for options - - userName: 'UserName' key of info dict - - alias: new alias - it is defined in components/contact.py - ''' - raise NotImplementedError() - def set_pinned(self, userName, isPinned=True): - ''' set pinned for a friend or a chatroom - for options - - userName: 'UserName' key of info dict - - isPinned: whether to pin - it is defined in components/contact.py - ''' - raise NotImplementedError() - def add_friend(self, userName, status=2, verifyContent='', autoUpdate=True): - ''' add a friend or accept a friend - for options - - userName: 'UserName' for friend's info dict - - status: - - for adding status should be 2 - - for accepting status should be 3 - - ticket: greeting message - - userInfo: friend's other info for adding into local storage - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): - ''' place for docs - for options - - if you want to get chatroom header: only set chatroomUserName - - if you want to get friend header: only set userName - - if you want to get chatroom member header: set both - it is defined in components/contact.py - ''' - raise NotImplementedError() - def create_chatroom(self, memberList, topic=''): - ''' create a chatroom - for creating - - its calling frequency is strictly limited - for options - - memberList: list of member info dict - - topic: topic of new chatroom - it is defined in components/contact.py - ''' - raise NotImplementedError() - def set_chatroom_name(self, chatroomUserName, name): - ''' set chatroom name - for setting - - it makes an updating of chatroom - - which means detailed info will be returned in heart loop - for options - - chatroomUserName: 'UserName' key of chatroom info dict - - name: new chatroom name - it is defined in components/contact.py - ''' - raise NotImplementedError() - def delete_member_from_chatroom(self, chatroomUserName, memberList): - ''' deletes members from chatroom - for deleting - - you can't delete yourself - - if so, no one will be deleted - - strict-limited frequency - for options - - chatroomUserName: 'UserName' key of chatroom info dict - - memberList: list of members' info dict - it is defined in components/contact.py - ''' - raise NotImplementedError() - def add_member_into_chatroom(self, chatroomUserName, memberList, - useInvitation=False): - ''' add members into chatroom - for adding - - you can't add yourself or member already in chatroom - - if so, no one will be added - - if member will over 40 after adding, invitation must be used - - strict-limited frequency - for options - - chatroomUserName: 'UserName' key of chatroom info dict - - memberList: list of members' info dict - - useInvitation: if invitation is not required, set this to use - it is defined in components/contact.py - ''' - raise NotImplementedError() - def send_raw_msg(self, msgType, content, toUserName): - ''' many messages are sent in a common way - for demo - .. code:: python - - @itchat.msg_register(itchat.content.CARD) - def reply(msg): - itchat.send_raw_msg(msg['MsgType'], msg['Content'], msg['FromUserName']) - - there are some little tricks here, you may discover them yourself - but remember they are tricks - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_msg(self, msg='Test Message', toUserName=None): - ''' send plain text message - for options - - msg: should be unicode if there's non-ascii words in msg - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def upload_file(self, fileDir, isPicture=False, isVideo=False, - toUserName='filehelper', file_=None, preparedFile=None): - ''' upload file to server and get mediaId - for options - - fileDir: dir for file ready for upload - - isPicture: whether file is a picture - - isVideo: whether file is a video - for return values - will return a ReturnValue - if succeeded, mediaId is in r['MediaId'] - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None): - ''' send attachment - for options - - fileDir: dir for file ready for upload - - mediaId: mediaId for file. - - if set, file will not be uploaded twice - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - ''' send image - for options - - fileDir: dir for file ready for upload - - if it's a gif, name it like 'xx.gif' - - mediaId: mediaId for file. - - if set, file will not be uploaded twice - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - ''' send video - for options - - fileDir: dir for file ready for upload - - if mediaId is set, it's unnecessary to set fileDir - - mediaId: mediaId for file. - - if set, file will not be uploaded twice - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send(self, msg, toUserName=None, mediaId=None): - ''' wrapped function for all the sending functions - for options - - msg: message starts with different string indicates different type - - list of type string: ['@fil@', '@img@', '@msg@', '@vid@'] - - they are for file, image, plain text, video - - if none of them matches, it will be sent like plain text - - toUserName: 'UserName' key of friend dict - - mediaId: if set, uploading will not be repeated - it is defined in components/messages.py - ''' - raise NotImplementedError() - def revoke(self, msgId, toUserName, localId=None): - ''' revoke message with its and msgId - for options - - msgId: message Id on server - - toUserName: 'UserName' key of friend dict - - localId: message Id at local (optional) - it is defined in components/messages.py - ''' - raise NotImplementedError() - def dump_login_status(self, fileDir=None): - ''' dump login status to a specific file - for option - - fileDir: dir for dumping login status - it is defined in components/hotreload.py - ''' - raise NotImplementedError() - def load_login_status(self, fileDir, - loginCallback=None, exitCallback=None): - ''' load login status from a specific file - for option - - fileDir: file for loading login status - - loginCallback: callback after successfully logged in - - if not set, screen is cleared and qrcode is deleted - - exitCallback: callback after logged out - - it contains calling of logout - it is defined in components/hotreload.py - ''' - raise NotImplementedError() - def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', - enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - ''' log in like web wechat does - for log in - - a QR code will be downloaded and opened - - then scanning status is logged, it paused for you confirm - - finally it logged in and show your nickName - for options - - hotReload: enable hot reload - - statusStorageDir: dir for storing log in status - - enableCmdQR: show qrcode in command line - - integers can be used to fit strange char length - - picDir: place for storing qrcode - - loginCallback: callback after successfully logged in - - if not set, screen is cleared and qrcode is deleted - - exitCallback: callback after logged out - - it contains calling of logout - - qrCallback: method that should accept uuid, status, qrcode - for usage - ..code::python - - import itchat - itchat.auto_login() - - it is defined in components/register.py - and of course every single move in login can be called outside - - you may scan source code to see how - - and modified according to your own demond - ''' - raise NotImplementedError() - def configured_reply(self): - ''' determine the type of message and reply if its method is defined - however, I use a strange way to determine whether a msg is from massive platform - I haven't found a better solution here - The main problem I'm worrying about is the mismatching of new friends added on phone - If you have any good idea, pleeeease report an issue. I will be more than grateful. - ''' - raise NotImplementedError() - def msg_register(self, msgType, - isFriendChat=False, isGroupChat=False, isMpChat=False): - ''' a decorator constructor - return a specific decorator based on information given - ''' - raise NotImplementedError() - def run(self, debug=True, blockThread=True): - ''' start auto respond - for option - - debug: if set, debug info will be shown on screen - it is defined in components/register.py - ''' - raise NotImplementedError() - def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, - wechatAccount=None): - return self.storageClass.search_friends(name, userName, remarkName, - nickName, wechatAccount) - def search_chatrooms(self, name=None, userName=None): - return self.storageClass.search_chatrooms(name, userName) - def search_mps(self, name=None, userName=None): - return self.storageClass.search_mps(name, userName) - -load_components(Core) From b157b9529efbdd0f399876c8ae061511790aa1bf Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:20:05 +0800 Subject: [PATCH 12/13] Delete utils.py --- itchat/utils.py | 159 ------------------------------------------------ 1 file changed, 159 deletions(-) delete mode 100644 itchat/utils.py diff --git a/itchat/utils.py b/itchat/utils.py deleted file mode 100644 index e217368c..00000000 --- a/itchat/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -import re, os, sys, subprocess, copy, traceback, logging - -try: - from HTMLParser import HTMLParser -except ImportError: - from html.parser import HTMLParser -try: - from urllib import quote as _quote - quote = lambda n: _quote(n.encode('utf8', 'replace')) -except ImportError: - from urllib.parse import quote - -import requests - -from . import config - -logger = logging.getLogger('itchat') - -emojiRegex = re.compile(r'') -htmlParser = HTMLParser() -try: - b = u'\u2588' - sys.stdout.write(b + '\r') - sys.stdout.flush() -except UnicodeEncodeError: - BLOCK = 'MM' -else: - BLOCK = b -friendInfoTemplate = {} -for k in ('UserName', 'City', 'DisplayName', 'PYQuanPin', 'RemarkPYInitial', 'Province', - 'KeyWord', 'RemarkName', 'PYInitial', 'EncryChatRoomId', 'Alias', 'Signature', - 'NickName', 'RemarkPYQuanPin', 'HeadImgUrl'): - friendInfoTemplate[k] = '' -for k in ('UniFriend', 'Sex', 'AppAccountFlag', 'VerifyFlag', 'ChatRoomId', 'HideInputBarFlag', - 'AttrStatus', 'SnsFlag', 'MemberCount', 'OwnerUin', 'ContactFlag', 'Uin', - 'StarFriend', 'Statues'): - friendInfoTemplate[k] = 0 -friendInfoTemplate['MemberList'] = [] - -def clear_screen(): - os.system('cls' if config.OS == 'Windows' else 'clear') - -def emoji_formatter(d, k): - ''' _emoji_deebugger is for bugs about emoji match caused by wechat backstage - like :face with tears of joy: will be replaced with :cat face with tears of joy: - ''' - def _emoji_debugger(d, k): - s = d[k].replace('') # fix missing bug - def __fix_miss_match(m): - return '' % ({ - '1f63c': '1f601', '1f639': '1f602', '1f63a': '1f603', - '1f4ab': '1f616', '1f64d': '1f614', '1f63b': '1f60d', - '1f63d': '1f618', '1f64e': '1f621', '1f63f': '1f622', - }.get(m.group(1), m.group(1))) - return emojiRegex.sub(__fix_miss_match, s) - def _emoji_formatter(m): - s = m.group(1) - if len(s) == 6: - return ('\\U%s\\U%s'%(s[:2].rjust(8, '0'), s[2:].rjust(8, '0')) - ).encode('utf8').decode('unicode-escape', 'replace') - elif len(s) == 10: - return ('\\U%s\\U%s'%(s[:5].rjust(8, '0'), s[5:].rjust(8, '0')) - ).encode('utf8').decode('unicode-escape', 'replace') - else: - return ('\\U%s'%m.group(1).rjust(8, '0') - ).encode('utf8').decode('unicode-escape', 'replace') - d[k] = _emoji_debugger(d, k) - d[k] = emojiRegex.sub(_emoji_formatter, d[k]) - -def msg_formatter(d, k): - emoji_formatter(d, k) - d[k] = d[k].replace('
', '\n') - d[k] = htmlParser.unescape(d[k]) - -def check_file(fileDir): - try: - with open(fileDir): - pass - return True - except: - return False - -def print_qr(fileDir): - if config.OS == 'Darwin': - subprocess.call(['open', fileDir]) - elif config.OS == 'Linux': - subprocess.call(['xdg-open', fileDir]) - else: - os.startfile(fileDir) - -def print_cmd_qr(qrText, white=BLOCK, black=' ', enableCmdQR=True): - blockCount = int(enableCmdQR) - if abs(blockCount) == 0: - blockCount = 1 - white *= abs(blockCount) - if blockCount < 0: - white, black = black, white - sys.stdout.write(' '*50 + '\r') - sys.stdout.flush() - qr = qrText.replace('0', white).replace('1', black) - sys.stdout.write(qr) - sys.stdout.flush() - -def struct_friend_info(knownInfo): - member = copy.deepcopy(friendInfoTemplate) - for k, v in copy.deepcopy(knownInfo).items(): member[k] = v - return member - -def search_dict_list(l, key, value): - ''' Search a list of dict - * return dict with specific value & key ''' - for i in l: - if i.get(key) == value: - return i - -def print_line(msg, oneLine = False): - if oneLine: - sys.stdout.write(' '*40 + '\r') - sys.stdout.flush() - else: - sys.stdout.write('\n') - sys.stdout.write(msg.encode(sys.stdin.encoding or 'utf8', 'replace' - ).decode(sys.stdin.encoding or 'utf8', 'replace')) - sys.stdout.flush() - -def test_connect(retryTime=5): - for i in range(retryTime): - try: - r = requests.get(config.BASE_URL) - return True - except: - if i == retryTime - 1: - logger.error(traceback.format_exc()) - return False - -def contact_deep_copy(core, contact): - with core.storageClass.updateLock: - return copy.deepcopy(contact) - -def get_image_postfix(data): - data = data[:20] - if b'GIF' in data: - return 'gif' - elif b'PNG' in data: - return 'png' - elif b'JFIF' in data: - return 'jpg' - return '' - -def update_info_dict(oldInfoDict, newInfoDict): - ''' only normal values will be updated here - because newInfoDict is normal dict, so it's not necessary to consider templates - ''' - for k, v in newInfoDict.items(): - if any((isinstance(v, t) for t in (tuple, list, dict))): - pass # these values will be updated somewhere else - elif oldInfoDict.get(k) is None or v not in (None, '', '0', 0): - oldInfoDict[k] = v From f1efacf2180f550064dc22b768d2847111e786dc Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:20:30 +0800 Subject: [PATCH 13/13] Add files via upload --- itchat/__init__.py | 69 +++++++ itchat/core.py | 461 +++++++++++++++++++++++++++++++++++++++++++++ itchat/utils.py | 153 +++++++++++++++ 3 files changed, 683 insertions(+) create mode 100644 itchat/__init__.py create mode 100644 itchat/core.py create mode 100644 itchat/utils.py diff --git a/itchat/__init__.py b/itchat/__init__.py new file mode 100644 index 00000000..a7cb466b --- /dev/null +++ b/itchat/__init__.py @@ -0,0 +1,69 @@ +from . import content +from .core import Core +from .config import VERSION +from .log import set_logging + +__version__ = VERSION + +instanceList = [] + +def new_instance(): + newInstance = Core() + instanceList.append(newInstance) + return newInstance + +originInstance = new_instance() + +# I really want to use sys.modules[__name__] = originInstance +# but it makes auto-fill a real mess, so forgive me for my following ** +# actually it toke me less than 30 seconds, god bless Uganda + +# components.login +login = originInstance.login +get_QRuuid = originInstance.get_QRuuid +get_QR = originInstance.get_QR +check_login = originInstance.check_login +web_init = originInstance.web_init +show_mobile_login = originInstance.show_mobile_login +start_receiving = originInstance.start_receiving +get_msg = originInstance.get_msg +logout = originInstance.logout +# components.contact +update_chatroom = originInstance.update_chatroom +update_friend = originInstance.update_friend +get_contact = originInstance.get_contact +get_friends = originInstance.get_friends +get_chatrooms = originInstance.get_chatrooms +get_mps = originInstance.get_mps +set_alias = originInstance.set_alias +set_pinned = originInstance.set_pinned +add_friend = originInstance.add_friend +get_head_img = originInstance.get_head_img +create_chatroom = originInstance.create_chatroom +set_chatroom_name = originInstance.set_chatroom_name +delete_member_from_chatroom = originInstance.delete_member_from_chatroom +add_member_into_chatroom = originInstance.add_member_into_chatroom +# components.messages +send_raw_msg = originInstance.send_raw_msg +send_msg = originInstance.send_msg +upload_file = originInstance.upload_file +send_file = originInstance.send_file +send_image = originInstance.send_image +send_video = originInstance.send_video +send = originInstance.send +revoke = originInstance.revoke +# components.hotreload +dump_login_status = originInstance.dump_login_status +load_login_status = originInstance.load_login_status +# components.register +auto_login = originInstance.auto_login +configured_reply = originInstance.configured_reply +msg_register = originInstance.msg_register +error_register = originInstance.error_register +run = originInstance.run +# other functions +search_friends = originInstance.search_friends +search_chatrooms = originInstance.search_chatrooms +search_mps = originInstance.search_mps +set_logging = set_logging + diff --git a/itchat/core.py b/itchat/core.py new file mode 100644 index 00000000..837ad25b --- /dev/null +++ b/itchat/core.py @@ -0,0 +1,461 @@ +import logging + +import requests + +from . import config, storage, utils, log +from .components import load_components + +class Core(object): + def __init__(self): + ''' init is the only method defined in core.py + alive is value showing whether core is running + - you should call logout method to change it + - after logout, a core object can login again + storageClass only uses basic python types + - so for advanced uses, inherit it yourself + receivingRetryCount is for receiving loop retry + - it's 5 now, but actually even 1 is enough + - failing is failing + ''' + self.alive, self.isLogging = False, False + self.storageClass = storage.Storage(self) + self.memberList = self.storageClass.memberList + self.mpList = self.storageClass.mpList + self.chatroomList = self.storageClass.chatroomList + self.msgList = self.storageClass.msgList + self.loginInfo = {} + self.s = requests.Session() + self.uuid = None + self.functionDict = {'FriendChat': {}, 'GroupChat': {}, 'MpChat': {},'Error':[]} + self.useHotReload, self.hotReloadDir = False, 'itchat.pkl' + self.receivingRetryCount = 5 + def login(self, enableCmdQR=False, picDir=None, qrCallback=None, + loginCallback=None, exitCallback=None): + ''' log in like web wechat does + for log in + - a QR code will be downloaded and opened + - then scanning status is logged, it paused for you confirm + - finally it logged in and show your nickName + for options + - enableCmdQR: show qrcode in command line + - integers can be used to fit strange char length + - picDir: place for storing qrcode + - qrCallback: method that should accept uuid, status, qrcode + - loginCallback: callback after successfully logged in + - if not set, screen is cleared and qrcode is deleted + - exitCallback: callback after logged out + - it contains calling of logout + for usage + ..code::python + + import itchat + itchat.login() + + it is defined in components/login.py + and of course every single move in login can be called outside + - you may scan source code to see how + - and modified according to your own demand + ''' + raise NotImplementedError() + def get_QRuuid(self): + ''' get uuid for qrcode + uuid is the symbol of qrcode + - for logging in, you need to get a uuid first + - for downloading qrcode, you need to pass uuid to it + - for checking login status, uuid is also required + if uuid has timed out, just get another + it is defined in components/login.py + ''' + raise NotImplementedError() + def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): + ''' download and show qrcode + for options + - uuid: if uuid is not set, latest uuid you fetched will be used + - enableCmdQR: show qrcode in cmd + - picDir: where to store qrcode + - qrCallback: method that should accept uuid, status, qrcode + it is defined in components/login.py + ''' + raise NotImplementedError() + def check_login(self, uuid=None): + ''' check login status + for options: + - uuid: if uuid is not set, latest uuid you fetched will be used + for return values: + - a string will be returned + - for meaning of return values + - 200: log in successfully + - 201: waiting for press confirm + - 408: uuid timed out + - 0 : unknown error + for processing: + - syncUrl and fileUrl is set + - BaseRequest is set + blocks until reaches any of above status + it is defined in components/login.py + ''' + raise NotImplementedError() + def web_init(self): + ''' get info necessary for initializing + for processing: + - own account info is set + - inviteStartCount is set + - syncKey is set + - part of contact is fetched + it is defined in components/login.py + ''' + raise NotImplementedError() + def show_mobile_login(self): + ''' show web wechat login sign + the sign is on the top of mobile phone wechat + sign will be added after sometime even without calling this function + it is defined in components/login.py + ''' + raise NotImplementedError() + def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): + ''' open a thread for heart loop and receiving messages + for options: + - exitCallback: callback after logged out + - it contains calling of logout + - getReceivingFnOnly: if True thread will not be created and started. Instead, receive fn will be returned. + for processing: + - messages: msgs are formatted and passed on to registered fns + - contact : chatrooms are updated when related info is received + it is defined in components/login.py + ''' + raise NotImplementedError() + def get_msg(self): + ''' fetch messages + for fetching + - method blocks for sometime util + - new messages are to be received + - or anytime they like + - synckey is updated with returned synccheckkey + it is defined in components/login.py + ''' + raise NotImplementedError() + def logout(self): + ''' logout + if core is now alive + logout will tell wechat backstage to logout + and core gets ready for another login + it is defined in components/login.py + ''' + raise NotImplementedError() + def update_chatroom(self, userName, detailedMember=False): + ''' update chatroom + for chatroom contact + - a chatroom contact need updating to be detailed + - detailed means members, encryid, etc + - auto updating of heart loop is a more detailed updating + - member uin will also be filled + - once called, updated info will be stored + for options + - userName: 'UserName' key of chatroom or a list of it + - detailedMember: whether to get members of contact + it is defined in components/contact.py + ''' + raise NotImplementedError() + def update_friend(self, userName): + ''' update chatroom + for friend contact + - once called, updated info will be stored + for options + - userName: 'UserName' key of a friend or a list of it + it is defined in components/contact.py + ''' + raise NotImplementedError() + def get_contact(self, update=False): + ''' fetch part of contact + for part + - all the massive platforms and friends are fetched + - if update, only starred chatrooms are fetched + for options + - update: if not set, local value will be returned + for results + - chatroomList will be returned + it is defined in components/contact.py + ''' + raise NotImplementedError() + def get_friends(self, update=False): + ''' fetch friends list + for options + - update: if not set, local value will be returned + for results + - a list of friends' info dicts will be returned + it is defined in components/contact.py + ''' + raise NotImplementedError() + def get_chatrooms(self, update=False, contactOnly=False): + ''' fetch chatrooms list + for options + - update: if not set, local value will be returned + - contactOnly: if set, only starred chatrooms will be returned + for results + - a list of chatrooms' info dicts will be returned + it is defined in components/contact.py + ''' + raise NotImplementedError() + def get_mps(self, update=False): + ''' fetch massive platforms list + for options + - update: if not set, local value will be returned + for results + - a list of platforms' info dicts will be returned + it is defined in components/contact.py + ''' + raise NotImplementedError() + def set_alias(self, userName, alias): + ''' set alias for a friend + for options + - userName: 'UserName' key of info dict + - alias: new alias + it is defined in components/contact.py + ''' + raise NotImplementedError() + def set_pinned(self, userName, isPinned=True): + ''' set pinned for a friend or a chatroom + for options + - userName: 'UserName' key of info dict + - isPinned: whether to pin + it is defined in components/contact.py + ''' + raise NotImplementedError() + def add_friend(self, userName, status=2, verifyContent='', autoUpdate=True): + ''' add a friend or accept a friend + for options + - userName: 'UserName' for friend's info dict + - status: + - for adding status should be 2 + - for accepting status should be 3 + - ticket: greeting message + - userInfo: friend's other info for adding into local storage + it is defined in components/contact.py + ''' + raise NotImplementedError() + def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): + ''' place for docs + for options + - if you want to get chatroom header: only set chatroomUserName + - if you want to get friend header: only set userName + - if you want to get chatroom member header: set both + it is defined in components/contact.py + ''' + raise NotImplementedError() + def create_chatroom(self, memberList, topic=''): + ''' create a chatroom + for creating + - its calling frequency is strictly limited + for options + - memberList: list of member info dict + - topic: topic of new chatroom + it is defined in components/contact.py + ''' + raise NotImplementedError() + def set_chatroom_name(self, chatroomUserName, name): + ''' set chatroom name + for setting + - it makes an updating of chatroom + - which means detailed info will be returned in heart loop + for options + - chatroomUserName: 'UserName' key of chatroom info dict + - name: new chatroom name + it is defined in components/contact.py + ''' + raise NotImplementedError() + def delete_member_from_chatroom(self, chatroomUserName, memberList): + ''' deletes members from chatroom + for deleting + - you can't delete yourself + - if so, no one will be deleted + - strict-limited frequency + for options + - chatroomUserName: 'UserName' key of chatroom info dict + - memberList: list of members' info dict + it is defined in components/contact.py + ''' + raise NotImplementedError() + def add_member_into_chatroom(self, chatroomUserName, memberList, + useInvitation=False): + ''' add members into chatroom + for adding + - you can't add yourself or member already in chatroom + - if so, no one will be added + - if member will over 40 after adding, invitation must be used + - strict-limited frequency + for options + - chatroomUserName: 'UserName' key of chatroom info dict + - memberList: list of members' info dict + - useInvitation: if invitation is not required, set this to use + it is defined in components/contact.py + ''' + raise NotImplementedError() + def send_raw_msg(self, msgType, content, toUserName): + ''' many messages are sent in a common way + for demo + .. code:: python + + @itchat.msg_register(itchat.content.CARD) + def reply(msg): + itchat.send_raw_msg(msg['MsgType'], msg['Content'], msg['FromUserName']) + + there are some little tricks here, you may discover them yourself + but remember they are tricks + it is defined in components/messages.py + ''' + raise NotImplementedError() + def send_msg(self, msg='Test Message', toUserName=None): + ''' send plain text message + for options + - msg: should be unicode if there's non-ascii words in msg + - toUserName: 'UserName' key of friend dict + it is defined in components/messages.py + ''' + raise NotImplementedError() + def upload_file(self, fileDir, isPicture=False, isVideo=False, + toUserName='filehelper', file_=None, preparedFile=None): + ''' upload file to server and get mediaId + for options + - fileDir: dir for file ready for upload + - isPicture: whether file is a picture + - isVideo: whether file is a video + for return values + will return a ReturnValue + if succeeded, mediaId is in r['MediaId'] + it is defined in components/messages.py + ''' + raise NotImplementedError() + def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None): + ''' send attachment + for options + - fileDir: dir for file ready for upload + - mediaId: mediaId for file. + - if set, file will not be uploaded twice + - toUserName: 'UserName' key of friend dict + it is defined in components/messages.py + ''' + raise NotImplementedError() + def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None): + ''' send image + for options + - fileDir: dir for file ready for upload + - if it's a gif, name it like 'xx.gif' + - mediaId: mediaId for file. + - if set, file will not be uploaded twice + - toUserName: 'UserName' key of friend dict + it is defined in components/messages.py + ''' + raise NotImplementedError() + def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None): + ''' send video + for options + - fileDir: dir for file ready for upload + - if mediaId is set, it's unnecessary to set fileDir + - mediaId: mediaId for file. + - if set, file will not be uploaded twice + - toUserName: 'UserName' key of friend dict + it is defined in components/messages.py + ''' + raise NotImplementedError() + def send(self, msg, toUserName=None, mediaId=None): + ''' wrapped function for all the sending functions + for options + - msg: message starts with different string indicates different type + - list of type string: ['@fil@', '@img@', '@msg@', '@vid@'] + - they are for file, image, plain text, video + - if none of them matches, it will be sent like plain text + - toUserName: 'UserName' key of friend dict + - mediaId: if set, uploading will not be repeated + it is defined in components/messages.py + ''' + raise NotImplementedError() + def revoke(self, msgId, toUserName, localId=None): + ''' revoke message with its and msgId + for options + - msgId: message Id on server + - toUserName: 'UserName' key of friend dict + - localId: message Id at local (optional) + it is defined in components/messages.py + ''' + raise NotImplementedError() + def dump_login_status(self, fileDir=None): + ''' dump login status to a specific file + for option + - fileDir: dir for dumping login status + it is defined in components/hotreload.py + ''' + raise NotImplementedError() + def load_login_status(self, fileDir, + loginCallback=None, exitCallback=None): + ''' load login status from a specific file + for option + - fileDir: file for loading login status + - loginCallback: callback after successfully logged in + - if not set, screen is cleared and qrcode is deleted + - exitCallback: callback after logged out + - it contains calling of logout + it is defined in components/hotreload.py + ''' + raise NotImplementedError() + def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', + enableCmdQR=False, picDir=None, qrCallback=None, + loginCallback=None, exitCallback=None): + ''' log in like web wechat does + for log in + - a QR code will be downloaded and opened + - then scanning status is logged, it paused for you confirm + - finally it logged in and show your nickName + for options + - hotReload: enable hot reload + - statusStorageDir: dir for storing log in status + - enableCmdQR: show qrcode in command line + - integers can be used to fit strange char length + - picDir: place for storing qrcode + - loginCallback: callback after successfully logged in + - if not set, screen is cleared and qrcode is deleted + - exitCallback: callback after logged out + - it contains calling of logout + - qrCallback: method that should accept uuid, status, qrcode + for usage + ..code::python + + import itchat + itchat.auto_login() + + it is defined in components/register.py + and of course every single move in login can be called outside + - you may scan source code to see how + - and modified according to your own demond + ''' + raise NotImplementedError() + def configured_reply(self): + ''' determine the type of message and reply if its method is defined + however, I use a strange way to determine whether a msg is from massive platform + I haven't found a better solution here + The main problem I'm worrying about is the mismatching of new friends added on phone + If you have any good idea, pleeeease report an issue. I will be more than grateful. + ''' + raise NotImplementedError() + def msg_register(self, msgType, + isFriendChat=False, isGroupChat=False, isMpChat=False): + ''' a decorator constructor + return a specific decorator based on information given + ''' + raise NotImplementedError() + def run(self, debug=True, blockThread=True): + ''' start auto respond + for option + - debug: if set, debug info will be shown on screen + it is defined in components/register.py + ''' + raise NotImplementedError() + def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, + wechatAccount=None): + return self.storageClass.search_friends(name, userName, remarkName, + nickName, wechatAccount) + def search_chatrooms(self, name=None, userName=None): + return self.storageClass.search_chatrooms(name, userName) + def search_mps(self, name=None, userName=None): + return self.storageClass.search_mps(name, userName) +requests.adapters.DEFAULT_RETRIES = 10 +load_components(Core) diff --git a/itchat/utils.py b/itchat/utils.py new file mode 100644 index 00000000..8b46540e --- /dev/null +++ b/itchat/utils.py @@ -0,0 +1,153 @@ +import re, os, sys, subprocess, copy, traceback, logging + +try: + from HTMLParser import HTMLParser +except ImportError: + from html.parser import HTMLParser + +import requests + +from . import config + +logger = logging.getLogger('itchat') + +emojiRegex = re.compile(r'') +htmlParser = HTMLParser() +try: + b = u'\u2588' + sys.stdout.write(b + '\r') + sys.stdout.flush() +except UnicodeEncodeError: + BLOCK = 'MM' +else: + BLOCK = b +friendInfoTemplate = {} +for k in ('UserName', 'City', 'DisplayName', 'PYQuanPin', 'RemarkPYInitial', 'Province', + 'KeyWord', 'RemarkName', 'PYInitial', 'EncryChatRoomId', 'Alias', 'Signature', + 'NickName', 'RemarkPYQuanPin', 'HeadImgUrl'): + friendInfoTemplate[k] = '' +for k in ('UniFriend', 'Sex', 'AppAccountFlag', 'VerifyFlag', 'ChatRoomId', 'HideInputBarFlag', + 'AttrStatus', 'SnsFlag', 'MemberCount', 'OwnerUin', 'ContactFlag', 'Uin', + 'StarFriend', 'Statues'): + friendInfoTemplate[k] = 0 +friendInfoTemplate['MemberList'] = [] + +def clear_screen(): + os.system('cls' if config.OS == 'Windows' else 'clear') + +def emoji_formatter(d, k): + ''' _emoji_deebugger is for bugs about emoji match caused by wechat backstage + like :face with tears of joy: will be replaced with :cat face with tears of joy: + ''' + def _emoji_debugger(d, k): + s = d[k].replace('') # fix missing bug + def __fix_miss_match(m): + return '' % ({ + '1f63c': '1f601', '1f639': '1f602', '1f63a': '1f603', + '1f4ab': '1f616', '1f64d': '1f614', '1f63b': '1f60d', + '1f63d': '1f618', '1f64e': '1f621', '1f63f': '1f622', + }.get(m.group(1), m.group(1))) + return emojiRegex.sub(__fix_miss_match, s) + def _emoji_formatter(m): + s = m.group(1) + if len(s) == 6: + return ('\\U%s\\U%s'%(s[:2].rjust(8, '0'), s[2:].rjust(8, '0')) + ).encode('utf8').decode('unicode-escape', 'replace') + elif len(s) == 10: + return ('\\U%s\\U%s'%(s[:5].rjust(8, '0'), s[5:].rjust(8, '0')) + ).encode('utf8').decode('unicode-escape', 'replace') + else: + return ('\\U%s'%m.group(1).rjust(8, '0') + ).encode('utf8').decode('unicode-escape', 'replace') + d[k] = _emoji_debugger(d, k) + d[k] = emojiRegex.sub(_emoji_formatter, d[k]) + +def msg_formatter(d, k): + emoji_formatter(d, k) + d[k] = d[k].replace('
', '\n') + d[k] = htmlParser.unescape(d[k]) + +def check_file(fileDir): + try: + with open(fileDir): + pass + return True + except: + return False + +def print_qr(fileDir): + if config.OS == 'Darwin': + subprocess.call(['open', fileDir]) + elif config.OS == 'Linux': + subprocess.call(['xdg-open', fileDir]) + else: + os.startfile(fileDir) + +def print_cmd_qr(qrText, white=BLOCK, black=' ', enableCmdQR=True): + blockCount = int(enableCmdQR) + if abs(blockCount) == 0: + blockCount = 1 + white *= abs(blockCount) + if blockCount < 0: + white, black = black, white + sys.stdout.write(' '*50 + '\r') + sys.stdout.flush() + qr = qrText.replace('0', white).replace('1', black) + sys.stdout.write(qr) + sys.stdout.flush() + +def struct_friend_info(knownInfo): + member = copy.deepcopy(friendInfoTemplate) + for k, v in copy.deepcopy(knownInfo).items(): member[k] = v + return member + +def search_dict_list(l, key, value): + ''' Search a list of dict + * return dict with specific value & key ''' + for i in l: + if i.get(key) == value: + return i + +def print_line(msg, oneLine = False): + if oneLine: + sys.stdout.write(' '*40 + '\r') + sys.stdout.flush() + else: + sys.stdout.write('\n') + sys.stdout.write(msg.encode(sys.stdin.encoding or 'utf8', 'replace' + ).decode(sys.stdin.encoding or 'utf8', 'replace')) + sys.stdout.flush() + +def test_connect(retryTime=15): + for i in range(retryTime): + try: + r = requests.get(config.BASE_URL) + return True + except: + if i == retryTime - 1: + return traceback.format_exc() + +def contact_deep_copy(core, contact): + with core.storageClass.updateLock: + return copy.deepcopy(contact) + +def get_image_postfix(data): + data = data[:20] + if b'GIF' in data: + return 'gif' + elif b'PNG' in data: + return 'png' + elif b'JFIF' in data: + return 'jpg' + return '' + +def update_info_dict(oldInfoDict, newInfoDict): + ''' only normal values will be updated here + because newInfoDict is normal dict, so it's not necessary to consider templates + ''' + for k, v in newInfoDict.items(): + if any((isinstance(v, t) for t in (tuple, list, dict))): + pass # these values will be updated somewhere else + elif oldInfoDict.get(k) is None or v not in (None, '', '0', 0): + oldInfoDict[k] = v