Repository: Urinx/WeixinBot Branch: master Commit: d9edcd2c9203 Files: 28 Total size: 197.5 KB Directory structure: gitextract_stqn26qf/ ├── .gitignore ├── LICENSE ├── README.md ├── wxbot_demo_py3/ │ ├── requirements.txt │ └── weixin.py └── wxbot_project_py2.7/ ├── README.md ├── config/ │ ├── __init__.py │ ├── config_manager.py │ ├── constant.py │ ├── log.py │ ├── requirements.txt │ └── wechat.conf.bak ├── db/ │ ├── __init__.py │ ├── mysql_db.py │ └── sqlite_db.py ├── docker/ │ ├── Dockerfile │ └── README.md ├── flask_templates/ │ ├── index.html │ └── upload.html ├── wechat/ │ ├── __init__.py │ ├── utils.py │ ├── wechat.py │ └── wechat_apis.py ├── weixin_bot.py └── wx_handler/ ├── __init__.py ├── bot.py ├── sendgrid_mail.py └── wechat_msg_processor.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # pycharm .idea *.pyc wxbot_demo_py3/saved/ wxbot_project_py2.7/tmp_data/ wxbot_project_py2.7/wechat/wechat_js_backup/ wxbot_project_py2.7/config/wechat.conf wxbot_project_py2.7/wiki/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # WeixinBot [![star this repo](http://github-svg-buttons.herokuapp.com/star.svg?user=Urinx&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/Urinx/WeixinBot) [![fork this repo](http://github-svg-buttons.herokuapp.com/fork.svg?user=Urinx&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/Urinx/WeixinBot/fork) ![python](https://img.shields.io/badge/python-2.7%20&%203.6-ff69b4.svg) 网页版微信API,包含终端版微信及微信机器人 ## Contents * [Demo](#Demo) * [Web Weixin Pipeline](#Web-Weixin-Pipeline) * [Web Weixin API](#Web-Weixin-API) * [Discussion Group](#Discussion-Group) * [Recent Update](#Recent-Update) ## Demo 为了确保能正常运行示例脚本,请安装所需的第三方包。 ``` pip install -r requirements.txt ``` 注:下面演示的图片与功能可能不是最新的,具体请看源码。
按照操作指示在手机微信上扫描二维码然后登录,你可以选择是否开启自动回复模式。 ![2](imgs/2.png) 开启自动回复模式后,如果接收到的是文字消息就会自动回复,包括群消息。 ![3](imgs/3.png) 名片,链接,动画表情和地址位置消息。 ![4](imgs/4.png) ![5](imgs/5.png) 网页版上有的功能目前基本上都能支持。 ## Web Weixin Pipeline ``` +--------------+ +---------------+ +---------------+ | | | | | | | Get UUID | | Get Contact | | Status Notify | | | | | | | +-------+------+ +-------^-------+ +-------^-------+ | | | | +-------+ +--------+ | | | +-------v------+ +-----+--+------+ +--------------+ | | | | | | | Get QRCode | | Weixin Init +------> Sync Check <----+ | | | | | | | +-------+------+ +-------^-------+ +-------+------+ | | | | | | | +-----------+ | | | +-------v------+ +-------+--------+ +-------v-------+ | | Confirm Login | | | | +------> Login +---------------> New Login Page | | Weixin Sync | | | | | | | | | +------+-------+ +----------------+ +---------------+ | | |QRCode Scaned| +-------------+ ``` ## Web Weixin API ### 登录 | API | 获取 UUID | | --- | --------- | | url | https://login.weixin.qq.com/jslogin | | method | POST | | data | URL Encode | | params | **appid**: `应用ID`
**fun**: new `应用类型`
**lang**: zh\_CN `语言`
**_**: `时间戳` | 返回数据(String): ``` window.QRLogin.code = 200; window.QRLogin.uuid = "xxx" ``` > 注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是`wx782c26e4c19acffb`,在微信客户端上显示为应用名称为`Web微信`;现在用的是`wxeb7ec651dd0aefa9`,显示名称为`微信网页版`。

| API | 绑定登陆(webwxpushloginurl) | | --- | --------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxpushloginurl | | method | GET | | params | **uin**: xxx | 返回数据(String): ``` {'msg': 'all ok', 'uuid': 'xxx', 'ret': '0'} 通过这种方式可以省掉扫二维码这步操作,更加方便 ```
| API | 生成二维码 | | --- | --------- | | url | https://login.weixin.qq.com/l/ `uuid` | | method | GET |
| API | 二维码扫描登录 | | --- | --------- | | url | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login | | method | GET | | params | **tip**: 1 `未扫描` 0 `已扫描`
**uuid**: xxx
**_**: `时间戳` | 返回数据(String): ``` window.code=xxx; xxx: 408 登陆超时 201 扫描成功 200 确认登录 当返回200时,还会有 window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx"; ```
| API | webwxnewloginpage | | --- | --------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage | | method | GET | | params | **ticket**: xxx
**uuid**: xxx
**lang**: zh_CN `语言`
**scan**: xxx
**fun**: new | 返回数据(XML): ``` 0 OK xxx xxx xxx xxx 1 ```
### 微信初始化 | API | webwxinit | | --- | --------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket=xxx&skey=xxx&r=xxx | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | | params | {
     BaseRequest: {
         Uin: xxx,
         Sid: xxx,
         Skey: xxx,
         DeviceID: xxx,
     }
} | 返回数据(JSON): ``` { "BaseResponse": { "Ret": 0, "ErrMsg": "" }, "Count": 11, "ContactList": [...], "SyncKey": { "Count": 4, "List": [ { "Key": 1, "Val": 635705559 }, ... ] }, "User": { "Uin": xxx, "UserName": xxx, "NickName": xxx, "HeadImgUrl": xxx, "RemarkName": "", "PYInitial": "", "PYQuanPin": "", "RemarkPYInitial": "", "RemarkPYQuanPin": "", "HideInputBarFlag": 0, "StarFriend": 0, "Sex": 1, "Signature": "Apt-get install B", "AppAccountFlag": 0, "VerifyFlag": 0, "ContactFlag": 0, "WebWxPluginSwitch": 0, "HeadImgFlag": 1, "SnsFlag": 17 }, "ChatSet": xxx, "SKey": xxx, "ClientVersion": 369297683, "SystemTime": 1453124908, "GrayScale": 1, "InviteStartCount": 40, "MPSubscribeMsgCount": 2, "MPSubscribeMsgList": [...], "ClickReportInterval": 600000 } ```
| API | webwxstatusnotify | | --- | --------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify?lang=zh_CN&pass_ticket=xxx | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Code: 3,
     FromUserName: `自己ID`,
     ToUserName: `自己ID`,
     ClientMsgId: `时间戳`
} | 返回数据(JSON): ``` { "BaseResponse": { "Ret": 0, "ErrMsg": "" }, ... } ```
### 获取联系人信息 | API | webwxgetcontact | | --- | --------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin//webwxgetcontact?pass_ticket=xxx&skey=xxx&r=xxx | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | 返回数据(JSON): ``` { "BaseResponse": { "Ret": 0, "ErrMsg": "" }, "MemberCount": 334, "MemberList": [ { "Uin": 0, "UserName": xxx, "NickName": "Urinx", "HeadImgUrl": xxx, "ContactFlag": 3, "MemberCount": 0, "MemberList": [], "RemarkName": "", "HideInputBarFlag": 0, "Sex": 0, "Signature": "你好,我们是地球三体组织。在这里,你将感受到不一样的思维模式,以及颠覆常规的世界观。而我们的目标,就是以三体人的智慧,引领人类未来科学技术500年。", "VerifyFlag": 8, "OwnerUin": 0, "PYInitial": "URINX", "PYQuanPin": "Urinx", "RemarkPYInitial": "", "RemarkPYQuanPin": "", "StarFriend": 0, "AppAccountFlag": 0, "Statues": 0, "AttrStatus": 0, "Province": "", "City": "", "Alias": "Urinxs", "SnsFlag": 0, "UniFriend": 0, "DisplayName": "", "ChatRoomId": 0, "KeyWord": "gh_", "EncryChatRoomId": "" }, ... ], "Seq": 0 } ```
| API | webwxbatchgetcontact | | --- | --------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=xxx&pass_ticket=xxx | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Count: `群数量`,
     List: [
         { UserName: `群ID`, EncryChatRoomId: "" },
         ...
     ],
} | 返回数据(JSON)同上

### 同步刷新 | API | synccheck | | --- | --------- | | protocol | https | | host | webpush.weixin.qq.com
webpush.wx2.qq.com
webpush.wx8.qq.com
webpush.wx.qq.com
webpush.web2.wechat.com
webpush.web.wechat.com | | path | /cgi-bin/mmwebwx-bin/synccheck | | method | GET | | data | URL Encode | | params | **r**: `时间戳`
**sid**: xxx
**uin**: xxx
**skey**: xxx
**deviceid**: xxx
**synckey**: xxx
**_**: `时间戳` | 返回数据(String): ``` window.synccheck={retcode:"xxx",selector:"xxx"} retcode: 0 正常 1100 失败/登出微信 selector: 0 正常 2 新的消息 7 进入/离开聊天界面 ```
| API | webwxsync | | --- | --------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=xxx&skey=xxx&pass_ticket=xxx | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     SyncKey: xxx,
     rr: `时间戳取反`
} | 返回数据(JSON): ``` { 'BaseResponse': {'ErrMsg': '', 'Ret': 0}, 'SyncKey': { 'Count': 7, 'List': [ {'Val': 636214192, 'Key': 1}, ... ] }, 'ContinueFlag': 0, 'AddMsgCount': 1, 'AddMsgList': [ { 'FromUserName': '', 'PlayLength': 0, 'RecommendInfo': {...}, 'Content': "", 'StatusNotifyUserName': '', 'StatusNotifyCode': 5, 'Status': 3, 'VoiceLength': 0, 'ToUserName': '', 'ForwardFlag': 0, 'AppMsgType': 0, 'AppInfo': {'Type': 0, 'AppID': ''}, 'Url': '', 'ImgStatus': 1, 'MsgType': 51, 'ImgHeight': 0, 'MediaId': '', 'FileName': '', 'FileSize': '', ... }, ... ], 'ModChatRoomMemberCount': 0, 'ModContactList': [], 'DelContactList': [], 'ModChatRoomMemberList': [], 'DelContactCount': 0, ... } ```
### 消息接口 | API | webwxsendmsg | | --- | ------------ | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=xxx | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Msg: {
         Type: 1 `文字消息`,
         Content: `要发送的消息`,
         FromUserName: `自己ID`,
         ToUserName: `好友ID`,
         LocalID: `与clientMsgId相同`,
         ClientMsgId: `时间戳左移4位随后补上4位随机数`
     }
} | 返回数据(JSON): ``` { "BaseResponse": { "Ret": 0, "ErrMsg": "" }, ... } ``` | API | webwxrevokemsg | | --- | ------------ | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     SvrMsgId: msg_id,
     ToUserName: user_id,
     ClientMsgId: local_msg_id
} | 返回数据(JSON): ``` { "BaseResponse": { "Ret": 0, "ErrMsg": "" } } ``` #### 发送表情 | API | webwxsendmsgemotion | | --- | ------------ | | url | https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=xxx | | method | POST | | data | JSON | | header | ContentType: application/json; charset=UTF-8 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Msg: {
         Type: 47 `emoji消息`,
         EmojiFlag: 2,
         MediaId: `表情上传后的媒体ID`,
         FromUserName: `自己ID`,
         ToUserName: `好友ID`,
         LocalID: `与clientMsgId相同`,
         ClientMsgId: `时间戳左移4位随后补上4位随机数`
     }
} |
### 图片接口 | API | webwxgeticon | | --- | ------------ | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgeticon | | method | GET | | params | **seq**: `数字,可为空`
**username**: `ID`
**skey**: xxx |
| API | webwxgetheadimg | | --- | --------------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetheadimg | | method | GET | | params | **seq**: `数字,可为空`
**username**: `群ID`
**skey**: xxx |
| API | webwxgetmsgimg | | --- | --------------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg | | method | GET | | params | **MsgID**: `消息ID`
**type**: slave `略缩图` or `为空时加载原图`
**skey**: xxx |
### 多媒体接口 | API | webwxgetvideo | | --- | --------------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo | | method | GET | | params | **msgid**: `消息ID`
**skey**: xxx |
| API | webwxgetvoice | | --- | --------------- | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice | | method | GET | | params | **msgid**: `消息ID`
**skey**: xxx |
### 账号类型 | 类型 | 说明 | | :--: | --- | | 个人账号 | 以`@`开头,例如:`@xxx` | | 群聊 | 以`@@`开头,例如:`@@xxx` | | 公众号/服务号 | 以`@`开头,但其`VerifyFlag` & 8 != 0

`VerifyFlag`:
         一般个人公众号/服务号:8
         一般企业的服务号:24
         微信官方账号`微信团队`:56 | | 特殊账号 | 像文件传输助手之类的账号,有特殊的ID,目前已知的有:
`filehelper`, `newsapp`, `fmessage`, `weibo`, `qqmail`, `tmessage`, `qmessage`, `qqsync`, `floatbottle`, `lbsapp`, `shakeapp`, `medianote`, `qqfriend`, `readerapp`, `blogapp`, `facebookapp`, `masssendapp`, `meishiapp`, `feedsapp`, `voip`, `blogappweixin`, `weixin`, `brandsessionholder`, `weixinreminder`, `officialaccounts`, `notification_messages`, `wxitil`, `userexperience_alarm`, `notification_messages` |
### 消息类型 消息一般格式: ``` { "FromUserName": "", "ToUserName": "", "Content": "", "StatusNotifyUserName": "", "ImgWidth": 0, "PlayLength": 0, "RecommendInfo": {...}, "StatusNotifyCode": 4, "NewMsgId": "", "Status": 3, "VoiceLength": 0, "ForwardFlag": 0, "AppMsgType": 0, "Ticket": "", "AppInfo": {...}, "Url": "", "ImgStatus": 1, "MsgType": 1, "ImgHeight": 0, "MediaId": "", "MsgId": "", "FileName": "", "HasProductId": 0, "FileSize": "", "CreateTime": 1454602196, "SubMsgType": 0 } ```
| MsgType | 说明 | | ------- | --- | | 1 | 文本消息 | | 3 | 图片消息 | | 34 | 语音消息 | | 37 | 好友确认消息 | | 40 | POSSIBLEFRIEND_MSG | | 42 | 共享名片 | | 43 | 视频消息 | | 47 | 动画表情 | | 48 | 位置消息 | | 49 | 分享链接 | | 50 | VOIPMSG | | 51 | 微信初始化消息 | | 52 | VOIPNOTIFY | | 53 | VOIPINVITE | | 62 | 小视频 | | 9999 | SYSNOTICE | | 10000 | 系统消息 | | 10002 | 撤回消息 |
**微信初始化消息** ```html MsgType: 51 FromUserName: 自己ID ToUserName: 自己ID StatusNotifyUserName: 最近联系的联系人ID Content: // 最近联系的联系人 filehelper,xxx@chatroom,wxid_xxx,xxx,... // 朋友圈 MomentsUnreadMsgStatus 1454502365 // 未读的功能账号消息,群发助手,漂流瓶等 ``` **文本消息** ``` MsgType: 1 FromUserName: 发送方ID ToUserName: 接收方ID Content: 消息内容 ``` **图片消息** ```html MsgType: 3 FromUserName: 发送方ID ToUserName: 接收方ID MsgId: 用于获取图片 Content: ``` **小视频消息** ```html MsgType: 62 FromUserName: 发送方ID ToUserName: 接收方ID MsgId: 用于获取小视频 Content: ``` **地理位置消息** ``` MsgType: 1 FromUserName: 发送方ID ToUserName: 接收方ID Content: http://weixin.qq.com/cgi-bin/redirectforward?args=xxx // 属于文本消息,只不过内容是一个跳转到地图的链接 ``` **名片消息** ```js MsgType: 42 FromUserName: 发送方ID ToUserName: 接收方ID Content: RecommendInfo: { "UserName": "xxx", // ID "Province": "xxx", "City": "xxx", "Scene": 17, "QQNum": 0, "Content": "", "Alias": "xxx", // 微信号 "OpCode": 0, "Signature": "", "Ticket": "", "Sex": 0, // 1:男, 2:女 "NickName": "xxx", // 昵称 "AttrStatus": 4293221, "VerifyFlag": 0 } ``` **语音消息** ```html MsgType: 34 FromUserName: 发送方ID ToUserName: 接收方ID MsgId: 用于获取语音 Content: ``` **动画表情** ```html MsgType: 47 FromUserName: 发送方ID ToUserName: 接收方ID Content: ``` **普通链接或应用分享消息** ```html MsgType: 49 AppMsgType: 5 FromUserName: 发送方ID ToUserName: 接收方ID Url: 链接地址 FileName: 链接标题 Content: 5 ... ``` **音乐链接消息** ```html MsgType: 49 AppMsgType: 3 FromUserName: 发送方ID ToUserName: 接收方ID Url: 链接地址 FileName: 音乐名 AppInfo: // 分享链接的应用 { Type: 0, AppID: wx485a97c844086dc9 } Content: 3 0 0 http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46 http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46 0 http://imgcache.qq.com/music/photo/album/63/180_albumpic_143163_0.jpg 0 29 摇一摇搜歌 ``` **群消息** ``` MsgType: 1 FromUserName: @@xxx ToUserName: @xxx Content: @xxx:
xxx ``` **红包消息** ``` MsgType: 49 AppMsgType: 2001 FromUserName: 发送方ID ToUserName: 接收方ID Content: 未知 ``` 注:根据网页版的代码可以看到未来可能支持查看红包消息,但目前走的是系统消息,见下。 **系统消息** ``` MsgType: 10000 FromUserName: 发送方ID ToUserName: 自己ID Content: "你已添加了 xxx ,现在可以开始聊天了。" "如果陌生人主动添加你为朋友,请谨慎核实对方身份。" "收到红包,请在手机上查看" ``` ## Discussion Group 如果你希望和 WeixinBot 的其他开发者交流,或者有什么问题和建议,欢迎大家加入微信群【Youth fed the dog】一起讨论。扫描下面的二维码添加机器人为好友,并回复【Aidog】获取入群链接。
join us
注:这个不是群的二维码,是机器人拉你入群,记得回复机器人【Aidog】哦~ (secret code: Aidog) ## Recent Update - association_login 目前网页版微信已经可以脱离扫码,但是依然需要在客户端进行确认登录。 ================================================ FILE: wxbot_demo_py3/requirements.txt ================================================ colorama coloredlogs humanfriendly lxml qrcode requests six requests_toolbelt pyqrcode certifi ================================================ FILE: wxbot_demo_py3/weixin.py ================================================ #!/usr/bin/env python # coding: utf-8 import qrcode from pyqrcode import QRCode import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse import http.cookiejar import requests import xml.dom.minidom import json import time import ssl import re import sys import os import subprocess import random import multiprocessing import platform import logging import http.client from collections import defaultdict from urllib.parse import urlparse from lxml import html from socket import timeout as timeout_error #import pdb # for media upload import mimetypes from requests_toolbelt.multipart.encoder import MultipartEncoder def catchKeyboardInterrupt(fn): def wrapper(*args): try: return fn(*args) except KeyboardInterrupt: print('\n[*] 强制退出程序') logging.debug('[*] 强制退出程序') return wrapper def _decode_list(data): rv = [] for item in data: if isinstance(item, str): item = item.encode('utf-8') elif isinstance(item, list): item = _decode_list(item) elif isinstance(item, dict): item = _decode_dict(item) rv.append(item) return rv def _decode_dict(data): rv = {} for key, value in data.items(): if isinstance(key, str): key = key.encode('utf-8') if isinstance(value, str): value = value.encode('utf-8') elif isinstance(value, list): value = _decode_list(value) elif isinstance(value, dict): value = _decode_dict(value) rv[key] = value return rv class WebWeixin(object): def __str__(self): description = \ "=========================\n" + \ "[#] Web Weixin\n" + \ "[#] Debug Mode: " + str(self.DEBUG) + "\n" + \ "[#] Uuid: " + self.uuid + "\n" + \ "[#] Uin: " + str(self.uin) + "\n" + \ "[#] Sid: " + self.sid + "\n" + \ "[#] Skey: " + self.skey + "\n" + \ "[#] DeviceId: " + self.deviceId + "\n" + \ "[#] PassTicket: " + self.pass_ticket + "\n" + \ "=========================" return description def __init__(self): self.DEBUG = False self.commandLineQRCode = False self.uuid = '' self.base_uri = '' self.redirect_uri = '' self.uin = '' self.sid = '' self.skey = '' self.pass_ticket = '' self.deviceId = 'e' + repr(random.random())[2:17] self.BaseRequest = {} self.synckey = '' self.SyncKey = [] self.User = [] self.MemberList = [] self.ContactList = [] # 好友 self.GroupList = [] # 群 self.GroupMemeberList = [] # 群友 self.PublicUsersList = [] # 公众号/服务号 self.SpecialUsersList = [] # 特殊账号 self.autoReplyMode = False self.syncHost = '' self.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36' self.interactive = False self.autoOpen = False self.saveFolder = os.path.join(os.getcwd(), 'saved') self.saveSubFolders = {'webwxgeticon': 'icons', 'webwxgetheadimg': 'headimgs', 'webwxgetmsgimg': 'msgimgs', 'webwxgetvideo': 'videos', 'webwxgetvoice': 'voices', '_showQRCodeImg': 'qrcodes'} self.appid = 'wx782c26e4c19acffb' self.lang = 'zh_CN' self.lastCheckTs = time.time() self.memberCount = 0 self.SpecialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'] self.TimeOut = 20 # 同步最短时间间隔(单位:秒) self.media_count = -1 self.cookie = http.cookiejar.CookieJar() opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookie)) opener.addheaders = [('User-agent', self.user_agent)] urllib.request.install_opener(opener) def loadConfig(self, config): if config['DEBUG']: self.DEBUG = config['DEBUG'] if config['autoReplyMode']: self.autoReplyMode = config['autoReplyMode'] if config['user_agent']: self.user_agent = config['user_agent'] if config['interactive']: self.interactive = config['interactive'] if config['autoOpen']: self.autoOpen = config['autoOpen'] def getUUID(self): url = 'https://login.weixin.qq.com/jslogin' params = { 'appid': self.appid, 'fun': 'new', 'lang': self.lang, '_': int(time.time()), } #r = requests.get(url=url, params=params) #r.encoding = 'utf-8' #data = r.text data = self._post(url, params, False).decode("utf-8") if data == '': return False regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' pm = re.search(regx, data) if pm: code = pm.group(1) self.uuid = pm.group(2) return code == '200' return False def genQRCode(self): #return self._showQRCodeImg() if sys.platform.startswith('win'): self._showQRCodeImg('win') elif sys.platform.find('darwin') >= 0: self._showQRCodeImg('macos') else: self._str2qr('https://login.weixin.qq.com/l/' + self.uuid) def _showQRCodeImg(self, str): if self.commandLineQRCode: qrCode = QRCode('https://login.weixin.qq.com/l/' + self.uuid) self._showCommandLineQRCode(qrCode.text(1)) else: url = 'https://login.weixin.qq.com/qrcode/' + self.uuid params = { 't': 'webwx', '_': int(time.time()) } data = self._post(url, params, False) if data == '': return QRCODE_PATH = self._saveFile('qrcode.jpg', data, '_showQRCodeImg') if str == 'win': os.startfile(QRCODE_PATH) elif str == 'macos': subprocess.call(["open", QRCODE_PATH]) else: return def _showCommandLineQRCode(self, qr_data, enableCmdQR=2): try: b = u'\u2588' sys.stdout.write(b + '\r') sys.stdout.flush() except UnicodeEncodeError: white = 'MM' else: white = b black = ' ' 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 = qr_data.replace('0', white).replace('1', black) sys.stdout.write(qr) sys.stdout.flush() def waitForLogin(self, tip=1): time.sleep(tip) url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % ( tip, self.uuid, int(time.time())) data = self._get(url) if data == '': return False pm = re.search(r"window.code=(\d+);", data) code = pm.group(1) if code == '201': return True elif code == '200': pm = re.search(r'window.redirect_uri="(\S+?)";', data) r_uri = pm.group(1) + '&fun=new' self.redirect_uri = r_uri self.base_uri = r_uri[:r_uri.rfind('/')] return True elif code == '408': self._echo('[登陆超时] \n') else: self._echo('[登陆异常] \n') return False def login(self): data = self._get(self.redirect_uri) if data == '': return False doc = xml.dom.minidom.parseString(data) root = doc.documentElement for node in root.childNodes: if node.nodeName == 'skey': self.skey = node.childNodes[0].data elif node.nodeName == 'wxsid': self.sid = node.childNodes[0].data elif node.nodeName == 'wxuin': self.uin = node.childNodes[0].data elif node.nodeName == 'pass_ticket': self.pass_ticket = node.childNodes[0].data if '' in (self.skey, self.sid, self.uin, self.pass_ticket): return False self.BaseRequest = { 'Uin': int(self.uin), 'Sid': self.sid, 'Skey': self.skey, 'DeviceID': self.deviceId, } return True def webwxinit(self): url = self.base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % ( self.pass_ticket, self.skey, int(time.time())) params = { 'BaseRequest': self.BaseRequest } dic = self._post(url, params) if dic == '': return False self.SyncKey = dic['SyncKey'] self.User = dic['User'] # synckey for synccheck self.synckey = '|'.join( [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) return dic['BaseResponse']['Ret'] == 0 def webwxstatusnotify(self): url = self.base_uri + \ '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket) params = { 'BaseRequest': self.BaseRequest, "Code": 3, "FromUserName": self.User['UserName'], "ToUserName": self.User['UserName'], "ClientMsgId": int(time.time()) } dic = self._post(url, params) if dic == '': return False return dic['BaseResponse']['Ret'] == 0 def webwxgetcontact(self): SpecialUsers = self.SpecialUsers url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % ( self.pass_ticket, self.skey, int(time.time())) dic = self._post(url, {}) if dic == '': return False self.MemberCount = dic['MemberCount'] self.MemberList = dic['MemberList'] ContactList = self.MemberList[:] GroupList = self.GroupList[:] PublicUsersList = self.PublicUsersList[:] SpecialUsersList = self.SpecialUsersList[:] for i in range(len(ContactList) - 1, -1, -1): Contact = ContactList[i] if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 ContactList.remove(Contact) self.PublicUsersList.append(Contact) elif Contact['UserName'] in SpecialUsers: # 特殊账号 ContactList.remove(Contact) self.SpecialUsersList.append(Contact) elif '@@' in Contact['UserName']: # 群聊 ContactList.remove(Contact) self.GroupList.append(Contact) elif Contact['UserName'] == self.User['UserName']: # 自己 ContactList.remove(Contact) self.ContactList = ContactList return True def webwxbatchgetcontact(self): url = self.base_uri + \ '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % ( int(time.time()), self.pass_ticket) params = { 'BaseRequest': self.BaseRequest, "Count": len(self.GroupList), "List": [{"UserName": g['UserName'], "EncryChatRoomId":""} for g in self.GroupList] } dic = self._post(url, params) if dic == '': return False # blabla ... ContactList = dic['ContactList'] ContactCount = dic['Count'] self.GroupList = ContactList for i in range(len(ContactList) - 1, -1, -1): Contact = ContactList[i] MemberList = Contact['MemberList'] for member in MemberList: self.GroupMemeberList.append(member) return True def getNameById(self, id): url = self.base_uri + \ '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % ( int(time.time()), self.pass_ticket) params = { 'BaseRequest': self.BaseRequest, "Count": 1, "List": [{"UserName": id, "EncryChatRoomId": ""}] } dic = self._post(url, params) if dic == '': return None # blabla ... return dic['ContactList'] def testsynccheck(self): SyncHost = ['wx2.qq.com', 'webpush.wx2.qq.com', 'wx8.qq.com', 'webpush.wx8.qq.com', 'qq.com', 'webpush.wx.qq.com', 'web2.wechat.com', 'webpush.web2.wechat.com', 'wechat.com', 'webpush.web.wechat.com', 'webpush.weixin.qq.com', 'webpush.wechat.com', 'webpush1.wechat.com', 'webpush2.wechat.com', 'webpush.wx.qq.com', 'webpush2.wx.qq.com'] for host in SyncHost: self.syncHost = host [retcode, selector] = self.synccheck() if retcode == '0': return True return False def synccheck(self): params = { 'r': int(time.time()), 'sid': self.sid, 'uin': self.uin, 'skey': self.skey, 'deviceid': self.deviceId, 'synckey': self.synckey, '_': int(time.time()), } url = 'https://' + self.syncHost + '/cgi-bin/mmwebwx-bin/synccheck?' + urllib.parse.urlencode(params) data = self._get(url, timeout=5) if data == '': return [-1,-1] pm = re.search( r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data) retcode = pm.group(1) selector = pm.group(2) return [retcode, selector] def webwxsync(self): url = self.base_uri + \ '/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( self.sid, self.skey, self.pass_ticket) params = { 'BaseRequest': self.BaseRequest, 'SyncKey': self.SyncKey, 'rr': ~int(time.time()) } dic = self._post(url, params) if dic == '': return None if self.DEBUG: print(json.dumps(dic, indent=4)) (json.dumps(dic, indent=4)) if dic['BaseResponse']['Ret'] == 0: self.SyncKey = dic['SyncKey'] self.synckey = '|'.join( [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) return dic def webwxsendmsg(self, word, to='filehelper'): url = self.base_uri + \ '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket) clientMsgId = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') params = { 'BaseRequest': self.BaseRequest, 'Msg': { "Type": 1, "Content": self._transcoding(word), "FromUserName": self.User['UserName'], "ToUserName": to, "LocalID": clientMsgId, "ClientMsgId": clientMsgId } } headers = {'content-type': 'application/json; charset=UTF-8'} data = json.dumps(params, ensure_ascii=False).encode('utf8') r = requests.post(url, data=data, headers=headers) dic = r.json() return dic['BaseResponse']['Ret'] == 0 def webwxuploadmedia(self, image_name): url = 'https://file2.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' # 计数器 self.media_count = self.media_count + 1 # 文件名 file_name = image_name # MIME格式 # mime_type = application/pdf, image/jpeg, image/png, etc. mime_type = mimetypes.guess_type(image_name, strict=False)[0] # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc # pic格式,直接显示。doc格式则显示为文件。 media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc' # 上一次修改日期 lastModifieDate = 'Thu Mar 17 2016 00:55:10 GMT+0800 (CST)' # 文件大小 file_size = os.path.getsize(file_name) # PassTicket pass_ticket = self.pass_ticket # clientMediaId client_media_id = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') # webwx_data_ticket webwx_data_ticket = '' for item in self.cookie: if item.name == 'webwx_data_ticket': webwx_data_ticket = item.value break if (webwx_data_ticket == ''): return "None Fuck Cookie" uploadmediarequest = json.dumps({ "BaseRequest": self.BaseRequest, "ClientMediaId": client_media_id, "TotalLen": file_size, "StartPos": 0, "DataLen": file_size, "MediaType": 4 }, ensure_ascii=False).encode('utf8') multipart_encoder = MultipartEncoder( fields={ 'id': 'WU_FILE_' + str(self.media_count), 'name': file_name, 'type': mime_type, 'lastModifieDate': lastModifieDate, 'size': str(file_size), 'mediatype': media_type, 'uploadmediarequest': uploadmediarequest, 'webwx_data_ticket': webwx_data_ticket, 'pass_ticket': pass_ticket, 'filename': (file_name, open(file_name, 'rb'), mime_type.split('/')[1]) }, boundary='-----------------------------1575017231431605357584454111' ) headers = { 'Host': 'file2.wx.qq.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Referer': 'https://wx2.qq.com/', 'Content-Type': multipart_encoder.content_type, 'Origin': 'https://wx2.qq.com', 'Connection': 'keep-alive', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache' } r = requests.post(url, data=multipart_encoder, headers=headers) response_json = r.json() if response_json['BaseResponse']['Ret'] == 0: return response_json return None def webwxsendmsgimg(self, user_id, media_id): url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s' % self.pass_ticket clientMsgId = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') data_json = { "BaseRequest": self.BaseRequest, "Msg": { "Type": 3, "MediaId": media_id, "FromUserName": self.User['UserName'], "ToUserName": user_id, "LocalID": clientMsgId, "ClientMsgId": clientMsgId } } headers = {'content-type': 'application/json; charset=UTF-8'} data = json.dumps(data_json, ensure_ascii=False).encode('utf8') r = requests.post(url, data=data, headers=headers) dic = r.json() return dic['BaseResponse']['Ret'] == 0 def webwxsendmsgemotion(self, user_id, media_id): url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket clientMsgId = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') data_json = { "BaseRequest": self.BaseRequest, "Msg": { "Type": 47, "EmojiFlag": 2, "MediaId": media_id, "FromUserName": self.User['UserName'], "ToUserName": user_id, "LocalID": clientMsgId, "ClientMsgId": clientMsgId } } headers = {'content-type': 'application/json; charset=UTF-8'} data = json.dumps(data_json, ensure_ascii=False).encode('utf8') r = requests.post(url, data=data, headers=headers) dic = r.json() if self.DEBUG: print(json.dumps(dic, indent=4)) logging.debug(json.dumps(dic, indent=4)) return dic['BaseResponse']['Ret'] == 0 def _saveFile(self, filename, data, api=None): fn = filename if self.saveSubFolders[api]: dirName = os.path.join(self.saveFolder, self.saveSubFolders[api]) if not os.path.exists(dirName): os.makedirs(dirName) fn = os.path.join(dirName, filename) logging.debug('Saved file: %s' % fn) with open(fn, 'wb') as f: f.write(data) f.close() return fn def webwxgeticon(self, id): url = self.base_uri + \ '/webwxgeticon?username=%s&skey=%s' % (id, self.skey) data = self._get(url) if data == '': return '' fn = 'img_' + id + '.jpg' return self._saveFile(fn, data, 'webwxgeticon') def webwxgetheadimg(self, id): url = self.base_uri + \ '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey) data = self._get(url) if data == '': return '' fn = 'img_' + id + '.jpg' return self._saveFile(fn, data, 'webwxgetheadimg') def webwxgetmsgimg(self, msgid): url = self.base_uri + \ '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) data = self._get(url) if data == '': return '' fn = 'img_' + msgid + '.jpg' return self._saveFile(fn, data, 'webwxgetmsgimg') # Not work now for weixin haven't support this API def webwxgetvideo(self, msgid): url = self.base_uri + \ '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey) data = self._get(url, api='webwxgetvideo') if data == '': return '' fn = 'video_' + msgid + '.mp4' return self._saveFile(fn, data, 'webwxgetvideo') def webwxgetvoice(self, msgid): url = self.base_uri + \ '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) data = self._get(url, api='webwxgetvoice') if data == '': return '' fn = 'voice_' + msgid + '.mp3' return self._saveFile(fn, data, 'webwxgetvoice') def getGroupName(self, id): name = '未知群' for member in self.GroupList: if member['UserName'] == id: name = member['NickName'] if name == '未知群': # 现有群里面查不到 GroupList = self.getNameById(id) for group in GroupList: self.GroupList.append(group) if group['UserName'] == id: name = group['NickName'] MemberList = group['MemberList'] for member in MemberList: self.GroupMemeberList.append(member) return name def getUserRemarkName(self, id): name = '未知群' if id[:2] == '@@' else '陌生人' if id == self.User['UserName']: return self.User['NickName'] # 自己 if id[:2] == '@@': # 群 name = self.getGroupName(id) else: # 特殊账号 for member in self.SpecialUsersList: if member['UserName'] == id: name = member['RemarkName'] if member[ 'RemarkName'] else member['NickName'] # 公众号或服务号 for member in self.PublicUsersList: if member['UserName'] == id: name = member['RemarkName'] if member[ 'RemarkName'] else member['NickName'] # 直接联系人 for member in self.ContactList: if member['UserName'] == id: name = member['RemarkName'] if member[ 'RemarkName'] else member['NickName'] # 群友 for member in self.GroupMemeberList: if member['UserName'] == id: name = member['DisplayName'] if member[ 'DisplayName'] else member['NickName'] if name == '未知群' or name == '陌生人': logging.debug(id) return name def getUSerID(self, name): for member in self.MemberList: if name == member['RemarkName'] or name == member['NickName']: return member['UserName'] return None def _showMsg(self, message): srcName = None dstName = None groupName = None content = None msg = message logging.debug(msg) if msg['raw_msg']: srcName = self.getUserRemarkName(msg['raw_msg']['FromUserName']) dstName = self.getUserRemarkName(msg['raw_msg']['ToUserName']) content = msg['raw_msg']['Content'].replace( '<', '<').replace('>', '>') message_id = msg['raw_msg']['MsgId'] if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: # 地理位置消息 data = self._get(content) if data == '': return data.decode('gbk').encode('utf-8') pos = self._searchContent('title', data, 'xml') temp = self._get(content) if temp == '': return tree = html.fromstring(temp) url = tree.xpath('//html/body/div/img')[0].attrib['src'] for item in urlparse(url).query.split('&'): if item.split('=')[0] == 'center': loc = item.split('=')[-1:] content = '%s 发送了一个 位置消息 - 我在 [%s](%s) @ %s]' % ( srcName, pos, url, loc) if msg['raw_msg']['ToUserName'] == 'filehelper': # 文件传输助手 dstName = '文件传输助手' if msg['raw_msg']['FromUserName'][:2] == '@@': # 接收到来自群的消息 if ":
" in content: [people, content] = content.split(':
', 1) groupName = srcName srcName = self.getUserRemarkName(people) dstName = 'GROUP' else: groupName = srcName srcName = 'SYSTEM' elif msg['raw_msg']['ToUserName'][:2] == '@@': # 自己发给群的消息 groupName = dstName dstName = 'GROUP' # 收到了红包 if content == '收到红包,请在手机上查看': msg['message'] = content # 指定了消息内容 if 'message' in list(msg.keys()): content = msg['message'] if groupName != None: print('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), srcName.strip(), dstName.strip(), content.replace('
', '\n'))) logging.info('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), srcName.strip(), dstName.strip(), content.replace('
', '\n'))) else: print('%s %s -> %s: %s' % (message_id, srcName.strip(), dstName.strip(), content.replace('
', '\n'))) logging.info('%s %s -> %s: %s' % (message_id, srcName.strip(), dstName.strip(), content.replace('
', '\n'))) def handleMsg(self, r): for msg in r['AddMsgList']: print('[*] 你有新的消息,请注意查收') logging.debug('[*] 你有新的消息,请注意查收') if self.DEBUG: fn = 'msg' + str(int(random.random() * 1000)) + '.json' with open(fn, 'w') as f: f.write(json.dumps(msg)) print('[*] 该消息已储存到文件: ' + fn) logging.debug('[*] 该消息已储存到文件: %s' % (fn)) msgType = msg['MsgType'] name = self.getUserRemarkName(msg['FromUserName']) content = msg['Content'].replace('<', '<').replace('>', '>') msgid = msg['MsgId'] if msgType == 1: raw_msg = {'raw_msg': msg} self._showMsg(raw_msg) #自己加的代码-------------------------------------------# #if self.autoReplyRevokeMode: # store #自己加的代码-------------------------------------------# if self.autoReplyMode: ans = self._xiaodoubi(content) + '\n[微信机器人自动回复]' if self.webwxsendmsg(ans, msg['FromUserName']): print('自动回复: ' + ans) logging.info('自动回复: ' + ans) else: print('自动回复失败') logging.info('自动回复失败') elif msgType == 3: image = self.webwxgetmsgimg(msgid) raw_msg = {'raw_msg': msg, 'message': '%s 发送了一张图片: %s' % (name, image)} self._showMsg(raw_msg) self._safe_open(image) elif msgType == 34: voice = self.webwxgetvoice(msgid) raw_msg = {'raw_msg': msg, 'message': '%s 发了一段语音: %s' % (name, voice)} self._showMsg(raw_msg) self._safe_open(voice) elif msgType == 42: info = msg['RecommendInfo'] print('%s 发送了一张名片:' % name) print('=========================') print('= 昵称: %s' % info['NickName']) print('= 微信号: %s' % info['Alias']) print('= 地区: %s %s' % (info['Province'], info['City'])) print('= 性别: %s' % ['未知', '男', '女'][info['Sex']]) print('=========================') raw_msg = {'raw_msg': msg, 'message': '%s 发送了一张名片: %s' % ( name.strip(), json.dumps(info))} self._showMsg(raw_msg) elif msgType == 47: url = self._searchContent('cdnurl', content) raw_msg = {'raw_msg': msg, 'message': '%s 发了一个动画表情,点击下面链接查看: %s' % (name, url)} self._showMsg(raw_msg) self._safe_open(url) elif msgType == 49: appMsgType = defaultdict(lambda: "") appMsgType.update({5: '链接', 3: '音乐', 7: '微博'}) print('%s 分享了一个%s:' % (name, appMsgType[msg['AppMsgType']])) print('=========================') print('= 标题: %s' % msg['FileName']) print('= 描述: %s' % self._searchContent('des', content, 'xml')) print('= 链接: %s' % msg['Url']) print('= 来自: %s' % self._searchContent('appname', content, 'xml')) print('=========================') card = { 'title': msg['FileName'], 'description': self._searchContent('des', content, 'xml'), 'url': msg['Url'], 'appname': self._searchContent('appname', content, 'xml') } raw_msg = {'raw_msg': msg, 'message': '%s 分享了一个%s: %s' % ( name, appMsgType[msg['AppMsgType']], json.dumps(card))} self._showMsg(raw_msg) elif msgType == 51: raw_msg = {'raw_msg': msg, 'message': '[*] 成功获取联系人信息'} self._showMsg(raw_msg) elif msgType == 62: video = self.webwxgetvideo(msgid) raw_msg = {'raw_msg': msg, 'message': '%s 发了一段小视频: %s' % (name, video)} self._showMsg(raw_msg) self._safe_open(video) elif msgType == 10002: raw_msg = {'raw_msg': msg, 'message': '%s 撤回了一条消息' % name} self._showMsg(raw_msg) else: logging.debug('[*] 该消息类型为: %d,可能是表情,图片, 链接或红包: %s' % (msg['MsgType'], json.dumps(msg))) raw_msg = { 'raw_msg': msg, 'message': '[*] 该消息类型为: %d,可能是表情,图片, 链接或红包' % msg['MsgType']} self._showMsg(raw_msg) def listenMsgMode(self): print('[*] 进入消息监听模式 ... 成功') logging.debug('[*] 进入消息监听模式 ... 成功') self._run('[*] 进行同步线路测试 ... ', self.testsynccheck) playWeChat = 0 redEnvelope = 0 while True: self.lastCheckTs = time.time() [retcode, selector] = self.synccheck() if self.DEBUG: print('retcode: %s, selector: %s' % (retcode, selector)) logging.debug('retcode: %s, selector: %s' % (retcode, selector)) if retcode == '1100': print('[*] 你在手机上登出了微信,债见') logging.debug('[*] 你在手机上登出了微信,债见') break if retcode == '1101': print('[*] 你在其他地方登录了 WEB 版微信,债见') logging.debug('[*] 你在其他地方登录了 WEB 版微信,债见') break elif retcode == '0': if selector == '2': r = self.webwxsync() if r is not None: self.handleMsg(r) elif selector == '6': # TODO redEnvelope += 1 print('[*] 收到疑似红包消息 %d 次' % redEnvelope) logging.debug('[*] 收到疑似红包消息 %d 次' % redEnvelope) elif selector == '7': playWeChat += 1 print('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat) logging.debug('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat) r = self.webwxsync() elif selector == '0': time.sleep(1) if (time.time() - self.lastCheckTs) <= 20: time.sleep(time.time() - self.lastCheckTs) def sendMsg(self, name, word, isfile=False): id = self.getUSerID(name) if id: if isfile: with open(word, 'r') as f: for line in f.readlines(): line = line.replace('\n', '') self._echo('-> ' + name + ': ' + line) if self.webwxsendmsg(line, id): print(' [成功]') else: print(' [失败]') time.sleep(1) else: if self.webwxsendmsg(word, id): print('[*] 消息发送成功') logging.debug('[*] 消息发送成功') else: print('[*] 消息发送失败') logging.debug('[*] 消息发送失败') else: print('[*] 此用户不存在') logging.debug('[*] 此用户不存在') def sendMsgToAll(self, word): for contact in self.ContactList: name = contact['RemarkName'] if contact[ 'RemarkName'] else contact['NickName'] id = contact['UserName'] self._echo('-> ' + name + ': ' + word) if self.webwxsendmsg(word, id): print(' [成功]') else: print(' [失败]') time.sleep(1) def sendImg(self, name, file_name): response = self.webwxuploadmedia(file_name) media_id = "" if response is not None: media_id = response['MediaId'] user_id = self.getUSerID(name) response = self.webwxsendmsgimg(user_id, media_id) def sendEmotion(self, name, file_name): response = self.webwxuploadmedia(file_name) media_id = "" if response is not None: media_id = response['MediaId'] user_id = self.getUSerID(name) response = self.webwxsendmsgemotion(user_id, media_id) @catchKeyboardInterrupt def start(self): self._echo('[*] 微信网页版 ... 开动') print() logging.debug('[*] 微信网页版 ... 开动') while True: self._run('[*] 正在获取 uuid ... ', self.getUUID) self._echo('[*] 正在获取二维码 ... 成功') print() logging.debug('[*] 微信网页版 ... 开动') self.genQRCode() print('[*] 请使用微信扫描二维码以登录 ... ') if not self.waitForLogin(): continue print('[*] 请在手机上点击确认以登录 ... ') if not self.waitForLogin(0): continue break self._run('[*] 正在登录 ... ', self.login) self._run('[*] 微信初始化 ... ', self.webwxinit) self._run('[*] 开启状态通知 ... ', self.webwxstatusnotify) self._run('[*] 获取联系人 ... ', self.webwxgetcontact) self._echo('[*] 应有 %s 个联系人,读取到联系人 %d 个' % (self.MemberCount, len(self.MemberList))) print() self._echo('[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号' % (len(self.GroupList), len(self.ContactList), len(self.SpecialUsersList), len(self.PublicUsersList))) print() self._run('[*] 获取群 ... ', self.webwxbatchgetcontact) logging.debug('[*] 微信网页版 ... 开动') if self.DEBUG: print(self) logging.debug(self) if self.interactive and input('[*] 是否开启自动回复模式(y/n): ') == 'y': self.autoReplyMode = True print('[*] 自动回复模式 ... 开启') logging.debug('[*] 自动回复模式 ... 开启') else: print('[*] 自动回复模式 ... 关闭') logging.debug('[*] 自动回复模式 ... 关闭') if sys.platform.startswith('win'): import _thread _thread.start_new_thread(self.listenMsgMode()) else: listenProcess = multiprocessing.Process(target=self.listenMsgMode) listenProcess.start() while True: text = input('') if text == 'quit': listenProcess.terminate() print('[*] 退出微信') logging.debug('[*] 退出微信') exit() elif text[:2] == '->': [name, word] = text[2:].split(':') if name == 'all': self.sendMsgToAll(word) else: self.sendMsg(name, word) elif text[:3] == 'm->': [name, file] = text[3:].split(':') self.sendMsg(name, file, True) elif text[:3] == 'f->': print('发送文件') logging.debug('发送文件') elif text[:3] == 'i->': print('发送图片') [name, file_name] = text[3:].split(':') self.sendImg(name, file_name) logging.debug('发送图片') elif text[:3] == 'e->': print('发送表情') [name, file_name] = text[3:].split(':') self.sendEmotion(name, file_name) logging.debug('发送表情') def _safe_open(self, path): if self.autoOpen: if platform.system() == "Linux": os.system("xdg-open %s &" % path) else: os.system('open %s &' % path) def _run(self, str, func, *args): self._echo(str) if func(*args): print('成功') logging.debug('%s... 成功' % (str)) else: print('失败\n[*] 退出程序') logging.debug('%s... 失败' % (str)) logging.debug('[*] 退出程序') exit() def _echo(self, str): sys.stdout.write(str) sys.stdout.flush() def _printQR(self, mat): for i in mat: BLACK = '\033[40m \033[0m' WHITE = '\033[47m \033[0m' print(''.join([BLACK if j else WHITE for j in i])) def _str2qr(self, str): print(str) qr = qrcode.QRCode() qr.border = 1 qr.add_data(str) qr.make() # img = qr.make_image() # img.save("qrcode.png") #mat = qr.get_matrix() #self._printQR(mat) # qr.print_tty() or qr.print_ascii() qr.print_ascii(invert=True) def _transcoding(self, data): if not data: return data result = None if type(data) == str: result = data elif type(data) == str: result = data.decode('utf-8') return result def _get(self, url: object, api: object = None, timeout: object = None) -> object: request = urllib.request.Request(url=url) request.add_header('Referer', 'https://wx.qq.com/') if api == 'webwxgetvoice': request.add_header('Range', 'bytes=0-') if api == 'webwxgetvideo': request.add_header('Range', 'bytes=0-') try: response = urllib.request.urlopen(request, timeout=timeout) if timeout else urllib.request.urlopen(request) if api == 'webwxgetvoice' or api == 'webwxgetvideo': data = response.read() else: data = response.read().decode('utf-8') logging.debug(url) return data except urllib.error.HTTPError as e: logging.error('HTTPError = ' + str(e.code)) except urllib.error.URLError as e: logging.error('URLError = ' + str(e.reason)) except http.client.HTTPException as e: logging.error('HTTPException') except timeout_error as e: pass except ssl.CertificateError as e: pass except Exception: import traceback logging.error('generic exception: ' + traceback.format_exc()) return '' def _post(self, url: object, params: object, jsonfmt: object = True) -> object: if jsonfmt: data = (json.dumps(params)).encode() request = urllib.request.Request(url=url, data=data) request.add_header( 'ContentType', 'application/json; charset=UTF-8') else: request = urllib.request.Request(url=url, data=urllib.parse.urlencode(params).encode(encoding='utf-8')) try: response = urllib.request.urlopen(request) data = response.read() if jsonfmt: return json.loads(data.decode('utf-8') )#object_hook=_decode_dict) return data except urllib.error.HTTPError as e: logging.error('HTTPError = ' + str(e.code)) except urllib.error.URLError as e: logging.error('URLError = ' + str(e.reason)) except http.client.HTTPException as e: logging.error('HTTPException') except Exception: import traceback logging.error('generic exception: ' + traceback.format_exc()) return '' def _xiaodoubi(self, word): url = 'http://www.xiaodoubi.com/bot/chat.php' try: r = requests.post(url, data={'chat': word}) return r.content except: return "让我一个人静静 T_T..." def _simsimi(self, word): key = '' url = 'http://sandbox.api.simsimi.com/request.p?key=%s&lc=ch&ft=0.0&text=%s' % ( key, word) r = requests.get(url) ans = r.json() if ans['result'] == '100': return ans['response'] else: return '你在说什么,风太大听不清列' def _searchContent(self, key, content, fmat='attr'): if fmat == 'attr': pm = re.search(key + '\s?=\s?"([^"<]+)"', content) if pm: return pm.group(1) elif fmat == 'xml': pm = re.search('<{0}>([^<]+)'.format(key), content) if not pm: pm = re.search( '<{0}><\!\[CDATA\[(.*?)\]\]>'.format(key), content) if pm: return pm.group(1) return '未知' class UnicodeStreamFilter: def __init__(self, target): self.target = target self.encoding = 'utf-8' self.errors = 'replace' self.encode_to = self.target.encoding def write(self, s): if type(s) == str: s = s.encode().decode('utf-8') s = s.encode(self.encode_to, self.errors).decode(self.encode_to) self.target.write(s) def flush(self): self.target.flush() if sys.stdout.encoding == 'cp936': sys.stdout = UnicodeStreamFilter(sys.stdout) if __name__ == '__main__': logger = logging.getLogger(__name__) if not sys.platform.startswith('win'): import coloredlogs coloredlogs.install(level='DEBUG') webwx = WebWeixin() webwx.start() ================================================ FILE: wxbot_project_py2.7/README.md ================================================ # wxbot_project_py2.7 目录结构: ```bash . ├── README.md ├── config │   ├── __init__.py │   ├── config_manager.py │   ├── constant.py │   ├── log.py │   ├── requirements.txt │   └── wechat.conf.bak ├── db │   ├── __init__.py │   ├── mysql_db.py │   └── sqlite_db.py ├── docker │   ├── Dockerfile │   └── README.md ├── flask_templates │   ├── index.html │   └── upload.html ├── wechat │   ├── __init__.py │   ├── utils.py │   ├── wechat_apis.py │   └── wechat.py ├── weixin_bot.py └── wx_handler ├── __init__.py ├── bot.py ├── sendgrid_mail.py └── wechat_msg_processor.py ``` ================================================ FILE: wxbot_project_py2.7/config/__init__.py ================================================ #!/usr/bin/env python # coding: utf-8 from config_manager import ConfigManager from constant import Constant from log import Log ================================================ FILE: wxbot_project_py2.7/config/config_manager.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from constant import Constant #--------------------------------------------------- import ConfigParser import os #=================================================== class ConfigManager(object): def __init__(self): self.config = Constant.WECHAT_CONFIG_FILE self.cp = ConfigParser.ConfigParser() self.cp.read(self.config) data_dir = self.get('setting', 'prefix') upload_dir = self.getpath('uploaddir') if not os.path.exists(data_dir): os.makedirs(data_dir) if not os.path.exists(upload_dir): os.makedirs(upload_dir) def get(self, section, option): return self.cp.get(section, option) def set(self, section, option, value): self.cp.set(section, option, value) self.cp.write(open(self.config, 'w')) def getpath(self, dir): prefix = self.get('setting', 'prefix') return prefix + self.get('setting', dir) def setup_database(self): path = self.get('setting', 'prefix') conf = [ path + self.get('setting', 'uploaddir'), path + self.get('setting', 'datadir'), path + self.get('setting', 'logdir'), ] return conf def set_wechat_config(self, conf): for [key, value] in conf.items(): self.cp.set('wechat', key, value) self.cp.write(open(self.config, 'w')) def get_wechat_config(self): uin = self.cp.get('wechat', 'uin') last_login = self.cp.get('wechat', 'last_login') conf = [ self.cp.get('wechat', 'uuid'), self.cp.get('wechat', 'redirect_uri'), int(uin if uin else 0), self.cp.get('wechat', 'sid'), self.cp.get('wechat', 'skey'), self.cp.get('wechat', 'pass_ticket'), self.cp.get('wechat', 'synckey'), self.cp.get('wechat', 'device_id'), float(last_login if last_login else 0), ] return conf def get_wechat_media_dir(self): prefix = self.get('setting', 'prefix') path = prefix + self.cp.get('setting', 'mediapath') return { 'webwxgeticon': path + '/icons', 'webwxgetheadimg': path + '/headimgs', 'webwxgetmsgimg': path + '/msgimgs', 'webwxgetvideo': path + '/videos', 'webwxgetvoice': path + '/voices', '_showQRCodeImg': path + '/qrcodes', } def get_pickle_files(self): prefix = self.get('setting', 'prefix') return { 'User': prefix + self.get('setting', 'contact_user'), 'MemberList': prefix + self.get('setting', 'contact_member_list'), 'GroupList': prefix + self.get('setting', 'contact_group_list'), 'GroupMemeberList': prefix + self.get('setting', 'contact_group_memeber_list'), 'SpecialUsersList': prefix + self.get('setting', 'contact_special_users_list'), } def get_cookie(self): prefix = self.get('setting', 'prefix') path = prefix + self.get('setting', 'cookie') basedir = os.path.dirname(path) if not os.path.exists(basedir): os.makedirs(basedir) return path def mysql(self): mysql = { 'host': self.get('mysql', 'host'), 'port': self.cp.getint('mysql', 'port'), 'user': self.get('mysql', 'user'), 'passwd': self.get('mysql', 'passwd'), 'database': self.get('mysql', 'database'), } return mysql ================================================ FILE: wxbot_project_py2.7/config/constant.py ================================================ #!/usr/bin/env python # coding: utf-8 import time class Constant(object): """ @brief All used constants are listed here """ WECHAT_CONFIG_FILE = 'config/wechat.conf' LOGGING_LOGGER_NAME = 'WeChat' QRCODE_BLACK = '\033[40m \033[0m' QRCODE_WHITE = '\033[47m \033[0m' HTTP_HEADER_USERAGENT = [('User-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36')] HTTP_HEADER_CONTENTTYPE = ['ContentType', 'application/json; charset=UTF-8'] HTTP_HEADER_CONNECTION = ['Connection', 'keep-alive'] HTTP_HEADER_REFERER = ['Referer', 'https://wx.qq.com/'] HTTP_HEADER_RANGE = ['Range', 'bytes=0-'] REGEX_EMOJI = r'' SERVER_LOG_FORMAT = '%(asctime)s - %(pathname)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s' SERVER_UPLOAD_ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) SERVER_PAGE_UPLOAD = 'upload.html' SERVER_PAGE_INDEX = 'index.html' RUN_RESULT_SUCCESS = '成功 %ds\n' RUN_RESULT_FAIL = '失败\n[*] 退出程序\n' MAIN_RESTART = '[*] wait for restart' LOG_MSG_FILE = 'WeChat-Msgs-%Y-%m-%d.json' LOG_MSG_GROUP_LIST_FILE = 'group_list.json' LOG_MSG_QUIT = '\n[*] Force quit.\n' LOG_MSG_FAIL = '失败\n' LOG_MSG_SUCCESS = '成功\n' LOG_MSG_START = '[*] 微信网页版 ... 开动\n' LOG_MSG_RECOVER = '[*] 从配置文件中恢复 ... ' LOG_MSG_RECOVER_CONTACT = '[*] 从文件中恢复联系人数据 ... ' LOG_MSG_TRY_INIT = '[*] 尝试初始化 ... ' LOG_MSG_ASSOCIATION_LOGIN = '[*] 通过关联登录 ... ' LOG_MSG_GET_UUID = '[*] 正在获取 uuid ... ' LOG_MSG_GET_QRCODE = '[*] 正在获取二维码 ... 成功\n' LOG_MSG_SCAN_QRCODE = '[*] 请使用微信扫描二维码以登录 ... \n' LOG_MSG_CONFIRM_LOGIN = '[*] 请在手机上点击确认以登录 ... \n' LOG_MSG_WAIT_LOGIN_ERR1 = '[登陆超时] \n' LOG_MSG_WAIT_LOGIN_ERR2 = '[登陆异常] \n' LOG_MSG_LOGIN = '[*] 正在登录 ... ' LOG_MSG_INIT = '[*] 微信初始化 ... ' LOG_MSG_STATUS_NOTIFY = '[*] 开启状态通知 ... ' LOG_MSG_GET_CONTACT = '[*] 获取联系人 ... ' LOG_MSG_CONTACT_COUNT = '[*] 应有 %s 个联系人,读取到联系人 %d 个\n' LOG_MSG_OTHER_CONTACT_COUNT = '[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号\n' LOG_MSG_GET_GROUP_MEMBER = '[*] 拉取群聊成员 ... ' LOG_MSG_SNAPSHOT = '[*] 保存配置 ... ' LOG_MSG_LOGOUT = '[*] 你在手机上登出了微信\n' LOG_MSG_LOGIN_OTHERWHERE = '[*] 你在其他地方登录了 WEB 版微信\n' LOG_MSG_QUIT_ON_PHONE = '[*] 你在手机上主动退出了\n' LOG_MSG_RUNTIME = '[*] Total run: %s\n' LOG_MSG_KILL_PROCESS = 'kill %d' LOG_MSG_NEW_MSG = '>>> %d 条新消息\n' LOG_MSG_LOCATION = '[位置] %s' LOG_MSG_PICTURE = '[图片] %s' LOG_MSG_VOICE = '[语音] %s' LOG_MSG_RECALL = '撤回了一条消息' LOG_MSG_ADD_FRIEND = '%s 请求添加你为好友' LOG_MSG_UNKNOWN_MSG = '[*] 该消息类型为: %d,内容: %s' LOG_MSG_VIDEO = '[小视频] %s' LOG_MSG_NOTIFY_PHONE = '[*] 提示手机网页版微信登录状态\n' LOG_MSG_EMOTION = '[表情] %s' LOG_MSG_NAME_CARD = ( '[名片]\n' '=========================\n' '= 昵称: %s\n' '= 微信号: %s\n' '= 地区: %s %s\n' '= 性别: %s\n' '=========================' ) LOG_MSG_SEX_OPTION = ['未知', '男', '女'] LOG_MSG_APP_LINK = ( '[%s]\n' '=========================\n' '= 标题: %s\n' '= 描述: %s\n' '= 链接: %s\n' '= 来自: %s\n' '=========================' ) LOG_MSG_APP_LINK_TYPE = {5: '链接', 3: '音乐', 7: '微博'} LOG_MSG_APP_IMG = ( '[图片]\n' '=========================\n' '= 文件: %s\n' '= 来自: %s\n' '=========================' ) LOG_MSG_SYSTEM = '系统消息' LOG_MSG_UNKNOWN_NAME = '未知_' LOG_MSG_UNKNOWN_GROUP_NAME = '未知群_' TABLE_GROUP_MSG_LOG = 'WeChatRoomMessage' TABLE_GROUP_MSG_LOG_COL = """ MsgID text, RoomOwnerID text, RoomName text, UserCount text, FromUserName text, ToUserName text, AttrStatus text, DisplayName text, Name text, MsgType text, FaceMsg text, TextMsg text, ImageMsg text, VideoMsg text, SoundMsg text, LinkMsg text, NameCardMsg text, LocationMsg text, RecallMsgID text, SysMsg text, MsgTime text, MsgTimestamp text """ @staticmethod def TABLE_GROUP_LIST(): return 'WeChatRoom_' + time.strftime('%Y%m%d', time.localtime()) TABLE_GROUP_LIST_COL = """ RoomName text, RoomID text, RoomOwnerID text, UserCount text, RoomIcon text """ @staticmethod def TABLE_GROUP_USER_LIST(): return 'WeChatRoomMember_' + time.strftime('%Y%m%d', time.localtime()) TABLE_GROUP_USER_LIST_COL = """ RoomID text, MemberID text, MemberNickName text, MemberDisplayName text, MemberAttrStatus text """ TABLE_RECORD_ENTER_GROUP = 'WeChatEnterGroupRecord' TABLE_RECORD_ENTER_GROUP_COL = """ MsgID text, RoomName text, FromUserName text, ToUserName text, Name text, EnterTime text """ TABLE_RECORD_RENAME_GROUP = 'WeChatRenameGroupRecord' TABLE_RECORD_RENAME_GROUP_COL = """ MsgID text, FromName text, ToName text, ModifyPeople text, ModifyTime text """ API_APPID = 'wx782c26e4c19acffb' API_WXAPPID = 'wx299208e619de7026' # Weibo # 'wxeb7ec651dd0aefa9' # Weixin API_LANG = 'zh_CN' API_USER_AGENT = ( 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/48.0.2564.109 Safari/537.36' ) API_SPECIAL_USER = [ 'newsapp', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 'voip', 'blogappweixin', 'brandsessionholder', 'weixin', 'weixinreminder', 'officialaccounts', 'wxitil', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'userexperience_alarm', ] EMOTICON = [ '[Smile]', '[Grimace]', '[Drool]', '[Scowl]', '[CoolGuy]', '[Sob]', '[Shy]', '[Silent]', '[Sleep]', '[Cry]', '[Awkward]', '[Angry]', '[Tongue]', '[Grin]', '[Surprise]', '[Frown]', '[Ruthless]', '[Blush]', '[Scream]', '[Puke]', '[Chuckle]', '[Joyful]', '[Slight]', '[Smug]', '[Hungry]', '[Drowsy]', '[Panic]', '[Sweat]', '[Laugh]', '[Commando]', '[Determined]', '[Scold]', '[Shocked]', '[Shhh]', '[Dizzy]', '[Tormented]', '[Toasted]', '[Skull]', '[Hammer]', '[Wave]', '[Relief]', '[DigNose]', '[Clap]', '[Shame]', '[Trick]',' [Bah!L]','[Bah!R]', '[Yawn]', '[Lookdown]', '[Wronged]', '[Puling]', '[Sly]', '[Kiss]', '[Uh-oh]', '[Whimper]', '[Cleaver]', '[Melon]', '[Beer]', '[Basketball]', '[PingPong]', '[Coffee]', '[Rice]', '[Pig]', '[Rose]', '[Wilt]', '[Lip]', '[Heart]', '[BrokenHeart]', '[Cake]', '[Lightning]', '[Bomb]', '[Dagger]', '[Soccer]', '[Ladybug]', '[Poop]', '[Moon]', '[Sun]', '[Gift]', '[Hug]', '[Strong]', '[Weak]', '[Shake]', '[Victory]', '[Admire]', '[Beckon]', '[Fist]', '[Pinky]', '[Love]', '[No]', '[OK]', '[InLove]', '[Blowkiss]', '[Waddle]', '[Tremble]', '[Aaagh!]', '[Twirl]', '[Kotow]', '[Lookback]', '[Jump]', '[Give-in]', u'\U0001f604', u'\U0001f637', u'\U0001f639', u'\U0001f61d', u'\U0001f632', u'\U0001f633', u'\U0001f631', u'\U0001f64d', u'\U0001f609', u'\U0001f60c', u'\U0001f612', u'\U0001f47f', u'\U0001f47b', u'\U0001f49d', u'\U0001f64f', u'\U0001f4aa', u'\U0001f4b5', u'\U0001f382', u'\U0001f388', u'\U0001f4e6', ] BOT_ZHIHU_URL_LATEST = 'http://news-at.zhihu.com/api/4/news/latest' BOT_ZHIHU_URL_DAILY = 'http://daily.zhihu.com/story/' BOT_TULING_API_KEY = '55e7f30895a0a10535984bae5ad294d1' BOT_TULING_API_URL = 'http://www.tuling123.com/openapi/api?key=%s&info=%s&userid=%s' BOT_TULING_BOT_REPLY = u'麻烦说的清楚一点,我听不懂你在说什么' ================================================ FILE: wxbot_project_py2.7/config/log.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from constant import Constant from config import ConfigManager #--------------------------------------------------- import logging import logging.config #=================================================== cm = ConfigManager() logging.config.fileConfig(Constant.WECHAT_CONFIG_FILE) # create logger Log = logging.getLogger(Constant.LOGGING_LOGGER_NAME) # 'application' code # Log.debug('debug message') # Log.info('info message') # Log.warn('warn message') # Log.error('error message') # Log.critical('critical message') ================================================ FILE: wxbot_project_py2.7/config/requirements.txt ================================================ qrcode flask requests requests_toolbelt pymysql sendgrid ================================================ FILE: wxbot_project_py2.7/config/wechat.conf.bak ================================================ [wechat] host = wx.qq.com uuid = redirect_uri = uin = sid = skey = pass_ticket = device_id = last_login = [setting] prefix = tmp_data/ database = WeChat.db datadir = Data/infos/ logdir = Logs mediapath = Data uploaddir = Data/upload qrcodedir = Data/qrcode server_port = 8080 cookie = Cookie/WeChat.cookie contact_user = Pickle/User.pkl contact_member_list = Pickle/MemberList.pkl contact_group_list = Pickle/GroupList.pkl contact_group_memeber_list = Pickle/GroupMemeberList.pkl contact_special_users_list = Pickle/SpecialUsersList.pkl server_mode = False server_log_file = server.log log_mode = False [mysql] host = localhost port = 3306 user = root passwd = root database = wechat [sendgrid] api_key = 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 from_email = wxbot@wechat.com to_email = xxx@example.com [loggers] keys = root,WeChat [handlers] keys = consoleHandler,fileHandler [formatters] keys = simpleFormatter [logger_root] level = DEBUG handlers = consoleHandler [logger_WeChat] level = DEBUG handlers = fileHandler qualname = WeChat propagate = 0 [handler_consoleHandler] class = StreamHandler level = DEBUG formatter = simpleFormatter args = (sys.stdout,) [handler_fileHandler] class = FileHandler level = DEBUG formatter = simpleFormatter args = ('tmp_data/wechat.log',) [formatter_simpleFormatter] format = %(asctime)s - %(name)s - %(levelname)s - %(message)s datefmt = ================================================ FILE: wxbot_project_py2.7/db/__init__.py ================================================ #!/usr/bin/env python # coding: utf-8 from sqlite_db import SqliteDB from mysql_db import MysqlDB ================================================ FILE: wxbot_project_py2.7/db/mysql_db.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from config import Log #--------------------------------------------------- import pymysql import threading import traceback #=================================================== def array_join(arr, c): t = '' for a in arr: t += "'%s'" % str(a).replace("'","\\\'") + c return t[:-len(c)] class MysqlDB(object): """ 修改服务器上的配置文件/etc/my.cnf,在对应位置添加以下设置: [client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-client-handshake = FALSE character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' """ def __init__(self, conf): self.conf = conf config = { 'host': conf['host'], 'port': conf['port'], 'user': conf['user'], 'passwd': conf['passwd'], 'charset':'utf8mb4', # 支持1-4个字节字符 'cursorclass': pymysql.cursors.DictCursor } self.conn = pymysql.connect(**config) self.conn.autocommit(1) # for thread-save self.lock = threading.Lock() self.create_db(conf['database']) self.conn.select_db(conf['database']) # cache table cols self.table_cols = {} for t in self.show_tables(): self.table_cols[t] = self.get_table_column_name(t) def show_database(self): c = self.conn.cursor() sql = 'SHOW DATABASES' Log.debug('DB -> %s' % sql) c.execute(sql) return [r['Database'] for r in c.fetchall()] def show_tables(self): c = self.conn.cursor() sql = 'SHOW TABLES' Log.debug('DB -> %s' % sql) c.execute(sql) return [r['Tables_in_'+self.conf['database']] for r in c.fetchall()] def create_db(self, db_name): """ @brief Creates a database @param db_name String """ if self.conf['database'] not in self.show_database(): sql = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % db_name Log.debug('DB -> %s' % sql) self.execute(sql) def create_table(self, table, cols): """ @brief Creates a table in database @param table String @param cols String, the cols in table """ if table not in self.table_cols: sql = 'CREATE TABLE IF NOT EXISTS %s(id int primary key auto_increment, %s) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % (table, cols) Log.debug('DB -> %s' % sql) self.execute(sql) self.table_cols[table] = ['id'] + [c.strip().split(' ')[0] for c in cols.split(',')] def delete_table(self, table): """ @brief Delete a table in database @param table String """ if table in self.table_cols: sql = "DROP TABLE IF EXISTS %s" % table Log.debug('DB -> %s' % sql) self.execute(sql) self.table_cols.pop(table) def insert(self, table, value): """ @brief Insert a row in table @param table String @param value Tuple """ col_name = self.table_cols[table][1:] sql = "INSERT INTO %s(%s) VALUES (%s)" % (table, str(','.join(col_name)), array_join(value, ',')) Log.debug('DB -> %s' % sql) self.execute(sql) def insertmany(self, table, values): """ @brief Insert many rows in table @param table String @param values Array of tuple """ col_name = self.table_cols[table][1:] sql = 'INSERT INTO %s(%s) VALUES (%s)' % (table, ','.join(col_name), ','.join(['%s'] * len(values[0]))) Log.debug('DB -> %s' % sql) self.execute(sql, values) def select(self, table, field='', condition=''): """ @brief select all result from table @param table String @param field String @param condition String @return result Tuple """ sql = "SELECT * FROM %s" % table if field and condition: sql += " WHERE %s='%s'" % (field, condition) Log.debug('DB -> %s' % sql) return self.execute(sql) def get_table_column_name(self, table): """ @brief select all result from table @param table String @return result Array """ c = self.conn.cursor() c.execute("SELECT * FROM %s" % table) names = list(map(lambda x: x[0], c.description)) return names def execute(self, sql, values=None): """ @brief execute sql commands, return result if it has @param sql String @param value Tuple @return result Array """ c = self.conn.cursor() self.lock.acquire() hasReturn = sql.lstrip().upper().startswith("SELECT") result = [] try: if values: c.executemany(sql, values) else: c.execute(sql) if hasReturn: result = c.fetchall() except Exception, e: Log.error(traceback.format_exc()) self.conn.rollback() finally: self.lock.release() if hasReturn: return result def delete(self, table, field='', condition=''): """ @brief execute sql commands, return result if it has @param table String @param field String @param condition String """ sql = "DELETE FROM %s WHERE %s=%s" % (table, field, condition) Log.debug('DB -> %s' % sql) self.execute(sql) def close(self): """ @brief close connection to database """ Log.debug('DB -> close') # 关闭数据库连接 self.conn.close() ================================================ FILE: wxbot_project_py2.7/db/sqlite_db.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from config import Log #--------------------------------------------------- import sqlite3 import threading import traceback #=================================================== def _dict_factory(cursor, row): aDict = {} for iField, field in enumerate (cursor.description): aDict [field [0]] = row [iField] return aDict class SqliteDB(object): def __init__(self, db_file): self.db_file = db_file # self.conn = sqlite3.connect(db_file, check_same_thread=False) # use 8-bit strings instead of unicode string self.conn.text_factory = str # not return a tuple but a dict with column name as key self.conn.row_factory = _dict_factory # for thread-save self.lock = threading.Lock() def set_conn(self): self._conn = sqlite3.connect(self.db_file,check_same_thread=False) @property def conn(self): try: self._conn.execute('select 1;') # check out conn except (sqlite3.ProgrammingError,AttributeError): # Cannot operate on a closed database self.set_conn() finally: return self._conn def create_table(self, table, cols): """ @brief Creates a table in database @param table String @param cols String, the cols in table """ sql = "CREATE TABLE if not exists %s (%s);" % (table, cols) Log.debug('DB -> %s' % sql) self.execute(sql) def delete_table(self, table): """ @brief Delete a table in database @param table String """ sql = "DROP TABLE if exists %s;" % table Log.debug('DB -> %s' % sql) self.execute(sql) def insert(self, table, value): """ @brief Insert a row in table @param table String @param value Tuple """ sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * len(value)) + ");") % table Log.debug('DB -> %s' % sql) self.execute(sql, value) def insertmany(self, table, values): """ @brief Insert many rows in table @param table String @param values Array of tuple """ c = self.conn.cursor() self.lock.acquire() n = len(values[0]) sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * n) + ");") % table Log.debug('DB -> %s' % sql) try: c.executemany(sql, values) except Exception, e: Log.error(traceback.format_exc()) finally: self.lock.release() self.conn.commit() def select(self, table, field='', condition=''): """ @brief select all result from table @param table String @param field String @param condition String @return result Tuple """ result = [] if field and condition: cond = (condition,) sql = "SELECT * FROM %s WHERE %s=?" % (table, field) Log.debug('DB -> %s' % sql) result = self.execute(sql, cond) else: sql = "SELECT * FROM %s" % table Log.debug('DB -> %s' % sql) result = self.execute(sql) return result def update(self, table, dic, condition=''): k_arr = [] v_arr = [] for (k, v) in dic.items(): k_arr.append('%s=?' % k) v_arr.append(v) sql = "UPDATE %s SET %s" % (table, ','.join(k_arr)) if condition: sql += " WHERE %s" % condition Log.debug('DB -> %s' % sql) self.execute(sql, tuple(v_arr)) def get_table_column_name(self, table): """ @brief select all result from table @param table String @return result Array """ c = self.conn.cursor() c.execute("SELECT * FROM %s" % table) names = list(map(lambda x: x[0], c.description)) return names def execute(self, sql, value=None): """ @brief execute sql commands, return result if it has @param sql String @param value Tuple @return result Array """ c = self.conn.cursor() self.lock.acquire() hasReturn = sql.lstrip().upper().startswith("SELECT") try: if value: c.execute(sql, value) else: c.execute(sql) if hasReturn: result = c.fetchall() except Exception, e: Log.error(traceback.format_exc()) finally: self.lock.release() self.conn.commit() if hasReturn: return result def delete(self, table, field='', condition=''): """ @brief execute sql commands, return result if it has @param table String @param field String @param condition String """ sql = "DELETE FROM %s WHERE %s=?" % (table, field) Log.debug('DB -> %s' % sql) cond = (condition,) self.execute(sql, cond) def close(self): """ @brief close connection to database """ Log.debug('DB -> close') self.conn.close() ================================================ FILE: wxbot_project_py2.7/docker/Dockerfile ================================================ FROM ubuntu:16.04 MAINTAINER Urinx RUN apt-get update && \ apt-get install -y python \ python-dev \ python-pip && \ apt-get clean && \ apt-get autoclean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* ADD weixin_bot.tar.gz / WORKDIR /weixin_bot RUN pip install -r config/requirements.txt EXPOSE 80 ENTRYPOINT ["./weixin_bot.py"] CMD [""] ================================================ FILE: wxbot_project_py2.7/docker/README.md ================================================ # 本地构建 wechat_bot docker 镜像 拉下镜像: ```bash docker pull ubuntu:16.04 ``` 打包本项目,将压缩包放到`docker`目录下: ``` tar -czf weixin_bot.tar.gz wxbot_project_py2.7/ ``` 切换到`docker`目录,执行`build`命令: ```bash docker build -t wechat-bot . ``` 导出镜像: ```bash docker save wechat-bot > wechat.tar ``` 导入镜像: ```bash docker load < wechat.tar ``` 运行: ```bash docker run -d -P --name xxx -v /src/data/dir:/Wechat_bot/test wechat-bot ``` 删除镜像: ```bash docker rmi -f wechat-bot ``` 查看log: ```bash docker log wechat-bot ``` ================================================ FILE: wxbot_project_py2.7/flask_templates/index.html ================================================ WeChat Bot Server
	#####################
	# WeChat Bot Server #
	#####################


## APIs

- /qrcode
  ------------------------
  @brief      login qrcode
  @method     get
  @return     raw image
  ------------------------

- /runtime
  -----------------------------
  @brief      basic info
  @method     get
  @return     application/json
              {
                'ret': 0,
                'runtime': '',
                'total_size': '',
                'db_size': '',
                'msg_count': '',
                'image_count': '',
                'voice_count': '',
                'video_count': '',
              }
  -----------------------------

- /group_list
  ------------------------
  @brief      list groups
  @method     get
  ------------------------

- /group_member_list/<g_id>
  -----------------------------
  @brief      list group member
  @method     get
  @param      g_id String
  -----------------------------

- /group_chat_log/<g_name>
  -------------------------------
  @brief      list group chat log
  @method     get
  @param      g_name String
  -------------------------------

- /upload
  -------------------------------
  @brief      upload a file
  @method     get/post
  @return     application/json
              {
                'ret': 0,
                'msg': '',
              }
  -------------------------------

- /send_msg/<to>/<msg>
  -------------------------------------------
  @brief      send message to user or gourp
  @method     get
  @param      to: String, user id or group id
  @param      msg: String, words
  @return     application/json
              {
                'ret': 0,
              }
  -------------------------------------------

- /send_img/<to>/<img>
  -------------------------------------------
  @brief      send image to user or gourp
  @method     get
  @param      to: String, user id or group id
  @param      img: String, image file name
  @return     application/json
              {
                'ret': 0,
              }
  -------------------------------------------

- /send_emot/<to>/<emot>
  -------------------------------------------
  @brief      send emotion to user or gourp
  @method     get
  @param      to: String, user id or group id
  @param      emot: String, emotion file name
  @return     application/json
              {
                'ret': 0,
              }
  -------------------------------------------

- /send_file/<to>/<file>
  -------------------------------------------
  @brief      send emotion to user or gourp
  @method     get
  @param      to: String, user id or group id
  @param      file: String, file name
  @return     application/json
              {
                'ret': 0,
              }
  -------------------------------------------

- /mass_send_msg
  -------------------------------------------
  @brief      send text to mass users or gourps
  @method     post
  @param      application/json
              {
                'to_list': [
                  'group_id',
                  ...
                ],
                'msg': '',
              }
  @return     application/json
              {
                'ret': 0,
                'unsend_list': [],
              }
  -------------------------------------------

- /mass_send_img
  -------------------------------------------
  @brief      send image to mass users or gourps
  @method     post
  @param      application/json
              {
                'to_list': [
                  'group_id',
                  ...
                ],
                'msg': '',
              }
  @return     application/json
              {
                'ret': 0,
                'unsend_list': [],
              }
  -------------------------------------------

- /mass_send_emot
  -------------------------------------------
  @brief      send emoticon to mass users or gourps
  @method     post
  @param      application/json
              {
                'to_list': [
                  'group_id',
                  ...
                ],
                'msg': '',
              }
  @return     application/json
              {
                'ret': 0,
                'unsend_list': [],
              }
  -------------------------------------------

- /mass_send_file
  -------------------------------------------
  @brief      send file to mass users or gourps
  @method     post
  @param      application/json
              {
                'to_list': [
                  'group_id',
                  ...
                ],
                'msg': '',
              }
  @return     application/json
              {
                'ret': 0,
                'unsend_list': [],
              }
  -------------------------------------------
================================================ FILE: wxbot_project_py2.7/flask_templates/upload.html ================================================ Upload new File

Upload new File

================================================ FILE: wxbot_project_py2.7/wechat/__init__.py ================================================ #!/usr/bin/env python # coding: utf-8 from wechat import WeChat ================================================ FILE: wxbot_project_py2.7/wechat/utils.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from config import Log from config import Constant #--------------------------------------------------- import qrcode import re import os import sys import json import urllib import urllib2 import cookielib import cPickle as pickle import traceback import time import hashlib #=================================================== def _decode_data(data): """ @brief decode array or dict to utf-8 @param data array or dict @return utf-8 """ if isinstance(data, dict): rv = {} for key, value in data.iteritems(): if isinstance(key, unicode): key = key.encode('utf-8') rv[key] = _decode_data(value) return rv elif isinstance(data, list): rv = [] for item in data: item = _decode_data(item) rv.append(item) return rv elif isinstance(data, unicode): return data.encode('utf-8') else: return data def str2qr_terminal(text): """ @brief convert string to qrcode matrix and outprint @param text The string """ Log.debug(text) qr = qrcode.QRCode() qr.border = 1 qr.add_data(text) mat = qr.get_matrix() print_qr(mat) def str2qr_image(text, image_path): """ @brief convert string to qrcode image & save @param text The string @param image_path Save image to the path """ qr = qrcode.QRCode() qr.border = 1 qr.add_data(text) qr.make(fit=True) img = qr.make_image() img.save(image_path) def print_qr(mat): for i in mat: BLACK = Constant.QRCODE_BLACK WHITE = Constant.QRCODE_WHITE echo(''.join([BLACK if j else WHITE for j in i])+'\n') def echo(str): Log.info(str[:-1]) sys.stdout.write(str) sys.stdout.flush() def run(str, func, *args): t = time.time() echo(str) r = False try: r = func(*args) except: Log.error(traceback.format_exc()) if r: totalTime = int(time.time() - t) echo(Constant.RUN_RESULT_SUCCESS % totalTime) else: echo(Constant.RUN_RESULT_FAIL) exit() def get(url, api=None): """ @brief http get request @param url String @param api wechat api @return http response """ Log.debug('GET -> ' + url) request = urllib2.Request(url=url) request.add_header(*Constant.HTTP_HEADER_CONNECTION) request.add_header(*Constant.HTTP_HEADER_REFERER) if api in ['webwxgetvoice', 'webwxgetvideo']: request.add_header(*Constant.HTTP_HEADER_RANGE) while True: try: response = urllib2.urlopen(request, timeout=30) data = response.read() response.close() if api == None: Log.debug(data) return data except (KeyboardInterrupt, SystemExit): raise except: Log.error(traceback.format_exc()) time.sleep(1) def post(url, params, jsonfmt=True): """ @brief http post request @param url String @param params Dict, post params @param jsonfmt Bool, whether is json format @return http response """ Log.debug('POST -> '+url) Log.debug(params) if jsonfmt: request = urllib2.Request(url=url, data=json.dumps(params, ensure_ascii=False).encode('utf8')) request.add_header(*Constant.HTTP_HEADER_CONTENTTYPE) else: request = urllib2.Request(url=url, data=urllib.urlencode(params)) while True: try: response = urllib2.urlopen(request, timeout=30) data = response.read() response.close() if jsonfmt: Log.debug(data) return json.loads(data, object_hook=_decode_data) return data except (KeyboardInterrupt, SystemExit): raise except: Log.error(traceback.format_exc()) time.sleep(1) def set_cookie(cookie_file): """ @brief Load cookie from file @param cookie_file @param user_agent @return cookie, LWPCookieJar """ cookie = cookielib.LWPCookieJar(cookie_file) try: cookie.load(ignore_discard=True) except: Log.error(traceback.format_exc()) opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) opener.addheaders = Constant.HTTP_HEADER_USERAGENT urllib2.install_opener(opener) return cookie def generate_file_name(filename): """ @brief generate file name @return new file name """ i = filename.rfind('.') ext = filename[i:] tmp = filename + str(int(time.time())) hash_md5 = hashlib.md5(tmp) return hash_md5.hexdigest() + ext def save_file(filename, data, dirName): """ @brief Saves raw data to file. @param filename String @param data Binary data @param dirName String @return file path """ Log.debug('save file: ' + filename) fn = filename if not os.path.exists(dirName): os.makedirs(dirName) fn = os.path.join(dirName, filename) with open(fn, 'wb') as f: f.write(data) return fn def save_json(filename, data, dirName, mode='w+'): """ @brief Saves dict to json file. @param filename String @param data Dict @param dirName String @return file path """ Log.debug('save json: ' + filename) fn = filename if not os.path.exists(dirName): os.makedirs(dirName) fn = os.path.join(dirName, filename) with open(fn, mode) as f: f.write(json.dumps(data, indent=4)+'\n') return fn def load_json(filepath): Log.debug('load json: ' + filepath) with open(filepath, 'r') as f: return _decode_data(json.loads(f.read())) def pickle_save(data, file): """ @brief Use pickle to save python object into file @param data The pyhton data @param file The file """ basedir = os.path.dirname(file) if not os.path.exists(basedir): os.makedirs(basedir) with open(file, 'wb') as f: pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) def pickle_load(file): """ @brief Use pickle to load python object from file @param file The file @return python data """ if os.path.isfile(file): with open(file, 'rb') as f: return pickle.load(f) return None def search_content(key, content, fmat='attr'): """ @brief Search content from xml or html format @param key String @param content String @param fmat attr xml @return String """ if fmat == 'attr': pm = re.search(key + '\s?=\s?"([^"<]+)"', content) if pm: return pm.group(1) elif fmat == 'xml': pm = re.search('<{0}>([^<]+)'.format(key), content) if not pm: pm = re.search( '<{0}><\!\[CDATA\[(.*?)\]\]>'.format(key), content) if pm: return pm.group(1) return 'unknown' def is_str(s): """ @brief Determines if string. @param s String @return True if string, False otherwise. """ return isinstance(s, basestring) def trans_coding(data): """ @brief Transform string to unicode @param data String @return unicode """ if not data: return data result = None if type(data) == unicode: result = data elif type(data) == str: result = data.decode('utf-8') return result def trans_emoji(text): """ @brief Transform emoji html text to unicode @param text String @return emoji unicode """ def _emoji(matched): hex = matched.group(1) return ('\\U%08x' % int(hex, 16)).decode('unicode-escape').encode('utf-8') replace_t = re.sub(Constant.REGEX_EMOJI, _emoji, text) return replace_t def auto_reload(mod): """ @brief reload modules @param mod: the need reload modules """ try: module = sys.modules[mod] except: Log.error(traceback.format_exc()) return False filename = module.__file__ # .pyc 修改时间不会变 # 所以就用 .py 的修改时间 if filename.endswith(".pyc"): filename = filename.replace(".pyc", ".py") mod_time = os.path.getmtime(filename) if not "loadtime" in module.__dict__: module.loadtime = 0 try: if mod_time > module.loadtime: reload(module) else: return False except: Log.error(traceback.format_exc()) return False module.loadtime = mod_time echo('[*] load \'%s\' successful.\n' % mod) return True def split_array(arr, n): for i in xrange(0, len(arr), n): yield arr[i:i+n] ================================================ FILE: wxbot_project_py2.7/wechat/wechat.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from utils import * from wechat_apis import WXAPI from config import ConfigManager from config import Constant from config import Log #--------------------------------------------------- import json import re import sys import os import time import random from collections import defaultdict from datetime import timedelta import traceback import Queue import threading #=================================================== class WeChat(WXAPI): def __str__(self): description = \ "=========================\n" + \ "[#] Web WeChat\n" + \ "[#] UUID: " + self.uuid + "\n" + \ "[#] Uin: " + str(self.uin) + "\n" + \ "[#] Sid: " + self.sid + "\n" + \ "[#] Skey: " + self.skey + "\n" + \ "[#] DeviceId: " + self.device_id + "\n" + \ "[#] PassTicket: " + self.pass_ticket + "\n" + \ "[#] Run Time: " + self.get_run_time() + '\n' + \ "=========================" return description def __init__(self, host='wx.qq.com'): super(WeChat, self).__init__(host) self.db = None self.save_data_folder = '' # 保存图片,语音,小视频的文件夹 self.last_login = 0 # 上次退出的时间 self.time_out = 5 # 同步时间间隔(单位:秒) self.msg_handler = None self.start_time = time.time() self.bot = None cm = ConfigManager() self.save_data_folders = cm.get_wechat_media_dir() self.cookie_file = cm.get_cookie() self.pickle_file = cm.get_pickle_files() self.log_mode = cm.get('setting', 'log_mode') == 'True' self.exit_code = 0 def start(self): echo(Constant.LOG_MSG_START) run(Constant.LOG_MSG_RECOVER, self.recover) timeOut = time.time() - self.last_login echo(Constant.LOG_MSG_TRY_INIT) if self.webwxinit(): echo(Constant.LOG_MSG_SUCCESS) run(Constant.LOG_MSG_RECOVER_CONTACT, self.recover_contacts) else: echo(Constant.LOG_MSG_FAIL) while True: # first try to login by uin without qrcode echo(Constant.LOG_MSG_ASSOCIATION_LOGIN) if self.association_login(): echo(Constant.LOG_MSG_SUCCESS) else: echo(Constant.LOG_MSG_FAIL) # scan qrcode to login run(Constant.LOG_MSG_GET_UUID, self.getuuid) echo(Constant.LOG_MSG_GET_QRCODE) self.genqrcode() echo(Constant.LOG_MSG_SCAN_QRCODE) if not self.waitforlogin(): continue echo(Constant.LOG_MSG_CONFIRM_LOGIN) if not self.waitforlogin(0): continue break run(Constant.LOG_MSG_LOGIN, self.login) run(Constant.LOG_MSG_INIT, self.webwxinit) run(Constant.LOG_MSG_STATUS_NOTIFY, self.webwxstatusnotify) run(Constant.LOG_MSG_GET_CONTACT, self.webwxgetcontact) echo(Constant.LOG_MSG_CONTACT_COUNT % ( self.MemberCount, len(self.MemberList) )) echo(Constant.LOG_MSG_OTHER_CONTACT_COUNT % ( len(self.GroupList), len(self.ContactList), len(self.SpecialUsersList), len(self.PublicUsersList) )) run(Constant.LOG_MSG_GET_GROUP_MEMBER, self.fetch_group_contacts) run(Constant.LOG_MSG_SNAPSHOT, self.snapshot) while True: [retcode, selector] = self.synccheck() Log.debug('retcode: %s, selector: %s' % (retcode, selector)) self.exit_code = int(retcode) if retcode == '1100': echo(Constant.LOG_MSG_LOGOUT) break if retcode == '1101': echo(Constant.LOG_MSG_LOGIN_OTHERWHERE) break if retcode == '1102': echo(Constant.LOG_MSG_QUIT_ON_PHONE) break elif retcode == '0': if selector == '2': r = self.webwxsync() if r is not None: try: self.handle_msg(r) except: Log.error(traceback.format_exc()) elif selector == '7': r = self.webwxsync() elif selector == '0': time.sleep(self.time_out) elif selector == '4': # 保存群聊到通讯录 # 修改群名称 # 新增或删除联系人 # 群聊成员数目变化 r = self.webwxsync() if r is not None: try: self.handle_mod(r) except: Log.error(traceback.format_exc()) elif selector == '3' or selector == '6': break else: r = self.webwxsync() Log.debug('webwxsync: %s\n' % json.dumps(r)) # 执行定时任务 if self.msg_handler: self.msg_handler.check_schedule_task() # if self.bot: # r = self.bot.time_schedule() # if r: # for g in self.GroupList: # echo('[*] 推送 -> %s: %s' % (g['NickName'], r)) # g_id = g['UserName'] # self.webwxsendmsg(r, g_id) def get_run_time(self): """ @brief get how long this run @return String """ totalTime = int(time.time() - self.start_time) t = timedelta(seconds=totalTime) return '%s Day %s' % (t.days, t) def stop(self): """ @brief Save some data and use shell to kill this process """ run(Constant.LOG_MSG_SNAPSHOT, self.snapshot) echo(Constant.LOG_MSG_RUNTIME % self.get_run_time()) # close database connect self.db.close() def fetch_group_contacts(self): """ @brief Fetches all groups contacts. @return Bool: whether operation succeed. @note This function must be finished in 180s """ Log.debug('fetch_group_contacts') # clean database if self.msg_handler: self.msg_handler.clean_db() # sqlite # ---------------------------------------------------- # group max_thread_num max_fetch_group_num time(s) # 197 10 10 108 # 197 10 15 95 # 197 20 10 103 # 197 10 20 55 # 197 5 30 39 # 197 4 50 35 # ---------------------------------------------------- # mysql # ---------------------------------------------------- # group max_thread_num max_fetch_group_num time(s) # 197 4 50 20 # ---------------------------------------------------- max_thread_num = 4 max_fetch_group_num = 50 group_list_queue = Queue.Queue() class GroupListThread(threading.Thread): def __init__(self, group_list_queue, wechat): threading.Thread.__init__(self) self.group_list_queue = group_list_queue self.wechat = wechat def run(self): while not self.group_list_queue.empty(): g_list = self.group_list_queue.get() gid_list = [] g_dict = {} for g in g_list: gid = g['UserName'] gid_list.append(gid) g_dict[gid] = g group_member_list = self.wechat.webwxbatchgetcontact(gid_list) for member_list in group_member_list: gid = member_list['UserName'] g = g_dict[gid] g['MemberCount'] = member_list['MemberCount'] g['OwnerUin'] = member_list['OwnerUin'] self.wechat.GroupMemeberList[gid] = member_list['MemberList'] # 如果使用 Mysql 则可以在多线程里操作数据库 # 否则请注释下列代码在主线程里更新群列表 # ----------------------------------- # 处理群成员 # if self.wechat.msg_handler: # self.wechat.msg_handler.handle_group_member_list(gid, member_list['MemberList']) # ----------------------------------- self.group_list_queue.task_done() for g_list in split_array(self.GroupList, max_fetch_group_num): group_list_queue.put(g_list) for i in range(max_thread_num): t = GroupListThread(group_list_queue, self) t.setDaemon(True) t.start() group_list_queue.join() if self.msg_handler: # 处理群 if self.GroupList: self.msg_handler.handle_group_list(self.GroupList) # 这个是用 sqlite 来存储群列表,sqlite 对多线程的支持不太好 # ---------------------------------------------------- # 处理群成员 for (gid, member_list) in self.GroupMemeberList.items(): self.msg_handler.handle_group_member_list(gid, member_list) # ---------------------------------------------------- return True def snapshot(self): """ @brief Save basic infos for next login. @return Bool: whether operation succeed. """ try: conf = { 'uuid': self.uuid, 'redirect_uri': self.redirect_uri, 'uin': self.uin, 'sid': self.sid, 'skey': self.skey, 'pass_ticket': self.pass_ticket, 'synckey': self.synckey, 'device_id': self.device_id, 'last_login': time.time(), } cm = ConfigManager() Log.debug('save wechat config') cm.set_wechat_config(conf) # save cookie Log.debug('save cookie') if self.cookie: self.cookie.save(ignore_discard=True) # save contacts Log.debug('save contacts') self.save_contacts() except Exception, e: Log.error(traceback.format_exc()) return False return True def recover(self): """ @brief Recover from snapshot data. @return Bool: whether operation succeed. """ cm = ConfigManager() [self.uuid, self.redirect_uri, self.uin, self.sid, self.skey, self.pass_ticket, self.synckey, device_id, self.last_login] = cm.get_wechat_config() if device_id: self.device_id = device_id self.base_request = { 'Uin': int(self.uin), 'Sid': self.sid, 'Skey': self.skey, 'DeviceID': self.device_id, } # set cookie Log.debug('set cookie') self.cookie = set_cookie(self.cookie_file) return True def save_contacts(self): """ @brief Save contacts. """ pickle_save(self.User, self.pickle_file['User']) pickle_save(self.MemberList, self.pickle_file['MemberList']) pickle_save(self.GroupList, self.pickle_file['GroupList']) pickle_save(self.GroupMemeberList, self.pickle_file['GroupMemeberList']) pickle_save(self.SpecialUsersList, self.pickle_file['SpecialUsersList']) def recover_contacts(self): """ @brief recover contacts. @return Bool: whether operation succeed. """ try: self.User = pickle_load(self.pickle_file['User']) self.MemberList = pickle_load(self.pickle_file['MemberList']) self.GroupList = pickle_load(self.pickle_file['GroupList']) self.GroupMemeberList = pickle_load(self.pickle_file['GroupMemeberList']) self.SpecialUsersList = pickle_load(self.pickle_file['SpecialUsersList']) return True except Exception, e: Log.error(traceback.format_exc()) return False def handle_mod(self, r): # ModContactCount: 变更联系人或群聊成员数目 # ModContactList: 变更联系人或群聊列表,或群名称改变 Log.debug('handle modify') self.handle_msg(r) for m in r['ModContactList']: if m['UserName'][:2] == '@@': # group in_list = False g_id = m['UserName'] for g in self.GroupList: # group member change if g_id == g['UserName']: g['MemberCount'] = m['MemberCount'] g['NickName'] = m['NickName'] self.GroupMemeberList[g_id] = m['MemberList'] in_list = True if self.msg_handler: self.msg_handler.handle_group_member_change(g_id, m['MemberList']) break if not in_list: # a new group self.GroupList.append(m) self.GroupMemeberList[g_id] = m['MemberList'] if self.msg_handler: self.msg_handler.handle_group_list_change(m) self.msg_handler.handle_group_member_change(g_id, m['MemberList']) elif m['UserName'][0] == '@': # user in_list = False for u in self.MemberList: u_id = m['UserName'] if u_id == u['UserName']: u = m in_list = True break # if don't have then add it if not in_list: self.MemberList.append(m) def handle_msg(self, r): """ @brief Recover from snapshot data. @param r Dict: message json """ Log.debug('handle message') if self.msg_handler: self.msg_handler.handle_wxsync(r) n = len(r['AddMsgList']) if n == 0: return if self.log_mode: echo(Constant.LOG_MSG_NEW_MSG % n) for msg in r['AddMsgList']: msgType = msg['MsgType'] msgId = msg['MsgId'] content = msg['Content'].replace('<', '<').replace('>', '>') raw_msg = None if msgType == self.wx_conf['MSGTYPE_TEXT']: # 地理位置消息 if content.find('pictype=location') != -1: location = content.split('
')[1][:-1] raw_msg = { 'raw_msg': msg, 'location': location, 'log': Constant.LOG_MSG_LOCATION % location } # 普通文本消息 else: text = content.split(':
')[-1] raw_msg = { 'raw_msg': msg, 'text': text, 'log': text.replace('
', '\n') } elif msgType == self.wx_conf['MSGTYPE_IMAGE']: data = self.webwxgetmsgimg(msgId) fn = 'img_' + msgId + '.jpg' dir = self.save_data_folders['webwxgetmsgimg'] path = save_file(fn, data, dir) raw_msg = {'raw_msg': msg, 'image': path, 'log': Constant.LOG_MSG_PICTURE % path} elif msgType == self.wx_conf['MSGTYPE_VOICE']: data = self.webwxgetvoice(msgId) fn = 'voice_' + msgId + '.mp3' dir = self.save_data_folders['webwxgetvoice'] path = save_file(fn, data, dir) raw_msg = {'raw_msg': msg, 'voice': path, 'log': Constant.LOG_MSG_VOICE % path} elif msgType == self.wx_conf['MSGTYPE_SHARECARD']: info = msg['RecommendInfo'] card = Constant.LOG_MSG_NAME_CARD % ( info['NickName'], info['Alias'], info['Province'], info['City'], Constant.LOG_MSG_SEX_OPTION[info['Sex']] ) namecard = '%s %s %s %s %s' % ( info['NickName'], info['Alias'], info['Province'], info['City'], Constant.LOG_MSG_SEX_OPTION[info['Sex']] ) raw_msg = { 'raw_msg': msg, 'namecard': namecard, 'log': card } elif msgType == self.wx_conf['MSGTYPE_EMOTICON']: url = search_content('cdnurl', content) raw_msg = {'raw_msg': msg, 'emoticon': url, 'log': Constant.LOG_MSG_EMOTION % url} elif msgType == self.wx_conf['MSGTYPE_APP']: card = '' # 链接, 音乐, 微博 if msg['AppMsgType'] in [ self.wx_conf['APPMSGTYPE_AUDIO'], self.wx_conf['APPMSGTYPE_URL'], self.wx_conf['APPMSGTYPE_OPEN'] ]: card = Constant.LOG_MSG_APP_LINK % ( Constant.LOG_MSG_APP_LINK_TYPE[msg['AppMsgType']], msg['FileName'], search_content('des', content, 'xml'), msg['Url'], search_content('appname', content, 'xml') ) raw_msg = { 'raw_msg': msg, 'link': msg['Url'], 'log': card } # 图片 elif msg['AppMsgType'] == self.wx_conf['APPMSGTYPE_IMG']: data = self.webwxgetmsgimg(msgId) fn = 'img_' + msgId + '.jpg' dir = self.save_data_folders['webwxgetmsgimg'] path = save_file(fn, data, dir) card = Constant.LOG_MSG_APP_IMG % ( path, search_content('appname', content, 'xml') ) raw_msg = { 'raw_msg': msg, 'image': path, 'log': card } else: raw_msg = { 'raw_msg': msg, 'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content) } elif msgType == self.wx_conf['MSGTYPE_STATUSNOTIFY']: Log.info(Constant.LOG_MSG_NOTIFY_PHONE) elif msgType == self.wx_conf['MSGTYPE_MICROVIDEO']: data = self.webwxgetvideo(msgId) fn = 'video_' + msgId + '.mp4' dir = self.save_data_folders['webwxgetvideo'] path = save_file(fn, data, dir) raw_msg = {'raw_msg': msg, 'video': path, 'log': Constant.LOG_MSG_VIDEO % path} elif msgType == self.wx_conf['MSGTYPE_RECALLED']: recall_id = search_content('msgid', content, 'xml') text = Constant.LOG_MSG_RECALL raw_msg = { 'raw_msg': msg, 'text': text, 'recall_msg_id': recall_id, 'log': text } elif msgType == self.wx_conf['MSGTYPE_SYS']: raw_msg = { 'raw_msg': msg, 'sys_notif': content, 'log': content } elif msgType == self.wx_conf['MSGTYPE_VERIFYMSG']: name = search_content('fromnickname', content) raw_msg = { 'raw_msg': msg, 'log': Constant.LOG_MSG_ADD_FRIEND % name } else: raw_msg = { 'raw_msg': msg, 'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content) } isGroupMsg = '@@' in msg['FromUserName']+msg['ToUserName'] if self.msg_handler and raw_msg: if isGroupMsg: # handle group messages g_msg = self.make_group_msg(raw_msg) self.msg_handler.handle_group_msg(g_msg) else: # handle personal messages self.msg_handler.handle_user_msg(raw_msg) if self.log_mode: self.show_msg(raw_msg) def make_group_msg(self, msg): """ @brief Package the group message for storage. @param msg Dict: raw msg @return raw_msg Dict: packged msg """ Log.debug('make group message') raw_msg = { 'raw_msg': msg['raw_msg'], 'msg_id': msg['raw_msg']['MsgId'], 'group_owner_uin': '', 'group_name': '', 'group_count': '', 'from_user_name': msg['raw_msg']['FromUserName'], 'to_user_name': msg['raw_msg']['ToUserName'], 'user_attrstatus': '', 'user_display_name': '', 'user_nickname': '', 'msg_type': msg['raw_msg']['MsgType'], 'text': '', 'link': '', 'image': '', 'video': '', 'voice': '', 'emoticon': '', 'namecard': '', 'location': '', 'recall_msg_id': '', 'sys_notif': '', 'time': '', 'timestamp': '', 'log': '', } content = msg['raw_msg']['Content'].replace( '<', '<').replace('>', '>') group = None src = None if msg['raw_msg']['FromUserName'][:2] == '@@': # 接收到来自群的消息 g_id = msg['raw_msg']['FromUserName'] group = self.get_group_by_id(g_id) if re.search(":
", content, re.IGNORECASE): u_id = content.split(':
')[0] src = self.get_group_user_by_id(u_id, g_id) elif msg['raw_msg']['ToUserName'][:2] == '@@': # 自己发给群的消息 g_id = msg['raw_msg']['ToUserName'] u_id = msg['raw_msg']['FromUserName'] src = self.get_group_user_by_id(u_id, g_id) group = self.get_group_by_id(g_id) if src: raw_msg['user_attrstatus'] = src['AttrStatus'] raw_msg['user_display_name'] = src['DisplayName'] raw_msg['user_nickname'] = src['NickName'] if group: raw_msg['group_count'] = group['MemberCount'] raw_msg['group_owner_uin'] = group['OwnerUin'] raw_msg['group_name'] = group['ShowName'] raw_msg['timestamp'] = msg['raw_msg']['CreateTime'] t = time.localtime(float(raw_msg['timestamp'])) raw_msg['time'] = time.strftime("%Y-%m-%d %T", t) for key in [ 'text', 'link', 'image', 'video', 'voice', 'emoticon', 'namecard', 'location', 'log', 'recall_msg_id', 'sys_notif' ]: if key in msg: raw_msg[key] = msg[key] return raw_msg def show_msg(self, message): """ @brief Log the message to stdout @param message Dict """ msg = message src = None dst = None group = None if msg and msg['raw_msg']: content = msg['raw_msg']['Content'] content = content.replace('<', '<').replace('>', '>') msg_id = msg['raw_msg']['MsgId'] if msg['raw_msg']['FromUserName'][:2] == '@@': # 接收到来自群的消息 g_id = msg['raw_msg']['FromUserName'] group = self.get_group_by_id(g_id) if re.search(":
", content, re.IGNORECASE): u_id = content.split(':
')[0] src = self.get_group_user_by_id(u_id, g_id) dst = {'ShowName': 'GROUP'} else: u_id = msg['raw_msg']['ToUserName'] src = {'ShowName': 'SYSTEM'} dst = self.get_group_user_by_id(u_id, g_id) elif msg['raw_msg']['ToUserName'][:2] == '@@': # 自己发给群的消息 g_id = msg['raw_msg']['ToUserName'] u_id = msg['raw_msg']['FromUserName'] group = self.get_group_by_id(g_id) src = self.get_group_user_by_id(u_id, g_id) dst = {'ShowName': 'GROUP'} else: # 非群聊消息 src = self.get_user_by_id(msg['raw_msg']['FromUserName']) dst = self.get_user_by_id(msg['raw_msg']['ToUserName']) if group: echo('%s |%s| %s -> %s: %s\n' % ( msg_id, trans_emoji(group['ShowName']), trans_emoji(src['ShowName']), dst['ShowName'], trans_emoji(msg['log']) )) else: echo('%s %s -> %s: %s\n' % ( msg_id, trans_emoji(src['ShowName']), trans_emoji(dst['ShowName']), trans_emoji(msg['log']) )) ================================================ FILE: wxbot_project_py2.7/wechat/wechat_apis.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from utils import * from config import Constant from config import Log #--------------------------------------------------- import sys import os import cookielib import random import requests import time import xml.dom.minidom # for media upload import mimetypes from requests_toolbelt.multipart.encoder import MultipartEncoder #=================================================== class WXAPI(object): def __init__(self, host): self.wx_host = host self.wx_filehost = '' self.wx_conf = {} # jsLogin时这个appid只能使用: wx782c26e4c19acffb self.appid = Constant.API_APPID self.uuid = '' self.redirect_uri = '' self.skey = '' self.sid = '' self.uin = '' self.pass_ticket = '' self.base_request = {} self.synckey_dic = {} self.synckey = '' self.device_id = 'e' + repr(random.random())[2:17] # device_id: 登录手机设备 # web wechat 的格式为: e123456789012345 (e+15位随机数) # mobile wechat 的格式为: A1234567890abcde (A+15位随机数字或字母) self.user_agent = Constant.API_USER_AGENT self.cookie = None self.conf_factory() self.User = [] # 登陆账号信息 self.MemberList = [] # 好友+群聊+公众号+特殊账号 self.MemberCount = 0 self.ContactList = [] # 好友 self.GroupList = [] # 群 self.GroupMemeberList = {} # 群聊成员字典 # "group_id": [ # {member}, ... # ] self.PublicUsersList = [] # 公众号/服务号 self.SpecialUsersList = [] # 特殊账号 self.media_count = 0 def conf_factory(self): e = self.wx_host # wx.qq.com t, o, n = "login.weixin.qq.com", "file.wx.qq.com", "webpush.weixin.qq.com" if e.find("wx2.qq.com") > -1: t, o, n = "login.wx2.qq.com", "file.wx2.qq.com", "webpush.wx2.qq.com" elif e.find("wx8.qq.com") > -1: t, o, n = "login.wx8.qq.com", "file.wx8.qq.com", "webpush.wx8.qq.com" elif e.find("qq.com") > -1: t, o, n = "login.wx.qq.com", "file.wx.qq.com", "webpush.wx.qq.com" elif e.find("web2.wechat.com") > -1: t, o, n = "login.web2.wechat.com", "file.web2.wechat.com", "webpush.web2.wechat.com" elif e.find("wechat.com") > -1: t, o, n = "login.web.wechat.com", "file.web.wechat.com", "webpush.web.wechat.com" self.wx_filehost = o conf = { 'LANG': Constant.API_LANG, 'SpecialUsers': Constant.API_SPECIAL_USER, 'API_jsLogin': "https://" + t + "/jslogin", 'API_qrcode': "https://login.weixin.qq.com/l/", 'API_qrcode_img': "https://login.weixin.qq.com/qrcode/", 'API_login': "https://" + t + "/cgi-bin/mmwebwx-bin/login", 'API_synccheck': "https://" + n + "/cgi-bin/mmwebwx-bin/synccheck", 'API_webwxdownloadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxgetmedia", 'API_webwxuploadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxuploadmedia", 'API_webwxpreview': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpreview", 'API_webwxinit': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxinit", 'API_webwxgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetcontact", 'API_webwxsync': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsync", 'API_webwxbatchgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxbatchgetcontact", 'API_webwxgeticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgeticon", 'API_webwxsendmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsg", 'API_webwxsendmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsgimg", 'API_webwxsendmsgvedio': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendvideomsg", 'API_webwxsendemoticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendemoticon", 'API_webwxsendappmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendappmsg", 'API_webwxgetheadimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetheadimg", 'API_webwxgetmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmsgimg", 'API_webwxgetmedia': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmedia", 'API_webwxgetvideo': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvideo", 'API_webwxlogout': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxlogout", 'API_webwxgetvoice': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvoice", 'API_webwxupdatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxupdatechatroom", 'API_webwxcreatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcreatechatroom", 'API_webwxstatusnotify': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatusnotify", 'API_webwxcheckurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckurl", 'API_webwxverifyuser': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxverifyuser", 'API_webwxfeedback': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendfeedback", 'API_webwxreport': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatreport", 'API_webwxsearch': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsearchcontact", 'API_webwxoplog': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxoplog", 'API_checkupload': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckupload", 'API_webwxrevokemsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxrevokemsg", 'API_webwxpushloginurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpushloginurl", 'CONTACTFLAG_CONTACT': 1, 'CONTACTFLAG_CHATCONTACT': 2, 'CONTACTFLAG_CHATROOMCONTACT': 4, 'CONTACTFLAG_BLACKLISTCONTACT': 8, 'CONTACTFLAG_DOMAINCONTACT': 16, 'CONTACTFLAG_HIDECONTACT': 32, 'CONTACTFLAG_FAVOURCONTACT': 64, 'CONTACTFLAG_3RDAPPCONTACT': 128, 'CONTACTFLAG_SNSBLACKLISTCONTACT': 256, 'CONTACTFLAG_NOTIFYCLOSECONTACT': 512, 'CONTACTFLAG_TOPCONTACT': 2048, 'MSGTYPE_TEXT': 1, 'MSGTYPE_IMAGE': 3, 'MSGTYPE_VOICE': 34, 'MSGTYPE_VIDEO': 43, 'MSGTYPE_MICROVIDEO': 62, 'MSGTYPE_EMOTICON': 47, 'MSGTYPE_APP': 49, 'MSGTYPE_VOIPMSG': 50, 'MSGTYPE_VOIPNOTIFY': 52, 'MSGTYPE_VOIPINVITE': 53, 'MSGTYPE_LOCATION': 48, 'MSGTYPE_STATUSNOTIFY': 51, 'MSGTYPE_SYSNOTICE': 9999, 'MSGTYPE_POSSIBLEFRIEND_MSG': 40, 'MSGTYPE_VERIFYMSG': 37, 'MSGTYPE_SHARECARD': 42, 'MSGTYPE_SYS': 10000, 'MSGTYPE_RECALLED': 10002, 'APPMSGTYPE_TEXT': 1, 'APPMSGTYPE_IMG': 2, 'APPMSGTYPE_AUDIO': 3, 'APPMSGTYPE_VIDEO': 4, 'APPMSGTYPE_URL': 5, 'APPMSGTYPE_ATTACH': 6, 'APPMSGTYPE_OPEN': 7, 'APPMSGTYPE_EMOJI': 8, 'APPMSGTYPE_VOICE_REMIND': 9, 'APPMSGTYPE_SCAN_GOOD': 10, 'APPMSGTYPE_GOOD': 13, 'APPMSGTYPE_EMOTION': 15, 'APPMSGTYPE_CARD_TICKET': 16, 'APPMSGTYPE_REALTIME_SHARE_LOCATION': 17, 'APPMSGTYPE_TRANSFERS': 2e3, 'APPMSGTYPE_RED_ENVELOPES': 2001, 'APPMSGTYPE_READER_TYPE': 100001, 'UPLOAD_MEDIA_TYPE_IMAGE': 1, 'UPLOAD_MEDIA_TYPE_VIDEO': 2, 'UPLOAD_MEDIA_TYPE_AUDIO': 3, 'UPLOAD_MEDIA_TYPE_ATTACHMENT': 4, } self.wx_conf = conf def getuuid(self): """ @brief Gets the uuid just used for login. @return Bool: whether operation succeed. """ url = self.wx_conf['API_jsLogin'] params = { 'appid': self.appid, 'fun': 'new', 'lang': self.wx_conf['LANG'], '_': int(time.time()), } data = post(url, params, False) regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' pm = re.search(regx, data) if pm: code = pm.group(1) self.uuid = pm.group(2) return code == '200' return False def genqrcode(self): """ @brief outprint the qrcode to stdout on macos/linux or open image on windows """ if sys.platform.startswith('win'): url = self.wx_conf['API_qrcode_img'] + self.uuid params = { 't': 'webwx', '_': int(time.time()) } data = post(url, params, False) if data == '': return qrcode_path = save_file('qrcode.jpg', data, './') os.startfile(qrcode_path) else: str2qr_terminal(self.wx_conf['API_qrcode'] + self.uuid) def waitforlogin(self, tip=1): """ @brief wait for scaning qrcode to login @param tip 1: wait for scan qrcode 0: wait for confirm @return Bool: whether operation succeed """ time.sleep(tip) url = self.wx_conf['API_login'] + '?tip=%s&uuid=%s&_=%s' % ( tip, self.uuid, int(time.time())) data = get(url) pm = re.search(r'window.code=(\d+);', data) code = pm.group(1) if code == '201': return True elif code == '200': pm = re.search(r'window.redirect_uri="(\S+?)";', data) r_uri = pm.group(1) + '&fun=new' self.redirect_uri = r_uri self.wx_host = r_uri.split('://')[1].split('/')[0] self.conf_factory() return True elif code == '408': echo(Constant.LOG_MSG_WAIT_LOGIN_ERR1) else: echo(Constant.LOG_MSG_WAIT_LOGIN_ERR2) return False def login(self): """ @brief login redirect_uri 有效时间是从扫码成功后算起, 大概是 300 秒,在此期间可以重新登录,但获取的联系人和群ID会改变 @return Bool: whether operation succeed """ data = get(self.redirect_uri) doc = xml.dom.minidom.parseString(data) root = doc.documentElement for node in root.childNodes: if node.nodeName == 'ret': if node.childNodes[0].data != "0": return False elif node.nodeName == 'skey': self.skey = node.childNodes[0].data elif node.nodeName == 'wxsid': self.sid = node.childNodes[0].data elif node.nodeName == 'wxuin': self.uin = node.childNodes[0].data elif node.nodeName == 'pass_ticket': self.pass_ticket = node.childNodes[0].data if '' in (self.skey, self.sid, self.uin, self.pass_ticket): return False self.base_request = { 'Uin': int(self.uin), 'Sid': self.sid, 'Skey': self.skey, 'DeviceID': self.device_id, } return True def webwxinit(self): """ @brief wechat initial 掉线后 300 秒可以重新使用此 api 登录 获取的联系人和群ID保持不变 @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxinit'] + \ '?pass_ticket=%s&skey=%s&r=%s' % ( self.pass_ticket, self.skey, int(time.time()) ) params = { 'BaseRequest': self.base_request } dic = post(url, params) self.User = dic['User'] self.make_synckey(dic) return dic['BaseResponse']['Ret'] == 0 def webwxstatusnotify(self): """ @brief notify the mobile phone, this not necessary @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxstatusnotify'] + \ '?lang=%s&pass_ticket=%s' % ( self.wx_conf['LANG'], self.pass_ticket ) params = { 'BaseRequest': self.base_request, "Code": 3, "FromUserName": self.User['UserName'], "ToUserName": self.User['UserName'], "ClientMsgId": int(time.time()) } dic = post(url, params) return dic['BaseResponse']['Ret'] == 0 def webwxgetcontact(self): """ @brief get all contacts: people, group, public user, special user @return Bool: whether operation succeed """ SpecialUsers = self.wx_conf['SpecialUsers'] url = self.wx_conf['API_webwxgetcontact'] + \ '?pass_ticket=%s&skey=%s&r=%s' % ( self.pass_ticket, self.skey, int(time.time()) ) dic = post(url, {}) self.MemberCount = dic['MemberCount'] self.MemberList = dic['MemberList'] ContactList = self.MemberList[:] GroupList = self.GroupList[:] PublicUsersList = self.PublicUsersList[:] SpecialUsersList = self.SpecialUsersList[:] for i in xrange(len(ContactList) - 1, -1, -1): Contact = ContactList[i] if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 ContactList.remove(Contact) self.PublicUsersList.append(Contact) elif Contact['UserName'] in SpecialUsers: # 特殊账号 ContactList.remove(Contact) self.SpecialUsersList.append(Contact) elif Contact['UserName'].find('@@') != -1: # 群聊 ContactList.remove(Contact) self.GroupList.append(Contact) elif Contact['UserName'] == self.User['UserName']: # 自己 ContactList.remove(Contact) self.ContactList = ContactList return True def webwxbatchgetcontact(self, gid_list): """ @brief get group contacts @param gid_list, The list of group id @return List, list of group contacts """ url = self.wx_conf['API_webwxbatchgetcontact'] + \ '?type=ex&r=%s&pass_ticket=%s' % ( int(time.time()), self.pass_ticket ) params = { 'BaseRequest': self.base_request, "Count": len(gid_list), "List": [{"UserName": gid, "EncryChatRoomId": ""} for gid in gid_list] } dic = post(url, params) return dic['ContactList'] def synccheck(self): """ @brief check whether there's a message @return [retcode, selector] retcode: 0 successful 1100 logout 1101 login otherwhere selector: 0 nothing 2 message 6 unkonwn 7 webwxsync """ params = { 'r': int(time.time()), 'sid': self.sid, 'uin': self.uin, 'skey': self.skey, 'deviceid': self.device_id, 'synckey': self.synckey, '_': int(time.time()), } url = self.wx_conf['API_synccheck'] + '?' + urllib.urlencode(params) data = get(url) reg = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' pm = re.search(reg, data) retcode = pm.group(1) selector = pm.group(2) return [retcode, selector] def webwxsync(self): """ @brief sync the messages @return Dict{} """ url = self.wx_conf['API_webwxsync'] + \ '?sid=%s&skey=%s&pass_ticket=%s' % ( self.sid, self.skey, self.pass_ticket ) params = { 'BaseRequest': self.base_request, 'SyncKey': self.synckey_dic, 'rr': ~int(time.time()) } dic = post(url, params) if dic['BaseResponse']['Ret'] == 0: self.make_synckey(dic) return dic def webwxgetmsgimg(self, msgid): """ @brief get image in message @param msgid The id of message @return binary data of image """ url = self.wx_conf['API_webwxgetmsgimg'] + \ '?MsgID=%s&skey=%s' % (msgid, self.skey) data = get(url, api='webwxgetmsgimg') return data def webwxgetvoice(self, msgid): """ @brief get voice in message @param msgid The id of message @return binary data of voice """ url = self.wx_conf['API_webwxgetvoice'] + \ '?msgid=%s&skey=%s' % (msgid, self.skey) data = get(url, api='webwxgetvoice') return data def webwxgetvideo(self, msgid): """ @brief get video in message @param msgid The id of message @return binary data of video """ url = self.wx_conf['API_webwxgetvideo'] + \ '?msgid=%s&skey=%s' % (msgid, self.skey) data = get(url, api='webwxgetvideo') return data def webwxgeticon(self, id): """ @brief get user small icon @param id String @return binary data of icon """ url = self.wx_conf['API_webwxgeticon'] + \ '?username=%s&skey=%s' % (id, self.skey) data = get(url, api='webwxgeticon') return data def webwxgetheadimg(self, id): """ @brief get user head image @param id String @return binary data of image """ url = self.wx_conf['API_webwxgetheadimg'] + \ '?username=%s&skey=%s' % (id, self.skey) data = get(url, api='webwxgetheadimg') return data def webwxsendmsg(self, word, to='filehelper'): """ @brief send text message @param word String @param to User id @return dic Dict """ url = self.wx_conf['API_webwxsendmsg'] + \ '?pass_ticket=%s' % (self.pass_ticket) clientMsgId = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') params = { 'BaseRequest': self.base_request, 'Msg': { "Type": 1, "Content": trans_coding(word), "FromUserName": self.User['UserName'], "ToUserName": to, "LocalID": clientMsgId, "ClientMsgId": clientMsgId } } dic = post(url, params) return dic def webwxuploadmedia(self, file_path): """ @brief upload image @param file_path String @return Dict: json """ url = self.wx_conf['API_webwxuploadmedia'] + '?f=json' # 计数器 self.media_count = self.media_count + 1 fn = file_path # mime_type: # 'application/pdf' # 'image/jpeg' # 'image/png' # ... mime_type = mimetypes.guess_type(fn, strict=False)[0] if not mime_type: mime_type = 'text/plain' # 文档格式 # 微信服务器目前应该支持3种类型: # pic 直接显示,包含图片,表情 # video 不清楚 # doc 显示为文件,包含PDF等 media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc' time_format = "%a %b %d %Y %T GMT%z (%Z)" last_modifie_date = time.strftime(time_format, time.localtime()) file_size = os.path.getsize(fn) pass_ticket = self.pass_ticket client_media_id = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') webwx_data_ticket = '' for item in self.cookie: if item.name == 'webwx_data_ticket': webwx_data_ticket = item.value break if (webwx_data_ticket == ''): Log.error("No Cookie\n") return None uploadmediarequest = json.dumps({ "BaseRequest": self.base_request, "ClientMediaId": client_media_id, "TotalLen": file_size, "StartPos": 0, "DataLen": file_size, "MediaType": 4 }, ensure_ascii=False).encode('utf8') multipart_encoder = MultipartEncoder( fields={ 'id': 'WU_FILE_' + str(self.media_count), 'name': fn, 'type': mime_type, 'lastModifieDate': last_modifie_date, 'size': str(file_size), 'mediatype': media_type, 'uploadmediarequest': uploadmediarequest, 'webwx_data_ticket': webwx_data_ticket, 'pass_ticket': pass_ticket, 'filename': ( fn, open(fn, 'rb'), mime_type.split('/')[1] ) }, boundary=( '-----------------------------' '1575017231431605357584454111' ) ) headers = { 'Host': self.wx_filehost, 'User-Agent': self.user_agent, 'Accept': ( 'text/html,application/xhtml+xml,' 'application/xml;q=0.9,*/*;q=0.8' ), 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Referer': 'https://' + self.wx_host, 'Content-Type': multipart_encoder.content_type, 'Origin': 'https://' + self.wx_host, 'Connection': 'keep-alive', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', } r = requests.post(url, data=multipart_encoder, headers=headers) dic = json.loads(r.text) #修复无法发送Media消息BUG if dic['BaseResponse']['Ret'] == 0: return dic return None def webwxsendmsgimg(self, user_id, media_id): """ @brief send image @param user_id String @param media_id String @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxsendmsgimg'] + \ '?fun=async&f=json&pass_ticket=%s' % self.pass_ticket clientMsgId = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') data_json = { "BaseRequest": self.base_request, "Msg": { "Type": 3, "MediaId": media_id, "FromUserName": self.User['UserName'], "ToUserName": user_id, "LocalID": clientMsgId, "ClientMsgId": clientMsgId } } r = post(url, data_json) return dic['BaseResponse']['Ret'] == 0 def webwxsendemoticon(self, user_id, media_id): """ @brief send image @param user_id String @param media_id String @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxsendemoticon'] + \ '?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket clientMsgId = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') data_json = { "BaseRequest": self.base_request, "Msg": { "Type": 47, "EmojiFlag": 2, "MediaId": media_id, "FromUserName": self.User['UserName'], "ToUserName": user_id, "LocalID": clientMsgId, "ClientMsgId": clientMsgId } } r = post(url, data_json) return dic['BaseResponse']['Ret'] == 0 def webwxsendappmsg(self, user_id, data): """ @brief send app msg @param user_id String @param data Dict @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxsendappmsg'] + \ '?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket clientMsgId = str(int(time.time() * 1000)) + \ str(random.random())[:5].replace('.', '') content = ''.join([ "" % data['appid'], # 可使用其它AppID "%s" % data['title'], "", "", "%d" % data['type'], "", "", "", "", "%d" % data['totallen'], "%s" % data['attachid'], "%s" % data['fileext'], "", "", "", ]) data_json = { "BaseRequest": self.base_request, "Msg": { "Type": data['type'], "Content": content, "FromUserName": self.User['UserName'], "ToUserName": user_id, "LocalID": clientMsgId, "ClientMsgId": clientMsgId }, "Scene": 0 } r = post(url, data_json) return dic['BaseResponse']['Ret'] == 0 def webwxcreatechatroom(self, uid_arr): """ @brief create a chat group @param uid_arr [String] @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxcreatechatroom'] + '?r=%s' % int(time.time()) params = { 'BaseRequest': self.base_request, 'Topic': '', 'MemberCount': len(uid_arr), 'MemberList': [{'UserName': uid} for uid in uid_arr], } dic = post(url, params) return dic['BaseResponse']['Ret'] == 0 def webwxupdatechatroom(self, add_arr, del_arr, invite_arr): """ @brief add/delete/invite member in chat group @param add_arr [uid: String] @param del_arr [uid: String] @param invite_arr [uid: String] @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxupdatechatroom'] + '?r=%s' % int(time.time()) params = { 'BaseRequest': self.base_request, 'ChatRoomName': '', 'NewTopic': '', 'AddMemberList': add_arr, 'DelMemberList': del_arr, 'InviteMemberList': invite_arr, } dic = post(url, params) return dic['BaseResponse']['Ret'] == 0 def webwxrevokemsg(self, msgid, user_id, client_msgid): """ @brief revoke a message @param msgid String @param user_id String @param client_msgid String @return Bool: whether operation succeed """ url = self.wx_conf['API_webwxrevokemsg'] + '?r=%s' % int(time.time()) params = { 'BaseRequest': self.base_request, 'SvrMsgId': msgid, 'ToUserName': user_id, 'ClientMsgId': client_msgid } dic = post(url, params) return dic['BaseResponse']['Ret'] == 0 def webwxpushloginurl(self, uin): """ @brief push a login confirm alert to mobile device @param uin String @return dic Dict """ url = self.wx_conf['API_webwxpushloginurl'] + '?uin=%s' % uin dic = eval(get(url)) return dic def association_login(self): """ @brief login without scan qrcode @return Bool: whether operation succeed """ if self.uin != '': dic = self.webwxpushloginurl(self.uin) if dic['ret'] == '0': self.uuid = dic['uuid'] return True return False def send_text(self, user_id, text): """ @brief send text @param user_id String @param text String @return Bool: whether operation succeed """ try: dic = self.webwxsendmsg(text, user_id) return dic['BaseResponse']['Ret'] == 0 except: return False def send_img(self, user_id, file_path): """ @brief send image @param user_id String @param file_path String @return Bool: whether operation succeed """ response = self.webwxuploadmedia(file_path) media_id = "" if response is not None: media_id = response['MediaId'] return self.webwxsendmsgimg(user_id, media_id) def send_emot(self, user_id, file_path): """ @brief send emotion @param user_id String @param file_path String @return Bool: whether operation succeed """ response = self.webwxuploadmedia(file_path) media_id = "" if response is not None: media_id = response['MediaId'] return self.webwxsendemoticon(user_id, media_id) def send_file(self, user_id, file_path): """ @brief send file @param user_id String @param file_path String @return Bool: whether operation succeed """ title = file_path.split('/')[-1] data = { 'appid': Constant.API_WXAPPID, 'title': title, 'totallen': '', 'attachid': '', 'type': self.wx_conf['APPMSGTYPE_ATTACH'], 'fileext': title.split('.')[-1], } response = self.webwxuploadmedia(file_path) if response is not None: data['totallen'] = response['StartPos'] data['attachid'] = response['MediaId'] else: Log.error('File upload error') return self.webwxsendappmsg(user_id, data) def make_synckey(self, dic): self.synckey_dic = dic['SyncKey'] def foo(x): return str(x['Key']) + '_' + str(x['Val']) # synckey for synccheck self.synckey = '|'.join( [foo(keyVal) for keyVal in self.synckey_dic['List']]) def revoke_msg(self, msgid, user_id, client_msgid): """ @brief revoke a message @param msgid String @param user_id String @param client_msgid String @return Bool: whether operation succeed """ return self.webwxrevokemsg(msgid, user_id, client_msgid) # ----------------------------------------------------- # The following is getting user & group infomation apis def get_user_by_id(self, user_id): """ @brief get all type of name by user id @param user_id The id of user @return Dict: { 'UserName' # 微信动态ID 'RemarkName' # 备注 'NickName' # 微信昵称 'ShowName' # Log显示用的 } """ UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id name = { 'UserName': user_id, 'RemarkName': '', 'NickName': '', 'ShowName': '', } name['ShowName'] = UnknownPeople # yourself if user_id == self.User['UserName']: name['RemarkName'] = self.User['RemarkName'] name['NickName'] = self.User['NickName'] name['ShowName'] = name['NickName'] else: # 联系人 for member in self.MemberList: if member['UserName'] == user_id: r, n = member['RemarkName'], member['NickName'] name['RemarkName'] = r name['NickName'] = n name['ShowName'] = r if r else n break # 特殊帐号 for member in self.SpecialUsersList: if member['UserName'] == user_id: name['RemarkName'] = user_id name['NickName'] = user_id name['ShowName'] = user_id break return name def get_group_user_by_id(self, user_id, group_id): """ @brief get group user by user id @param user_id The id of user @param group_id The id of group @return Dict: { 'UserName' # 微信动态ID 'NickName' # 微信昵称 'DisplayName' # 群聊显示名称 'ShowName' # Log显示用的 'AttrStatus' # 群用户id } """ UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id name = { 'UserName': user_id, 'NickName': '', 'DisplayName': '', 'ShowName': '', 'AttrStatus': '', } name['ShowName'] = UnknownPeople # 群友 if group_id in self.GroupMemeberList: for member in self.GroupMemeberList[group_id]: if member['UserName'] == user_id: n, d = member['NickName'], member['DisplayName'] name['NickName'] = n name['DisplayName'] = d name['AttrStatus'] = member['AttrStatus'] name['ShowName'] = d if d else n break return name def get_group_by_id(self, group_id): """ @brief get basic info by group id @param group_id The id of group @return Dict: { 'UserName' # 微信动态ID 'NickName' # 微信昵称 'DisplayName' # 群聊显示名称 'ShowName' # Log显示用的 'OwnerUin' # 群主ID 'MemberCount' # 群人数 } """ UnknownGroup = Constant.LOG_MSG_UNKNOWN_GROUP_NAME + group_id group = { 'UserName': group_id, 'NickName': '', 'DisplayName': '', 'ShowName': '', 'OwnerUin': '', 'MemberCount': '', } for member in self.GroupList: if member['UserName'] == group_id: group['NickName'] = member['NickName'] group['DisplayName'] = member.get('DisplayName', '') group['ShowName'] = member.get('NickName', UnknownGroup) group['OwnerUin'] = member.get('OwnerUin', '') group['MemberCount'] = member['MemberCount'] break return group def get_user_id(self, name): """ @brief Gets the user id. @param name The user nickname or remarkname @return The user id. """ for member in self.MemberList: if name == member['RemarkName'] or name == member['NickName']: return member['UserName'] return None ================================================ FILE: wxbot_project_py2.7/weixin_bot.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from wechat import WeChat from wechat.utils import * from wx_handler import WeChatMsgProcessor from wx_handler import Bot from wx_handler import SGMail from db import SqliteDB from db import MysqlDB from config import ConfigManager from config import Constant from config import Log #--------------------------------------------------- from flask import Flask, render_template, send_file, jsonify, request import threading import traceback import os import logging import time #=================================================== cm = ConfigManager() db = SqliteDB(cm.getpath('database')) # db = MysqlDB(cm.mysql()) wechat_msg_processor = WeChatMsgProcessor(db) wechat = WeChat(cm.get('wechat', 'host')) wechat.db = db wechat.bot = Bot() wechat.msg_handler = wechat_msg_processor wechat_msg_processor.wechat = wechat PORT = int(cm.get('setting', 'server_port')) app = Flask(__name__, template_folder='flask_templates') app.config['UPLOAD_FOLDER'] = cm.getpath('uploaddir') logger = logging.getLogger('werkzeug') log_format_str = Constant.SERVER_LOG_FORMAT formatter = logging.Formatter(log_format_str) flask_log_handler = logging.FileHandler(cm.getpath('server_log_file')) flask_log_handler.setLevel(logging.INFO) flask_log_handler.setFormatter(formatter) logger.addHandler(flask_log_handler) app.logger.addHandler(flask_log_handler) # sendgrid mail sg_apikey = cm.get('sendgrid', 'api_key') from_email = cm.get('sendgrid', 'from_email') to_email = cm.get('sendgrid', 'to_email') sg = SGMail(sg_apikey, from_email, to_email) @app.route('/') def index(): return render_template(Constant.SERVER_PAGE_INDEX) @app.route('/qrcode') def qrcode(): qdir = cm.getpath('qrcodedir') if not os.path.exists(qdir): os.makedirs(qdir) image_path = '%s/%s_%d.png' % (qdir, wechat.uuid, int(time.time()*100)) s = wechat.wx_conf['API_qrcode'] + wechat.uuid str2qr_image(s, image_path) return send_file(image_path, mimetype='image/png') @app.route("/group_list") def group_list(): """ @brief list groups """ result = wechat.db.select(Constant.TABLE_GROUP_LIST()) return jsonify({'count': len(result), 'group': result}) @app.route('/group_member_list/') def group_member_list(g_id): """ @brief list group member @param g_id String """ result = wechat.db.select(Constant.TABLE_GROUP_USER_LIST(), 'RoomID', g_id) return jsonify({'count': len(result), 'member': result}) @app.route('/group_chat_log/') def group_chat_log(g_name): """ @brief list group chat log @param g_name String """ result = wechat.db.select(Constant.TABLE_GROUP_MSG_LOG, 'RoomName', g_name) return jsonify({'count': len(result), 'chats': result}) @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1] in Constant.SERVER_UPLOAD_ALLOWED_EXTENSIONS j = {'ret': 1, 'msg': ''} # check if the post request has the file part if 'file' not in request.files: j['msg'] = 'No file part' return jsonify(j) # if user does not select file, browser also # submit a empty part without filename file = request.files['file'] if file.filename == '': j['msg'] = 'No selected file' elif file and allowed_file(file.filename): filename = generate_file_name(file.filename) file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(file_path) j['ret'] = 0 j['msg'] = filename else: j['msg'] = 'File type not support' return jsonify(j) else: return render_template(Constant.SERVER_PAGE_UPLOAD) @app.route('/send_msg//') def send_msg(to, msg): """ @brief send message to user or gourp @param to: String, user id or group id @param msg: String, words """ return jsonify({'ret': 0 if wechat.send_text(to, msg) else 1}) @app.route('/send_img//') def send_img(to, img): """ @brief send image to user or gourp @param to: String, user id or group id @param img: String, image file name """ img_path = os.path.join(app.config['UPLOAD_FOLDER'], img) return jsonify({'ret': 0 if wechat.send_img(to, img_path) else 1}) @app.route('/send_emot//') def send_emot(to, emot): """ @brief send emotion to user or gourp @param to: String, user id or group id @param emot: String, emotion file name """ emot_path = os.path.join(app.config['UPLOAD_FOLDER'], emot) return jsonify({'ret': 0 if wechat.send_emot(to, emot_path) else 1}) @app.route('/send_file//') def send_file(to, file): """ @brief send file to user or gourp @param to: String, user id or group id @param file: String, file name """ file_path = os.path.join(app.config['UPLOAD_FOLDER'], file) return jsonify({'ret': 0 if wechat.send_file(to, file_path) else 1}) def mass_send(method, data, func): j = {'ret': -1, 'unsend_list':[]} if method == 'POST' and data: to_list = data['to_list'] msg = data['msg'] media_type = data.get('media_type', '') if media_type in ['img', 'emot']: file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg) response = wechat.webwxuploadmedia(file_path) if response is not None: msg = response['MediaId'] elif media_type == 'file': file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg) data = { 'appid': Constant.API_WXAPPID, 'title': msg, 'totallen': '', 'attachid': '', 'type': wechat.wx_conf['APPMSGTYPE_ATTACH'], 'fileext': msg.split('.')[-1], } response = wechat.webwxuploadmedia(file_path) if response is not None: data['totallen'] = response['StartPos'] data['attachid'] = response['MediaId'] else: Log.error('File upload error') msg = data for groups in split_array(to_list, 20): for g in groups: r = func(g, msg) if not r: j['unsend_list'].append(g) time.sleep(1) j['ret'] = len(j['unsend_list']) return j @app.route('/mass_send_msg/', methods=["GET", "POST"]) def mass_send_msg(): """ @brief send text to mass users or gourps """ j = mass_send(request.method, request.json, wechat.send_text) return jsonify(j) @app.route('/mass_send_img', methods=["GET", "POST"]) def mass_send_img(): """ @brief send iamge to mass users or gourps """ j = mass_send(request.method, request.json, wechat.webwxsendmsgimg) return jsonify(j) @app.route('/mass_send_emot', methods=["GET", "POST"]) def mass_send_emot(): """ @brief send emoticon to mass users or gourps """ j = mass_send(request.method, request.json, wechat.webwxsendemoticon) return jsonify(j) @app.route('/mass_send_file', methods=["GET", "POST"]) def mass_send_file(): """ @brief send file to mass users or gourps """ j = mass_send(request.method, request.json, wechat.webwxsendappmsg) return jsonify(j) def run_server(): app.run(port=PORT) if cm.get('setting', 'server_mode') == 'True': serverProcess = threading.Thread(target=run_server) serverProcess.start() while True: try: wechat.start() except KeyboardInterrupt: echo(Constant.LOG_MSG_QUIT) wechat.exit_code = 1 else: Log.error(traceback.format_exc()) finally: wechat.stop() # send a mail to tell the wxbot is failing subject = 'wxbot stop message' log_file = open(eval(cm.get('handler_fileHandler', 'args'))[0], 'r') mail_content = '
' + str(wechat) + '\n\n-----\nLogs:\n-----\n\n' + ''.join(log_file.readlines()[-100:]) + '
' sg.send_mail(subject, mail_content, 'text/html') log_file.close() if wechat.exit_code == 0: echo(Constant.MAIN_RESTART) else: # kill process os.system(Constant.LOG_MSG_KILL_PROCESS % os.getpid()) ================================================ FILE: wxbot_project_py2.7/wx_handler/__init__.py ================================================ #!/usr/bin/env python # coding: utf-8 from wechat_msg_processor import WeChatMsgProcessor from bot import Bot from sendgrid_mail import SGMail ================================================ FILE: wxbot_project_py2.7/wx_handler/bot.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from wechat.utils import * from config import Constant #--------------------------------------------------- import random, time, json #=================================================== class Bot(object): def __init__(self): self.emoticons = Constant.EMOTICON self.gifs = [] self.last_time = time.time() def time_schedule(self): r = '' now = time.time() if int(now - self.last_time) > 3600: self.last_time = now url_latest = Constant.BOT_ZHIHU_URL_LATEST url_daily = Constant.BOT_ZHIHU_URL_DAILY data = get(url_latest) j = json.loads(data) story = j['stories'][random.randint(0, len(j['stories'])-1)] r = story['title'] + '\n' + url_daily + str(story['id']) return r.encode('utf-8') def reply(self, text): APIKEY = Constant.BOT_TULING_API_KEY api_url = Constant.BOT_TULING_API_URL % (APIKEY, text, '12345678') r = json.loads(get(api_url)) if r.get('code') == 100000 and r.get('text') != Constant.BOT_TULING_BOT_REPLY: p = random.randint(1, 10) if p > 3: return r['text'] elif p > 1: # send emoji if random.randint(1, 10) > 5: n = random.randint(0, len(self.emoticons)-1) m = random.randint(1, 3) reply = self.emoticons[n].encode('utf-8') * m return reply return '' ================================================ FILE: wxbot_project_py2.7/wx_handler/sendgrid_mail.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== import sendgrid from sendgrid.helpers.mail import * #=================================================== class SGMail(object): def __init__(self, apikey, from_email, to_email): self.sg = sendgrid.SendGridAPIClient(apikey=apikey) self.from_email = Email(from_email) self.to_email = Email(to_email) def send_mail(self, subject, plain_content, type='text/plain'): content = Content(type, plain_content) mail = Mail(self.from_email, subject, self.to_email, content) response = self.sg.client.mail.send.post(request_body=mail.get()) return response.status_code == 202 ================================================ FILE: wxbot_project_py2.7/wx_handler/wechat_msg_processor.py ================================================ #!/usr/bin/env python # coding: utf-8 #=================================================== from wechat.utils import * from config import ConfigManager from config import Constant from config import Log #--------------------------------------------------- import os import time import json import re #=================================================== class WeChatMsgProcessor(object): """ Process fetched data """ def __init__(self, db): self.db = db self.wechat = None # recieve `WeChat` class instance # for call some wechat apis # read config cm = ConfigManager() [self.upload_dir, self.data_dir, self.log_dir] = cm.setup_database() def clean_db(self): """ @brief clean database, delete table & create table """ self.db.delete_table(Constant.TABLE_GROUP_LIST()) self.db.delete_table(Constant.TABLE_GROUP_USER_LIST()) self.db.create_table(Constant.TABLE_GROUP_MSG_LOG, Constant.TABLE_GROUP_MSG_LOG_COL) self.db.create_table(Constant.TABLE_GROUP_LIST(), Constant.TABLE_GROUP_LIST_COL) self.db.create_table(Constant.TABLE_GROUP_USER_LIST(), Constant.TABLE_GROUP_USER_LIST_COL) self.db.create_table(Constant.TABLE_RECORD_ENTER_GROUP, Constant.TABLE_RECORD_ENTER_GROUP_COL) self.db.create_table(Constant.TABLE_RECORD_RENAME_GROUP, Constant.TABLE_RECORD_RENAME_GROUP_COL) def handle_wxsync(self, msg): """ @brief Recieve webwxsync message, saved into json @param msg Dict: webwxsync msg """ fn = time.strftime(Constant.LOG_MSG_FILE, time.localtime()) save_json(fn, msg, self.log_dir, 'a+') def handle_group_list(self, group_list): """ @brief handle group list & saved in DB @param group_list Array """ fn = Constant.LOG_MSG_GROUP_LIST_FILE save_json(fn, group_list, self.data_dir) cols = [( g['NickName'], g['UserName'], g['OwnerUin'], g['MemberCount'], g['HeadImgUrl'] ) for g in group_list] self.db.insertmany(Constant.TABLE_GROUP_LIST(), cols) def handle_group_member_list(self, group_id, member_list): """ @brief handle group member list & saved in DB @param member_list Array """ fn = group_id + '.json' save_json(fn, member_list, self.data_dir) cols = [( group_id, m['UserName'], m['NickName'], m['DisplayName'], m['AttrStatus'] ) for m in member_list] self.db.insertmany(Constant.TABLE_GROUP_USER_LIST(), cols) def handle_group_list_change(self, new_group): """ @brief handle adding a new group & saved in DB @param new_group Dict """ self.handle_group_list([new_group]) def handle_group_member_change(self, group_id, member_list): """ @brief handle group member changes & saved in DB @param group_id Dict @param member_list Dict """ self.db.delete(Constant.TABLE_GROUP_USER_LIST(), "RoomID", group_id) self.handle_group_member_list(group_id, member_list) def handle_group_msg(self, msg): """ @brief Recieve group messages @param msg Dict: packaged msg """ # rename media files for k in ['image', 'video', 'voice']: if msg[k]: t = time.localtime(float(msg['timestamp'])) time_str = time.strftime("%Y%m%d%H%M%S", t) # format: 时间_消息ID_群名 file_name = '/%s_%s_%s.' % (time_str, msg['msg_id'], msg['group_name']) new_name = re.sub(r'\/\w+\_\d+\.', file_name, msg[k]) Log.debug('rename file to %s' % new_name) os.rename(msg[k], new_name) msg[k] = new_name if msg['msg_type'] == 10000: # record member enter in group m = re.search(r'邀请(.+)加入了群聊', msg['sys_notif']) if m: name = m.group(1) col_enter_group = ( msg['msg_id'], msg['group_name'], msg['from_user_name'], msg['to_user_name'], name, msg['time'], ) self.db.insert(Constant.TABLE_RECORD_ENTER_GROUP, col_enter_group) # record rename group n = re.search(r'(.+)修改群名为“(.+)”', msg['sys_notif']) if n: people = n.group(1) to_name = n.group(2) col_rename_group = ( msg['msg_id'], msg['group_name'], to_name, people, msg['time'], ) self.db.insert(Constant.TABLE_RECORD_RENAME_GROUP, col_rename_group) # upadte group in GroupList for g in self.wechat.GroupList: if g['UserName'] == msg['from_user_name']: g['NickName'] = to_name break # normal group message col = ( msg['msg_id'], msg['group_owner_uin'], msg['group_name'], msg['group_count'], msg['from_user_name'], msg['to_user_name'], msg['user_attrstatus'], msg['user_display_name'], msg['user_nickname'], msg['msg_type'], msg['emoticon'], msg['text'], msg['image'], msg['video'], msg['voice'], msg['link'], msg['namecard'], msg['location'], msg['recall_msg_id'], msg['sys_notif'], msg['time'], msg['timestamp'] ) self.db.insert(Constant.TABLE_GROUP_MSG_LOG, col) text = msg['text'] if text and text[0] == '@': n = trans_coding(text).find(u'\u2005') name = trans_coding(text)[1:n].encode('utf-8') if name in [self.wechat.User['NickName'], self.wechat.User['RemarkName']]: self.handle_command(trans_coding(text)[n+1:].encode('utf-8'), msg) def handle_user_msg(self, msg): """ @brief Recieve personal messages @param msg Dict """ wechat = self.wechat text = trans_coding(msg['text']).encode('utf-8') uid = msg['raw_msg']['FromUserName'] if text == 'test_revoke': # 撤回消息测试 dic = wechat.webwxsendmsg('这条消息将被撤回', uid) wechat.revoke_msg(dic['MsgID'], uid, dic['LocalID']) elif text == 'reply': wechat.send_text(uid, '自动回复') def handle_command(self, cmd, msg): """ @brief handle msg of `@yourself cmd` @param cmd String @param msg Dict """ wechat = self.wechat g_id = '' for g in wechat.GroupList: if g['NickName'] == msg['group_name']: g_id = g['UserName'] cmd = cmd.strip() if cmd == 'runtime': wechat.send_text(g_id, wechat.get_run_time()) elif cmd == 'test_sendimg': wechat.send_img(g_id, 'test/emotion/7.gif') elif cmd == 'test_sendfile': wechat.send_file(g_id, 'test/Data/upload/shake.wav') elif cmd == 'test_bot': # reply bot # --------- if wechat.bot: r = wechat.bot.reply(cmd) if r: wechat.send_text(g_id, r) else: pass elif cmd == 'test_emot': img_name = [ '0.jpg', '1.jpeg', '2.gif', '3.jpg', '4.jpeg', '5.gif', '6.gif', '7.gif', '8.jpg', '9.jpg' ] name = img_name[int(time.time()) % 10] emot_path = os.path.join('test/emotion/', name) wechat.send_emot(g_id, emot_path) else: pass def check_schedule_task(self): # update group member list at 00:00 am every morning t = time.localtime() if t.tm_hour == 0 and t.tm_min <= 1: # update group member Log.debug('update group member list everyday') self.db.delete_table(Constant.TABLE_GROUP_LIST()) self.db.delete_table(Constant.TABLE_GROUP_USER_LIST()) self.db.create_table(Constant.TABLE_GROUP_LIST(), Constant.TABLE_GROUP_LIST_COL) self.db.create_table(Constant.TABLE_GROUP_USER_LIST(), Constant.TABLE_GROUP_USER_LIST_COL) self.wechat.fetch_group_contacts()