├── .gitignore ├── LICENSE ├── README.md ├── imgs ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 8.jpg └── groupQrcode.jpg ├── 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 /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # pycharm 94 | .idea 95 | 96 | *.pyc 97 | wxbot_demo_py3/saved/ 98 | wxbot_project_py2.7/tmp_data/ 99 | wxbot_project_py2.7/wechat/wechat_js_backup/ 100 | wxbot_project_py2.7/config/wechat.conf 101 | wxbot_project_py2.7/wiki/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | 网页版微信API,包含终端版微信及微信机器人 4 | 5 | ## Contents 6 | * [Demo](#Demo) 7 | * [Web Weixin Pipeline](#Web-Weixin-Pipeline) 8 | * [Web Weixin API](#Web-Weixin-API) 9 | * [Discussion Group](#Discussion-Group) 10 | * [Recent Update](#Recent-Update) 11 | 12 | ## Demo 13 | 为了确保能正常运行示例脚本,请安装所需的第三方包。 14 | 15 | ``` 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | 注:下面演示的图片与功能可能不是最新的,具体请看源码。 20 | 21 |
22 | 23 |
24 | 25 | 按照操作指示在手机微信上扫描二维码然后登录,你可以选择是否开启自动回复模式。 26 | 27 | ![2](imgs/2.png) 28 | 29 | 开启自动回复模式后,如果接收到的是文字消息就会自动回复,包括群消息。 30 | 31 | ![3](imgs/3.png) 32 | 33 | 名片,链接,动画表情和地址位置消息。 34 | 35 | ![4](imgs/4.png) 36 | 37 | ![5](imgs/5.png) 38 | 39 | 网页版上有的功能目前基本上都能支持。 40 | 41 | ## Web Weixin Pipeline 42 | 43 | ``` 44 | +--------------+ +---------------+ +---------------+ 45 | | | | | | | 46 | | Get UUID | | Get Contact | | Status Notify | 47 | | | | | | | 48 | +-------+------+ +-------^-------+ +-------^-------+ 49 | | | | 50 | | +-------+ +--------+ 51 | | | | 52 | +-------v------+ +-----+--+------+ +--------------+ 53 | | | | | | | 54 | | Get QRCode | | Weixin Init +------> Sync Check <----+ 55 | | | | | | | | 56 | +-------+------+ +-------^-------+ +-------+------+ | 57 | | | | | 58 | | | +-----------+ 59 | | | | 60 | +-------v------+ +-------+--------+ +-------v-------+ 61 | | | Confirm Login | | | | 62 | +------> Login +---------------> New Login Page | | Weixin Sync | 63 | | | | | | | | 64 | | +------+-------+ +----------------+ +---------------+ 65 | | | 66 | |QRCode Scaned| 67 | +-------------+ 68 | ``` 69 | 70 | 71 | ## Web Weixin API 72 | 73 | ### 登录 74 | 75 | | API | 获取 UUID | 76 | | --- | --------- | 77 | | url | https://login.weixin.qq.com/jslogin | 78 | | method | POST | 79 | | data | URL Encode | 80 | | params | **appid**: `应用ID`
**fun**: new `应用类型`
**lang**: zh\_CN `语言`
**_**: `时间戳` | 81 | 82 | 返回数据(String): 83 | ``` 84 | window.QRLogin.code = 200; window.QRLogin.uuid = "xxx" 85 | ``` 86 | > 注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是`wx782c26e4c19acffb`,在微信客户端上显示为应用名称为`Web微信`;现在用的是`wxeb7ec651dd0aefa9`,显示名称为`微信网页版`。 87 | 88 |
89 | 90 |
91 | 92 |
93 | 94 | | API | 绑定登陆(webwxpushloginurl) | 95 | | --- | --------- | 96 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxpushloginurl | 97 | | method | GET | 98 | | params | **uin**: xxx | 99 | 100 | 返回数据(String): 101 | ``` 102 | {'msg': 'all ok', 'uuid': 'xxx', 'ret': '0'} 103 | 104 | 通过这种方式可以省掉扫二维码这步操作,更加方便 105 | ``` 106 |
107 | 108 | | API | 生成二维码 | 109 | | --- | --------- | 110 | | url | https://login.weixin.qq.com/l/ `uuid` | 111 | | method | GET | 112 |
113 | 114 | | API | 二维码扫描登录 | 115 | | --- | --------- | 116 | | url | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login | 117 | | method | GET | 118 | | params | **tip**: 1 `未扫描` 0 `已扫描`
**uuid**: xxx
**_**: `时间戳` | 119 | 120 | 返回数据(String): 121 | ``` 122 | window.code=xxx; 123 | 124 | xxx: 125 | 408 登陆超时 126 | 201 扫描成功 127 | 200 确认登录 128 | 129 | 当返回200时,还会有 130 | window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx"; 131 | ``` 132 |
133 | 134 | | API | webwxnewloginpage | 135 | | --- | --------- | 136 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage | 137 | | method | GET | 138 | | params | **ticket**: xxx
**uuid**: xxx
**lang**: zh_CN `语言`
**scan**: xxx
**fun**: new | 139 | 140 | 返回数据(XML): 141 | ``` 142 | 143 | 0 144 | OK 145 | xxx 146 | xxx 147 | xxx 148 | xxx 149 | 1 150 | 151 | ``` 152 |
153 | 154 | ### 微信初始化 155 | 156 | | API | webwxinit | 157 | | --- | --------- | 158 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket=xxx&skey=xxx&r=xxx | 159 | | method | POST | 160 | | data | JSON | 161 | | header | ContentType: application/json; charset=UTF-8 | 162 | | params | {
     BaseRequest: {
         Uin: xxx,
         Sid: xxx,
         Skey: xxx,
         DeviceID: xxx,
     }
} | 163 | 164 | 返回数据(JSON): 165 | ``` 166 | { 167 | "BaseResponse": { 168 | "Ret": 0, 169 | "ErrMsg": "" 170 | }, 171 | "Count": 11, 172 | "ContactList": [...], 173 | "SyncKey": { 174 | "Count": 4, 175 | "List": [ 176 | { 177 | "Key": 1, 178 | "Val": 635705559 179 | }, 180 | ... 181 | ] 182 | }, 183 | "User": { 184 | "Uin": xxx, 185 | "UserName": xxx, 186 | "NickName": xxx, 187 | "HeadImgUrl": xxx, 188 | "RemarkName": "", 189 | "PYInitial": "", 190 | "PYQuanPin": "", 191 | "RemarkPYInitial": "", 192 | "RemarkPYQuanPin": "", 193 | "HideInputBarFlag": 0, 194 | "StarFriend": 0, 195 | "Sex": 1, 196 | "Signature": "Apt-get install B", 197 | "AppAccountFlag": 0, 198 | "VerifyFlag": 0, 199 | "ContactFlag": 0, 200 | "WebWxPluginSwitch": 0, 201 | "HeadImgFlag": 1, 202 | "SnsFlag": 17 203 | }, 204 | "ChatSet": xxx, 205 | "SKey": xxx, 206 | "ClientVersion": 369297683, 207 | "SystemTime": 1453124908, 208 | "GrayScale": 1, 209 | "InviteStartCount": 40, 210 | "MPSubscribeMsgCount": 2, 211 | "MPSubscribeMsgList": [...], 212 | "ClickReportInterval": 600000 213 | } 214 | ``` 215 |
216 | 217 | | API | webwxstatusnotify | 218 | | --- | --------- | 219 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify?lang=zh_CN&pass_ticket=xxx | 220 | | method | POST | 221 | | data | JSON | 222 | | header | ContentType: application/json; charset=UTF-8 | 223 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Code: 3,
     FromUserName: `自己ID`,
     ToUserName: `自己ID`,
     ClientMsgId: `时间戳`
} | 224 | 225 | 返回数据(JSON): 226 | ``` 227 | { 228 | "BaseResponse": { 229 | "Ret": 0, 230 | "ErrMsg": "" 231 | }, 232 | ... 233 | } 234 | ``` 235 |
236 | 237 | ### 获取联系人信息 238 | 239 | | API | webwxgetcontact | 240 | | --- | --------- | 241 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin//webwxgetcontact?pass_ticket=xxx&skey=xxx&r=xxx | 242 | | method | POST | 243 | | data | JSON | 244 | | header | ContentType: application/json; charset=UTF-8 | 245 | 246 | 返回数据(JSON): 247 | ``` 248 | { 249 | "BaseResponse": { 250 | "Ret": 0, 251 | "ErrMsg": "" 252 | }, 253 | "MemberCount": 334, 254 | "MemberList": [ 255 | { 256 | "Uin": 0, 257 | "UserName": xxx, 258 | "NickName": "Urinx", 259 | "HeadImgUrl": xxx, 260 | "ContactFlag": 3, 261 | "MemberCount": 0, 262 | "MemberList": [], 263 | "RemarkName": "", 264 | "HideInputBarFlag": 0, 265 | "Sex": 0, 266 | "Signature": "你好,我们是地球三体组织。在这里,你将感受到不一样的思维模式,以及颠覆常规的世界观。而我们的目标,就是以三体人的智慧,引领人类未来科学技术500年。", 267 | "VerifyFlag": 8, 268 | "OwnerUin": 0, 269 | "PYInitial": "URINX", 270 | "PYQuanPin": "Urinx", 271 | "RemarkPYInitial": "", 272 | "RemarkPYQuanPin": "", 273 | "StarFriend": 0, 274 | "AppAccountFlag": 0, 275 | "Statues": 0, 276 | "AttrStatus": 0, 277 | "Province": "", 278 | "City": "", 279 | "Alias": "Urinxs", 280 | "SnsFlag": 0, 281 | "UniFriend": 0, 282 | "DisplayName": "", 283 | "ChatRoomId": 0, 284 | "KeyWord": "gh_", 285 | "EncryChatRoomId": "" 286 | }, 287 | ... 288 | ], 289 | "Seq": 0 290 | } 291 | ``` 292 |
293 | 294 | | API | webwxbatchgetcontact | 295 | | --- | --------- | 296 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=xxx&pass_ticket=xxx | 297 | | method | POST | 298 | | data | JSON | 299 | | header | ContentType: application/json; charset=UTF-8 | 300 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Count: `群数量`,
     List: [
         { UserName: `群ID`, EncryChatRoomId: "" },
         ...
     ],
} | 301 | 302 | 返回数据(JSON)同上 303 |

304 | 305 | ### 同步刷新 306 | 307 | | API | synccheck | 308 | | --- | --------- | 309 | | protocol | https | 310 | | 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 | 311 | | path | /cgi-bin/mmwebwx-bin/synccheck | 312 | | method | GET | 313 | | data | URL Encode | 314 | | params | **r**: `时间戳`
**sid**: xxx
**uin**: xxx
**skey**: xxx
**deviceid**: xxx
**synckey**: xxx
**_**: `时间戳` | 315 | 316 | 返回数据(String): 317 | ``` 318 | window.synccheck={retcode:"xxx",selector:"xxx"} 319 | 320 | retcode: 321 | 0 正常 322 | 1100 失败/登出微信 323 | selector: 324 | 0 正常 325 | 2 新的消息 326 | 7 进入/离开聊天界面 327 | ``` 328 |
329 | 330 | | API | webwxsync | 331 | | --- | --------- | 332 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=xxx&skey=xxx&pass_ticket=xxx | 333 | | method | POST | 334 | | data | JSON | 335 | | header | ContentType: application/json; charset=UTF-8 | 336 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     SyncKey: xxx,
     rr: `时间戳取反`
} | 337 | 338 | 返回数据(JSON): 339 | ``` 340 | { 341 | 'BaseResponse': {'ErrMsg': '', 'Ret': 0}, 342 | 'SyncKey': { 343 | 'Count': 7, 344 | 'List': [ 345 | {'Val': 636214192, 'Key': 1}, 346 | ... 347 | ] 348 | }, 349 | 'ContinueFlag': 0, 350 | 'AddMsgCount': 1, 351 | 'AddMsgList': [ 352 | { 353 | 'FromUserName': '', 354 | 'PlayLength': 0, 355 | 'RecommendInfo': {...}, 356 | 'Content': "", 357 | 'StatusNotifyUserName': '', 358 | 'StatusNotifyCode': 5, 359 | 'Status': 3, 360 | 'VoiceLength': 0, 361 | 'ToUserName': '', 362 | 'ForwardFlag': 0, 363 | 'AppMsgType': 0, 364 | 'AppInfo': {'Type': 0, 'AppID': ''}, 365 | 'Url': '', 366 | 'ImgStatus': 1, 367 | 'MsgType': 51, 368 | 'ImgHeight': 0, 369 | 'MediaId': '', 370 | 'FileName': '', 371 | 'FileSize': '', 372 | ... 373 | }, 374 | ... 375 | ], 376 | 'ModChatRoomMemberCount': 0, 377 | 'ModContactList': [], 378 | 'DelContactList': [], 379 | 'ModChatRoomMemberList': [], 380 | 'DelContactCount': 0, 381 | ... 382 | } 383 | ``` 384 |
385 | 386 | ### 消息接口 387 | 388 | | API | webwxsendmsg | 389 | | --- | ------------ | 390 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=xxx | 391 | | method | POST | 392 | | data | JSON | 393 | | header | ContentType: application/json; charset=UTF-8 | 394 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Msg: {
         Type: 1 `文字消息`,
         Content: `要发送的消息`,
         FromUserName: `自己ID`,
         ToUserName: `好友ID`,
         LocalID: `与clientMsgId相同`,
         ClientMsgId: `时间戳左移4位随后补上4位随机数`
     }
} | 395 | 396 | 返回数据(JSON): 397 | ``` 398 | { 399 | "BaseResponse": { 400 | "Ret": 0, 401 | "ErrMsg": "" 402 | }, 403 | ... 404 | } 405 | ``` 406 | 407 | | API | webwxrevokemsg | 408 | | --- | ------------ | 409 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg | 410 | | method | POST | 411 | | data | JSON | 412 | | header | ContentType: application/json; charset=UTF-8 | 413 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     SvrMsgId: msg_id,
     ToUserName: user_id,
     ClientMsgId: local_msg_id
} | 414 | 415 | 返回数据(JSON): 416 | ``` 417 | { 418 | "BaseResponse": { 419 | "Ret": 0, 420 | "ErrMsg": "" 421 | } 422 | } 423 | ``` 424 | 425 | #### 发送表情 426 | 427 | | API | webwxsendmsgemotion | 428 | | --- | ------------ | 429 | | url | https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=xxx | 430 | | method | POST | 431 | | data | JSON | 432 | | header | ContentType: application/json; charset=UTF-8 | 433 | | 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位随机数`
     }
} | 434 | 435 |
436 | 437 | ### 图片接口 438 | 439 | | API | webwxgeticon | 440 | | --- | ------------ | 441 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgeticon | 442 | | method | GET | 443 | | params | **seq**: `数字,可为空`
**username**: `ID`
**skey**: xxx | 444 |
445 | 446 | | API | webwxgetheadimg | 447 | | --- | --------------- | 448 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetheadimg | 449 | | method | GET | 450 | | params | **seq**: `数字,可为空`
**username**: `群ID`
**skey**: xxx | 451 |
452 | 453 | | API | webwxgetmsgimg | 454 | | --- | --------------- | 455 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg | 456 | | method | GET | 457 | | params | **MsgID**: `消息ID`
**type**: slave `略缩图` or `为空时加载原图`
**skey**: xxx | 458 |
459 | 460 | ### 多媒体接口 461 | 462 | | API | webwxgetvideo | 463 | | --- | --------------- | 464 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo | 465 | | method | GET | 466 | | params | **msgid**: `消息ID`
**skey**: xxx | 467 |
468 | 469 | | API | webwxgetvoice | 470 | | --- | --------------- | 471 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice | 472 | | method | GET | 473 | | params | **msgid**: `消息ID`
**skey**: xxx | 474 |
475 | 476 | ### 账号类型 477 | 478 | | 类型 | 说明 | 479 | | :--: | --- | 480 | | 个人账号 | 以`@`开头,例如:`@xxx` | 481 | | 群聊 | 以`@@`开头,例如:`@@xxx` | 482 | | 公众号/服务号 | 以`@`开头,但其`VerifyFlag` & 8 != 0

`VerifyFlag`:
         一般个人公众号/服务号:8
         一般企业的服务号:24
         微信官方账号`微信团队`:56 | 483 | | 特殊账号 | 像文件传输助手之类的账号,有特殊的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` | 484 |
485 | 486 | ### 消息类型 487 | 488 | 消息一般格式: 489 | ``` 490 | { 491 | "FromUserName": "", 492 | "ToUserName": "", 493 | "Content": "", 494 | "StatusNotifyUserName": "", 495 | "ImgWidth": 0, 496 | "PlayLength": 0, 497 | "RecommendInfo": {...}, 498 | "StatusNotifyCode": 4, 499 | "NewMsgId": "", 500 | "Status": 3, 501 | "VoiceLength": 0, 502 | "ForwardFlag": 0, 503 | "AppMsgType": 0, 504 | "Ticket": "", 505 | "AppInfo": {...}, 506 | "Url": "", 507 | "ImgStatus": 1, 508 | "MsgType": 1, 509 | "ImgHeight": 0, 510 | "MediaId": "", 511 | "MsgId": "", 512 | "FileName": "", 513 | "HasProductId": 0, 514 | "FileSize": "", 515 | "CreateTime": 1454602196, 516 | "SubMsgType": 0 517 | } 518 | ``` 519 |
520 | 521 | | MsgType | 说明 | 522 | | ------- | --- | 523 | | 1 | 文本消息 | 524 | | 3 | 图片消息 | 525 | | 34 | 语音消息 | 526 | | 37 | 好友确认消息 | 527 | | 40 | POSSIBLEFRIEND_MSG | 528 | | 42 | 共享名片 | 529 | | 43 | 视频消息 | 530 | | 47 | 动画表情 | 531 | | 48 | 位置消息 | 532 | | 49 | 分享链接 | 533 | | 50 | VOIPMSG | 534 | | 51 | 微信初始化消息 | 535 | | 52 | VOIPNOTIFY | 536 | | 53 | VOIPINVITE | 537 | | 62 | 小视频 | 538 | | 9999 | SYSNOTICE | 539 | | 10000 | 系统消息 | 540 | | 10002 | 撤回消息 | 541 |
542 | 543 | **微信初始化消息** 544 | ```html 545 | MsgType: 51 546 | FromUserName: 自己ID 547 | ToUserName: 自己ID 548 | StatusNotifyUserName: 最近联系的联系人ID 549 | Content: 550 | 551 | 552 | 553 | // 最近联系的联系人 554 | filehelper,xxx@chatroom,wxid_xxx,xxx,... 555 | 556 | 557 | 558 | 559 | // 朋友圈 560 | MomentsUnreadMsgStatus 561 | 562 | 563 | 1454502365 564 | 565 | 566 | 567 | 568 | // 未读的功能账号消息,群发助手,漂流瓶等 569 | 570 | 571 | 572 | ``` 573 | 574 | **文本消息** 575 | ``` 576 | MsgType: 1 577 | FromUserName: 发送方ID 578 | ToUserName: 接收方ID 579 | Content: 消息内容 580 | ``` 581 | 582 | **图片消息** 583 | ```html 584 | MsgType: 3 585 | FromUserName: 发送方ID 586 | ToUserName: 接收方ID 587 | MsgId: 用于获取图片 588 | Content: 589 | 590 | 591 | 592 | 593 | ``` 594 | 595 | **小视频消息** 596 | ```html 597 | MsgType: 62 598 | FromUserName: 发送方ID 599 | ToUserName: 接收方ID 600 | MsgId: 用于获取小视频 601 | Content: 602 | 603 | 604 | 605 | 606 | ``` 607 | 608 | **地理位置消息** 609 | ``` 610 | MsgType: 1 611 | FromUserName: 发送方ID 612 | ToUserName: 接收方ID 613 | Content: http://weixin.qq.com/cgi-bin/redirectforward?args=xxx 614 | // 属于文本消息,只不过内容是一个跳转到地图的链接 615 | ``` 616 | 617 | **名片消息** 618 | ```js 619 | MsgType: 42 620 | FromUserName: 发送方ID 621 | ToUserName: 接收方ID 622 | Content: 623 | 624 | 625 | 626 | RecommendInfo: 627 | { 628 | "UserName": "xxx", // ID 629 | "Province": "xxx", 630 | "City": "xxx", 631 | "Scene": 17, 632 | "QQNum": 0, 633 | "Content": "", 634 | "Alias": "xxx", // 微信号 635 | "OpCode": 0, 636 | "Signature": "", 637 | "Ticket": "", 638 | "Sex": 0, // 1:男, 2:女 639 | "NickName": "xxx", // 昵称 640 | "AttrStatus": 4293221, 641 | "VerifyFlag": 0 642 | } 643 | ``` 644 | 645 | **语音消息** 646 | ```html 647 | MsgType: 34 648 | FromUserName: 发送方ID 649 | ToUserName: 接收方ID 650 | MsgId: 用于获取语音 651 | Content: 652 | 653 | 654 | 655 | ``` 656 | 657 | **动画表情** 658 | ```html 659 | MsgType: 47 660 | FromUserName: 发送方ID 661 | ToUserName: 接收方ID 662 | Content: 663 | 664 | 665 | 666 | 667 | ``` 668 | 669 | **普通链接或应用分享消息** 670 | ```html 671 | MsgType: 49 672 | AppMsgType: 5 673 | FromUserName: 发送方ID 674 | ToUserName: 接收方ID 675 | Url: 链接地址 676 | FileName: 链接标题 677 | Content: 678 | 679 | 680 | 681 | 682 | 5 683 | 684 | 685 | 686 | ... 687 | 688 | 689 | 690 | 691 | 692 | 693 | ``` 694 | 695 | **音乐链接消息** 696 | ```html 697 | MsgType: 49 698 | AppMsgType: 3 699 | FromUserName: 发送方ID 700 | ToUserName: 接收方ID 701 | Url: 链接地址 702 | FileName: 音乐名 703 | 704 | AppInfo: // 分享链接的应用 705 | { 706 | Type: 0, 707 | AppID: wx485a97c844086dc9 708 | } 709 | 710 | Content: 711 | 712 | 713 | 714 | 715 | 716 | 3 717 | 0 718 | 719 | 720 | 721 | 722 | 0 723 | 724 | 725 | 726 | http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46 727 | 728 | 729 | http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46 730 | 731 | 732 | 0 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | http://imgcache.qq.com/music/photo/album/63/180_albumpic_143163_0.jpg 743 | 744 | 745 | 746 | 747 | 0 748 | 749 | 29 750 | 摇一摇搜歌 751 | 752 | 753 | 754 | ``` 755 | 756 | **群消息** 757 | ``` 758 | MsgType: 1 759 | FromUserName: @@xxx 760 | ToUserName: @xxx 761 | Content: 762 | @xxx:
xxx 763 | ``` 764 | 765 | **红包消息** 766 | ``` 767 | MsgType: 49 768 | AppMsgType: 2001 769 | FromUserName: 发送方ID 770 | ToUserName: 接收方ID 771 | Content: 未知 772 | ``` 773 | 注:根据网页版的代码可以看到未来可能支持查看红包消息,但目前走的是系统消息,见下。 774 | 775 | **系统消息** 776 | ``` 777 | MsgType: 10000 778 | FromUserName: 发送方ID 779 | ToUserName: 自己ID 780 | Content: 781 | "你已添加了 xxx ,现在可以开始聊天了。" 782 | "如果陌生人主动添加你为朋友,请谨慎核实对方身份。" 783 | "收到红包,请在手机上查看" 784 | ``` 785 | 786 | 787 | ## Discussion Group 788 | 如果你希望和 WeixinBot 的其他开发者交流,或者有什么问题和建议,欢迎大家加入微信群【Youth fed the dog】一起讨论。扫描下面的二维码添加机器人为好友,并回复【Aidog】获取入群链接。 789 | 790 |
791 | join us 792 |
793 | 794 | 注:这个不是群的二维码,是机器人拉你入群,记得回复机器人【Aidog】哦~ (secret code: Aidog) 795 | 796 | ## Recent Update 797 | 798 | - association_login 799 | 目前网页版微信已经可以脱离扫码,但是依然需要在客户端进行确认登录。 800 | -------------------------------------------------------------------------------- /imgs/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urinx/WeixinBot/d9edcd2c9203fe7dd203b22b71bbc48a31e9492b/imgs/1.png -------------------------------------------------------------------------------- /imgs/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urinx/WeixinBot/d9edcd2c9203fe7dd203b22b71bbc48a31e9492b/imgs/2.png -------------------------------------------------------------------------------- /imgs/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urinx/WeixinBot/d9edcd2c9203fe7dd203b22b71bbc48a31e9492b/imgs/3.png -------------------------------------------------------------------------------- /imgs/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urinx/WeixinBot/d9edcd2c9203fe7dd203b22b71bbc48a31e9492b/imgs/4.png -------------------------------------------------------------------------------- /imgs/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urinx/WeixinBot/d9edcd2c9203fe7dd203b22b71bbc48a31e9492b/imgs/5.png -------------------------------------------------------------------------------- /imgs/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urinx/WeixinBot/d9edcd2c9203fe7dd203b22b71bbc48a31e9492b/imgs/8.jpg -------------------------------------------------------------------------------- /imgs/groupQrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urinx/WeixinBot/d9edcd2c9203fe7dd203b22b71bbc48a31e9492b/imgs/groupQrcode.jpg -------------------------------------------------------------------------------- /wxbot_demo_py3/requirements.txt: -------------------------------------------------------------------------------- 1 | colorama 2 | coloredlogs 3 | humanfriendly 4 | lxml 5 | qrcode 6 | requests 7 | six 8 | requests_toolbelt 9 | pyqrcode 10 | certifi 11 | -------------------------------------------------------------------------------- /wxbot_demo_py3/weixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import qrcode 4 | from pyqrcode import QRCode 5 | import urllib.request, urllib.parse, urllib.error 6 | import urllib.request, urllib.error, urllib.parse 7 | import http.cookiejar 8 | import requests 9 | import xml.dom.minidom 10 | import json 11 | import time 12 | import ssl 13 | import re 14 | import sys 15 | import os 16 | import subprocess 17 | import random 18 | import multiprocessing 19 | import platform 20 | import logging 21 | import http.client 22 | from collections import defaultdict 23 | from urllib.parse import urlparse 24 | from lxml import html 25 | from socket import timeout as timeout_error 26 | #import pdb 27 | 28 | # for media upload 29 | import mimetypes 30 | from requests_toolbelt.multipart.encoder import MultipartEncoder 31 | 32 | 33 | def catchKeyboardInterrupt(fn): 34 | def wrapper(*args): 35 | try: 36 | return fn(*args) 37 | except KeyboardInterrupt: 38 | print('\n[*] 强制退出程序') 39 | logging.debug('[*] 强制退出程序') 40 | return wrapper 41 | 42 | 43 | def _decode_list(data): 44 | rv = [] 45 | for item in data: 46 | if isinstance(item, str): 47 | item = item.encode('utf-8') 48 | elif isinstance(item, list): 49 | item = _decode_list(item) 50 | elif isinstance(item, dict): 51 | item = _decode_dict(item) 52 | rv.append(item) 53 | return rv 54 | 55 | 56 | def _decode_dict(data): 57 | rv = {} 58 | for key, value in data.items(): 59 | if isinstance(key, str): 60 | key = key.encode('utf-8') 61 | if isinstance(value, str): 62 | value = value.encode('utf-8') 63 | elif isinstance(value, list): 64 | value = _decode_list(value) 65 | elif isinstance(value, dict): 66 | value = _decode_dict(value) 67 | rv[key] = value 68 | return rv 69 | 70 | 71 | class WebWeixin(object): 72 | 73 | def __str__(self): 74 | description = \ 75 | "=========================\n" + \ 76 | "[#] Web Weixin\n" + \ 77 | "[#] Debug Mode: " + str(self.DEBUG) + "\n" + \ 78 | "[#] Uuid: " + self.uuid + "\n" + \ 79 | "[#] Uin: " + str(self.uin) + "\n" + \ 80 | "[#] Sid: " + self.sid + "\n" + \ 81 | "[#] Skey: " + self.skey + "\n" + \ 82 | "[#] DeviceId: " + self.deviceId + "\n" + \ 83 | "[#] PassTicket: " + self.pass_ticket + "\n" + \ 84 | "=========================" 85 | return description 86 | 87 | def __init__(self): 88 | self.DEBUG = False 89 | self.commandLineQRCode = False 90 | self.uuid = '' 91 | self.base_uri = '' 92 | self.redirect_uri = '' 93 | self.uin = '' 94 | self.sid = '' 95 | self.skey = '' 96 | self.pass_ticket = '' 97 | self.deviceId = 'e' + repr(random.random())[2:17] 98 | self.BaseRequest = {} 99 | self.synckey = '' 100 | self.SyncKey = [] 101 | self.User = [] 102 | self.MemberList = [] 103 | self.ContactList = [] # 好友 104 | self.GroupList = [] # 群 105 | self.GroupMemeberList = [] # 群友 106 | self.PublicUsersList = [] # 公众号/服务号 107 | self.SpecialUsersList = [] # 特殊账号 108 | self.autoReplyMode = False 109 | self.syncHost = '' 110 | 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' 111 | self.interactive = False 112 | self.autoOpen = False 113 | self.saveFolder = os.path.join(os.getcwd(), 'saved') 114 | self.saveSubFolders = {'webwxgeticon': 'icons', 'webwxgetheadimg': 'headimgs', 'webwxgetmsgimg': 'msgimgs', 115 | 'webwxgetvideo': 'videos', 'webwxgetvoice': 'voices', '_showQRCodeImg': 'qrcodes'} 116 | self.appid = 'wx782c26e4c19acffb' 117 | self.lang = 'zh_CN' 118 | self.lastCheckTs = time.time() 119 | self.memberCount = 0 120 | self.SpecialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 121 | 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'] 122 | self.TimeOut = 20 # 同步最短时间间隔(单位:秒) 123 | self.media_count = -1 124 | 125 | self.cookie = http.cookiejar.CookieJar() 126 | opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookie)) 127 | opener.addheaders = [('User-agent', self.user_agent)] 128 | urllib.request.install_opener(opener) 129 | 130 | def loadConfig(self, config): 131 | if config['DEBUG']: 132 | self.DEBUG = config['DEBUG'] 133 | if config['autoReplyMode']: 134 | self.autoReplyMode = config['autoReplyMode'] 135 | if config['user_agent']: 136 | self.user_agent = config['user_agent'] 137 | if config['interactive']: 138 | self.interactive = config['interactive'] 139 | if config['autoOpen']: 140 | self.autoOpen = config['autoOpen'] 141 | 142 | def getUUID(self): 143 | url = 'https://login.weixin.qq.com/jslogin' 144 | params = { 145 | 'appid': self.appid, 146 | 'fun': 'new', 147 | 'lang': self.lang, 148 | '_': int(time.time()), 149 | } 150 | #r = requests.get(url=url, params=params) 151 | #r.encoding = 'utf-8' 152 | #data = r.text 153 | data = self._post(url, params, False).decode("utf-8") 154 | if data == '': 155 | return False 156 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' 157 | pm = re.search(regx, data) 158 | if pm: 159 | code = pm.group(1) 160 | self.uuid = pm.group(2) 161 | return code == '200' 162 | return False 163 | 164 | def genQRCode(self): 165 | #return self._showQRCodeImg() 166 | if sys.platform.startswith('win'): 167 | self._showQRCodeImg('win') 168 | elif sys.platform.find('darwin') >= 0: 169 | self._showQRCodeImg('macos') 170 | else: 171 | self._str2qr('https://login.weixin.qq.com/l/' + self.uuid) 172 | 173 | def _showQRCodeImg(self, str): 174 | if self.commandLineQRCode: 175 | qrCode = QRCode('https://login.weixin.qq.com/l/' + self.uuid) 176 | self._showCommandLineQRCode(qrCode.text(1)) 177 | else: 178 | url = 'https://login.weixin.qq.com/qrcode/' + self.uuid 179 | params = { 180 | 't': 'webwx', 181 | '_': int(time.time()) 182 | } 183 | 184 | data = self._post(url, params, False) 185 | if data == '': 186 | return 187 | QRCODE_PATH = self._saveFile('qrcode.jpg', data, '_showQRCodeImg') 188 | if str == 'win': 189 | os.startfile(QRCODE_PATH) 190 | elif str == 'macos': 191 | subprocess.call(["open", QRCODE_PATH]) 192 | else: 193 | return 194 | 195 | def _showCommandLineQRCode(self, qr_data, enableCmdQR=2): 196 | try: 197 | b = u'\u2588' 198 | sys.stdout.write(b + '\r') 199 | sys.stdout.flush() 200 | except UnicodeEncodeError: 201 | white = 'MM' 202 | else: 203 | white = b 204 | black = ' ' 205 | blockCount = int(enableCmdQR) 206 | if abs(blockCount) == 0: 207 | blockCount = 1 208 | white *= abs(blockCount) 209 | if blockCount < 0: 210 | white, black = black, white 211 | sys.stdout.write(' ' * 50 + '\r') 212 | sys.stdout.flush() 213 | qr = qr_data.replace('0', white).replace('1', black) 214 | sys.stdout.write(qr) 215 | sys.stdout.flush() 216 | 217 | def waitForLogin(self, tip=1): 218 | time.sleep(tip) 219 | url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % ( 220 | tip, self.uuid, int(time.time())) 221 | data = self._get(url) 222 | if data == '': 223 | return False 224 | pm = re.search(r"window.code=(\d+);", data) 225 | code = pm.group(1) 226 | 227 | if code == '201': 228 | return True 229 | elif code == '200': 230 | pm = re.search(r'window.redirect_uri="(\S+?)";', data) 231 | r_uri = pm.group(1) + '&fun=new' 232 | self.redirect_uri = r_uri 233 | self.base_uri = r_uri[:r_uri.rfind('/')] 234 | return True 235 | elif code == '408': 236 | self._echo('[登陆超时] \n') 237 | else: 238 | self._echo('[登陆异常] \n') 239 | return False 240 | 241 | def login(self): 242 | data = self._get(self.redirect_uri) 243 | if data == '': 244 | return False 245 | doc = xml.dom.minidom.parseString(data) 246 | root = doc.documentElement 247 | 248 | for node in root.childNodes: 249 | if node.nodeName == 'skey': 250 | self.skey = node.childNodes[0].data 251 | elif node.nodeName == 'wxsid': 252 | self.sid = node.childNodes[0].data 253 | elif node.nodeName == 'wxuin': 254 | self.uin = node.childNodes[0].data 255 | elif node.nodeName == 'pass_ticket': 256 | self.pass_ticket = node.childNodes[0].data 257 | 258 | if '' in (self.skey, self.sid, self.uin, self.pass_ticket): 259 | return False 260 | 261 | self.BaseRequest = { 262 | 'Uin': int(self.uin), 263 | 'Sid': self.sid, 264 | 'Skey': self.skey, 265 | 'DeviceID': self.deviceId, 266 | } 267 | return True 268 | 269 | def webwxinit(self): 270 | url = self.base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % ( 271 | self.pass_ticket, self.skey, int(time.time())) 272 | params = { 273 | 'BaseRequest': self.BaseRequest 274 | } 275 | dic = self._post(url, params) 276 | if dic == '': 277 | return False 278 | self.SyncKey = dic['SyncKey'] 279 | self.User = dic['User'] 280 | # synckey for synccheck 281 | self.synckey = '|'.join( 282 | [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) 283 | 284 | return dic['BaseResponse']['Ret'] == 0 285 | 286 | def webwxstatusnotify(self): 287 | url = self.base_uri + \ 288 | '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket) 289 | params = { 290 | 'BaseRequest': self.BaseRequest, 291 | "Code": 3, 292 | "FromUserName": self.User['UserName'], 293 | "ToUserName": self.User['UserName'], 294 | "ClientMsgId": int(time.time()) 295 | } 296 | dic = self._post(url, params) 297 | if dic == '': 298 | return False 299 | 300 | return dic['BaseResponse']['Ret'] == 0 301 | 302 | def webwxgetcontact(self): 303 | SpecialUsers = self.SpecialUsers 304 | url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % ( 305 | self.pass_ticket, self.skey, int(time.time())) 306 | dic = self._post(url, {}) 307 | if dic == '': 308 | return False 309 | 310 | self.MemberCount = dic['MemberCount'] 311 | self.MemberList = dic['MemberList'] 312 | ContactList = self.MemberList[:] 313 | GroupList = self.GroupList[:] 314 | PublicUsersList = self.PublicUsersList[:] 315 | SpecialUsersList = self.SpecialUsersList[:] 316 | 317 | for i in range(len(ContactList) - 1, -1, -1): 318 | Contact = ContactList[i] 319 | if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 320 | ContactList.remove(Contact) 321 | self.PublicUsersList.append(Contact) 322 | elif Contact['UserName'] in SpecialUsers: # 特殊账号 323 | ContactList.remove(Contact) 324 | self.SpecialUsersList.append(Contact) 325 | elif '@@' in Contact['UserName']: # 群聊 326 | ContactList.remove(Contact) 327 | self.GroupList.append(Contact) 328 | elif Contact['UserName'] == self.User['UserName']: # 自己 329 | ContactList.remove(Contact) 330 | self.ContactList = ContactList 331 | 332 | return True 333 | 334 | def webwxbatchgetcontact(self): 335 | url = self.base_uri + \ 336 | '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % ( 337 | int(time.time()), self.pass_ticket) 338 | params = { 339 | 'BaseRequest': self.BaseRequest, 340 | "Count": len(self.GroupList), 341 | "List": [{"UserName": g['UserName'], "EncryChatRoomId":""} for g in self.GroupList] 342 | } 343 | dic = self._post(url, params) 344 | if dic == '': 345 | return False 346 | 347 | # blabla ... 348 | ContactList = dic['ContactList'] 349 | ContactCount = dic['Count'] 350 | self.GroupList = ContactList 351 | 352 | for i in range(len(ContactList) - 1, -1, -1): 353 | Contact = ContactList[i] 354 | MemberList = Contact['MemberList'] 355 | for member in MemberList: 356 | self.GroupMemeberList.append(member) 357 | return True 358 | 359 | def getNameById(self, id): 360 | url = self.base_uri + \ 361 | '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % ( 362 | int(time.time()), self.pass_ticket) 363 | params = { 364 | 'BaseRequest': self.BaseRequest, 365 | "Count": 1, 366 | "List": [{"UserName": id, "EncryChatRoomId": ""}] 367 | } 368 | dic = self._post(url, params) 369 | if dic == '': 370 | return None 371 | 372 | # blabla ... 373 | return dic['ContactList'] 374 | 375 | def testsynccheck(self): 376 | SyncHost = ['wx2.qq.com', 377 | 'webpush.wx2.qq.com', 378 | 'wx8.qq.com', 379 | 'webpush.wx8.qq.com', 380 | 'qq.com', 381 | 'webpush.wx.qq.com', 382 | 'web2.wechat.com', 383 | 'webpush.web2.wechat.com', 384 | 'wechat.com', 385 | 'webpush.web.wechat.com', 386 | 'webpush.weixin.qq.com', 387 | 'webpush.wechat.com', 388 | 'webpush1.wechat.com', 389 | 'webpush2.wechat.com', 390 | 'webpush.wx.qq.com', 391 | 'webpush2.wx.qq.com'] 392 | for host in SyncHost: 393 | self.syncHost = host 394 | [retcode, selector] = self.synccheck() 395 | if retcode == '0': 396 | return True 397 | return False 398 | 399 | def synccheck(self): 400 | params = { 401 | 'r': int(time.time()), 402 | 'sid': self.sid, 403 | 'uin': self.uin, 404 | 'skey': self.skey, 405 | 'deviceid': self.deviceId, 406 | 'synckey': self.synckey, 407 | '_': int(time.time()), 408 | } 409 | url = 'https://' + self.syncHost + '/cgi-bin/mmwebwx-bin/synccheck?' + urllib.parse.urlencode(params) 410 | data = self._get(url, timeout=5) 411 | if data == '': 412 | return [-1,-1] 413 | 414 | pm = re.search( 415 | r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data) 416 | retcode = pm.group(1) 417 | selector = pm.group(2) 418 | return [retcode, selector] 419 | 420 | def webwxsync(self): 421 | url = self.base_uri + \ 422 | '/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( 423 | self.sid, self.skey, self.pass_ticket) 424 | params = { 425 | 'BaseRequest': self.BaseRequest, 426 | 'SyncKey': self.SyncKey, 427 | 'rr': ~int(time.time()) 428 | } 429 | dic = self._post(url, params) 430 | if dic == '': 431 | return None 432 | if self.DEBUG: 433 | print(json.dumps(dic, indent=4)) 434 | (json.dumps(dic, indent=4)) 435 | 436 | if dic['BaseResponse']['Ret'] == 0: 437 | self.SyncKey = dic['SyncKey'] 438 | self.synckey = '|'.join( 439 | [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) 440 | return dic 441 | 442 | def webwxsendmsg(self, word, to='filehelper'): 443 | url = self.base_uri + \ 444 | '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket) 445 | clientMsgId = str(int(time.time() * 1000)) + \ 446 | str(random.random())[:5].replace('.', '') 447 | params = { 448 | 'BaseRequest': self.BaseRequest, 449 | 'Msg': { 450 | "Type": 1, 451 | "Content": self._transcoding(word), 452 | "FromUserName": self.User['UserName'], 453 | "ToUserName": to, 454 | "LocalID": clientMsgId, 455 | "ClientMsgId": clientMsgId 456 | } 457 | } 458 | headers = {'content-type': 'application/json; charset=UTF-8'} 459 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 460 | r = requests.post(url, data=data, headers=headers) 461 | dic = r.json() 462 | return dic['BaseResponse']['Ret'] == 0 463 | 464 | def webwxuploadmedia(self, image_name): 465 | url = 'https://file2.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' 466 | # 计数器 467 | self.media_count = self.media_count + 1 468 | # 文件名 469 | file_name = image_name 470 | # MIME格式 471 | # mime_type = application/pdf, image/jpeg, image/png, etc. 472 | mime_type = mimetypes.guess_type(image_name, strict=False)[0] 473 | # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc 474 | # pic格式,直接显示。doc格式则显示为文件。 475 | media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc' 476 | # 上一次修改日期 477 | lastModifieDate = 'Thu Mar 17 2016 00:55:10 GMT+0800 (CST)' 478 | # 文件大小 479 | file_size = os.path.getsize(file_name) 480 | # PassTicket 481 | pass_ticket = self.pass_ticket 482 | # clientMediaId 483 | client_media_id = str(int(time.time() * 1000)) + \ 484 | str(random.random())[:5].replace('.', '') 485 | # webwx_data_ticket 486 | webwx_data_ticket = '' 487 | for item in self.cookie: 488 | if item.name == 'webwx_data_ticket': 489 | webwx_data_ticket = item.value 490 | break 491 | if (webwx_data_ticket == ''): 492 | return "None Fuck Cookie" 493 | 494 | uploadmediarequest = json.dumps({ 495 | "BaseRequest": self.BaseRequest, 496 | "ClientMediaId": client_media_id, 497 | "TotalLen": file_size, 498 | "StartPos": 0, 499 | "DataLen": file_size, 500 | "MediaType": 4 501 | }, ensure_ascii=False).encode('utf8') 502 | 503 | multipart_encoder = MultipartEncoder( 504 | fields={ 505 | 'id': 'WU_FILE_' + str(self.media_count), 506 | 'name': file_name, 507 | 'type': mime_type, 508 | 'lastModifieDate': lastModifieDate, 509 | 'size': str(file_size), 510 | 'mediatype': media_type, 511 | 'uploadmediarequest': uploadmediarequest, 512 | 'webwx_data_ticket': webwx_data_ticket, 513 | 'pass_ticket': pass_ticket, 514 | 'filename': (file_name, open(file_name, 'rb'), mime_type.split('/')[1]) 515 | }, 516 | boundary='-----------------------------1575017231431605357584454111' 517 | ) 518 | 519 | headers = { 520 | 'Host': 'file2.wx.qq.com', 521 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0', 522 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 523 | 'Accept-Language': 'en-US,en;q=0.5', 524 | 'Accept-Encoding': 'gzip, deflate', 525 | 'Referer': 'https://wx2.qq.com/', 526 | 'Content-Type': multipart_encoder.content_type, 527 | 'Origin': 'https://wx2.qq.com', 528 | 'Connection': 'keep-alive', 529 | 'Pragma': 'no-cache', 530 | 'Cache-Control': 'no-cache' 531 | } 532 | 533 | r = requests.post(url, data=multipart_encoder, headers=headers) 534 | response_json = r.json() 535 | if response_json['BaseResponse']['Ret'] == 0: 536 | return response_json 537 | return None 538 | 539 | def webwxsendmsgimg(self, user_id, media_id): 540 | url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s' % self.pass_ticket 541 | clientMsgId = str(int(time.time() * 1000)) + \ 542 | str(random.random())[:5].replace('.', '') 543 | data_json = { 544 | "BaseRequest": self.BaseRequest, 545 | "Msg": { 546 | "Type": 3, 547 | "MediaId": media_id, 548 | "FromUserName": self.User['UserName'], 549 | "ToUserName": user_id, 550 | "LocalID": clientMsgId, 551 | "ClientMsgId": clientMsgId 552 | } 553 | } 554 | headers = {'content-type': 'application/json; charset=UTF-8'} 555 | data = json.dumps(data_json, ensure_ascii=False).encode('utf8') 556 | r = requests.post(url, data=data, headers=headers) 557 | dic = r.json() 558 | return dic['BaseResponse']['Ret'] == 0 559 | 560 | def webwxsendmsgemotion(self, user_id, media_id): 561 | url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket 562 | clientMsgId = str(int(time.time() * 1000)) + \ 563 | str(random.random())[:5].replace('.', '') 564 | data_json = { 565 | "BaseRequest": self.BaseRequest, 566 | "Msg": { 567 | "Type": 47, 568 | "EmojiFlag": 2, 569 | "MediaId": media_id, 570 | "FromUserName": self.User['UserName'], 571 | "ToUserName": user_id, 572 | "LocalID": clientMsgId, 573 | "ClientMsgId": clientMsgId 574 | } 575 | } 576 | headers = {'content-type': 'application/json; charset=UTF-8'} 577 | data = json.dumps(data_json, ensure_ascii=False).encode('utf8') 578 | r = requests.post(url, data=data, headers=headers) 579 | dic = r.json() 580 | if self.DEBUG: 581 | print(json.dumps(dic, indent=4)) 582 | logging.debug(json.dumps(dic, indent=4)) 583 | return dic['BaseResponse']['Ret'] == 0 584 | 585 | def _saveFile(self, filename, data, api=None): 586 | fn = filename 587 | if self.saveSubFolders[api]: 588 | dirName = os.path.join(self.saveFolder, self.saveSubFolders[api]) 589 | if not os.path.exists(dirName): 590 | os.makedirs(dirName) 591 | fn = os.path.join(dirName, filename) 592 | logging.debug('Saved file: %s' % fn) 593 | with open(fn, 'wb') as f: 594 | f.write(data) 595 | f.close() 596 | return fn 597 | 598 | def webwxgeticon(self, id): 599 | url = self.base_uri + \ 600 | '/webwxgeticon?username=%s&skey=%s' % (id, self.skey) 601 | data = self._get(url) 602 | if data == '': 603 | return '' 604 | fn = 'img_' + id + '.jpg' 605 | return self._saveFile(fn, data, 'webwxgeticon') 606 | 607 | def webwxgetheadimg(self, id): 608 | url = self.base_uri + \ 609 | '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey) 610 | data = self._get(url) 611 | if data == '': 612 | return '' 613 | fn = 'img_' + id + '.jpg' 614 | return self._saveFile(fn, data, 'webwxgetheadimg') 615 | 616 | def webwxgetmsgimg(self, msgid): 617 | url = self.base_uri + \ 618 | '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) 619 | data = self._get(url) 620 | if data == '': 621 | return '' 622 | fn = 'img_' + msgid + '.jpg' 623 | return self._saveFile(fn, data, 'webwxgetmsgimg') 624 | 625 | # Not work now for weixin haven't support this API 626 | def webwxgetvideo(self, msgid): 627 | url = self.base_uri + \ 628 | '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey) 629 | data = self._get(url, api='webwxgetvideo') 630 | if data == '': 631 | return '' 632 | fn = 'video_' + msgid + '.mp4' 633 | return self._saveFile(fn, data, 'webwxgetvideo') 634 | 635 | def webwxgetvoice(self, msgid): 636 | url = self.base_uri + \ 637 | '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) 638 | data = self._get(url, api='webwxgetvoice') 639 | if data == '': 640 | return '' 641 | fn = 'voice_' + msgid + '.mp3' 642 | return self._saveFile(fn, data, 'webwxgetvoice') 643 | 644 | def getGroupName(self, id): 645 | name = '未知群' 646 | for member in self.GroupList: 647 | if member['UserName'] == id: 648 | name = member['NickName'] 649 | if name == '未知群': 650 | # 现有群里面查不到 651 | GroupList = self.getNameById(id) 652 | for group in GroupList: 653 | self.GroupList.append(group) 654 | if group['UserName'] == id: 655 | name = group['NickName'] 656 | MemberList = group['MemberList'] 657 | for member in MemberList: 658 | self.GroupMemeberList.append(member) 659 | return name 660 | 661 | def getUserRemarkName(self, id): 662 | name = '未知群' if id[:2] == '@@' else '陌生人' 663 | if id == self.User['UserName']: 664 | return self.User['NickName'] # 自己 665 | 666 | if id[:2] == '@@': 667 | # 群 668 | name = self.getGroupName(id) 669 | else: 670 | # 特殊账号 671 | for member in self.SpecialUsersList: 672 | if member['UserName'] == id: 673 | name = member['RemarkName'] if member[ 674 | 'RemarkName'] else member['NickName'] 675 | 676 | # 公众号或服务号 677 | for member in self.PublicUsersList: 678 | if member['UserName'] == id: 679 | name = member['RemarkName'] if member[ 680 | 'RemarkName'] else member['NickName'] 681 | 682 | # 直接联系人 683 | for member in self.ContactList: 684 | if member['UserName'] == id: 685 | name = member['RemarkName'] if member[ 686 | 'RemarkName'] else member['NickName'] 687 | # 群友 688 | for member in self.GroupMemeberList: 689 | if member['UserName'] == id: 690 | name = member['DisplayName'] if member[ 691 | 'DisplayName'] else member['NickName'] 692 | 693 | if name == '未知群' or name == '陌生人': 694 | logging.debug(id) 695 | return name 696 | 697 | def getUSerID(self, name): 698 | for member in self.MemberList: 699 | if name == member['RemarkName'] or name == member['NickName']: 700 | return member['UserName'] 701 | return None 702 | 703 | def _showMsg(self, message): 704 | 705 | srcName = None 706 | dstName = None 707 | groupName = None 708 | content = None 709 | 710 | msg = message 711 | logging.debug(msg) 712 | 713 | if msg['raw_msg']: 714 | srcName = self.getUserRemarkName(msg['raw_msg']['FromUserName']) 715 | dstName = self.getUserRemarkName(msg['raw_msg']['ToUserName']) 716 | content = msg['raw_msg']['Content'].replace( 717 | '<', '<').replace('>', '>') 718 | message_id = msg['raw_msg']['MsgId'] 719 | 720 | if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: 721 | # 地理位置消息 722 | data = self._get(content) 723 | if data == '': 724 | return 725 | data.decode('gbk').encode('utf-8') 726 | pos = self._searchContent('title', data, 'xml') 727 | temp = self._get(content) 728 | if temp == '': 729 | return 730 | tree = html.fromstring(temp) 731 | url = tree.xpath('//html/body/div/img')[0].attrib['src'] 732 | 733 | for item in urlparse(url).query.split('&'): 734 | if item.split('=')[0] == 'center': 735 | loc = item.split('=')[-1:] 736 | 737 | content = '%s 发送了一个 位置消息 - 我在 [%s](%s) @ %s]' % ( 738 | srcName, pos, url, loc) 739 | 740 | if msg['raw_msg']['ToUserName'] == 'filehelper': 741 | # 文件传输助手 742 | dstName = '文件传输助手' 743 | 744 | if msg['raw_msg']['FromUserName'][:2] == '@@': 745 | # 接收到来自群的消息 746 | if ":
" in content: 747 | [people, content] = content.split(':
', 1) 748 | groupName = srcName 749 | srcName = self.getUserRemarkName(people) 750 | dstName = 'GROUP' 751 | else: 752 | groupName = srcName 753 | srcName = 'SYSTEM' 754 | elif msg['raw_msg']['ToUserName'][:2] == '@@': 755 | # 自己发给群的消息 756 | groupName = dstName 757 | dstName = 'GROUP' 758 | 759 | # 收到了红包 760 | if content == '收到红包,请在手机上查看': 761 | msg['message'] = content 762 | 763 | # 指定了消息内容 764 | if 'message' in list(msg.keys()): 765 | content = msg['message'] 766 | 767 | if groupName != None: 768 | print('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), srcName.strip(), dstName.strip(), content.replace('
', '\n'))) 769 | logging.info('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), 770 | srcName.strip(), dstName.strip(), content.replace('
', '\n'))) 771 | else: 772 | print('%s %s -> %s: %s' % (message_id, srcName.strip(), dstName.strip(), content.replace('
', '\n'))) 773 | logging.info('%s %s -> %s: %s' % (message_id, srcName.strip(), 774 | dstName.strip(), content.replace('
', '\n'))) 775 | 776 | def handleMsg(self, r): 777 | for msg in r['AddMsgList']: 778 | print('[*] 你有新的消息,请注意查收') 779 | logging.debug('[*] 你有新的消息,请注意查收') 780 | 781 | if self.DEBUG: 782 | fn = 'msg' + str(int(random.random() * 1000)) + '.json' 783 | with open(fn, 'w') as f: 784 | f.write(json.dumps(msg)) 785 | print('[*] 该消息已储存到文件: ' + fn) 786 | logging.debug('[*] 该消息已储存到文件: %s' % (fn)) 787 | 788 | msgType = msg['MsgType'] 789 | name = self.getUserRemarkName(msg['FromUserName']) 790 | content = msg['Content'].replace('<', '<').replace('>', '>') 791 | msgid = msg['MsgId'] 792 | 793 | if msgType == 1: 794 | raw_msg = {'raw_msg': msg} 795 | self._showMsg(raw_msg) 796 | #自己加的代码-------------------------------------------# 797 | #if self.autoReplyRevokeMode: 798 | # store 799 | #自己加的代码-------------------------------------------# 800 | if self.autoReplyMode: 801 | ans = self._xiaodoubi(content) + '\n[微信机器人自动回复]' 802 | if self.webwxsendmsg(ans, msg['FromUserName']): 803 | print('自动回复: ' + ans) 804 | logging.info('自动回复: ' + ans) 805 | else: 806 | print('自动回复失败') 807 | logging.info('自动回复失败') 808 | elif msgType == 3: 809 | image = self.webwxgetmsgimg(msgid) 810 | raw_msg = {'raw_msg': msg, 811 | 'message': '%s 发送了一张图片: %s' % (name, image)} 812 | self._showMsg(raw_msg) 813 | self._safe_open(image) 814 | elif msgType == 34: 815 | voice = self.webwxgetvoice(msgid) 816 | raw_msg = {'raw_msg': msg, 817 | 'message': '%s 发了一段语音: %s' % (name, voice)} 818 | self._showMsg(raw_msg) 819 | self._safe_open(voice) 820 | elif msgType == 42: 821 | info = msg['RecommendInfo'] 822 | print('%s 发送了一张名片:' % name) 823 | print('=========================') 824 | print('= 昵称: %s' % info['NickName']) 825 | print('= 微信号: %s' % info['Alias']) 826 | print('= 地区: %s %s' % (info['Province'], info['City'])) 827 | print('= 性别: %s' % ['未知', '男', '女'][info['Sex']]) 828 | print('=========================') 829 | raw_msg = {'raw_msg': msg, 'message': '%s 发送了一张名片: %s' % ( 830 | name.strip(), json.dumps(info))} 831 | self._showMsg(raw_msg) 832 | elif msgType == 47: 833 | url = self._searchContent('cdnurl', content) 834 | raw_msg = {'raw_msg': msg, 835 | 'message': '%s 发了一个动画表情,点击下面链接查看: %s' % (name, url)} 836 | self._showMsg(raw_msg) 837 | self._safe_open(url) 838 | elif msgType == 49: 839 | appMsgType = defaultdict(lambda: "") 840 | appMsgType.update({5: '链接', 3: '音乐', 7: '微博'}) 841 | print('%s 分享了一个%s:' % (name, appMsgType[msg['AppMsgType']])) 842 | print('=========================') 843 | print('= 标题: %s' % msg['FileName']) 844 | print('= 描述: %s' % self._searchContent('des', content, 'xml')) 845 | print('= 链接: %s' % msg['Url']) 846 | print('= 来自: %s' % self._searchContent('appname', content, 'xml')) 847 | print('=========================') 848 | card = { 849 | 'title': msg['FileName'], 850 | 'description': self._searchContent('des', content, 'xml'), 851 | 'url': msg['Url'], 852 | 'appname': self._searchContent('appname', content, 'xml') 853 | } 854 | raw_msg = {'raw_msg': msg, 'message': '%s 分享了一个%s: %s' % ( 855 | name, appMsgType[msg['AppMsgType']], json.dumps(card))} 856 | self._showMsg(raw_msg) 857 | elif msgType == 51: 858 | raw_msg = {'raw_msg': msg, 'message': '[*] 成功获取联系人信息'} 859 | self._showMsg(raw_msg) 860 | elif msgType == 62: 861 | video = self.webwxgetvideo(msgid) 862 | raw_msg = {'raw_msg': msg, 863 | 'message': '%s 发了一段小视频: %s' % (name, video)} 864 | self._showMsg(raw_msg) 865 | self._safe_open(video) 866 | elif msgType == 10002: 867 | raw_msg = {'raw_msg': msg, 'message': '%s 撤回了一条消息' % name} 868 | self._showMsg(raw_msg) 869 | else: 870 | logging.debug('[*] 该消息类型为: %d,可能是表情,图片, 链接或红包: %s' % 871 | (msg['MsgType'], json.dumps(msg))) 872 | raw_msg = { 873 | 'raw_msg': msg, 'message': '[*] 该消息类型为: %d,可能是表情,图片, 链接或红包' % msg['MsgType']} 874 | self._showMsg(raw_msg) 875 | 876 | def listenMsgMode(self): 877 | print('[*] 进入消息监听模式 ... 成功') 878 | logging.debug('[*] 进入消息监听模式 ... 成功') 879 | self._run('[*] 进行同步线路测试 ... ', self.testsynccheck) 880 | playWeChat = 0 881 | redEnvelope = 0 882 | while True: 883 | self.lastCheckTs = time.time() 884 | [retcode, selector] = self.synccheck() 885 | if self.DEBUG: 886 | print('retcode: %s, selector: %s' % (retcode, selector)) 887 | logging.debug('retcode: %s, selector: %s' % (retcode, selector)) 888 | if retcode == '1100': 889 | print('[*] 你在手机上登出了微信,债见') 890 | logging.debug('[*] 你在手机上登出了微信,债见') 891 | break 892 | if retcode == '1101': 893 | print('[*] 你在其他地方登录了 WEB 版微信,债见') 894 | logging.debug('[*] 你在其他地方登录了 WEB 版微信,债见') 895 | break 896 | elif retcode == '0': 897 | if selector == '2': 898 | r = self.webwxsync() 899 | if r is not None: 900 | self.handleMsg(r) 901 | elif selector == '6': 902 | # TODO 903 | redEnvelope += 1 904 | print('[*] 收到疑似红包消息 %d 次' % redEnvelope) 905 | logging.debug('[*] 收到疑似红包消息 %d 次' % redEnvelope) 906 | elif selector == '7': 907 | playWeChat += 1 908 | print('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat) 909 | logging.debug('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat) 910 | r = self.webwxsync() 911 | elif selector == '0': 912 | time.sleep(1) 913 | if (time.time() - self.lastCheckTs) <= 20: 914 | time.sleep(time.time() - self.lastCheckTs) 915 | 916 | def sendMsg(self, name, word, isfile=False): 917 | id = self.getUSerID(name) 918 | if id: 919 | if isfile: 920 | with open(word, 'r') as f: 921 | for line in f.readlines(): 922 | line = line.replace('\n', '') 923 | self._echo('-> ' + name + ': ' + line) 924 | if self.webwxsendmsg(line, id): 925 | print(' [成功]') 926 | else: 927 | print(' [失败]') 928 | time.sleep(1) 929 | else: 930 | if self.webwxsendmsg(word, id): 931 | print('[*] 消息发送成功') 932 | logging.debug('[*] 消息发送成功') 933 | else: 934 | print('[*] 消息发送失败') 935 | logging.debug('[*] 消息发送失败') 936 | else: 937 | print('[*] 此用户不存在') 938 | logging.debug('[*] 此用户不存在') 939 | 940 | def sendMsgToAll(self, word): 941 | for contact in self.ContactList: 942 | name = contact['RemarkName'] if contact[ 943 | 'RemarkName'] else contact['NickName'] 944 | id = contact['UserName'] 945 | self._echo('-> ' + name + ': ' + word) 946 | if self.webwxsendmsg(word, id): 947 | print(' [成功]') 948 | else: 949 | print(' [失败]') 950 | time.sleep(1) 951 | 952 | def sendImg(self, name, file_name): 953 | response = self.webwxuploadmedia(file_name) 954 | media_id = "" 955 | if response is not None: 956 | media_id = response['MediaId'] 957 | user_id = self.getUSerID(name) 958 | response = self.webwxsendmsgimg(user_id, media_id) 959 | 960 | def sendEmotion(self, name, file_name): 961 | response = self.webwxuploadmedia(file_name) 962 | media_id = "" 963 | if response is not None: 964 | media_id = response['MediaId'] 965 | user_id = self.getUSerID(name) 966 | response = self.webwxsendmsgemotion(user_id, media_id) 967 | 968 | @catchKeyboardInterrupt 969 | def start(self): 970 | self._echo('[*] 微信网页版 ... 开动') 971 | print() 972 | logging.debug('[*] 微信网页版 ... 开动') 973 | while True: 974 | self._run('[*] 正在获取 uuid ... ', self.getUUID) 975 | self._echo('[*] 正在获取二维码 ... 成功') 976 | print() 977 | logging.debug('[*] 微信网页版 ... 开动') 978 | self.genQRCode() 979 | print('[*] 请使用微信扫描二维码以登录 ... ') 980 | if not self.waitForLogin(): 981 | continue 982 | print('[*] 请在手机上点击确认以登录 ... ') 983 | if not self.waitForLogin(0): 984 | continue 985 | break 986 | 987 | self._run('[*] 正在登录 ... ', self.login) 988 | self._run('[*] 微信初始化 ... ', self.webwxinit) 989 | self._run('[*] 开启状态通知 ... ', self.webwxstatusnotify) 990 | self._run('[*] 获取联系人 ... ', self.webwxgetcontact) 991 | self._echo('[*] 应有 %s 个联系人,读取到联系人 %d 个' % 992 | (self.MemberCount, len(self.MemberList))) 993 | print() 994 | self._echo('[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号' % (len(self.GroupList), 995 | len(self.ContactList), len(self.SpecialUsersList), len(self.PublicUsersList))) 996 | print() 997 | self._run('[*] 获取群 ... ', self.webwxbatchgetcontact) 998 | logging.debug('[*] 微信网页版 ... 开动') 999 | if self.DEBUG: 1000 | print(self) 1001 | logging.debug(self) 1002 | 1003 | if self.interactive and input('[*] 是否开启自动回复模式(y/n): ') == 'y': 1004 | self.autoReplyMode = True 1005 | print('[*] 自动回复模式 ... 开启') 1006 | logging.debug('[*] 自动回复模式 ... 开启') 1007 | else: 1008 | print('[*] 自动回复模式 ... 关闭') 1009 | logging.debug('[*] 自动回复模式 ... 关闭') 1010 | 1011 | if sys.platform.startswith('win'): 1012 | import _thread 1013 | _thread.start_new_thread(self.listenMsgMode()) 1014 | else: 1015 | listenProcess = multiprocessing.Process(target=self.listenMsgMode) 1016 | listenProcess.start() 1017 | 1018 | while True: 1019 | text = input('') 1020 | if text == 'quit': 1021 | listenProcess.terminate() 1022 | print('[*] 退出微信') 1023 | logging.debug('[*] 退出微信') 1024 | exit() 1025 | elif text[:2] == '->': 1026 | [name, word] = text[2:].split(':') 1027 | if name == 'all': 1028 | self.sendMsgToAll(word) 1029 | else: 1030 | self.sendMsg(name, word) 1031 | elif text[:3] == 'm->': 1032 | [name, file] = text[3:].split(':') 1033 | self.sendMsg(name, file, True) 1034 | elif text[:3] == 'f->': 1035 | print('发送文件') 1036 | logging.debug('发送文件') 1037 | elif text[:3] == 'i->': 1038 | print('发送图片') 1039 | [name, file_name] = text[3:].split(':') 1040 | self.sendImg(name, file_name) 1041 | logging.debug('发送图片') 1042 | elif text[:3] == 'e->': 1043 | print('发送表情') 1044 | [name, file_name] = text[3:].split(':') 1045 | self.sendEmotion(name, file_name) 1046 | logging.debug('发送表情') 1047 | 1048 | def _safe_open(self, path): 1049 | if self.autoOpen: 1050 | if platform.system() == "Linux": 1051 | os.system("xdg-open %s &" % path) 1052 | else: 1053 | os.system('open %s &' % path) 1054 | 1055 | def _run(self, str, func, *args): 1056 | self._echo(str) 1057 | if func(*args): 1058 | print('成功') 1059 | logging.debug('%s... 成功' % (str)) 1060 | else: 1061 | print('失败\n[*] 退出程序') 1062 | logging.debug('%s... 失败' % (str)) 1063 | logging.debug('[*] 退出程序') 1064 | exit() 1065 | 1066 | def _echo(self, str): 1067 | sys.stdout.write(str) 1068 | sys.stdout.flush() 1069 | 1070 | def _printQR(self, mat): 1071 | for i in mat: 1072 | BLACK = '\033[40m \033[0m' 1073 | WHITE = '\033[47m \033[0m' 1074 | print(''.join([BLACK if j else WHITE for j in i])) 1075 | 1076 | def _str2qr(self, str): 1077 | print(str) 1078 | qr = qrcode.QRCode() 1079 | qr.border = 1 1080 | qr.add_data(str) 1081 | qr.make() 1082 | # img = qr.make_image() 1083 | # img.save("qrcode.png") 1084 | #mat = qr.get_matrix() 1085 | #self._printQR(mat) # qr.print_tty() or qr.print_ascii() 1086 | qr.print_ascii(invert=True) 1087 | 1088 | def _transcoding(self, data): 1089 | if not data: 1090 | return data 1091 | result = None 1092 | if type(data) == str: 1093 | result = data 1094 | elif type(data) == str: 1095 | result = data.decode('utf-8') 1096 | return result 1097 | 1098 | def _get(self, url: object, api: object = None, timeout: object = None) -> object: 1099 | request = urllib.request.Request(url=url) 1100 | request.add_header('Referer', 'https://wx.qq.com/') 1101 | if api == 'webwxgetvoice': 1102 | request.add_header('Range', 'bytes=0-') 1103 | if api == 'webwxgetvideo': 1104 | request.add_header('Range', 'bytes=0-') 1105 | try: 1106 | response = urllib.request.urlopen(request, timeout=timeout) if timeout else urllib.request.urlopen(request) 1107 | if api == 'webwxgetvoice' or api == 'webwxgetvideo': 1108 | data = response.read() 1109 | else: 1110 | data = response.read().decode('utf-8') 1111 | logging.debug(url) 1112 | return data 1113 | except urllib.error.HTTPError as e: 1114 | logging.error('HTTPError = ' + str(e.code)) 1115 | except urllib.error.URLError as e: 1116 | logging.error('URLError = ' + str(e.reason)) 1117 | except http.client.HTTPException as e: 1118 | logging.error('HTTPException') 1119 | except timeout_error as e: 1120 | pass 1121 | except ssl.CertificateError as e: 1122 | pass 1123 | except Exception: 1124 | import traceback 1125 | logging.error('generic exception: ' + traceback.format_exc()) 1126 | return '' 1127 | 1128 | def _post(self, url: object, params: object, jsonfmt: object = True) -> object: 1129 | if jsonfmt: 1130 | data = (json.dumps(params)).encode() 1131 | 1132 | request = urllib.request.Request(url=url, data=data) 1133 | request.add_header( 1134 | 'ContentType', 'application/json; charset=UTF-8') 1135 | else: 1136 | request = urllib.request.Request(url=url, data=urllib.parse.urlencode(params).encode(encoding='utf-8')) 1137 | 1138 | 1139 | try: 1140 | response = urllib.request.urlopen(request) 1141 | data = response.read() 1142 | if jsonfmt: 1143 | return json.loads(data.decode('utf-8') )#object_hook=_decode_dict) 1144 | return data 1145 | except urllib.error.HTTPError as e: 1146 | logging.error('HTTPError = ' + str(e.code)) 1147 | except urllib.error.URLError as e: 1148 | logging.error('URLError = ' + str(e.reason)) 1149 | except http.client.HTTPException as e: 1150 | logging.error('HTTPException') 1151 | except Exception: 1152 | import traceback 1153 | logging.error('generic exception: ' + traceback.format_exc()) 1154 | 1155 | return '' 1156 | 1157 | def _xiaodoubi(self, word): 1158 | url = 'http://www.xiaodoubi.com/bot/chat.php' 1159 | try: 1160 | r = requests.post(url, data={'chat': word}) 1161 | return r.content 1162 | except: 1163 | return "让我一个人静静 T_T..." 1164 | 1165 | def _simsimi(self, word): 1166 | key = '' 1167 | url = 'http://sandbox.api.simsimi.com/request.p?key=%s&lc=ch&ft=0.0&text=%s' % ( 1168 | key, word) 1169 | r = requests.get(url) 1170 | ans = r.json() 1171 | if ans['result'] == '100': 1172 | return ans['response'] 1173 | else: 1174 | return '你在说什么,风太大听不清列' 1175 | 1176 | def _searchContent(self, key, content, fmat='attr'): 1177 | if fmat == 'attr': 1178 | pm = re.search(key + '\s?=\s?"([^"<]+)"', content) 1179 | if pm: 1180 | return pm.group(1) 1181 | elif fmat == 'xml': 1182 | pm = re.search('<{0}>([^<]+)'.format(key), content) 1183 | if not pm: 1184 | pm = re.search( 1185 | '<{0}><\!\[CDATA\[(.*?)\]\]>'.format(key), content) 1186 | if pm: 1187 | return pm.group(1) 1188 | return '未知' 1189 | 1190 | 1191 | class UnicodeStreamFilter: 1192 | 1193 | def __init__(self, target): 1194 | self.target = target 1195 | self.encoding = 'utf-8' 1196 | self.errors = 'replace' 1197 | self.encode_to = self.target.encoding 1198 | 1199 | def write(self, s): 1200 | if type(s) == str: 1201 | s = s.encode().decode('utf-8') 1202 | s = s.encode(self.encode_to, self.errors).decode(self.encode_to) 1203 | self.target.write(s) 1204 | 1205 | def flush(self): 1206 | self.target.flush() 1207 | 1208 | if sys.stdout.encoding == 'cp936': 1209 | sys.stdout = UnicodeStreamFilter(sys.stdout) 1210 | 1211 | 1212 | if __name__ == '__main__': 1213 | logger = logging.getLogger(__name__) 1214 | if not sys.platform.startswith('win'): 1215 | import coloredlogs 1216 | coloredlogs.install(level='DEBUG') 1217 | 1218 | webwx = WebWeixin() 1219 | webwx.start() 1220 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/README.md: -------------------------------------------------------------------------------- 1 | # wxbot_project_py2.7 2 | 3 | 目录结构: 4 | ```bash 5 | . 6 | ├── README.md 7 | ├── config 8 | │   ├── __init__.py 9 | │   ├── config_manager.py 10 | │   ├── constant.py 11 | │   ├── log.py 12 | │   ├── requirements.txt 13 | │   └── wechat.conf.bak 14 | ├── db 15 | │   ├── __init__.py 16 | │   ├── mysql_db.py 17 | │   └── sqlite_db.py 18 | ├── docker 19 | │   ├── Dockerfile 20 | │   └── README.md 21 | ├── flask_templates 22 | │   ├── index.html 23 | │   └── upload.html 24 | ├── wechat 25 | │   ├── __init__.py 26 | │   ├── utils.py 27 | │   ├── wechat_apis.py 28 | │   └── wechat.py 29 | ├── weixin_bot.py 30 | └── wx_handler 31 | ├── __init__.py 32 | ├── bot.py 33 | ├── sendgrid_mail.py 34 | └── wechat_msg_processor.py 35 | ``` 36 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/config/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from config_manager import ConfigManager 5 | from constant import Constant 6 | from log import Log -------------------------------------------------------------------------------- /wxbot_project_py2.7/config/config_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from constant import Constant 6 | #--------------------------------------------------- 7 | import ConfigParser 8 | import os 9 | #=================================================== 10 | 11 | 12 | class ConfigManager(object): 13 | 14 | def __init__(self): 15 | self.config = Constant.WECHAT_CONFIG_FILE 16 | self.cp = ConfigParser.ConfigParser() 17 | self.cp.read(self.config) 18 | 19 | data_dir = self.get('setting', 'prefix') 20 | upload_dir = self.getpath('uploaddir') 21 | if not os.path.exists(data_dir): 22 | os.makedirs(data_dir) 23 | if not os.path.exists(upload_dir): 24 | os.makedirs(upload_dir) 25 | 26 | def get(self, section, option): 27 | return self.cp.get(section, option) 28 | 29 | def set(self, section, option, value): 30 | self.cp.set(section, option, value) 31 | self.cp.write(open(self.config, 'w')) 32 | 33 | def getpath(self, dir): 34 | prefix = self.get('setting', 'prefix') 35 | return prefix + self.get('setting', dir) 36 | 37 | def setup_database(self): 38 | path = self.get('setting', 'prefix') 39 | conf = [ 40 | path + self.get('setting', 'uploaddir'), 41 | path + self.get('setting', 'datadir'), 42 | path + self.get('setting', 'logdir'), 43 | ] 44 | return conf 45 | 46 | def set_wechat_config(self, conf): 47 | for [key, value] in conf.items(): 48 | self.cp.set('wechat', key, value) 49 | self.cp.write(open(self.config, 'w')) 50 | 51 | def get_wechat_config(self): 52 | uin = self.cp.get('wechat', 'uin') 53 | last_login = self.cp.get('wechat', 'last_login') 54 | conf = [ 55 | self.cp.get('wechat', 'uuid'), 56 | self.cp.get('wechat', 'redirect_uri'), 57 | int(uin if uin else 0), 58 | self.cp.get('wechat', 'sid'), 59 | self.cp.get('wechat', 'skey'), 60 | self.cp.get('wechat', 'pass_ticket'), 61 | self.cp.get('wechat', 'synckey'), 62 | self.cp.get('wechat', 'device_id'), 63 | float(last_login if last_login else 0), 64 | ] 65 | return conf 66 | 67 | def get_wechat_media_dir(self): 68 | prefix = self.get('setting', 'prefix') 69 | path = prefix + self.cp.get('setting', 'mediapath') 70 | return { 71 | 'webwxgeticon': path + '/icons', 72 | 'webwxgetheadimg': path + '/headimgs', 73 | 'webwxgetmsgimg': path + '/msgimgs', 74 | 'webwxgetvideo': path + '/videos', 75 | 'webwxgetvoice': path + '/voices', 76 | '_showQRCodeImg': path + '/qrcodes', 77 | } 78 | 79 | def get_pickle_files(self): 80 | prefix = self.get('setting', 'prefix') 81 | return { 82 | 'User': prefix + self.get('setting', 'contact_user'), 83 | 'MemberList': prefix + self.get('setting', 'contact_member_list'), 84 | 'GroupList': prefix + self.get('setting', 'contact_group_list'), 85 | 'GroupMemeberList': prefix + self.get('setting', 'contact_group_memeber_list'), 86 | 'SpecialUsersList': prefix + self.get('setting', 'contact_special_users_list'), 87 | } 88 | 89 | def get_cookie(self): 90 | prefix = self.get('setting', 'prefix') 91 | path = prefix + self.get('setting', 'cookie') 92 | basedir = os.path.dirname(path) 93 | if not os.path.exists(basedir): 94 | os.makedirs(basedir) 95 | return path 96 | 97 | def mysql(self): 98 | mysql = { 99 | 'host': self.get('mysql', 'host'), 100 | 'port': self.cp.getint('mysql', 'port'), 101 | 'user': self.get('mysql', 'user'), 102 | 'passwd': self.get('mysql', 'passwd'), 103 | 'database': self.get('mysql', 'database'), 104 | } 105 | return mysql 106 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/config/constant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import time 4 | 5 | class Constant(object): 6 | """ 7 | @brief All used constants are listed here 8 | """ 9 | 10 | WECHAT_CONFIG_FILE = 'config/wechat.conf' 11 | LOGGING_LOGGER_NAME = 'WeChat' 12 | 13 | QRCODE_BLACK = '\033[40m \033[0m' 14 | QRCODE_WHITE = '\033[47m \033[0m' 15 | 16 | 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')] 17 | HTTP_HEADER_CONTENTTYPE = ['ContentType', 'application/json; charset=UTF-8'] 18 | HTTP_HEADER_CONNECTION = ['Connection', 'keep-alive'] 19 | HTTP_HEADER_REFERER = ['Referer', 'https://wx.qq.com/'] 20 | HTTP_HEADER_RANGE = ['Range', 'bytes=0-'] 21 | 22 | REGEX_EMOJI = r'' 23 | 24 | SERVER_LOG_FORMAT = '%(asctime)s - %(pathname)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s' 25 | SERVER_UPLOAD_ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) 26 | SERVER_PAGE_UPLOAD = 'upload.html' 27 | SERVER_PAGE_INDEX = 'index.html' 28 | 29 | RUN_RESULT_SUCCESS = '成功 %ds\n' 30 | RUN_RESULT_FAIL = '失败\n[*] 退出程序\n' 31 | MAIN_RESTART = '[*] wait for restart' 32 | LOG_MSG_FILE = 'WeChat-Msgs-%Y-%m-%d.json' 33 | LOG_MSG_GROUP_LIST_FILE = 'group_list.json' 34 | LOG_MSG_QUIT = '\n[*] Force quit.\n' 35 | LOG_MSG_FAIL = '失败\n' 36 | LOG_MSG_SUCCESS = '成功\n' 37 | LOG_MSG_START = '[*] 微信网页版 ... 开动\n' 38 | LOG_MSG_RECOVER = '[*] 从配置文件中恢复 ... ' 39 | LOG_MSG_RECOVER_CONTACT = '[*] 从文件中恢复联系人数据 ... ' 40 | LOG_MSG_TRY_INIT = '[*] 尝试初始化 ... ' 41 | LOG_MSG_ASSOCIATION_LOGIN = '[*] 通过关联登录 ... ' 42 | LOG_MSG_GET_UUID = '[*] 正在获取 uuid ... ' 43 | LOG_MSG_GET_QRCODE = '[*] 正在获取二维码 ... 成功\n' 44 | LOG_MSG_SCAN_QRCODE = '[*] 请使用微信扫描二维码以登录 ... \n' 45 | LOG_MSG_CONFIRM_LOGIN = '[*] 请在手机上点击确认以登录 ... \n' 46 | LOG_MSG_WAIT_LOGIN_ERR1 = '[登陆超时] \n' 47 | LOG_MSG_WAIT_LOGIN_ERR2 = '[登陆异常] \n' 48 | LOG_MSG_LOGIN = '[*] 正在登录 ... ' 49 | LOG_MSG_INIT = '[*] 微信初始化 ... ' 50 | LOG_MSG_STATUS_NOTIFY = '[*] 开启状态通知 ... ' 51 | LOG_MSG_GET_CONTACT = '[*] 获取联系人 ... ' 52 | LOG_MSG_CONTACT_COUNT = '[*] 应有 %s 个联系人,读取到联系人 %d 个\n' 53 | LOG_MSG_OTHER_CONTACT_COUNT = '[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号\n' 54 | LOG_MSG_GET_GROUP_MEMBER = '[*] 拉取群聊成员 ... ' 55 | LOG_MSG_SNAPSHOT = '[*] 保存配置 ... ' 56 | LOG_MSG_LOGOUT = '[*] 你在手机上登出了微信\n' 57 | LOG_MSG_LOGIN_OTHERWHERE = '[*] 你在其他地方登录了 WEB 版微信\n' 58 | LOG_MSG_QUIT_ON_PHONE = '[*] 你在手机上主动退出了\n' 59 | LOG_MSG_RUNTIME = '[*] Total run: %s\n' 60 | LOG_MSG_KILL_PROCESS = 'kill %d' 61 | LOG_MSG_NEW_MSG = '>>> %d 条新消息\n' 62 | LOG_MSG_LOCATION = '[位置] %s' 63 | LOG_MSG_PICTURE = '[图片] %s' 64 | LOG_MSG_VOICE = '[语音] %s' 65 | LOG_MSG_RECALL = '撤回了一条消息' 66 | LOG_MSG_ADD_FRIEND = '%s 请求添加你为好友' 67 | LOG_MSG_UNKNOWN_MSG = '[*] 该消息类型为: %d,内容: %s' 68 | LOG_MSG_VIDEO = '[小视频] %s' 69 | LOG_MSG_NOTIFY_PHONE = '[*] 提示手机网页版微信登录状态\n' 70 | LOG_MSG_EMOTION = '[表情] %s' 71 | LOG_MSG_NAME_CARD = ( 72 | '[名片]\n' 73 | '=========================\n' 74 | '= 昵称: %s\n' 75 | '= 微信号: %s\n' 76 | '= 地区: %s %s\n' 77 | '= 性别: %s\n' 78 | '=========================' 79 | ) 80 | LOG_MSG_SEX_OPTION = ['未知', '男', '女'] 81 | LOG_MSG_APP_LINK = ( 82 | '[%s]\n' 83 | '=========================\n' 84 | '= 标题: %s\n' 85 | '= 描述: %s\n' 86 | '= 链接: %s\n' 87 | '= 来自: %s\n' 88 | '=========================' 89 | ) 90 | LOG_MSG_APP_LINK_TYPE = {5: '链接', 3: '音乐', 7: '微博'} 91 | LOG_MSG_APP_IMG = ( 92 | '[图片]\n' 93 | '=========================\n' 94 | '= 文件: %s\n' 95 | '= 来自: %s\n' 96 | '=========================' 97 | ) 98 | LOG_MSG_SYSTEM = '系统消息' 99 | LOG_MSG_UNKNOWN_NAME = '未知_' 100 | LOG_MSG_UNKNOWN_GROUP_NAME = '未知群_' 101 | 102 | TABLE_GROUP_MSG_LOG = 'WeChatRoomMessage' 103 | TABLE_GROUP_MSG_LOG_COL = """ 104 | MsgID text, 105 | RoomOwnerID text, 106 | RoomName text, 107 | UserCount text, 108 | FromUserName text, 109 | ToUserName text, 110 | AttrStatus text, 111 | DisplayName text, 112 | Name text, 113 | MsgType text, 114 | FaceMsg text, 115 | TextMsg text, 116 | ImageMsg text, 117 | VideoMsg text, 118 | SoundMsg text, 119 | LinkMsg text, 120 | NameCardMsg text, 121 | LocationMsg text, 122 | RecallMsgID text, 123 | SysMsg text, 124 | MsgTime text, 125 | MsgTimestamp text 126 | """ 127 | 128 | @staticmethod 129 | def TABLE_GROUP_LIST(): 130 | return 'WeChatRoom_' + time.strftime('%Y%m%d', time.localtime()) 131 | 132 | TABLE_GROUP_LIST_COL = """ 133 | RoomName text, 134 | RoomID text, 135 | RoomOwnerID text, 136 | UserCount text, 137 | RoomIcon text 138 | """ 139 | 140 | @staticmethod 141 | def TABLE_GROUP_USER_LIST(): 142 | return 'WeChatRoomMember_' + time.strftime('%Y%m%d', time.localtime()) 143 | 144 | TABLE_GROUP_USER_LIST_COL = """ 145 | RoomID text, 146 | MemberID text, 147 | MemberNickName text, 148 | MemberDisplayName text, 149 | MemberAttrStatus text 150 | """ 151 | TABLE_RECORD_ENTER_GROUP = 'WeChatEnterGroupRecord' 152 | TABLE_RECORD_ENTER_GROUP_COL = """ 153 | MsgID text, 154 | RoomName text, 155 | FromUserName text, 156 | ToUserName text, 157 | Name text, 158 | EnterTime text 159 | """ 160 | TABLE_RECORD_RENAME_GROUP = 'WeChatRenameGroupRecord' 161 | TABLE_RECORD_RENAME_GROUP_COL = """ 162 | MsgID text, 163 | FromName text, 164 | ToName text, 165 | ModifyPeople text, 166 | ModifyTime text 167 | """ 168 | 169 | API_APPID = 'wx782c26e4c19acffb' 170 | API_WXAPPID = 'wx299208e619de7026' # Weibo 171 | # 'wxeb7ec651dd0aefa9' # Weixin 172 | API_LANG = 'zh_CN' 173 | API_USER_AGENT = ( 174 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) ' 175 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 176 | 'Chrome/48.0.2564.109 Safari/537.36' 177 | ) 178 | API_SPECIAL_USER = [ 179 | 'newsapp', 'filehelper', 'weibo', 'qqmail', 180 | 'fmessage', 'tmessage', 'qmessage', 'qqsync', 181 | 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 182 | 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 183 | 'masssendapp', 'meishiapp', 'feedsapp', 'voip', 184 | 'blogappweixin', 'brandsessionholder', 'weixin', 185 | 'weixinreminder', 'officialaccounts', 'wxitil', 186 | 'notification_messages', 'wxid_novlwrv3lqwv11', 187 | 'gh_22b87fa7cb3c', 'userexperience_alarm', 188 | ] 189 | 190 | EMOTICON = [ 191 | '[Smile]', '[Grimace]', '[Drool]', '[Scowl]', '[CoolGuy]', '[Sob]', '[Shy]', 192 | '[Silent]', '[Sleep]', '[Cry]', '[Awkward]', '[Angry]', '[Tongue]', '[Grin]', 193 | '[Surprise]', '[Frown]', '[Ruthless]', '[Blush]', '[Scream]', '[Puke]', 194 | '[Chuckle]', '[Joyful]', '[Slight]', '[Smug]', '[Hungry]', '[Drowsy]', '[Panic]', 195 | '[Sweat]', '[Laugh]', '[Commando]', '[Determined]', '[Scold]', '[Shocked]', '[Shhh]', 196 | '[Dizzy]', '[Tormented]', '[Toasted]', '[Skull]', '[Hammer]', '[Wave]', 197 | '[Relief]', '[DigNose]', '[Clap]', '[Shame]', '[Trick]',' [Bah!L]','[Bah!R]', 198 | '[Yawn]', '[Lookdown]', '[Wronged]', '[Puling]', '[Sly]', '[Kiss]', '[Uh-oh]', 199 | '[Whimper]', '[Cleaver]', '[Melon]', '[Beer]', '[Basketball]', '[PingPong]', 200 | '[Coffee]', '[Rice]', '[Pig]', '[Rose]', '[Wilt]', '[Lip]', '[Heart]', 201 | '[BrokenHeart]', '[Cake]', '[Lightning]', '[Bomb]', '[Dagger]', '[Soccer]', '[Ladybug]', 202 | '[Poop]', '[Moon]', '[Sun]', '[Gift]', '[Hug]', '[Strong]', 203 | '[Weak]', '[Shake]', '[Victory]', '[Admire]', '[Beckon]', '[Fist]', '[Pinky]', 204 | '[Love]', '[No]', '[OK]', '[InLove]', '[Blowkiss]', '[Waddle]', '[Tremble]', 205 | '[Aaagh!]', '[Twirl]', '[Kotow]', '[Lookback]', '[Jump]', '[Give-in]', 206 | u'\U0001f604', u'\U0001f637', u'\U0001f639', u'\U0001f61d', u'\U0001f632', u'\U0001f633', 207 | u'\U0001f631', u'\U0001f64d', u'\U0001f609', u'\U0001f60c', u'\U0001f612', u'\U0001f47f', 208 | u'\U0001f47b', u'\U0001f49d', u'\U0001f64f', u'\U0001f4aa', u'\U0001f4b5', u'\U0001f382', 209 | u'\U0001f388', u'\U0001f4e6', 210 | ] 211 | BOT_ZHIHU_URL_LATEST = 'http://news-at.zhihu.com/api/4/news/latest' 212 | BOT_ZHIHU_URL_DAILY = 'http://daily.zhihu.com/story/' 213 | BOT_TULING_API_KEY = '55e7f30895a0a10535984bae5ad294d1' 214 | BOT_TULING_API_URL = 'http://www.tuling123.com/openapi/api?key=%s&info=%s&userid=%s' 215 | BOT_TULING_BOT_REPLY = u'麻烦说的清楚一点,我听不懂你在说什么' 216 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/config/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from constant import Constant 6 | from config import ConfigManager 7 | #--------------------------------------------------- 8 | import logging 9 | import logging.config 10 | #=================================================== 11 | 12 | cm = ConfigManager() 13 | 14 | logging.config.fileConfig(Constant.WECHAT_CONFIG_FILE) 15 | # create logger 16 | Log = logging.getLogger(Constant.LOGGING_LOGGER_NAME) 17 | 18 | # 'application' code 19 | # Log.debug('debug message') 20 | # Log.info('info message') 21 | # Log.warn('warn message') 22 | # Log.error('error message') 23 | # Log.critical('critical message') 24 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/config/requirements.txt: -------------------------------------------------------------------------------- 1 | qrcode 2 | flask 3 | requests 4 | requests_toolbelt 5 | pymysql 6 | sendgrid 7 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/config/wechat.conf.bak: -------------------------------------------------------------------------------- 1 | [wechat] 2 | host = wx.qq.com 3 | uuid = 4 | redirect_uri = 5 | uin = 6 | sid = 7 | skey = 8 | pass_ticket = 9 | device_id = 10 | last_login = 11 | 12 | [setting] 13 | prefix = tmp_data/ 14 | database = WeChat.db 15 | datadir = Data/infos/ 16 | logdir = Logs 17 | mediapath = Data 18 | uploaddir = Data/upload 19 | qrcodedir = Data/qrcode 20 | server_port = 8080 21 | cookie = Cookie/WeChat.cookie 22 | contact_user = Pickle/User.pkl 23 | contact_member_list = Pickle/MemberList.pkl 24 | contact_group_list = Pickle/GroupList.pkl 25 | contact_group_memeber_list = Pickle/GroupMemeberList.pkl 26 | contact_special_users_list = Pickle/SpecialUsersList.pkl 27 | server_mode = False 28 | server_log_file = server.log 29 | log_mode = False 30 | 31 | [mysql] 32 | host = localhost 33 | port = 3306 34 | user = root 35 | passwd = root 36 | database = wechat 37 | 38 | [sendgrid] 39 | api_key = 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 40 | from_email = wxbot@wechat.com 41 | to_email = xxx@example.com 42 | 43 | [loggers] 44 | keys = root,WeChat 45 | 46 | [handlers] 47 | keys = consoleHandler,fileHandler 48 | 49 | [formatters] 50 | keys = simpleFormatter 51 | 52 | [logger_root] 53 | level = DEBUG 54 | handlers = consoleHandler 55 | 56 | [logger_WeChat] 57 | level = DEBUG 58 | handlers = fileHandler 59 | qualname = WeChat 60 | propagate = 0 61 | 62 | [handler_consoleHandler] 63 | class = StreamHandler 64 | level = DEBUG 65 | formatter = simpleFormatter 66 | args = (sys.stdout,) 67 | 68 | [handler_fileHandler] 69 | class = FileHandler 70 | level = DEBUG 71 | formatter = simpleFormatter 72 | args = ('tmp_data/wechat.log',) 73 | 74 | [formatter_simpleFormatter] 75 | format = %(asctime)s - %(name)s - %(levelname)s - %(message)s 76 | datefmt = 77 | 78 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/db/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from sqlite_db import SqliteDB 5 | from mysql_db import MysqlDB -------------------------------------------------------------------------------- /wxbot_project_py2.7/db/mysql_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from config import Log 6 | #--------------------------------------------------- 7 | import pymysql 8 | import threading 9 | import traceback 10 | #=================================================== 11 | 12 | def array_join(arr, c): 13 | t = '' 14 | for a in arr: 15 | t += "'%s'" % str(a).replace("'","\\\'") + c 16 | return t[:-len(c)] 17 | 18 | class MysqlDB(object): 19 | """ 20 | 修改服务器上的配置文件/etc/my.cnf,在对应位置添加以下设置: 21 | [client] 22 | default-character-set = utf8mb4 23 | 24 | [mysql] 25 | default-character-set = utf8mb4 26 | 27 | [mysqld] 28 | character-set-client-handshake = FALSE 29 | character-set-server = utf8mb4 30 | collation-server = utf8mb4_unicode_ci 31 | init_connect='SET NAMES utf8mb4' 32 | """ 33 | 34 | def __init__(self, conf): 35 | self.conf = conf 36 | config = { 37 | 'host': conf['host'], 38 | 'port': conf['port'], 39 | 'user': conf['user'], 40 | 'passwd': conf['passwd'], 41 | 'charset':'utf8mb4', # 支持1-4个字节字符 42 | 'cursorclass': pymysql.cursors.DictCursor 43 | } 44 | self.conn = pymysql.connect(**config) 45 | self.conn.autocommit(1) 46 | # for thread-save 47 | self.lock = threading.Lock() 48 | 49 | self.create_db(conf['database']) 50 | self.conn.select_db(conf['database']) 51 | 52 | # cache table cols 53 | self.table_cols = {} 54 | for t in self.show_tables(): 55 | self.table_cols[t] = self.get_table_column_name(t) 56 | 57 | def show_database(self): 58 | c = self.conn.cursor() 59 | sql = 'SHOW DATABASES' 60 | Log.debug('DB -> %s' % sql) 61 | c.execute(sql) 62 | return [r['Database'] for r in c.fetchall()] 63 | 64 | def show_tables(self): 65 | c = self.conn.cursor() 66 | sql = 'SHOW TABLES' 67 | Log.debug('DB -> %s' % sql) 68 | c.execute(sql) 69 | return [r['Tables_in_'+self.conf['database']] for r in c.fetchall()] 70 | 71 | def create_db(self, db_name): 72 | """ 73 | @brief Creates a database 74 | @param db_name String 75 | """ 76 | if self.conf['database'] not in self.show_database(): 77 | sql = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % db_name 78 | Log.debug('DB -> %s' % sql) 79 | self.execute(sql) 80 | 81 | def create_table(self, table, cols): 82 | """ 83 | @brief Creates a table in database 84 | @param table String 85 | @param cols String, the cols in table 86 | """ 87 | if table not in self.table_cols: 88 | sql = 'CREATE TABLE IF NOT EXISTS %s(id int primary key auto_increment, %s) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % (table, cols) 89 | Log.debug('DB -> %s' % sql) 90 | self.execute(sql) 91 | self.table_cols[table] = ['id'] + [c.strip().split(' ')[0] for c in cols.split(',')] 92 | 93 | def delete_table(self, table): 94 | """ 95 | @brief Delete a table in database 96 | @param table String 97 | """ 98 | if table in self.table_cols: 99 | sql = "DROP TABLE IF EXISTS %s" % table 100 | Log.debug('DB -> %s' % sql) 101 | self.execute(sql) 102 | self.table_cols.pop(table) 103 | 104 | def insert(self, table, value): 105 | """ 106 | @brief Insert a row in table 107 | @param table String 108 | @param value Tuple 109 | """ 110 | col_name = self.table_cols[table][1:] 111 | sql = "INSERT INTO %s(%s) VALUES (%s)" % (table, str(','.join(col_name)), array_join(value, ',')) 112 | Log.debug('DB -> %s' % sql) 113 | self.execute(sql) 114 | 115 | def insertmany(self, table, values): 116 | """ 117 | @brief Insert many rows in table 118 | @param table String 119 | @param values Array of tuple 120 | """ 121 | col_name = self.table_cols[table][1:] 122 | sql = 'INSERT INTO %s(%s) VALUES (%s)' % (table, ','.join(col_name), ','.join(['%s'] * len(values[0]))) 123 | Log.debug('DB -> %s' % sql) 124 | self.execute(sql, values) 125 | 126 | def select(self, table, field='', condition=''): 127 | """ 128 | @brief select all result from table 129 | @param table String 130 | @param field String 131 | @param condition String 132 | @return result Tuple 133 | """ 134 | sql = "SELECT * FROM %s" % table 135 | if field and condition: 136 | sql += " WHERE %s='%s'" % (field, condition) 137 | Log.debug('DB -> %s' % sql) 138 | return self.execute(sql) 139 | 140 | def get_table_column_name(self, table): 141 | """ 142 | @brief select all result from table 143 | @param table String 144 | @return result Array 145 | """ 146 | c = self.conn.cursor() 147 | c.execute("SELECT * FROM %s" % table) 148 | names = list(map(lambda x: x[0], c.description)) 149 | return names 150 | 151 | def execute(self, sql, values=None): 152 | """ 153 | @brief execute sql commands, return result if it has 154 | @param sql String 155 | @param value Tuple 156 | @return result Array 157 | """ 158 | c = self.conn.cursor() 159 | self.lock.acquire() 160 | hasReturn = sql.lstrip().upper().startswith("SELECT") 161 | 162 | result = [] 163 | try: 164 | if values: 165 | c.executemany(sql, values) 166 | else: 167 | c.execute(sql) 168 | 169 | if hasReturn: 170 | result = c.fetchall() 171 | 172 | except Exception, e: 173 | Log.error(traceback.format_exc()) 174 | self.conn.rollback() 175 | finally: 176 | self.lock.release() 177 | 178 | if hasReturn: 179 | return result 180 | 181 | def delete(self, table, field='', condition=''): 182 | """ 183 | @brief execute sql commands, return result if it has 184 | @param table String 185 | @param field String 186 | @param condition String 187 | """ 188 | sql = "DELETE FROM %s WHERE %s=%s" % (table, field, condition) 189 | Log.debug('DB -> %s' % sql) 190 | self.execute(sql) 191 | 192 | def close(self): 193 | """ 194 | @brief close connection to database 195 | """ 196 | Log.debug('DB -> close') 197 | # 关闭数据库连接 198 | self.conn.close() 199 | 200 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/db/sqlite_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from config import Log 6 | #--------------------------------------------------- 7 | import sqlite3 8 | import threading 9 | import traceback 10 | #=================================================== 11 | 12 | 13 | def _dict_factory(cursor, row): 14 | aDict = {} 15 | for iField, field in enumerate (cursor.description): 16 | aDict [field [0]] = row [iField] 17 | return aDict 18 | 19 | 20 | class SqliteDB(object): 21 | 22 | def __init__(self, db_file): 23 | self.db_file = db_file 24 | # self.conn = sqlite3.connect(db_file, check_same_thread=False) 25 | # use 8-bit strings instead of unicode string 26 | self.conn.text_factory = str 27 | # not return a tuple but a dict with column name as key 28 | self.conn.row_factory = _dict_factory 29 | # for thread-save 30 | self.lock = threading.Lock() 31 | 32 | def set_conn(self): 33 | self._conn = sqlite3.connect(self.db_file,check_same_thread=False) 34 | 35 | @property 36 | def conn(self): 37 | try: 38 | self._conn.execute('select 1;') 39 | # check out conn 40 | except (sqlite3.ProgrammingError,AttributeError): 41 | # Cannot operate on a closed database 42 | self.set_conn() 43 | 44 | finally: 45 | return self._conn 46 | 47 | 48 | def create_table(self, table, cols): 49 | """ 50 | @brief Creates a table in database 51 | @param table String 52 | @param cols String, the cols in table 53 | """ 54 | sql = "CREATE TABLE if not exists %s (%s);" % (table, cols) 55 | Log.debug('DB -> %s' % sql) 56 | self.execute(sql) 57 | 58 | def delete_table(self, table): 59 | """ 60 | @brief Delete a table in database 61 | @param table String 62 | """ 63 | sql = "DROP TABLE if exists %s;" % table 64 | Log.debug('DB -> %s' % sql) 65 | self.execute(sql) 66 | 67 | def insert(self, table, value): 68 | """ 69 | @brief Insert a row in table 70 | @param table String 71 | @param value Tuple 72 | """ 73 | sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * len(value)) + ");") % table 74 | Log.debug('DB -> %s' % sql) 75 | self.execute(sql, value) 76 | 77 | def insertmany(self, table, values): 78 | """ 79 | @brief Insert many rows in table 80 | @param table String 81 | @param values Array of tuple 82 | """ 83 | c = self.conn.cursor() 84 | self.lock.acquire() 85 | n = len(values[0]) 86 | sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * n) + ");") % table 87 | Log.debug('DB -> %s' % sql) 88 | 89 | try: 90 | c.executemany(sql, values) 91 | except Exception, e: 92 | Log.error(traceback.format_exc()) 93 | finally: 94 | self.lock.release() 95 | 96 | self.conn.commit() 97 | 98 | def select(self, table, field='', condition=''): 99 | """ 100 | @brief select all result from table 101 | @param table String 102 | @param field String 103 | @param condition String 104 | @return result Tuple 105 | """ 106 | result = [] 107 | if field and condition: 108 | cond = (condition,) 109 | sql = "SELECT * FROM %s WHERE %s=?" % (table, field) 110 | Log.debug('DB -> %s' % sql) 111 | result = self.execute(sql, cond) 112 | else: 113 | sql = "SELECT * FROM %s" % table 114 | Log.debug('DB -> %s' % sql) 115 | result = self.execute(sql) 116 | return result 117 | 118 | def update(self, table, dic, condition=''): 119 | k_arr = [] 120 | v_arr = [] 121 | for (k, v) in dic.items(): 122 | k_arr.append('%s=?' % k) 123 | v_arr.append(v) 124 | 125 | sql = "UPDATE %s SET %s" % (table, ','.join(k_arr)) 126 | if condition: 127 | sql += " WHERE %s" % condition 128 | 129 | Log.debug('DB -> %s' % sql) 130 | self.execute(sql, tuple(v_arr)) 131 | 132 | def get_table_column_name(self, table): 133 | """ 134 | @brief select all result from table 135 | @param table String 136 | @return result Array 137 | """ 138 | c = self.conn.cursor() 139 | c.execute("SELECT * FROM %s" % table) 140 | names = list(map(lambda x: x[0], c.description)) 141 | return names 142 | 143 | def execute(self, sql, value=None): 144 | """ 145 | @brief execute sql commands, return result if it has 146 | @param sql String 147 | @param value Tuple 148 | @return result Array 149 | """ 150 | c = self.conn.cursor() 151 | self.lock.acquire() 152 | hasReturn = sql.lstrip().upper().startswith("SELECT") 153 | 154 | try: 155 | if value: 156 | c.execute(sql, value) 157 | else: 158 | c.execute(sql) 159 | 160 | if hasReturn: 161 | result = c.fetchall() 162 | except Exception, e: 163 | Log.error(traceback.format_exc()) 164 | finally: 165 | self.lock.release() 166 | 167 | self.conn.commit() 168 | 169 | if hasReturn: 170 | return result 171 | 172 | def delete(self, table, field='', condition=''): 173 | """ 174 | @brief execute sql commands, return result if it has 175 | @param table String 176 | @param field String 177 | @param condition String 178 | """ 179 | sql = "DELETE FROM %s WHERE %s=?" % (table, field) 180 | Log.debug('DB -> %s' % sql) 181 | cond = (condition,) 182 | self.execute(sql, cond) 183 | 184 | def close(self): 185 | """ 186 | @brief close connection to database 187 | """ 188 | Log.debug('DB -> close') 189 | self.conn.close() 190 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | MAINTAINER Urinx 3 | 4 | RUN apt-get update && \ 5 | apt-get install -y python \ 6 | python-dev \ 7 | python-pip && \ 8 | apt-get clean && \ 9 | apt-get autoclean && \ 10 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 11 | 12 | ADD weixin_bot.tar.gz / 13 | WORKDIR /weixin_bot 14 | 15 | RUN pip install -r config/requirements.txt 16 | EXPOSE 80 17 | ENTRYPOINT ["./weixin_bot.py"] 18 | CMD [""] 19 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/docker/README.md: -------------------------------------------------------------------------------- 1 | # 本地构建 wechat_bot docker 镜像 2 | 3 | 拉下镜像: 4 | 5 | ```bash 6 | docker pull ubuntu:16.04 7 | ``` 8 | 9 | 打包本项目,将压缩包放到`docker`目录下: 10 | 11 | ``` 12 | tar -czf weixin_bot.tar.gz wxbot_project_py2.7/ 13 | ``` 14 | 15 | 切换到`docker`目录,执行`build`命令: 16 | 17 | ```bash 18 | docker build -t wechat-bot . 19 | ``` 20 | 21 | 导出镜像: 22 | 23 | ```bash 24 | docker save wechat-bot > wechat.tar 25 | ``` 26 | 27 | 导入镜像: 28 | 29 | ```bash 30 | docker load < wechat.tar 31 | ``` 32 | 33 | 运行: 34 | 35 | ```bash 36 | docker run -d -P --name xxx -v /src/data/dir:/Wechat_bot/test wechat-bot 37 | ``` 38 | 39 | 删除镜像: 40 | 41 | ```bash 42 | docker rmi -f wechat-bot 43 | ``` 44 | 45 | 查看log: 46 | 47 | ```bash 48 | docker log wechat-bot 49 | ``` 50 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/flask_templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WeChat Bot Server 6 | 15 | 16 | 17 | 18 |
19 |
 20 | 	#####################
 21 | 	# WeChat Bot Server #
 22 | 	#####################
 23 | 
 24 | 
 25 | ## APIs
 26 | 
 27 | - /qrcode
 28 |   ------------------------
 29 |   @brief      login qrcode
 30 |   @method     get
 31 |   @return     raw image
 32 |   ------------------------
 33 | 
 34 | - /runtime
 35 |   -----------------------------
 36 |   @brief      basic info
 37 |   @method     get
 38 |   @return     application/json
 39 |               {
 40 |                 'ret': 0,
 41 |                 'runtime': '',
 42 |                 'total_size': '',
 43 |                 'db_size': '',
 44 |                 'msg_count': '',
 45 |                 'image_count': '',
 46 |                 'voice_count': '',
 47 |                 'video_count': '',
 48 |               }
 49 |   -----------------------------
 50 | 
 51 | - /group_list
 52 |   ------------------------
 53 |   @brief      list groups
 54 |   @method     get
 55 |   ------------------------
 56 | 
 57 | - /group_member_list/<g_id>
 58 |   -----------------------------
 59 |   @brief      list group member
 60 |   @method     get
 61 |   @param      g_id String
 62 |   -----------------------------
 63 | 
 64 | - /group_chat_log/<g_name>
 65 |   -------------------------------
 66 |   @brief      list group chat log
 67 |   @method     get
 68 |   @param      g_name String
 69 |   -------------------------------
 70 | 
 71 | - /upload
 72 |   -------------------------------
 73 |   @brief      upload a file
 74 |   @method     get/post
 75 |   @return     application/json
 76 |               {
 77 |                 'ret': 0,
 78 |                 'msg': '',
 79 |               }
 80 |   -------------------------------
 81 | 
 82 | - /send_msg/<to>/<msg>
 83 |   -------------------------------------------
 84 |   @brief      send message to user or gourp
 85 |   @method     get
 86 |   @param      to: String, user id or group id
 87 |   @param      msg: String, words
 88 |   @return     application/json
 89 |               {
 90 |                 'ret': 0,
 91 |               }
 92 |   -------------------------------------------
 93 | 
 94 | - /send_img/<to>/<img>
 95 |   -------------------------------------------
 96 |   @brief      send image to user or gourp
 97 |   @method     get
 98 |   @param      to: String, user id or group id
 99 |   @param      img: String, image file name
100 |   @return     application/json
101 |               {
102 |                 'ret': 0,
103 |               }
104 |   -------------------------------------------
105 | 
106 | - /send_emot/<to>/<emot>
107 |   -------------------------------------------
108 |   @brief      send emotion to user or gourp
109 |   @method     get
110 |   @param      to: String, user id or group id
111 |   @param      emot: String, emotion file name
112 |   @return     application/json
113 |               {
114 |                 'ret': 0,
115 |               }
116 |   -------------------------------------------
117 | 
118 | - /send_file/<to>/<file>
119 |   -------------------------------------------
120 |   @brief      send emotion to user or gourp
121 |   @method     get
122 |   @param      to: String, user id or group id
123 |   @param      file: String, file name
124 |   @return     application/json
125 |               {
126 |                 'ret': 0,
127 |               }
128 |   -------------------------------------------
129 | 
130 | - /mass_send_msg
131 |   -------------------------------------------
132 |   @brief      send text to mass users or gourps
133 |   @method     post
134 |   @param      application/json
135 |               {
136 |                 'to_list': [
137 |                   'group_id',
138 |                   ...
139 |                 ],
140 |                 'msg': '',
141 |               }
142 |   @return     application/json
143 |               {
144 |                 'ret': 0,
145 |                 'unsend_list': [],
146 |               }
147 |   -------------------------------------------
148 | 
149 | - /mass_send_img
150 |   -------------------------------------------
151 |   @brief      send image to mass users or gourps
152 |   @method     post
153 |   @param      application/json
154 |               {
155 |                 'to_list': [
156 |                   'group_id',
157 |                   ...
158 |                 ],
159 |                 'msg': '',
160 |               }
161 |   @return     application/json
162 |               {
163 |                 'ret': 0,
164 |                 'unsend_list': [],
165 |               }
166 |   -------------------------------------------
167 | 
168 | - /mass_send_emot
169 |   -------------------------------------------
170 |   @brief      send emoticon to mass users or gourps
171 |   @method     post
172 |   @param      application/json
173 |               {
174 |                 'to_list': [
175 |                   'group_id',
176 |                   ...
177 |                 ],
178 |                 'msg': '',
179 |               }
180 |   @return     application/json
181 |               {
182 |                 'ret': 0,
183 |                 'unsend_list': [],
184 |               }
185 |   -------------------------------------------
186 | 
187 | - /mass_send_file
188 |   -------------------------------------------
189 |   @brief      send file to mass users or gourps
190 |   @method     post
191 |   @param      application/json
192 |               {
193 |                 'to_list': [
194 |                   'group_id',
195 |                   ...
196 |                 ],
197 |                 'msg': '',
198 |               }
199 |   @return     application/json
200 |               {
201 |                 'ret': 0,
202 |                 'unsend_list': [],
203 |               }
204 |   -------------------------------------------
205 | 
206 |
207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/flask_templates/upload.html: -------------------------------------------------------------------------------- 1 | 2 | Upload new File 3 |

Upload new File

4 |
5 |

6 | 7 |

-------------------------------------------------------------------------------- /wxbot_project_py2.7/wechat/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from wechat import WeChat -------------------------------------------------------------------------------- /wxbot_project_py2.7/wechat/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from config import Log 6 | from config import Constant 7 | #--------------------------------------------------- 8 | import qrcode 9 | import re 10 | import os 11 | import sys 12 | import json 13 | import urllib 14 | import urllib2 15 | import cookielib 16 | import cPickle as pickle 17 | import traceback 18 | import time 19 | import hashlib 20 | #=================================================== 21 | 22 | 23 | def _decode_data(data): 24 | """ 25 | @brief decode array or dict to utf-8 26 | @param data array or dict 27 | @return utf-8 28 | """ 29 | if isinstance(data, dict): 30 | rv = {} 31 | for key, value in data.iteritems(): 32 | if isinstance(key, unicode): 33 | key = key.encode('utf-8') 34 | rv[key] = _decode_data(value) 35 | return rv 36 | elif isinstance(data, list): 37 | rv = [] 38 | for item in data: 39 | item = _decode_data(item) 40 | rv.append(item) 41 | return rv 42 | elif isinstance(data, unicode): 43 | return data.encode('utf-8') 44 | else: 45 | return data 46 | 47 | 48 | def str2qr_terminal(text): 49 | """ 50 | @brief convert string to qrcode matrix and outprint 51 | @param text The string 52 | """ 53 | Log.debug(text) 54 | qr = qrcode.QRCode() 55 | qr.border = 1 56 | qr.add_data(text) 57 | mat = qr.get_matrix() 58 | print_qr(mat) 59 | 60 | 61 | def str2qr_image(text, image_path): 62 | """ 63 | @brief convert string to qrcode image & save 64 | @param text The string 65 | @param image_path Save image to the path 66 | """ 67 | qr = qrcode.QRCode() 68 | qr.border = 1 69 | qr.add_data(text) 70 | qr.make(fit=True) 71 | img = qr.make_image() 72 | img.save(image_path) 73 | 74 | 75 | def print_qr(mat): 76 | for i in mat: 77 | BLACK = Constant.QRCODE_BLACK 78 | WHITE = Constant.QRCODE_WHITE 79 | echo(''.join([BLACK if j else WHITE for j in i])+'\n') 80 | 81 | 82 | def echo(str): 83 | Log.info(str[:-1]) 84 | sys.stdout.write(str) 85 | sys.stdout.flush() 86 | 87 | 88 | def run(str, func, *args): 89 | t = time.time() 90 | echo(str) 91 | r = False 92 | try: 93 | r = func(*args) 94 | except: 95 | Log.error(traceback.format_exc()) 96 | if r: 97 | totalTime = int(time.time() - t) 98 | echo(Constant.RUN_RESULT_SUCCESS % totalTime) 99 | else: 100 | echo(Constant.RUN_RESULT_FAIL) 101 | exit() 102 | 103 | 104 | def get(url, api=None): 105 | """ 106 | @brief http get request 107 | @param url String 108 | @param api wechat api 109 | @return http response 110 | """ 111 | Log.debug('GET -> ' + url) 112 | request = urllib2.Request(url=url) 113 | request.add_header(*Constant.HTTP_HEADER_CONNECTION) 114 | request.add_header(*Constant.HTTP_HEADER_REFERER) 115 | if api in ['webwxgetvoice', 'webwxgetvideo']: 116 | request.add_header(*Constant.HTTP_HEADER_RANGE) 117 | 118 | while True: 119 | try: 120 | response = urllib2.urlopen(request, timeout=30) 121 | data = response.read() 122 | response.close() 123 | if api == None: 124 | Log.debug(data) 125 | return data 126 | except (KeyboardInterrupt, SystemExit): 127 | raise 128 | except: 129 | Log.error(traceback.format_exc()) 130 | 131 | time.sleep(1) 132 | 133 | 134 | def post(url, params, jsonfmt=True): 135 | """ 136 | @brief http post request 137 | @param url String 138 | @param params Dict, post params 139 | @param jsonfmt Bool, whether is json format 140 | @return http response 141 | """ 142 | Log.debug('POST -> '+url) 143 | Log.debug(params) 144 | if jsonfmt: 145 | request = urllib2.Request(url=url, data=json.dumps(params, ensure_ascii=False).encode('utf8')) 146 | request.add_header(*Constant.HTTP_HEADER_CONTENTTYPE) 147 | else: 148 | request = urllib2.Request(url=url, data=urllib.urlencode(params)) 149 | 150 | while True: 151 | try: 152 | response = urllib2.urlopen(request, timeout=30) 153 | data = response.read() 154 | response.close() 155 | 156 | if jsonfmt: 157 | Log.debug(data) 158 | return json.loads(data, object_hook=_decode_data) 159 | return data 160 | except (KeyboardInterrupt, SystemExit): 161 | raise 162 | except: 163 | Log.error(traceback.format_exc()) 164 | 165 | time.sleep(1) 166 | 167 | 168 | def set_cookie(cookie_file): 169 | """ 170 | @brief Load cookie from file 171 | @param cookie_file 172 | @param user_agent 173 | @return cookie, LWPCookieJar 174 | """ 175 | cookie = cookielib.LWPCookieJar(cookie_file) 176 | try: 177 | cookie.load(ignore_discard=True) 178 | except: 179 | Log.error(traceback.format_exc()) 180 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) 181 | opener.addheaders = Constant.HTTP_HEADER_USERAGENT 182 | urllib2.install_opener(opener) 183 | return cookie 184 | 185 | 186 | def generate_file_name(filename): 187 | """ 188 | @brief generate file name 189 | @return new file name 190 | """ 191 | i = filename.rfind('.') 192 | ext = filename[i:] 193 | tmp = filename + str(int(time.time())) 194 | hash_md5 = hashlib.md5(tmp) 195 | return hash_md5.hexdigest() + ext 196 | 197 | 198 | def save_file(filename, data, dirName): 199 | """ 200 | @brief Saves raw data to file. 201 | @param filename String 202 | @param data Binary data 203 | @param dirName String 204 | @return file path 205 | """ 206 | Log.debug('save file: ' + filename) 207 | fn = filename 208 | if not os.path.exists(dirName): 209 | os.makedirs(dirName) 210 | fn = os.path.join(dirName, filename) 211 | with open(fn, 'wb') as f: 212 | f.write(data) 213 | return fn 214 | 215 | 216 | def save_json(filename, data, dirName, mode='w+'): 217 | """ 218 | @brief Saves dict to json file. 219 | @param filename String 220 | @param data Dict 221 | @param dirName String 222 | @return file path 223 | """ 224 | Log.debug('save json: ' + filename) 225 | fn = filename 226 | if not os.path.exists(dirName): 227 | os.makedirs(dirName) 228 | fn = os.path.join(dirName, filename) 229 | with open(fn, mode) as f: 230 | f.write(json.dumps(data, indent=4)+'\n') 231 | return fn 232 | 233 | 234 | def load_json(filepath): 235 | Log.debug('load json: ' + filepath) 236 | with open(filepath, 'r') as f: 237 | return _decode_data(json.loads(f.read())) 238 | 239 | 240 | def pickle_save(data, file): 241 | """ 242 | @brief Use pickle to save python object into file 243 | @param data The pyhton data 244 | @param file The file 245 | """ 246 | basedir = os.path.dirname(file) 247 | if not os.path.exists(basedir): 248 | os.makedirs(basedir) 249 | with open(file, 'wb') as f: 250 | pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) 251 | 252 | 253 | def pickle_load(file): 254 | """ 255 | @brief Use pickle to load python object from file 256 | @param file The file 257 | @return python data 258 | """ 259 | if os.path.isfile(file): 260 | with open(file, 'rb') as f: 261 | return pickle.load(f) 262 | return None 263 | 264 | 265 | def search_content(key, content, fmat='attr'): 266 | """ 267 | @brief Search content from xml or html format 268 | @param key String 269 | @param content String 270 | @param fmat attr 271 | xml 272 | @return String 273 | """ 274 | if fmat == 'attr': 275 | pm = re.search(key + '\s?=\s?"([^"<]+)"', content) 276 | if pm: 277 | return pm.group(1) 278 | elif fmat == 'xml': 279 | pm = re.search('<{0}>([^<]+)'.format(key), content) 280 | if not pm: 281 | pm = re.search( 282 | '<{0}><\!\[CDATA\[(.*?)\]\]>'.format(key), content) 283 | if pm: 284 | return pm.group(1) 285 | return 'unknown' 286 | 287 | 288 | def is_str(s): 289 | """ 290 | @brief Determines if string. 291 | @param s String 292 | @return True if string, False otherwise. 293 | """ 294 | return isinstance(s, basestring) 295 | 296 | 297 | def trans_coding(data): 298 | """ 299 | @brief Transform string to unicode 300 | @param data String 301 | @return unicode 302 | """ 303 | if not data: 304 | return data 305 | result = None 306 | if type(data) == unicode: 307 | result = data 308 | elif type(data) == str: 309 | result = data.decode('utf-8') 310 | return result 311 | 312 | 313 | def trans_emoji(text): 314 | """ 315 | @brief Transform emoji html text to unicode 316 | @param text String 317 | @return emoji unicode 318 | """ 319 | def _emoji(matched): 320 | hex = matched.group(1) 321 | return ('\\U%08x' % int(hex, 16)).decode('unicode-escape').encode('utf-8') 322 | 323 | replace_t = re.sub(Constant.REGEX_EMOJI, _emoji, text) 324 | return replace_t 325 | 326 | 327 | def auto_reload(mod): 328 | """ 329 | @brief reload modules 330 | @param mod: the need reload modules 331 | """ 332 | try: 333 | module = sys.modules[mod] 334 | except: 335 | Log.error(traceback.format_exc()) 336 | return False 337 | 338 | filename = module.__file__ 339 | # .pyc 修改时间不会变 340 | # 所以就用 .py 的修改时间 341 | if filename.endswith(".pyc"): 342 | filename = filename.replace(".pyc", ".py") 343 | mod_time = os.path.getmtime(filename) 344 | if not "loadtime" in module.__dict__: 345 | module.loadtime = 0 346 | 347 | try: 348 | if mod_time > module.loadtime: 349 | reload(module) 350 | else: 351 | return False 352 | except: 353 | Log.error(traceback.format_exc()) 354 | return False 355 | 356 | module.loadtime = mod_time 357 | 358 | echo('[*] load \'%s\' successful.\n' % mod) 359 | return True 360 | 361 | 362 | def split_array(arr, n): 363 | for i in xrange(0, len(arr), n): 364 | yield arr[i:i+n] 365 | 366 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/wechat/wechat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from utils import * 6 | from wechat_apis import WXAPI 7 | from config import ConfigManager 8 | from config import Constant 9 | from config import Log 10 | #--------------------------------------------------- 11 | import json 12 | import re 13 | import sys 14 | import os 15 | import time 16 | import random 17 | from collections import defaultdict 18 | from datetime import timedelta 19 | import traceback 20 | import Queue 21 | import threading 22 | #=================================================== 23 | 24 | 25 | class WeChat(WXAPI): 26 | 27 | def __str__(self): 28 | description = \ 29 | "=========================\n" + \ 30 | "[#] Web WeChat\n" + \ 31 | "[#] UUID: " + self.uuid + "\n" + \ 32 | "[#] Uin: " + str(self.uin) + "\n" + \ 33 | "[#] Sid: " + self.sid + "\n" + \ 34 | "[#] Skey: " + self.skey + "\n" + \ 35 | "[#] DeviceId: " + self.device_id + "\n" + \ 36 | "[#] PassTicket: " + self.pass_ticket + "\n" + \ 37 | "[#] Run Time: " + self.get_run_time() + '\n' + \ 38 | "=========================" 39 | return description 40 | 41 | def __init__(self, host='wx.qq.com'): 42 | super(WeChat, self).__init__(host) 43 | 44 | self.db = None 45 | self.save_data_folder = '' # 保存图片,语音,小视频的文件夹 46 | self.last_login = 0 # 上次退出的时间 47 | self.time_out = 5 # 同步时间间隔(单位:秒) 48 | self.msg_handler = None 49 | self.start_time = time.time() 50 | self.bot = None 51 | 52 | cm = ConfigManager() 53 | self.save_data_folders = cm.get_wechat_media_dir() 54 | self.cookie_file = cm.get_cookie() 55 | self.pickle_file = cm.get_pickle_files() 56 | self.log_mode = cm.get('setting', 'log_mode') == 'True' 57 | self.exit_code = 0 58 | 59 | def start(self): 60 | echo(Constant.LOG_MSG_START) 61 | run(Constant.LOG_MSG_RECOVER, self.recover) 62 | 63 | timeOut = time.time() - self.last_login 64 | echo(Constant.LOG_MSG_TRY_INIT) 65 | if self.webwxinit(): 66 | echo(Constant.LOG_MSG_SUCCESS) 67 | run(Constant.LOG_MSG_RECOVER_CONTACT, self.recover_contacts) 68 | else: 69 | echo(Constant.LOG_MSG_FAIL) 70 | 71 | while True: 72 | # first try to login by uin without qrcode 73 | echo(Constant.LOG_MSG_ASSOCIATION_LOGIN) 74 | if self.association_login(): 75 | echo(Constant.LOG_MSG_SUCCESS) 76 | else: 77 | echo(Constant.LOG_MSG_FAIL) 78 | # scan qrcode to login 79 | run(Constant.LOG_MSG_GET_UUID, self.getuuid) 80 | echo(Constant.LOG_MSG_GET_QRCODE) 81 | self.genqrcode() 82 | echo(Constant.LOG_MSG_SCAN_QRCODE) 83 | 84 | if not self.waitforlogin(): 85 | continue 86 | echo(Constant.LOG_MSG_CONFIRM_LOGIN) 87 | if not self.waitforlogin(0): 88 | continue 89 | break 90 | 91 | run(Constant.LOG_MSG_LOGIN, self.login) 92 | run(Constant.LOG_MSG_INIT, self.webwxinit) 93 | run(Constant.LOG_MSG_STATUS_NOTIFY, self.webwxstatusnotify) 94 | run(Constant.LOG_MSG_GET_CONTACT, self.webwxgetcontact) 95 | echo(Constant.LOG_MSG_CONTACT_COUNT % ( 96 | self.MemberCount, len(self.MemberList) 97 | )) 98 | echo(Constant.LOG_MSG_OTHER_CONTACT_COUNT % ( 99 | len(self.GroupList), len(self.ContactList), 100 | len(self.SpecialUsersList), len(self.PublicUsersList) 101 | )) 102 | run(Constant.LOG_MSG_GET_GROUP_MEMBER, self.fetch_group_contacts) 103 | 104 | run(Constant.LOG_MSG_SNAPSHOT, self.snapshot) 105 | 106 | while True: 107 | [retcode, selector] = self.synccheck() 108 | Log.debug('retcode: %s, selector: %s' % (retcode, selector)) 109 | self.exit_code = int(retcode) 110 | 111 | if retcode == '1100': 112 | echo(Constant.LOG_MSG_LOGOUT) 113 | break 114 | if retcode == '1101': 115 | echo(Constant.LOG_MSG_LOGIN_OTHERWHERE) 116 | break 117 | if retcode == '1102': 118 | echo(Constant.LOG_MSG_QUIT_ON_PHONE) 119 | break 120 | elif retcode == '0': 121 | if selector == '2': 122 | r = self.webwxsync() 123 | if r is not None: 124 | try: 125 | self.handle_msg(r) 126 | except: 127 | Log.error(traceback.format_exc()) 128 | elif selector == '7': 129 | r = self.webwxsync() 130 | elif selector == '0': 131 | time.sleep(self.time_out) 132 | elif selector == '4': 133 | # 保存群聊到通讯录 134 | # 修改群名称 135 | # 新增或删除联系人 136 | # 群聊成员数目变化 137 | r = self.webwxsync() 138 | if r is not None: 139 | try: 140 | self.handle_mod(r) 141 | except: 142 | Log.error(traceback.format_exc()) 143 | elif selector == '3' or selector == '6': 144 | break 145 | else: 146 | r = self.webwxsync() 147 | Log.debug('webwxsync: %s\n' % json.dumps(r)) 148 | 149 | # 执行定时任务 150 | if self.msg_handler: 151 | self.msg_handler.check_schedule_task() 152 | 153 | # if self.bot: 154 | # r = self.bot.time_schedule() 155 | # if r: 156 | # for g in self.GroupList: 157 | # echo('[*] 推送 -> %s: %s' % (g['NickName'], r)) 158 | # g_id = g['UserName'] 159 | # self.webwxsendmsg(r, g_id) 160 | 161 | def get_run_time(self): 162 | """ 163 | @brief get how long this run 164 | @return String 165 | """ 166 | totalTime = int(time.time() - self.start_time) 167 | t = timedelta(seconds=totalTime) 168 | return '%s Day %s' % (t.days, t) 169 | 170 | def stop(self): 171 | """ 172 | @brief Save some data and use shell to kill this process 173 | """ 174 | run(Constant.LOG_MSG_SNAPSHOT, self.snapshot) 175 | echo(Constant.LOG_MSG_RUNTIME % self.get_run_time()) 176 | # close database connect 177 | self.db.close() 178 | 179 | def fetch_group_contacts(self): 180 | """ 181 | @brief Fetches all groups contacts. 182 | @return Bool: whether operation succeed. 183 | @note This function must be finished in 180s 184 | """ 185 | Log.debug('fetch_group_contacts') 186 | # clean database 187 | if self.msg_handler: 188 | self.msg_handler.clean_db() 189 | 190 | # sqlite 191 | # ---------------------------------------------------- 192 | # group max_thread_num max_fetch_group_num time(s) 193 | # 197 10 10 108 194 | # 197 10 15 95 195 | # 197 20 10 103 196 | # 197 10 20 55 197 | # 197 5 30 39 198 | # 197 4 50 35 199 | # ---------------------------------------------------- 200 | # mysql 201 | # ---------------------------------------------------- 202 | # group max_thread_num max_fetch_group_num time(s) 203 | # 197 4 50 20 204 | # ---------------------------------------------------- 205 | 206 | max_thread_num = 4 207 | max_fetch_group_num = 50 208 | group_list_queue = Queue.Queue() 209 | 210 | class GroupListThread(threading.Thread): 211 | 212 | def __init__(self, group_list_queue, wechat): 213 | threading.Thread.__init__(self) 214 | self.group_list_queue = group_list_queue 215 | self.wechat = wechat 216 | 217 | def run(self): 218 | while not self.group_list_queue.empty(): 219 | g_list = self.group_list_queue.get() 220 | gid_list = [] 221 | g_dict = {} 222 | for g in g_list: 223 | gid = g['UserName'] 224 | gid_list.append(gid) 225 | g_dict[gid] = g 226 | 227 | group_member_list = self.wechat.webwxbatchgetcontact(gid_list) 228 | 229 | for member_list in group_member_list: 230 | gid = member_list['UserName'] 231 | g = g_dict[gid] 232 | g['MemberCount'] = member_list['MemberCount'] 233 | g['OwnerUin'] = member_list['OwnerUin'] 234 | self.wechat.GroupMemeberList[gid] = member_list['MemberList'] 235 | 236 | # 如果使用 Mysql 则可以在多线程里操作数据库 237 | # 否则请注释下列代码在主线程里更新群列表 238 | # ----------------------------------- 239 | # 处理群成员 240 | # if self.wechat.msg_handler: 241 | # self.wechat.msg_handler.handle_group_member_list(gid, member_list['MemberList']) 242 | # ----------------------------------- 243 | 244 | self.group_list_queue.task_done() 245 | 246 | for g_list in split_array(self.GroupList, max_fetch_group_num): 247 | group_list_queue.put(g_list) 248 | 249 | for i in range(max_thread_num): 250 | t = GroupListThread(group_list_queue, self) 251 | t.setDaemon(True) 252 | t.start() 253 | 254 | group_list_queue.join() 255 | 256 | if self.msg_handler: 257 | # 处理群 258 | if self.GroupList: 259 | self.msg_handler.handle_group_list(self.GroupList) 260 | 261 | # 这个是用 sqlite 来存储群列表,sqlite 对多线程的支持不太好 262 | # ---------------------------------------------------- 263 | # 处理群成员 264 | for (gid, member_list) in self.GroupMemeberList.items(): 265 | self.msg_handler.handle_group_member_list(gid, member_list) 266 | # ---------------------------------------------------- 267 | 268 | return True 269 | 270 | def snapshot(self): 271 | """ 272 | @brief Save basic infos for next login. 273 | @return Bool: whether operation succeed. 274 | """ 275 | try: 276 | conf = { 277 | 'uuid': self.uuid, 278 | 'redirect_uri': self.redirect_uri, 279 | 'uin': self.uin, 280 | 'sid': self.sid, 281 | 'skey': self.skey, 282 | 'pass_ticket': self.pass_ticket, 283 | 'synckey': self.synckey, 284 | 'device_id': self.device_id, 285 | 'last_login': time.time(), 286 | } 287 | cm = ConfigManager() 288 | Log.debug('save wechat config') 289 | cm.set_wechat_config(conf) 290 | 291 | # save cookie 292 | Log.debug('save cookie') 293 | if self.cookie: 294 | self.cookie.save(ignore_discard=True) 295 | 296 | # save contacts 297 | Log.debug('save contacts') 298 | self.save_contacts() 299 | except Exception, e: 300 | Log.error(traceback.format_exc()) 301 | return False 302 | return True 303 | 304 | def recover(self): 305 | """ 306 | @brief Recover from snapshot data. 307 | @return Bool: whether operation succeed. 308 | """ 309 | cm = ConfigManager() 310 | [self.uuid, self.redirect_uri, self.uin, 311 | self.sid, self.skey, self.pass_ticket, 312 | self.synckey, device_id, self.last_login] = cm.get_wechat_config() 313 | 314 | if device_id: 315 | self.device_id = device_id 316 | 317 | self.base_request = { 318 | 'Uin': int(self.uin), 319 | 'Sid': self.sid, 320 | 'Skey': self.skey, 321 | 'DeviceID': self.device_id, 322 | } 323 | 324 | # set cookie 325 | Log.debug('set cookie') 326 | self.cookie = set_cookie(self.cookie_file) 327 | 328 | return True 329 | 330 | def save_contacts(self): 331 | """ 332 | @brief Save contacts. 333 | """ 334 | pickle_save(self.User, self.pickle_file['User']) 335 | pickle_save(self.MemberList, self.pickle_file['MemberList']) 336 | pickle_save(self.GroupList, self.pickle_file['GroupList']) 337 | pickle_save(self.GroupMemeberList, self.pickle_file['GroupMemeberList']) 338 | pickle_save(self.SpecialUsersList, self.pickle_file['SpecialUsersList']) 339 | 340 | def recover_contacts(self): 341 | """ 342 | @brief recover contacts. 343 | @return Bool: whether operation succeed. 344 | """ 345 | try: 346 | self.User = pickle_load(self.pickle_file['User']) 347 | self.MemberList = pickle_load(self.pickle_file['MemberList']) 348 | self.GroupList = pickle_load(self.pickle_file['GroupList']) 349 | self.GroupMemeberList = pickle_load(self.pickle_file['GroupMemeberList']) 350 | self.SpecialUsersList = pickle_load(self.pickle_file['SpecialUsersList']) 351 | return True 352 | except Exception, e: 353 | Log.error(traceback.format_exc()) 354 | return False 355 | 356 | def handle_mod(self, r): 357 | # ModContactCount: 变更联系人或群聊成员数目 358 | # ModContactList: 变更联系人或群聊列表,或群名称改变 359 | Log.debug('handle modify') 360 | self.handle_msg(r) 361 | for m in r['ModContactList']: 362 | if m['UserName'][:2] == '@@': 363 | # group 364 | in_list = False 365 | g_id = m['UserName'] 366 | for g in self.GroupList: 367 | # group member change 368 | if g_id == g['UserName']: 369 | g['MemberCount'] = m['MemberCount'] 370 | g['NickName'] = m['NickName'] 371 | self.GroupMemeberList[g_id] = m['MemberList'] 372 | in_list = True 373 | if self.msg_handler: 374 | self.msg_handler.handle_group_member_change(g_id, m['MemberList']) 375 | break 376 | if not in_list: 377 | # a new group 378 | self.GroupList.append(m) 379 | self.GroupMemeberList[g_id] = m['MemberList'] 380 | if self.msg_handler: 381 | self.msg_handler.handle_group_list_change(m) 382 | self.msg_handler.handle_group_member_change(g_id, m['MemberList']) 383 | 384 | elif m['UserName'][0] == '@': 385 | # user 386 | in_list = False 387 | for u in self.MemberList: 388 | u_id = m['UserName'] 389 | if u_id == u['UserName']: 390 | u = m 391 | in_list = True 392 | break 393 | # if don't have then add it 394 | if not in_list: 395 | self.MemberList.append(m) 396 | 397 | def handle_msg(self, r): 398 | """ 399 | @brief Recover from snapshot data. 400 | @param r Dict: message json 401 | """ 402 | Log.debug('handle message') 403 | if self.msg_handler: 404 | self.msg_handler.handle_wxsync(r) 405 | 406 | n = len(r['AddMsgList']) 407 | if n == 0: 408 | return 409 | 410 | if self.log_mode: 411 | echo(Constant.LOG_MSG_NEW_MSG % n) 412 | 413 | for msg in r['AddMsgList']: 414 | 415 | msgType = msg['MsgType'] 416 | msgId = msg['MsgId'] 417 | content = msg['Content'].replace('<', '<').replace('>', '>') 418 | raw_msg = None 419 | 420 | if msgType == self.wx_conf['MSGTYPE_TEXT']: 421 | # 地理位置消息 422 | if content.find('pictype=location') != -1: 423 | location = content.split('
')[1][:-1] 424 | raw_msg = { 425 | 'raw_msg': msg, 426 | 'location': location, 427 | 'log': Constant.LOG_MSG_LOCATION % location 428 | } 429 | # 普通文本消息 430 | else: 431 | text = content.split(':
')[-1] 432 | raw_msg = { 433 | 'raw_msg': msg, 434 | 'text': text, 435 | 'log': text.replace('
', '\n') 436 | } 437 | elif msgType == self.wx_conf['MSGTYPE_IMAGE']: 438 | data = self.webwxgetmsgimg(msgId) 439 | fn = 'img_' + msgId + '.jpg' 440 | dir = self.save_data_folders['webwxgetmsgimg'] 441 | path = save_file(fn, data, dir) 442 | raw_msg = {'raw_msg': msg, 443 | 'image': path, 444 | 'log': Constant.LOG_MSG_PICTURE % path} 445 | elif msgType == self.wx_conf['MSGTYPE_VOICE']: 446 | data = self.webwxgetvoice(msgId) 447 | fn = 'voice_' + msgId + '.mp3' 448 | dir = self.save_data_folders['webwxgetvoice'] 449 | path = save_file(fn, data, dir) 450 | raw_msg = {'raw_msg': msg, 451 | 'voice': path, 452 | 'log': Constant.LOG_MSG_VOICE % path} 453 | elif msgType == self.wx_conf['MSGTYPE_SHARECARD']: 454 | info = msg['RecommendInfo'] 455 | card = Constant.LOG_MSG_NAME_CARD % ( 456 | info['NickName'], 457 | info['Alias'], 458 | info['Province'], info['City'], 459 | Constant.LOG_MSG_SEX_OPTION[info['Sex']] 460 | ) 461 | namecard = '%s %s %s %s %s' % ( 462 | info['NickName'], info['Alias'], info['Province'], 463 | info['City'], Constant.LOG_MSG_SEX_OPTION[info['Sex']] 464 | ) 465 | raw_msg = { 466 | 'raw_msg': msg, 467 | 'namecard': namecard, 468 | 'log': card 469 | } 470 | elif msgType == self.wx_conf['MSGTYPE_EMOTICON']: 471 | url = search_content('cdnurl', content) 472 | raw_msg = {'raw_msg': msg, 473 | 'emoticon': url, 474 | 'log': Constant.LOG_MSG_EMOTION % url} 475 | elif msgType == self.wx_conf['MSGTYPE_APP']: 476 | card = '' 477 | # 链接, 音乐, 微博 478 | if msg['AppMsgType'] in [ 479 | self.wx_conf['APPMSGTYPE_AUDIO'], 480 | self.wx_conf['APPMSGTYPE_URL'], 481 | self.wx_conf['APPMSGTYPE_OPEN'] 482 | ]: 483 | card = Constant.LOG_MSG_APP_LINK % ( 484 | Constant.LOG_MSG_APP_LINK_TYPE[msg['AppMsgType']], 485 | msg['FileName'], 486 | search_content('des', content, 'xml'), 487 | msg['Url'], 488 | search_content('appname', content, 'xml') 489 | ) 490 | raw_msg = { 491 | 'raw_msg': msg, 492 | 'link': msg['Url'], 493 | 'log': card 494 | } 495 | # 图片 496 | elif msg['AppMsgType'] == self.wx_conf['APPMSGTYPE_IMG']: 497 | data = self.webwxgetmsgimg(msgId) 498 | fn = 'img_' + msgId + '.jpg' 499 | dir = self.save_data_folders['webwxgetmsgimg'] 500 | path = save_file(fn, data, dir) 501 | card = Constant.LOG_MSG_APP_IMG % ( 502 | path, 503 | search_content('appname', content, 'xml') 504 | ) 505 | raw_msg = { 506 | 'raw_msg': msg, 507 | 'image': path, 508 | 'log': card 509 | } 510 | else: 511 | raw_msg = { 512 | 'raw_msg': msg, 513 | 'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content) 514 | } 515 | elif msgType == self.wx_conf['MSGTYPE_STATUSNOTIFY']: 516 | Log.info(Constant.LOG_MSG_NOTIFY_PHONE) 517 | elif msgType == self.wx_conf['MSGTYPE_MICROVIDEO']: 518 | data = self.webwxgetvideo(msgId) 519 | fn = 'video_' + msgId + '.mp4' 520 | dir = self.save_data_folders['webwxgetvideo'] 521 | path = save_file(fn, data, dir) 522 | raw_msg = {'raw_msg': msg, 523 | 'video': path, 524 | 'log': Constant.LOG_MSG_VIDEO % path} 525 | elif msgType == self.wx_conf['MSGTYPE_RECALLED']: 526 | recall_id = search_content('msgid', content, 'xml') 527 | text = Constant.LOG_MSG_RECALL 528 | raw_msg = { 529 | 'raw_msg': msg, 530 | 'text': text, 531 | 'recall_msg_id': recall_id, 532 | 'log': text 533 | } 534 | elif msgType == self.wx_conf['MSGTYPE_SYS']: 535 | raw_msg = { 536 | 'raw_msg': msg, 537 | 'sys_notif': content, 538 | 'log': content 539 | } 540 | elif msgType == self.wx_conf['MSGTYPE_VERIFYMSG']: 541 | name = search_content('fromnickname', content) 542 | raw_msg = { 543 | 'raw_msg': msg, 544 | 'log': Constant.LOG_MSG_ADD_FRIEND % name 545 | } 546 | else: 547 | raw_msg = { 548 | 'raw_msg': msg, 549 | 'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content) 550 | } 551 | 552 | isGroupMsg = '@@' in msg['FromUserName']+msg['ToUserName'] 553 | if self.msg_handler and raw_msg: 554 | if isGroupMsg: 555 | # handle group messages 556 | g_msg = self.make_group_msg(raw_msg) 557 | self.msg_handler.handle_group_msg(g_msg) 558 | else: 559 | # handle personal messages 560 | self.msg_handler.handle_user_msg(raw_msg) 561 | 562 | if self.log_mode: 563 | self.show_msg(raw_msg) 564 | 565 | def make_group_msg(self, msg): 566 | """ 567 | @brief Package the group message for storage. 568 | @param msg Dict: raw msg 569 | @return raw_msg Dict: packged msg 570 | """ 571 | Log.debug('make group message') 572 | raw_msg = { 573 | 'raw_msg': msg['raw_msg'], 574 | 'msg_id': msg['raw_msg']['MsgId'], 575 | 'group_owner_uin': '', 576 | 'group_name': '', 577 | 'group_count': '', 578 | 'from_user_name': msg['raw_msg']['FromUserName'], 579 | 'to_user_name': msg['raw_msg']['ToUserName'], 580 | 'user_attrstatus': '', 581 | 'user_display_name': '', 582 | 'user_nickname': '', 583 | 'msg_type': msg['raw_msg']['MsgType'], 584 | 'text': '', 585 | 'link': '', 586 | 'image': '', 587 | 'video': '', 588 | 'voice': '', 589 | 'emoticon': '', 590 | 'namecard': '', 591 | 'location': '', 592 | 'recall_msg_id': '', 593 | 'sys_notif': '', 594 | 'time': '', 595 | 'timestamp': '', 596 | 'log': '', 597 | } 598 | content = msg['raw_msg']['Content'].replace( 599 | '<', '<').replace('>', '>') 600 | 601 | group = None 602 | src = None 603 | 604 | if msg['raw_msg']['FromUserName'][:2] == '@@': 605 | # 接收到来自群的消息 606 | g_id = msg['raw_msg']['FromUserName'] 607 | group = self.get_group_by_id(g_id) 608 | 609 | if re.search(":
", content, re.IGNORECASE): 610 | u_id = content.split(':
')[0] 611 | src = self.get_group_user_by_id(u_id, g_id) 612 | 613 | elif msg['raw_msg']['ToUserName'][:2] == '@@': 614 | # 自己发给群的消息 615 | g_id = msg['raw_msg']['ToUserName'] 616 | u_id = msg['raw_msg']['FromUserName'] 617 | src = self.get_group_user_by_id(u_id, g_id) 618 | group = self.get_group_by_id(g_id) 619 | 620 | if src: 621 | raw_msg['user_attrstatus'] = src['AttrStatus'] 622 | raw_msg['user_display_name'] = src['DisplayName'] 623 | raw_msg['user_nickname'] = src['NickName'] 624 | if group: 625 | raw_msg['group_count'] = group['MemberCount'] 626 | raw_msg['group_owner_uin'] = group['OwnerUin'] 627 | raw_msg['group_name'] = group['ShowName'] 628 | 629 | raw_msg['timestamp'] = msg['raw_msg']['CreateTime'] 630 | t = time.localtime(float(raw_msg['timestamp'])) 631 | raw_msg['time'] = time.strftime("%Y-%m-%d %T", t) 632 | 633 | for key in [ 634 | 'text', 'link', 'image', 'video', 'voice', 635 | 'emoticon', 'namecard', 'location', 'log', 636 | 'recall_msg_id', 'sys_notif' 637 | ]: 638 | if key in msg: 639 | raw_msg[key] = msg[key] 640 | 641 | return raw_msg 642 | 643 | def show_msg(self, message): 644 | """ 645 | @brief Log the message to stdout 646 | @param message Dict 647 | """ 648 | msg = message 649 | src = None 650 | dst = None 651 | group = None 652 | 653 | if msg and msg['raw_msg']: 654 | 655 | content = msg['raw_msg']['Content'] 656 | content = content.replace('<', '<').replace('>', '>') 657 | msg_id = msg['raw_msg']['MsgId'] 658 | 659 | if msg['raw_msg']['FromUserName'][:2] == '@@': 660 | # 接收到来自群的消息 661 | g_id = msg['raw_msg']['FromUserName'] 662 | group = self.get_group_by_id(g_id) 663 | 664 | if re.search(":
", content, re.IGNORECASE): 665 | u_id = content.split(':
')[0] 666 | src = self.get_group_user_by_id(u_id, g_id) 667 | dst = {'ShowName': 'GROUP'} 668 | else: 669 | u_id = msg['raw_msg']['ToUserName'] 670 | src = {'ShowName': 'SYSTEM'} 671 | dst = self.get_group_user_by_id(u_id, g_id) 672 | elif msg['raw_msg']['ToUserName'][:2] == '@@': 673 | # 自己发给群的消息 674 | g_id = msg['raw_msg']['ToUserName'] 675 | u_id = msg['raw_msg']['FromUserName'] 676 | group = self.get_group_by_id(g_id) 677 | src = self.get_group_user_by_id(u_id, g_id) 678 | dst = {'ShowName': 'GROUP'} 679 | else: 680 | # 非群聊消息 681 | src = self.get_user_by_id(msg['raw_msg']['FromUserName']) 682 | dst = self.get_user_by_id(msg['raw_msg']['ToUserName']) 683 | 684 | if group: 685 | echo('%s |%s| %s -> %s: %s\n' % ( 686 | msg_id, 687 | trans_emoji(group['ShowName']), 688 | trans_emoji(src['ShowName']), 689 | dst['ShowName'], 690 | trans_emoji(msg['log']) 691 | )) 692 | else: 693 | echo('%s %s -> %s: %s\n' % ( 694 | msg_id, 695 | trans_emoji(src['ShowName']), 696 | trans_emoji(dst['ShowName']), 697 | trans_emoji(msg['log']) 698 | )) 699 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/wechat/wechat_apis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from utils import * 6 | from config import Constant 7 | from config import Log 8 | #--------------------------------------------------- 9 | import sys 10 | import os 11 | import cookielib 12 | import random 13 | import requests 14 | import time 15 | import xml.dom.minidom 16 | # for media upload 17 | import mimetypes 18 | from requests_toolbelt.multipart.encoder import MultipartEncoder 19 | #=================================================== 20 | 21 | 22 | class WXAPI(object): 23 | 24 | def __init__(self, host): 25 | self.wx_host = host 26 | self.wx_filehost = '' 27 | self.wx_conf = {} 28 | # jsLogin时这个appid只能使用: wx782c26e4c19acffb 29 | self.appid = Constant.API_APPID 30 | self.uuid = '' 31 | self.redirect_uri = '' 32 | self.skey = '' 33 | self.sid = '' 34 | self.uin = '' 35 | self.pass_ticket = '' 36 | self.base_request = {} 37 | self.synckey_dic = {} 38 | self.synckey = '' 39 | self.device_id = 'e' + repr(random.random())[2:17] 40 | # device_id: 登录手机设备 41 | # web wechat 的格式为: e123456789012345 (e+15位随机数) 42 | # mobile wechat 的格式为: A1234567890abcde (A+15位随机数字或字母) 43 | self.user_agent = Constant.API_USER_AGENT 44 | self.cookie = None 45 | 46 | self.conf_factory() 47 | 48 | self.User = [] # 登陆账号信息 49 | self.MemberList = [] # 好友+群聊+公众号+特殊账号 50 | self.MemberCount = 0 51 | self.ContactList = [] # 好友 52 | self.GroupList = [] # 群 53 | self.GroupMemeberList = {} # 群聊成员字典 54 | # "group_id": [ 55 | # {member}, ... 56 | # ] 57 | self.PublicUsersList = [] # 公众号/服务号 58 | self.SpecialUsersList = [] # 特殊账号 59 | 60 | self.media_count = 0 61 | 62 | def conf_factory(self): 63 | e = self.wx_host # wx.qq.com 64 | t, o, n = "login.weixin.qq.com", "file.wx.qq.com", "webpush.weixin.qq.com" 65 | 66 | if e.find("wx2.qq.com") > -1: 67 | t, o, n = "login.wx2.qq.com", "file.wx2.qq.com", "webpush.wx2.qq.com" 68 | elif e.find("wx8.qq.com") > -1: 69 | t, o, n = "login.wx8.qq.com", "file.wx8.qq.com", "webpush.wx8.qq.com" 70 | elif e.find("qq.com") > -1: 71 | t, o, n = "login.wx.qq.com", "file.wx.qq.com", "webpush.wx.qq.com" 72 | elif e.find("web2.wechat.com") > -1: 73 | t, o, n = "login.web2.wechat.com", "file.web2.wechat.com", "webpush.web2.wechat.com" 74 | elif e.find("wechat.com") > -1: 75 | t, o, n = "login.web.wechat.com", "file.web.wechat.com", "webpush.web.wechat.com" 76 | 77 | self.wx_filehost = o 78 | conf = { 79 | 'LANG': Constant.API_LANG, 80 | 'SpecialUsers': Constant.API_SPECIAL_USER, 81 | 'API_jsLogin': "https://" + t + "/jslogin", 82 | 'API_qrcode': "https://login.weixin.qq.com/l/", 83 | 'API_qrcode_img': "https://login.weixin.qq.com/qrcode/", 84 | 'API_login': "https://" + t + "/cgi-bin/mmwebwx-bin/login", 85 | 'API_synccheck': "https://" + n + "/cgi-bin/mmwebwx-bin/synccheck", 86 | 'API_webwxdownloadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxgetmedia", 87 | 'API_webwxuploadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxuploadmedia", 88 | 'API_webwxpreview': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpreview", 89 | 'API_webwxinit': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxinit", 90 | 'API_webwxgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetcontact", 91 | 'API_webwxsync': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsync", 92 | 'API_webwxbatchgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxbatchgetcontact", 93 | 'API_webwxgeticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgeticon", 94 | 'API_webwxsendmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsg", 95 | 'API_webwxsendmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsgimg", 96 | 'API_webwxsendmsgvedio': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendvideomsg", 97 | 'API_webwxsendemoticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendemoticon", 98 | 'API_webwxsendappmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendappmsg", 99 | 'API_webwxgetheadimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetheadimg", 100 | 'API_webwxgetmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmsgimg", 101 | 'API_webwxgetmedia': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmedia", 102 | 'API_webwxgetvideo': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvideo", 103 | 'API_webwxlogout': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxlogout", 104 | 'API_webwxgetvoice': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvoice", 105 | 'API_webwxupdatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxupdatechatroom", 106 | 'API_webwxcreatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcreatechatroom", 107 | 'API_webwxstatusnotify': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatusnotify", 108 | 'API_webwxcheckurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckurl", 109 | 'API_webwxverifyuser': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxverifyuser", 110 | 'API_webwxfeedback': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendfeedback", 111 | 'API_webwxreport': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatreport", 112 | 'API_webwxsearch': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsearchcontact", 113 | 'API_webwxoplog': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxoplog", 114 | 'API_checkupload': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckupload", 115 | 'API_webwxrevokemsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxrevokemsg", 116 | 'API_webwxpushloginurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpushloginurl", 117 | 'CONTACTFLAG_CONTACT': 1, 118 | 'CONTACTFLAG_CHATCONTACT': 2, 119 | 'CONTACTFLAG_CHATROOMCONTACT': 4, 120 | 'CONTACTFLAG_BLACKLISTCONTACT': 8, 121 | 'CONTACTFLAG_DOMAINCONTACT': 16, 122 | 'CONTACTFLAG_HIDECONTACT': 32, 123 | 'CONTACTFLAG_FAVOURCONTACT': 64, 124 | 'CONTACTFLAG_3RDAPPCONTACT': 128, 125 | 'CONTACTFLAG_SNSBLACKLISTCONTACT': 256, 126 | 'CONTACTFLAG_NOTIFYCLOSECONTACT': 512, 127 | 'CONTACTFLAG_TOPCONTACT': 2048, 128 | 'MSGTYPE_TEXT': 1, 129 | 'MSGTYPE_IMAGE': 3, 130 | 'MSGTYPE_VOICE': 34, 131 | 'MSGTYPE_VIDEO': 43, 132 | 'MSGTYPE_MICROVIDEO': 62, 133 | 'MSGTYPE_EMOTICON': 47, 134 | 'MSGTYPE_APP': 49, 135 | 'MSGTYPE_VOIPMSG': 50, 136 | 'MSGTYPE_VOIPNOTIFY': 52, 137 | 'MSGTYPE_VOIPINVITE': 53, 138 | 'MSGTYPE_LOCATION': 48, 139 | 'MSGTYPE_STATUSNOTIFY': 51, 140 | 'MSGTYPE_SYSNOTICE': 9999, 141 | 'MSGTYPE_POSSIBLEFRIEND_MSG': 40, 142 | 'MSGTYPE_VERIFYMSG': 37, 143 | 'MSGTYPE_SHARECARD': 42, 144 | 'MSGTYPE_SYS': 10000, 145 | 'MSGTYPE_RECALLED': 10002, 146 | 'APPMSGTYPE_TEXT': 1, 147 | 'APPMSGTYPE_IMG': 2, 148 | 'APPMSGTYPE_AUDIO': 3, 149 | 'APPMSGTYPE_VIDEO': 4, 150 | 'APPMSGTYPE_URL': 5, 151 | 'APPMSGTYPE_ATTACH': 6, 152 | 'APPMSGTYPE_OPEN': 7, 153 | 'APPMSGTYPE_EMOJI': 8, 154 | 'APPMSGTYPE_VOICE_REMIND': 9, 155 | 'APPMSGTYPE_SCAN_GOOD': 10, 156 | 'APPMSGTYPE_GOOD': 13, 157 | 'APPMSGTYPE_EMOTION': 15, 158 | 'APPMSGTYPE_CARD_TICKET': 16, 159 | 'APPMSGTYPE_REALTIME_SHARE_LOCATION': 17, 160 | 'APPMSGTYPE_TRANSFERS': 2e3, 161 | 'APPMSGTYPE_RED_ENVELOPES': 2001, 162 | 'APPMSGTYPE_READER_TYPE': 100001, 163 | 'UPLOAD_MEDIA_TYPE_IMAGE': 1, 164 | 'UPLOAD_MEDIA_TYPE_VIDEO': 2, 165 | 'UPLOAD_MEDIA_TYPE_AUDIO': 3, 166 | 'UPLOAD_MEDIA_TYPE_ATTACHMENT': 4, 167 | } 168 | self.wx_conf = conf 169 | 170 | def getuuid(self): 171 | """ 172 | @brief Gets the uuid just used for login. 173 | @return Bool: whether operation succeed. 174 | """ 175 | url = self.wx_conf['API_jsLogin'] 176 | params = { 177 | 'appid': self.appid, 178 | 'fun': 'new', 179 | 'lang': self.wx_conf['LANG'], 180 | '_': int(time.time()), 181 | } 182 | data = post(url, params, False) 183 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' 184 | pm = re.search(regx, data) 185 | if pm: 186 | code = pm.group(1) 187 | self.uuid = pm.group(2) 188 | return code == '200' 189 | return False 190 | 191 | def genqrcode(self): 192 | """ 193 | @brief outprint the qrcode to stdout on macos/linux 194 | or open image on windows 195 | """ 196 | if sys.platform.startswith('win'): 197 | url = self.wx_conf['API_qrcode_img'] + self.uuid 198 | params = { 199 | 't': 'webwx', 200 | '_': int(time.time()) 201 | } 202 | data = post(url, params, False) 203 | if data == '': 204 | return 205 | qrcode_path = save_file('qrcode.jpg', data, './') 206 | os.startfile(qrcode_path) 207 | else: 208 | str2qr_terminal(self.wx_conf['API_qrcode'] + self.uuid) 209 | 210 | def waitforlogin(self, tip=1): 211 | """ 212 | @brief wait for scaning qrcode to login 213 | @param tip 1: wait for scan qrcode 214 | 0: wait for confirm 215 | @return Bool: whether operation succeed 216 | """ 217 | time.sleep(tip) 218 | url = self.wx_conf['API_login'] + '?tip=%s&uuid=%s&_=%s' % ( 219 | tip, self.uuid, int(time.time())) 220 | data = get(url) 221 | pm = re.search(r'window.code=(\d+);', data) 222 | code = pm.group(1) 223 | 224 | if code == '201': 225 | return True 226 | elif code == '200': 227 | pm = re.search(r'window.redirect_uri="(\S+?)";', data) 228 | r_uri = pm.group(1) + '&fun=new' 229 | self.redirect_uri = r_uri 230 | self.wx_host = r_uri.split('://')[1].split('/')[0] 231 | self.conf_factory() 232 | return True 233 | elif code == '408': 234 | echo(Constant.LOG_MSG_WAIT_LOGIN_ERR1) 235 | else: 236 | echo(Constant.LOG_MSG_WAIT_LOGIN_ERR2) 237 | return False 238 | 239 | def login(self): 240 | """ 241 | @brief login 242 | redirect_uri 有效时间是从扫码成功后算起, 243 | 大概是 300 秒,在此期间可以重新登录,但获取的联系人和群ID会改变 244 | @return Bool: whether operation succeed 245 | """ 246 | data = get(self.redirect_uri) 247 | doc = xml.dom.minidom.parseString(data) 248 | root = doc.documentElement 249 | 250 | for node in root.childNodes: 251 | if node.nodeName == 'ret': 252 | if node.childNodes[0].data != "0": 253 | return False 254 | elif node.nodeName == 'skey': 255 | self.skey = node.childNodes[0].data 256 | elif node.nodeName == 'wxsid': 257 | self.sid = node.childNodes[0].data 258 | elif node.nodeName == 'wxuin': 259 | self.uin = node.childNodes[0].data 260 | elif node.nodeName == 'pass_ticket': 261 | self.pass_ticket = node.childNodes[0].data 262 | 263 | if '' in (self.skey, self.sid, self.uin, self.pass_ticket): 264 | return False 265 | 266 | self.base_request = { 267 | 'Uin': int(self.uin), 268 | 'Sid': self.sid, 269 | 'Skey': self.skey, 270 | 'DeviceID': self.device_id, 271 | } 272 | 273 | return True 274 | 275 | def webwxinit(self): 276 | """ 277 | @brief wechat initial 278 | 掉线后 300 秒可以重新使用此 api 登录 279 | 获取的联系人和群ID保持不变 280 | @return Bool: whether operation succeed 281 | """ 282 | url = self.wx_conf['API_webwxinit'] + \ 283 | '?pass_ticket=%s&skey=%s&r=%s' % ( 284 | self.pass_ticket, self.skey, int(time.time()) 285 | ) 286 | params = { 287 | 'BaseRequest': self.base_request 288 | } 289 | dic = post(url, params) 290 | self.User = dic['User'] 291 | self.make_synckey(dic) 292 | 293 | return dic['BaseResponse']['Ret'] == 0 294 | 295 | def webwxstatusnotify(self): 296 | """ 297 | @brief notify the mobile phone, this not necessary 298 | @return Bool: whether operation succeed 299 | """ 300 | url = self.wx_conf['API_webwxstatusnotify'] + \ 301 | '?lang=%s&pass_ticket=%s' % ( 302 | self.wx_conf['LANG'], self.pass_ticket 303 | ) 304 | params = { 305 | 'BaseRequest': self.base_request, 306 | "Code": 3, 307 | "FromUserName": self.User['UserName'], 308 | "ToUserName": self.User['UserName'], 309 | "ClientMsgId": int(time.time()) 310 | } 311 | dic = post(url, params) 312 | 313 | return dic['BaseResponse']['Ret'] == 0 314 | 315 | def webwxgetcontact(self): 316 | """ 317 | @brief get all contacts: people, group, public user, special user 318 | @return Bool: whether operation succeed 319 | """ 320 | SpecialUsers = self.wx_conf['SpecialUsers'] 321 | url = self.wx_conf['API_webwxgetcontact'] + \ 322 | '?pass_ticket=%s&skey=%s&r=%s' % ( 323 | self.pass_ticket, self.skey, int(time.time()) 324 | ) 325 | dic = post(url, {}) 326 | 327 | self.MemberCount = dic['MemberCount'] 328 | self.MemberList = dic['MemberList'] 329 | ContactList = self.MemberList[:] 330 | GroupList = self.GroupList[:] 331 | PublicUsersList = self.PublicUsersList[:] 332 | SpecialUsersList = self.SpecialUsersList[:] 333 | 334 | for i in xrange(len(ContactList) - 1, -1, -1): 335 | Contact = ContactList[i] 336 | if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 337 | ContactList.remove(Contact) 338 | self.PublicUsersList.append(Contact) 339 | elif Contact['UserName'] in SpecialUsers: # 特殊账号 340 | ContactList.remove(Contact) 341 | self.SpecialUsersList.append(Contact) 342 | elif Contact['UserName'].find('@@') != -1: # 群聊 343 | ContactList.remove(Contact) 344 | self.GroupList.append(Contact) 345 | elif Contact['UserName'] == self.User['UserName']: # 自己 346 | ContactList.remove(Contact) 347 | self.ContactList = ContactList 348 | 349 | return True 350 | 351 | def webwxbatchgetcontact(self, gid_list): 352 | """ 353 | @brief get group contacts 354 | @param gid_list, The list of group id 355 | @return List, list of group contacts 356 | """ 357 | url = self.wx_conf['API_webwxbatchgetcontact'] + \ 358 | '?type=ex&r=%s&pass_ticket=%s' % ( 359 | int(time.time()), self.pass_ticket 360 | ) 361 | params = { 362 | 'BaseRequest': self.base_request, 363 | "Count": len(gid_list), 364 | "List": [{"UserName": gid, "EncryChatRoomId": ""} for gid in gid_list] 365 | } 366 | dic = post(url, params) 367 | return dic['ContactList'] 368 | 369 | def synccheck(self): 370 | """ 371 | @brief check whether there's a message 372 | @return [retcode, selector] 373 | retcode: 0 successful 374 | 1100 logout 375 | 1101 login otherwhere 376 | selector: 0 nothing 377 | 2 message 378 | 6 unkonwn 379 | 7 webwxsync 380 | """ 381 | params = { 382 | 'r': int(time.time()), 383 | 'sid': self.sid, 384 | 'uin': self.uin, 385 | 'skey': self.skey, 386 | 'deviceid': self.device_id, 387 | 'synckey': self.synckey, 388 | '_': int(time.time()), 389 | } 390 | url = self.wx_conf['API_synccheck'] + '?' + urllib.urlencode(params) 391 | data = get(url) 392 | reg = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' 393 | pm = re.search(reg, data) 394 | retcode = pm.group(1) 395 | selector = pm.group(2) 396 | return [retcode, selector] 397 | 398 | def webwxsync(self): 399 | """ 400 | @brief sync the messages 401 | @return Dict{} 402 | """ 403 | url = self.wx_conf['API_webwxsync'] + \ 404 | '?sid=%s&skey=%s&pass_ticket=%s' % ( 405 | self.sid, self.skey, self.pass_ticket 406 | ) 407 | params = { 408 | 'BaseRequest': self.base_request, 409 | 'SyncKey': self.synckey_dic, 410 | 'rr': ~int(time.time()) 411 | } 412 | dic = post(url, params) 413 | 414 | if dic['BaseResponse']['Ret'] == 0: 415 | self.make_synckey(dic) 416 | return dic 417 | 418 | def webwxgetmsgimg(self, msgid): 419 | """ 420 | @brief get image in message 421 | @param msgid The id of message 422 | @return binary data of image 423 | """ 424 | url = self.wx_conf['API_webwxgetmsgimg'] + \ 425 | '?MsgID=%s&skey=%s' % (msgid, self.skey) 426 | data = get(url, api='webwxgetmsgimg') 427 | return data 428 | 429 | def webwxgetvoice(self, msgid): 430 | """ 431 | @brief get voice in message 432 | @param msgid The id of message 433 | @return binary data of voice 434 | """ 435 | url = self.wx_conf['API_webwxgetvoice'] + \ 436 | '?msgid=%s&skey=%s' % (msgid, self.skey) 437 | data = get(url, api='webwxgetvoice') 438 | return data 439 | 440 | def webwxgetvideo(self, msgid): 441 | """ 442 | @brief get video in message 443 | @param msgid The id of message 444 | @return binary data of video 445 | """ 446 | url = self.wx_conf['API_webwxgetvideo'] + \ 447 | '?msgid=%s&skey=%s' % (msgid, self.skey) 448 | data = get(url, api='webwxgetvideo') 449 | return data 450 | 451 | def webwxgeticon(self, id): 452 | """ 453 | @brief get user small icon 454 | @param id String 455 | @return binary data of icon 456 | """ 457 | url = self.wx_conf['API_webwxgeticon'] + \ 458 | '?username=%s&skey=%s' % (id, self.skey) 459 | data = get(url, api='webwxgeticon') 460 | return data 461 | 462 | def webwxgetheadimg(self, id): 463 | """ 464 | @brief get user head image 465 | @param id String 466 | @return binary data of image 467 | """ 468 | url = self.wx_conf['API_webwxgetheadimg'] + \ 469 | '?username=%s&skey=%s' % (id, self.skey) 470 | data = get(url, api='webwxgetheadimg') 471 | return data 472 | 473 | def webwxsendmsg(self, word, to='filehelper'): 474 | """ 475 | @brief send text message 476 | @param word String 477 | @param to User id 478 | @return dic Dict 479 | """ 480 | url = self.wx_conf['API_webwxsendmsg'] + \ 481 | '?pass_ticket=%s' % (self.pass_ticket) 482 | clientMsgId = str(int(time.time() * 1000)) + \ 483 | str(random.random())[:5].replace('.', '') 484 | params = { 485 | 'BaseRequest': self.base_request, 486 | 'Msg': { 487 | "Type": 1, 488 | "Content": trans_coding(word), 489 | "FromUserName": self.User['UserName'], 490 | "ToUserName": to, 491 | "LocalID": clientMsgId, 492 | "ClientMsgId": clientMsgId 493 | } 494 | } 495 | dic = post(url, params) 496 | return dic 497 | 498 | def webwxuploadmedia(self, file_path): 499 | """ 500 | @brief upload image 501 | @param file_path String 502 | @return Dict: json 503 | """ 504 | url = self.wx_conf['API_webwxuploadmedia'] + '?f=json' 505 | # 计数器 506 | self.media_count = self.media_count + 1 507 | fn = file_path 508 | # mime_type: 509 | # 'application/pdf' 510 | # 'image/jpeg' 511 | # 'image/png' 512 | # ... 513 | mime_type = mimetypes.guess_type(fn, strict=False)[0] 514 | if not mime_type: 515 | mime_type = 'text/plain' 516 | # 文档格式 517 | # 微信服务器目前应该支持3种类型: 518 | # pic 直接显示,包含图片,表情 519 | # video 不清楚 520 | # doc 显示为文件,包含PDF等 521 | media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc' 522 | time_format = "%a %b %d %Y %T GMT%z (%Z)" 523 | last_modifie_date = time.strftime(time_format, time.localtime()) 524 | file_size = os.path.getsize(fn) 525 | pass_ticket = self.pass_ticket 526 | client_media_id = str(int(time.time() * 1000)) + \ 527 | str(random.random())[:5].replace('.', '') 528 | 529 | webwx_data_ticket = '' 530 | for item in self.cookie: 531 | if item.name == 'webwx_data_ticket': 532 | webwx_data_ticket = item.value 533 | break 534 | if (webwx_data_ticket == ''): 535 | Log.error("No Cookie\n") 536 | return None 537 | 538 | uploadmediarequest = json.dumps({ 539 | "BaseRequest": self.base_request, 540 | "ClientMediaId": client_media_id, 541 | "TotalLen": file_size, 542 | "StartPos": 0, 543 | "DataLen": file_size, 544 | "MediaType": 4 545 | }, ensure_ascii=False).encode('utf8') 546 | 547 | multipart_encoder = MultipartEncoder( 548 | fields={ 549 | 'id': 'WU_FILE_' + str(self.media_count), 550 | 'name': fn, 551 | 'type': mime_type, 552 | 'lastModifieDate': last_modifie_date, 553 | 'size': str(file_size), 554 | 'mediatype': media_type, 555 | 'uploadmediarequest': uploadmediarequest, 556 | 'webwx_data_ticket': webwx_data_ticket, 557 | 'pass_ticket': pass_ticket, 558 | 'filename': ( 559 | fn, 560 | open(fn, 'rb'), 561 | mime_type.split('/')[1] 562 | ) 563 | }, 564 | boundary=( 565 | '-----------------------------' 566 | '1575017231431605357584454111' 567 | ) 568 | ) 569 | 570 | headers = { 571 | 'Host': self.wx_filehost, 572 | 'User-Agent': self.user_agent, 573 | 'Accept': ( 574 | 'text/html,application/xhtml+xml,' 575 | 'application/xml;q=0.9,*/*;q=0.8' 576 | ), 577 | 'Accept-Language': 'en-US,en;q=0.5', 578 | 'Accept-Encoding': 'gzip, deflate', 579 | 'Referer': 'https://' + self.wx_host, 580 | 'Content-Type': multipart_encoder.content_type, 581 | 'Origin': 'https://' + self.wx_host, 582 | 'Connection': 'keep-alive', 583 | 'Pragma': 'no-cache', 584 | 'Cache-Control': 'no-cache', 585 | } 586 | 587 | r = requests.post(url, data=multipart_encoder, headers=headers) 588 | dic = json.loads(r.text) #修复无法发送Media消息BUG 589 | if dic['BaseResponse']['Ret'] == 0: 590 | return dic 591 | return None 592 | 593 | def webwxsendmsgimg(self, user_id, media_id): 594 | """ 595 | @brief send image 596 | @param user_id String 597 | @param media_id String 598 | @return Bool: whether operation succeed 599 | """ 600 | url = self.wx_conf['API_webwxsendmsgimg'] + \ 601 | '?fun=async&f=json&pass_ticket=%s' % self.pass_ticket 602 | clientMsgId = str(int(time.time() * 1000)) + \ 603 | str(random.random())[:5].replace('.', '') 604 | data_json = { 605 | "BaseRequest": self.base_request, 606 | "Msg": { 607 | "Type": 3, 608 | "MediaId": media_id, 609 | "FromUserName": self.User['UserName'], 610 | "ToUserName": user_id, 611 | "LocalID": clientMsgId, 612 | "ClientMsgId": clientMsgId 613 | } 614 | } 615 | r = post(url, data_json) 616 | return dic['BaseResponse']['Ret'] == 0 617 | 618 | def webwxsendemoticon(self, user_id, media_id): 619 | """ 620 | @brief send image 621 | @param user_id String 622 | @param media_id String 623 | @return Bool: whether operation succeed 624 | """ 625 | url = self.wx_conf['API_webwxsendemoticon'] + \ 626 | '?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket 627 | clientMsgId = str(int(time.time() * 1000)) + \ 628 | str(random.random())[:5].replace('.', '') 629 | data_json = { 630 | "BaseRequest": self.base_request, 631 | "Msg": { 632 | "Type": 47, 633 | "EmojiFlag": 2, 634 | "MediaId": media_id, 635 | "FromUserName": self.User['UserName'], 636 | "ToUserName": user_id, 637 | "LocalID": clientMsgId, 638 | "ClientMsgId": clientMsgId 639 | } 640 | } 641 | r = post(url, data_json) 642 | return dic['BaseResponse']['Ret'] == 0 643 | 644 | def webwxsendappmsg(self, user_id, data): 645 | """ 646 | @brief send app msg 647 | @param user_id String 648 | @param data Dict 649 | @return Bool: whether operation succeed 650 | """ 651 | url = self.wx_conf['API_webwxsendappmsg'] + \ 652 | '?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket 653 | clientMsgId = str(int(time.time() * 1000)) + \ 654 | str(random.random())[:5].replace('.', '') 655 | content = ''.join([ 656 | "" % data['appid'], # 可使用其它AppID 657 | "%s" % data['title'], 658 | "", 659 | "", 660 | "%d" % data['type'], 661 | "", 662 | "", 663 | "", 664 | "", 665 | "%d" % data['totallen'], 666 | "%s" % data['attachid'], 667 | "%s" % data['fileext'], 668 | "", 669 | "", 670 | "", 671 | ]) 672 | data_json = { 673 | "BaseRequest": self.base_request, 674 | "Msg": { 675 | "Type": data['type'], 676 | "Content": content, 677 | "FromUserName": self.User['UserName'], 678 | "ToUserName": user_id, 679 | "LocalID": clientMsgId, 680 | "ClientMsgId": clientMsgId 681 | }, 682 | "Scene": 0 683 | } 684 | r = post(url, data_json) 685 | return dic['BaseResponse']['Ret'] == 0 686 | 687 | def webwxcreatechatroom(self, uid_arr): 688 | """ 689 | @brief create a chat group 690 | @param uid_arr [String] 691 | @return Bool: whether operation succeed 692 | """ 693 | url = self.wx_conf['API_webwxcreatechatroom'] + '?r=%s' % int(time.time()) 694 | params = { 695 | 'BaseRequest': self.base_request, 696 | 'Topic': '', 697 | 'MemberCount': len(uid_arr), 698 | 'MemberList': [{'UserName': uid} for uid in uid_arr], 699 | } 700 | dic = post(url, params) 701 | return dic['BaseResponse']['Ret'] == 0 702 | 703 | def webwxupdatechatroom(self, add_arr, del_arr, invite_arr): 704 | """ 705 | @brief add/delete/invite member in chat group 706 | @param add_arr [uid: String] 707 | @param del_arr [uid: String] 708 | @param invite_arr [uid: String] 709 | @return Bool: whether operation succeed 710 | """ 711 | url = self.wx_conf['API_webwxupdatechatroom'] + '?r=%s' % int(time.time()) 712 | params = { 713 | 'BaseRequest': self.base_request, 714 | 'ChatRoomName': '', 715 | 'NewTopic': '', 716 | 'AddMemberList': add_arr, 717 | 'DelMemberList': del_arr, 718 | 'InviteMemberList': invite_arr, 719 | } 720 | dic = post(url, params) 721 | return dic['BaseResponse']['Ret'] == 0 722 | 723 | def webwxrevokemsg(self, msgid, user_id, client_msgid): 724 | """ 725 | @brief revoke a message 726 | @param msgid String 727 | @param user_id String 728 | @param client_msgid String 729 | @return Bool: whether operation succeed 730 | """ 731 | url = self.wx_conf['API_webwxrevokemsg'] + '?r=%s' % int(time.time()) 732 | params = { 733 | 'BaseRequest': self.base_request, 734 | 'SvrMsgId': msgid, 735 | 'ToUserName': user_id, 736 | 'ClientMsgId': client_msgid 737 | } 738 | dic = post(url, params) 739 | return dic['BaseResponse']['Ret'] == 0 740 | 741 | def webwxpushloginurl(self, uin): 742 | """ 743 | @brief push a login confirm alert to mobile device 744 | @param uin String 745 | @return dic Dict 746 | """ 747 | url = self.wx_conf['API_webwxpushloginurl'] + '?uin=%s' % uin 748 | dic = eval(get(url)) 749 | return dic 750 | 751 | def association_login(self): 752 | """ 753 | @brief login without scan qrcode 754 | @return Bool: whether operation succeed 755 | """ 756 | if self.uin != '': 757 | dic = self.webwxpushloginurl(self.uin) 758 | if dic['ret'] == '0': 759 | self.uuid = dic['uuid'] 760 | return True 761 | return False 762 | 763 | def send_text(self, user_id, text): 764 | """ 765 | @brief send text 766 | @param user_id String 767 | @param text String 768 | @return Bool: whether operation succeed 769 | """ 770 | try: 771 | dic = self.webwxsendmsg(text, user_id) 772 | return dic['BaseResponse']['Ret'] == 0 773 | except: 774 | return False 775 | 776 | def send_img(self, user_id, file_path): 777 | """ 778 | @brief send image 779 | @param user_id String 780 | @param file_path String 781 | @return Bool: whether operation succeed 782 | """ 783 | response = self.webwxuploadmedia(file_path) 784 | media_id = "" 785 | if response is not None: 786 | media_id = response['MediaId'] 787 | return self.webwxsendmsgimg(user_id, media_id) 788 | 789 | def send_emot(self, user_id, file_path): 790 | """ 791 | @brief send emotion 792 | @param user_id String 793 | @param file_path String 794 | @return Bool: whether operation succeed 795 | """ 796 | response = self.webwxuploadmedia(file_path) 797 | media_id = "" 798 | if response is not None: 799 | media_id = response['MediaId'] 800 | return self.webwxsendemoticon(user_id, media_id) 801 | 802 | def send_file(self, user_id, file_path): 803 | """ 804 | @brief send file 805 | @param user_id String 806 | @param file_path String 807 | @return Bool: whether operation succeed 808 | """ 809 | title = file_path.split('/')[-1] 810 | data = { 811 | 'appid': Constant.API_WXAPPID, 812 | 'title': title, 813 | 'totallen': '', 814 | 'attachid': '', 815 | 'type': self.wx_conf['APPMSGTYPE_ATTACH'], 816 | 'fileext': title.split('.')[-1], 817 | } 818 | 819 | response = self.webwxuploadmedia(file_path) 820 | if response is not None: 821 | data['totallen'] = response['StartPos'] 822 | data['attachid'] = response['MediaId'] 823 | else: 824 | Log.error('File upload error') 825 | 826 | return self.webwxsendappmsg(user_id, data) 827 | 828 | def make_synckey(self, dic): 829 | self.synckey_dic = dic['SyncKey'] 830 | 831 | def foo(x): 832 | return str(x['Key']) + '_' + str(x['Val']) 833 | 834 | # synckey for synccheck 835 | self.synckey = '|'.join( 836 | [foo(keyVal) for keyVal in self.synckey_dic['List']]) 837 | 838 | def revoke_msg(self, msgid, user_id, client_msgid): 839 | """ 840 | @brief revoke a message 841 | @param msgid String 842 | @param user_id String 843 | @param client_msgid String 844 | @return Bool: whether operation succeed 845 | """ 846 | return self.webwxrevokemsg(msgid, user_id, client_msgid) 847 | 848 | # ----------------------------------------------------- 849 | # The following is getting user & group infomation apis 850 | def get_user_by_id(self, user_id): 851 | """ 852 | @brief get all type of name by user id 853 | @param user_id The id of user 854 | @return Dict: { 855 | 'UserName' # 微信动态ID 856 | 'RemarkName' # 备注 857 | 'NickName' # 微信昵称 858 | 'ShowName' # Log显示用的 859 | } 860 | """ 861 | UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id 862 | name = { 863 | 'UserName': user_id, 864 | 'RemarkName': '', 865 | 'NickName': '', 866 | 'ShowName': '', 867 | } 868 | name['ShowName'] = UnknownPeople 869 | 870 | # yourself 871 | if user_id == self.User['UserName']: 872 | name['RemarkName'] = self.User['RemarkName'] 873 | name['NickName'] = self.User['NickName'] 874 | name['ShowName'] = name['NickName'] 875 | else: 876 | # 联系人 877 | for member in self.MemberList: 878 | if member['UserName'] == user_id: 879 | r, n = member['RemarkName'], member['NickName'] 880 | name['RemarkName'] = r 881 | name['NickName'] = n 882 | name['ShowName'] = r if r else n 883 | break 884 | # 特殊帐号 885 | for member in self.SpecialUsersList: 886 | if member['UserName'] == user_id: 887 | name['RemarkName'] = user_id 888 | name['NickName'] = user_id 889 | name['ShowName'] = user_id 890 | break 891 | 892 | return name 893 | 894 | def get_group_user_by_id(self, user_id, group_id): 895 | """ 896 | @brief get group user by user id 897 | @param user_id The id of user 898 | @param group_id The id of group 899 | @return Dict: { 900 | 'UserName' # 微信动态ID 901 | 'NickName' # 微信昵称 902 | 'DisplayName' # 群聊显示名称 903 | 'ShowName' # Log显示用的 904 | 'AttrStatus' # 群用户id 905 | } 906 | """ 907 | UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id 908 | name = { 909 | 'UserName': user_id, 910 | 'NickName': '', 911 | 'DisplayName': '', 912 | 'ShowName': '', 913 | 'AttrStatus': '', 914 | } 915 | name['ShowName'] = UnknownPeople 916 | 917 | # 群友 918 | if group_id in self.GroupMemeberList: 919 | for member in self.GroupMemeberList[group_id]: 920 | if member['UserName'] == user_id: 921 | n, d = member['NickName'], member['DisplayName'] 922 | name['NickName'] = n 923 | name['DisplayName'] = d 924 | name['AttrStatus'] = member['AttrStatus'] 925 | name['ShowName'] = d if d else n 926 | break 927 | 928 | return name 929 | 930 | def get_group_by_id(self, group_id): 931 | """ 932 | @brief get basic info by group id 933 | @param group_id The id of group 934 | @return Dict: { 935 | 'UserName' # 微信动态ID 936 | 'NickName' # 微信昵称 937 | 'DisplayName' # 群聊显示名称 938 | 'ShowName' # Log显示用的 939 | 'OwnerUin' # 群主ID 940 | 'MemberCount' # 群人数 941 | } 942 | """ 943 | UnknownGroup = Constant.LOG_MSG_UNKNOWN_GROUP_NAME + group_id 944 | group = { 945 | 'UserName': group_id, 946 | 'NickName': '', 947 | 'DisplayName': '', 948 | 'ShowName': '', 949 | 'OwnerUin': '', 950 | 'MemberCount': '', 951 | } 952 | 953 | for member in self.GroupList: 954 | if member['UserName'] == group_id: 955 | group['NickName'] = member['NickName'] 956 | group['DisplayName'] = member.get('DisplayName', '') 957 | group['ShowName'] = member.get('NickName', UnknownGroup) 958 | group['OwnerUin'] = member.get('OwnerUin', '') 959 | group['MemberCount'] = member['MemberCount'] 960 | break 961 | 962 | return group 963 | 964 | def get_user_id(self, name): 965 | """ 966 | @brief Gets the user id. 967 | @param name The user nickname or remarkname 968 | @return The user id. 969 | """ 970 | for member in self.MemberList: 971 | if name == member['RemarkName'] or name == member['NickName']: 972 | return member['UserName'] 973 | return None 974 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/weixin_bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from wechat import WeChat 6 | from wechat.utils import * 7 | from wx_handler import WeChatMsgProcessor 8 | from wx_handler import Bot 9 | from wx_handler import SGMail 10 | from db import SqliteDB 11 | from db import MysqlDB 12 | from config import ConfigManager 13 | from config import Constant 14 | from config import Log 15 | #--------------------------------------------------- 16 | from flask import Flask, render_template, send_file, jsonify, request 17 | import threading 18 | import traceback 19 | import os 20 | import logging 21 | import time 22 | #=================================================== 23 | 24 | 25 | cm = ConfigManager() 26 | db = SqliteDB(cm.getpath('database')) 27 | # db = MysqlDB(cm.mysql()) 28 | wechat_msg_processor = WeChatMsgProcessor(db) 29 | wechat = WeChat(cm.get('wechat', 'host')) 30 | wechat.db = db 31 | wechat.bot = Bot() 32 | wechat.msg_handler = wechat_msg_processor 33 | wechat_msg_processor.wechat = wechat 34 | 35 | PORT = int(cm.get('setting', 'server_port')) 36 | app = Flask(__name__, template_folder='flask_templates') 37 | app.config['UPLOAD_FOLDER'] = cm.getpath('uploaddir') 38 | 39 | logger = logging.getLogger('werkzeug') 40 | log_format_str = Constant.SERVER_LOG_FORMAT 41 | formatter = logging.Formatter(log_format_str) 42 | flask_log_handler = logging.FileHandler(cm.getpath('server_log_file')) 43 | flask_log_handler.setLevel(logging.INFO) 44 | flask_log_handler.setFormatter(formatter) 45 | logger.addHandler(flask_log_handler) 46 | app.logger.addHandler(flask_log_handler) 47 | 48 | # sendgrid mail 49 | sg_apikey = cm.get('sendgrid', 'api_key') 50 | from_email = cm.get('sendgrid', 'from_email') 51 | to_email = cm.get('sendgrid', 'to_email') 52 | sg = SGMail(sg_apikey, from_email, to_email) 53 | 54 | @app.route('/') 55 | def index(): 56 | return render_template(Constant.SERVER_PAGE_INDEX) 57 | 58 | 59 | @app.route('/qrcode') 60 | def qrcode(): 61 | qdir = cm.getpath('qrcodedir') 62 | if not os.path.exists(qdir): 63 | os.makedirs(qdir) 64 | image_path = '%s/%s_%d.png' % (qdir, wechat.uuid, int(time.time()*100)) 65 | s = wechat.wx_conf['API_qrcode'] + wechat.uuid 66 | str2qr_image(s, image_path) 67 | return send_file(image_path, mimetype='image/png') 68 | 69 | 70 | @app.route("/group_list") 71 | def group_list(): 72 | """ 73 | @brief list groups 74 | """ 75 | result = wechat.db.select(Constant.TABLE_GROUP_LIST()) 76 | return jsonify({'count': len(result), 'group': result}) 77 | 78 | 79 | @app.route('/group_member_list/') 80 | def group_member_list(g_id): 81 | """ 82 | @brief list group member 83 | @param g_id String 84 | """ 85 | result = wechat.db.select(Constant.TABLE_GROUP_USER_LIST(), 'RoomID', g_id) 86 | return jsonify({'count': len(result), 'member': result}) 87 | 88 | 89 | @app.route('/group_chat_log/') 90 | def group_chat_log(g_name): 91 | """ 92 | @brief list group chat log 93 | @param g_name String 94 | """ 95 | result = wechat.db.select(Constant.TABLE_GROUP_MSG_LOG, 'RoomName', g_name) 96 | return jsonify({'count': len(result), 'chats': result}) 97 | 98 | 99 | @app.route('/upload', methods=['GET', 'POST']) 100 | def upload_file(): 101 | if request.method == 'POST': 102 | def allowed_file(filename): 103 | return '.' in filename and \ 104 | filename.rsplit('.', 1)[1] in Constant.SERVER_UPLOAD_ALLOWED_EXTENSIONS 105 | 106 | j = {'ret': 1, 'msg': ''} 107 | 108 | # check if the post request has the file part 109 | if 'file' not in request.files: 110 | j['msg'] = 'No file part' 111 | return jsonify(j) 112 | 113 | # if user does not select file, browser also 114 | # submit a empty part without filename 115 | file = request.files['file'] 116 | if file.filename == '': 117 | j['msg'] = 'No selected file' 118 | elif file and allowed_file(file.filename): 119 | filename = generate_file_name(file.filename) 120 | file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) 121 | file.save(file_path) 122 | j['ret'] = 0 123 | j['msg'] = filename 124 | else: 125 | j['msg'] = 'File type not support' 126 | return jsonify(j) 127 | else: 128 | return render_template(Constant.SERVER_PAGE_UPLOAD) 129 | 130 | 131 | @app.route('/send_msg//') 132 | def send_msg(to, msg): 133 | """ 134 | @brief send message to user or gourp 135 | @param to: String, user id or group id 136 | @param msg: String, words 137 | """ 138 | return jsonify({'ret': 0 if wechat.send_text(to, msg) else 1}) 139 | 140 | 141 | @app.route('/send_img//') 142 | def send_img(to, img): 143 | """ 144 | @brief send image to user or gourp 145 | @param to: String, user id or group id 146 | @param img: String, image file name 147 | """ 148 | img_path = os.path.join(app.config['UPLOAD_FOLDER'], img) 149 | return jsonify({'ret': 0 if wechat.send_img(to, img_path) else 1}) 150 | 151 | 152 | @app.route('/send_emot//') 153 | def send_emot(to, emot): 154 | """ 155 | @brief send emotion to user or gourp 156 | @param to: String, user id or group id 157 | @param emot: String, emotion file name 158 | """ 159 | emot_path = os.path.join(app.config['UPLOAD_FOLDER'], emot) 160 | return jsonify({'ret': 0 if wechat.send_emot(to, emot_path) else 1}) 161 | 162 | 163 | @app.route('/send_file//') 164 | def send_file(to, file): 165 | """ 166 | @brief send file to user or gourp 167 | @param to: String, user id or group id 168 | @param file: String, file name 169 | """ 170 | file_path = os.path.join(app.config['UPLOAD_FOLDER'], file) 171 | return jsonify({'ret': 0 if wechat.send_file(to, file_path) else 1}) 172 | 173 | 174 | def mass_send(method, data, func): 175 | j = {'ret': -1, 'unsend_list':[]} 176 | if method == 'POST' and data: 177 | to_list = data['to_list'] 178 | msg = data['msg'] 179 | media_type = data.get('media_type', '') 180 | 181 | if media_type in ['img', 'emot']: 182 | file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg) 183 | response = wechat.webwxuploadmedia(file_path) 184 | if response is not None: 185 | msg = response['MediaId'] 186 | elif media_type == 'file': 187 | file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg) 188 | data = { 189 | 'appid': Constant.API_WXAPPID, 190 | 'title': msg, 191 | 'totallen': '', 192 | 'attachid': '', 193 | 'type': wechat.wx_conf['APPMSGTYPE_ATTACH'], 194 | 'fileext': msg.split('.')[-1], 195 | } 196 | response = wechat.webwxuploadmedia(file_path) 197 | if response is not None: 198 | data['totallen'] = response['StartPos'] 199 | data['attachid'] = response['MediaId'] 200 | else: 201 | Log.error('File upload error') 202 | msg = data 203 | 204 | for groups in split_array(to_list, 20): 205 | for g in groups: 206 | r = func(g, msg) 207 | if not r: 208 | j['unsend_list'].append(g) 209 | time.sleep(1) 210 | 211 | j['ret'] = len(j['unsend_list']) 212 | 213 | return j 214 | 215 | 216 | @app.route('/mass_send_msg/', methods=["GET", "POST"]) 217 | def mass_send_msg(): 218 | """ 219 | @brief send text to mass users or gourps 220 | """ 221 | j = mass_send(request.method, request.json, wechat.send_text) 222 | return jsonify(j) 223 | 224 | 225 | @app.route('/mass_send_img', methods=["GET", "POST"]) 226 | def mass_send_img(): 227 | """ 228 | @brief send iamge to mass users or gourps 229 | """ 230 | j = mass_send(request.method, request.json, wechat.webwxsendmsgimg) 231 | return jsonify(j) 232 | 233 | 234 | @app.route('/mass_send_emot', methods=["GET", "POST"]) 235 | def mass_send_emot(): 236 | """ 237 | @brief send emoticon to mass users or gourps 238 | """ 239 | j = mass_send(request.method, request.json, wechat.webwxsendemoticon) 240 | return jsonify(j) 241 | 242 | 243 | @app.route('/mass_send_file', methods=["GET", "POST"]) 244 | def mass_send_file(): 245 | """ 246 | @brief send file to mass users or gourps 247 | """ 248 | j = mass_send(request.method, request.json, wechat.webwxsendappmsg) 249 | return jsonify(j) 250 | 251 | 252 | def run_server(): 253 | app.run(port=PORT) 254 | 255 | if cm.get('setting', 'server_mode') == 'True': 256 | serverProcess = threading.Thread(target=run_server) 257 | serverProcess.start() 258 | 259 | while True: 260 | try: 261 | wechat.start() 262 | except KeyboardInterrupt: 263 | echo(Constant.LOG_MSG_QUIT) 264 | wechat.exit_code = 1 265 | else: 266 | Log.error(traceback.format_exc()) 267 | finally: 268 | wechat.stop() 269 | 270 | # send a mail to tell the wxbot is failing 271 | subject = 'wxbot stop message' 272 | log_file = open(eval(cm.get('handler_fileHandler', 'args'))[0], 'r') 273 | mail_content = '
' + str(wechat) + '\n\n-----\nLogs:\n-----\n\n' + ''.join(log_file.readlines()[-100:]) + '
' 274 | sg.send_mail(subject, mail_content, 'text/html') 275 | log_file.close() 276 | 277 | if wechat.exit_code == 0: 278 | echo(Constant.MAIN_RESTART) 279 | else: 280 | # kill process 281 | os.system(Constant.LOG_MSG_KILL_PROCESS % os.getpid()) 282 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/wx_handler/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from wechat_msg_processor import WeChatMsgProcessor 5 | from bot import Bot 6 | from sendgrid_mail import SGMail -------------------------------------------------------------------------------- /wxbot_project_py2.7/wx_handler/bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from wechat.utils import * 6 | from config import Constant 7 | #--------------------------------------------------- 8 | import random, time, json 9 | #=================================================== 10 | 11 | 12 | class Bot(object): 13 | 14 | def __init__(self): 15 | self.emoticons = Constant.EMOTICON 16 | self.gifs = [] 17 | self.last_time = time.time() 18 | 19 | def time_schedule(self): 20 | r = '' 21 | now = time.time() 22 | if int(now - self.last_time) > 3600: 23 | self.last_time = now 24 | url_latest = Constant.BOT_ZHIHU_URL_LATEST 25 | url_daily = Constant.BOT_ZHIHU_URL_DAILY 26 | data = get(url_latest) 27 | j = json.loads(data) 28 | story = j['stories'][random.randint(0, len(j['stories'])-1)] 29 | r = story['title'] + '\n' + url_daily + str(story['id']) 30 | return r.encode('utf-8') 31 | 32 | def reply(self, text): 33 | APIKEY = Constant.BOT_TULING_API_KEY 34 | api_url = Constant.BOT_TULING_API_URL % (APIKEY, text, '12345678') 35 | r = json.loads(get(api_url)) 36 | if r.get('code') == 100000 and r.get('text') != Constant.BOT_TULING_BOT_REPLY: 37 | p = random.randint(1, 10) 38 | if p > 3: 39 | return r['text'] 40 | elif p > 1: 41 | # send emoji 42 | if random.randint(1, 10) > 5: 43 | n = random.randint(0, len(self.emoticons)-1) 44 | m = random.randint(1, 3) 45 | reply = self.emoticons[n].encode('utf-8') * m 46 | return reply 47 | return '' 48 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/wx_handler/sendgrid_mail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | import sendgrid 6 | from sendgrid.helpers.mail import * 7 | #=================================================== 8 | 9 | class SGMail(object): 10 | 11 | def __init__(self, apikey, from_email, to_email): 12 | self.sg = sendgrid.SendGridAPIClient(apikey=apikey) 13 | self.from_email = Email(from_email) 14 | self.to_email = Email(to_email) 15 | 16 | def send_mail(self, subject, plain_content, type='text/plain'): 17 | content = Content(type, plain_content) 18 | mail = Mail(self.from_email, subject, self.to_email, content) 19 | response = self.sg.client.mail.send.post(request_body=mail.get()) 20 | return response.status_code == 202 21 | -------------------------------------------------------------------------------- /wxbot_project_py2.7/wx_handler/wechat_msg_processor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #=================================================== 5 | from wechat.utils import * 6 | from config import ConfigManager 7 | from config import Constant 8 | from config import Log 9 | #--------------------------------------------------- 10 | import os 11 | import time 12 | import json 13 | import re 14 | #=================================================== 15 | 16 | 17 | class WeChatMsgProcessor(object): 18 | """ 19 | Process fetched data 20 | """ 21 | 22 | def __init__(self, db): 23 | self.db = db 24 | self.wechat = None # recieve `WeChat` class instance 25 | # for call some wechat apis 26 | 27 | # read config 28 | cm = ConfigManager() 29 | [self.upload_dir, self.data_dir, self.log_dir] = cm.setup_database() 30 | 31 | def clean_db(self): 32 | """ 33 | @brief clean database, delete table & create table 34 | """ 35 | self.db.delete_table(Constant.TABLE_GROUP_LIST()) 36 | self.db.delete_table(Constant.TABLE_GROUP_USER_LIST()) 37 | self.db.create_table(Constant.TABLE_GROUP_MSG_LOG, Constant.TABLE_GROUP_MSG_LOG_COL) 38 | self.db.create_table(Constant.TABLE_GROUP_LIST(), Constant.TABLE_GROUP_LIST_COL) 39 | self.db.create_table(Constant.TABLE_GROUP_USER_LIST(), Constant.TABLE_GROUP_USER_LIST_COL) 40 | self.db.create_table(Constant.TABLE_RECORD_ENTER_GROUP, Constant.TABLE_RECORD_ENTER_GROUP_COL) 41 | self.db.create_table(Constant.TABLE_RECORD_RENAME_GROUP, Constant.TABLE_RECORD_RENAME_GROUP_COL) 42 | 43 | def handle_wxsync(self, msg): 44 | """ 45 | @brief Recieve webwxsync message, saved into json 46 | @param msg Dict: webwxsync msg 47 | """ 48 | fn = time.strftime(Constant.LOG_MSG_FILE, time.localtime()) 49 | save_json(fn, msg, self.log_dir, 'a+') 50 | 51 | def handle_group_list(self, group_list): 52 | """ 53 | @brief handle group list & saved in DB 54 | @param group_list Array 55 | """ 56 | fn = Constant.LOG_MSG_GROUP_LIST_FILE 57 | save_json(fn, group_list, self.data_dir) 58 | cols = [( 59 | g['NickName'], 60 | g['UserName'], 61 | g['OwnerUin'], 62 | g['MemberCount'], 63 | g['HeadImgUrl'] 64 | ) for g in group_list] 65 | self.db.insertmany(Constant.TABLE_GROUP_LIST(), cols) 66 | 67 | def handle_group_member_list(self, group_id, member_list): 68 | """ 69 | @brief handle group member list & saved in DB 70 | @param member_list Array 71 | """ 72 | fn = group_id + '.json' 73 | save_json(fn, member_list, self.data_dir) 74 | cols = [( 75 | group_id, 76 | m['UserName'], 77 | m['NickName'], 78 | m['DisplayName'], 79 | m['AttrStatus'] 80 | ) for m in member_list] 81 | self.db.insertmany(Constant.TABLE_GROUP_USER_LIST(), cols) 82 | 83 | def handle_group_list_change(self, new_group): 84 | """ 85 | @brief handle adding a new group & saved in DB 86 | @param new_group Dict 87 | """ 88 | self.handle_group_list([new_group]) 89 | 90 | def handle_group_member_change(self, group_id, member_list): 91 | """ 92 | @brief handle group member changes & saved in DB 93 | @param group_id Dict 94 | @param member_list Dict 95 | """ 96 | self.db.delete(Constant.TABLE_GROUP_USER_LIST(), "RoomID", group_id) 97 | self.handle_group_member_list(group_id, member_list) 98 | 99 | def handle_group_msg(self, msg): 100 | """ 101 | @brief Recieve group messages 102 | @param msg Dict: packaged msg 103 | """ 104 | # rename media files 105 | for k in ['image', 'video', 'voice']: 106 | if msg[k]: 107 | t = time.localtime(float(msg['timestamp'])) 108 | time_str = time.strftime("%Y%m%d%H%M%S", t) 109 | # format: 时间_消息ID_群名 110 | file_name = '/%s_%s_%s.' % (time_str, msg['msg_id'], msg['group_name']) 111 | new_name = re.sub(r'\/\w+\_\d+\.', file_name, msg[k]) 112 | Log.debug('rename file to %s' % new_name) 113 | os.rename(msg[k], new_name) 114 | msg[k] = new_name 115 | 116 | if msg['msg_type'] == 10000: 117 | # record member enter in group 118 | m = re.search(r'邀请(.+)加入了群聊', msg['sys_notif']) 119 | if m: 120 | name = m.group(1) 121 | col_enter_group = ( 122 | msg['msg_id'], 123 | msg['group_name'], 124 | msg['from_user_name'], 125 | msg['to_user_name'], 126 | name, 127 | msg['time'], 128 | ) 129 | self.db.insert(Constant.TABLE_RECORD_ENTER_GROUP, col_enter_group) 130 | 131 | # record rename group 132 | n = re.search(r'(.+)修改群名为“(.+)”', msg['sys_notif']) 133 | if n: 134 | people = n.group(1) 135 | to_name = n.group(2) 136 | col_rename_group = ( 137 | msg['msg_id'], 138 | msg['group_name'], 139 | to_name, 140 | people, 141 | msg['time'], 142 | ) 143 | self.db.insert(Constant.TABLE_RECORD_RENAME_GROUP, col_rename_group) 144 | 145 | # upadte group in GroupList 146 | for g in self.wechat.GroupList: 147 | if g['UserName'] == msg['from_user_name']: 148 | g['NickName'] = to_name 149 | break 150 | 151 | # normal group message 152 | col = ( 153 | msg['msg_id'], 154 | msg['group_owner_uin'], 155 | msg['group_name'], 156 | msg['group_count'], 157 | msg['from_user_name'], 158 | msg['to_user_name'], 159 | msg['user_attrstatus'], 160 | msg['user_display_name'], 161 | msg['user_nickname'], 162 | msg['msg_type'], 163 | msg['emoticon'], 164 | msg['text'], 165 | msg['image'], 166 | msg['video'], 167 | msg['voice'], 168 | msg['link'], 169 | msg['namecard'], 170 | msg['location'], 171 | msg['recall_msg_id'], 172 | msg['sys_notif'], 173 | msg['time'], 174 | msg['timestamp'] 175 | ) 176 | self.db.insert(Constant.TABLE_GROUP_MSG_LOG, col) 177 | 178 | text = msg['text'] 179 | if text and text[0] == '@': 180 | n = trans_coding(text).find(u'\u2005') 181 | name = trans_coding(text)[1:n].encode('utf-8') 182 | if name in [self.wechat.User['NickName'], self.wechat.User['RemarkName']]: 183 | self.handle_command(trans_coding(text)[n+1:].encode('utf-8'), msg) 184 | 185 | def handle_user_msg(self, msg): 186 | """ 187 | @brief Recieve personal messages 188 | @param msg Dict 189 | """ 190 | wechat = self.wechat 191 | 192 | text = trans_coding(msg['text']).encode('utf-8') 193 | uid = msg['raw_msg']['FromUserName'] 194 | 195 | if text == 'test_revoke': # 撤回消息测试 196 | dic = wechat.webwxsendmsg('这条消息将被撤回', uid) 197 | wechat.revoke_msg(dic['MsgID'], uid, dic['LocalID']) 198 | elif text == 'reply': 199 | wechat.send_text(uid, '自动回复') 200 | 201 | 202 | def handle_command(self, cmd, msg): 203 | """ 204 | @brief handle msg of `@yourself cmd` 205 | @param cmd String 206 | @param msg Dict 207 | """ 208 | wechat = self.wechat 209 | g_id = '' 210 | for g in wechat.GroupList: 211 | if g['NickName'] == msg['group_name']: 212 | g_id = g['UserName'] 213 | 214 | cmd = cmd.strip() 215 | if cmd == 'runtime': 216 | wechat.send_text(g_id, wechat.get_run_time()) 217 | elif cmd == 'test_sendimg': 218 | wechat.send_img(g_id, 'test/emotion/7.gif') 219 | elif cmd == 'test_sendfile': 220 | wechat.send_file(g_id, 'test/Data/upload/shake.wav') 221 | elif cmd == 'test_bot': 222 | # reply bot 223 | # --------- 224 | if wechat.bot: 225 | r = wechat.bot.reply(cmd) 226 | if r: 227 | wechat.send_text(g_id, r) 228 | else: 229 | pass 230 | elif cmd == 'test_emot': 231 | img_name = [ 232 | '0.jpg', '1.jpeg', '2.gif', '3.jpg', '4.jpeg', 233 | '5.gif', '6.gif', '7.gif', '8.jpg', '9.jpg' 234 | ] 235 | name = img_name[int(time.time()) % 10] 236 | emot_path = os.path.join('test/emotion/', name) 237 | wechat.send_emot(g_id, emot_path) 238 | else: 239 | pass 240 | 241 | def check_schedule_task(self): 242 | # update group member list at 00:00 am every morning 243 | t = time.localtime() 244 | if t.tm_hour == 0 and t.tm_min <= 1: 245 | # update group member 246 | Log.debug('update group member list everyday') 247 | self.db.delete_table(Constant.TABLE_GROUP_LIST()) 248 | self.db.delete_table(Constant.TABLE_GROUP_USER_LIST()) 249 | self.db.create_table(Constant.TABLE_GROUP_LIST(), Constant.TABLE_GROUP_LIST_COL) 250 | self.db.create_table(Constant.TABLE_GROUP_USER_LIST(), Constant.TABLE_GROUP_USER_LIST_COL) 251 | self.wechat.fetch_group_contacts() 252 | 253 | --------------------------------------------------------------------------------