├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── genReq.sh ├── requirements.txt ├── screenshot ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png └── 8.jpg └── weixin.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 -------------------------------------------------------------------------------- /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 | # WeixinGroupSyncBot [![star this repo](http://github-svg-buttons.herokuapp.com/star.svg?user=buaagg&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/buaagg/WeixinBot) [![fork this repo](http://github-svg-buttons.herokuapp.com/fork.svg?user=buaagg&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/buaagg/WeixinBot/fork) ![python](https://img.shields.io/badge/python-2.7-ff69b4.svg) 2 | 3 | 网页版微信API,包含终端版微信,用于MSRA Alumni在不同群之间交流。 4 | 5 | ## Remark 6 | 7 | ### Todo 8 | 9 | * 检查群昵称是否满足条件,否则发出提醒。 10 | * 每隔一段时间(1h?)向大家报告自己还活着。 11 | * 消息撤回 12 | * [BUG] 发送图片后,狂报retcode: 0, selector: 2, 可以稳定复现, https://github.com/Urinx/WeixinBot/issues/61 和 https://github.com/Urinx/WeixinBot/issues/49 都有描述 13 | * log持久化和切割 14 | * 代码重构,API和业务逻辑耦合得太紧密,不利于adapt到其他的应用场景 15 | * 显示群名重写成①②③④⑤⑥⑦⑧⑨⑩. 16 | * 能不能把cookie存下来,这样就不用每次登陆都扫码啦。 17 | ***** 弄清楚cookie换一台机器还可以用么? 18 | 19 | ### Limitations (of WebWeChatAPI) 20 | 21 | * 对于名片,通过浏览器抓包只能抓到UserID,抓不到微信名,所以对于名片的转发似乎不可行。 22 | * 对于at功能,貌似Web微信不支持at人,想通过浏览器抓包都不知道怎么发请求。 23 | mac的微信客户端支持at,不过用charles抓到的都是乱码,等待抓包能手。 24 | 25 | ### Environment 26 | 27 | 依赖的python包 28 | 29 | sudo pip install qrcode lxml requests_toolbelt coloredlogs 30 | 31 | 此外建议运行的时候用screen 32 | 33 | sudo apt-get install screen 34 | 35 | ## Demo 36 | 运行 `weixin.py` 运行结果如下 37 | 38 | ![1](screenshot/1.png) 39 | 40 | 按照操作指示在手机微信上扫描二维码然后登录。 41 | 42 | 现在,名片,链接,动画表情和地址位置消息都可以正常接收。 43 | 44 | ![4](screenshot/4.png) 45 | 46 | ![5](screenshot/5.png) 47 | 48 | **目前支持的命令**: 49 | 50 | `->[昵称或ID]:[内容]` 给好友发送消息 51 | 52 | `m->[昵称或ID]:[文件路径]` 给好友发送文件中的内容 53 | 54 | ![6](screenshot/6.png) 55 | 56 | `f->[昵称或ID]:[文件路径]` 给好友发送文件 57 | 58 | `i->[昵称或ID]:[图片路径]` 给好友发送图片 59 | 60 | `e->[昵称或ID]:[文件路径]` 给好友发送表情(jpg/gif) 61 | 62 | `quit` 退出程序 63 | 64 | ![7](screenshot/7.png) 65 | 66 | 注意,以上命令均不包含方括号。 67 | 68 | ## Web Weixin Pipeline 69 | 70 | ``` 71 | +--------------+ +---------------+ +---------------+ 72 | | | | | | | 73 | | Get UUID | | Get Contact | | Status Notify | 74 | | | | | | | 75 | +-------+------+ +-------^-------+ +-------^-------+ 76 | | | | 77 | | +-------+ +--------+ 78 | | | | 79 | +-------v------+ +-----+--+------+ +--------------+ 80 | | | | | | | 81 | | Get QRCode | | Weixin Init +------> Sync Check <----+ 82 | | | | | | | | 83 | +-------+------+ +-------^-------+ +-------+------+ | 84 | | | | | 85 | | | +-----------+ 86 | | | | 87 | +-------v------+ +-------+--------+ +-------v-------+ 88 | | | Confirm Login | | | | 89 | +------> Login +---------------> New Login Page | | Weixin Sync | 90 | | | | | | | | 91 | | +------+-------+ +----------------+ +---------------+ 92 | | | 93 | |QRCode Scaned| 94 | +-------------+ 95 | ``` 96 | 97 | ## Web Weixin API 98 | 99 | ### 登录 100 | 101 | | API | 获取 UUID | 102 | | --- | --------- | 103 | | url | https://login.weixin.qq.com/jslogin | 104 | | method | POST | 105 | | data | URL Encode | 106 | | params | **appid**: `应用ID`
**fun**: new `应用类型`
**lang**: zh\_CN `语言`
**_**: `时间戳` | 107 | 108 | 返回数据(String): 109 | ``` 110 | window.QRLogin.code = 200; window.QRLogin.uuid = "xxx" 111 | ``` 112 | > 注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是`wx782c26e4c19acffb`,在微信客户端上显示为应用名称为`Web微信`;现在用的是`wxeb7ec651dd0aefa9`,显示名称为`微信网页版`。 113 | 114 |
115 | 116 | | API | 生成二维码 | 117 | | --- | --------- | 118 | | url | https://login.weixin.qq.com/l/ `uuid` | 119 |
120 | 121 | | API | 二维码扫描登录 | 122 | | --- | --------- | 123 | | url | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login | 124 | | method | GET | 125 | | params | **tip**: 1 `未扫描` 0 `已扫描`
**uuid**: xxx
**_**: `时间戳` | 126 | 127 | 返回数据(String): 128 | ``` 129 | window.code=xxx; 130 | 131 | xxx: 132 | 408 登陆超时 133 | 201 扫描成功 134 | 200 确认登录 135 | 136 | 当返回200时,还会有 137 | window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx"; 138 | ``` 139 |
140 | 141 | | API | webwxnewloginpage | 142 | | --- | --------- | 143 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage | 144 | | method | GET | 145 | | params | **ticket**: xxx
**uuid**: xxx
**lang**: zh_CN `语言`
**scan**: xxx
**fun**: new | 146 | 147 | 返回数据(XML): 148 | ``` 149 | 150 | 0 151 | OK 152 | xxx 153 | xxx 154 | xxx 155 | xxx 156 | 1 157 | 158 | ``` 159 |
160 | 161 | ### 微信初始化 162 | 163 | | API | webwxinit | 164 | | --- | --------- | 165 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket=xxx&skey=xxx&r=xxx | 166 | | method | POST | 167 | | data | JSON | 168 | | header | ContentType: application/json; charset=UTF-8 | 169 | | params | {
     BaseRequest: {
         Uin: xxx,
         Sid: xxx,
         Skey: xxx,
         DeviceID: xxx,
     }
} | 170 | 171 | 返回数据(JSON): 172 | ``` 173 | { 174 | "BaseResponse": { 175 | "Ret": 0, 176 | "ErrMsg": "" 177 | }, 178 | "Count": 11, 179 | "ContactList": [...], 180 | "SyncKey": { 181 | "Count": 4, 182 | "List": [ 183 | { 184 | "Key": 1, 185 | "Val": 635705559 186 | }, 187 | ... 188 | ] 189 | }, 190 | "User": { 191 | "Uin": xxx, 192 | "UserName": xxx, 193 | "NickName": xxx, 194 | "HeadImgUrl": xxx, 195 | "RemarkName": "", 196 | "PYInitial": "", 197 | "PYQuanPin": "", 198 | "RemarkPYInitial": "", 199 | "RemarkPYQuanPin": "", 200 | "HideInputBarFlag": 0, 201 | "StarFriend": 0, 202 | "Sex": 1, 203 | "Signature": "Apt-get install B", 204 | "AppAccountFlag": 0, 205 | "VerifyFlag": 0, 206 | "ContactFlag": 0, 207 | "WebWxPluginSwitch": 0, 208 | "HeadImgFlag": 1, 209 | "SnsFlag": 17 210 | }, 211 | "ChatSet": xxx, 212 | "SKey": xxx, 213 | "ClientVersion": 369297683, 214 | "SystemTime": 1453124908, 215 | "GrayScale": 1, 216 | "InviteStartCount": 40, 217 | "MPSubscribeMsgCount": 2, 218 | "MPSubscribeMsgList": [...], 219 | "ClickReportInterval": 600000 220 | } 221 | ``` 222 |
223 | 224 | | API | webwxstatusnotify | 225 | | --- | --------- | 226 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify?lang=zh_CN&pass_ticket=xxx | 227 | | method | POST | 228 | | data | JSON | 229 | | header | ContentType: application/json; charset=UTF-8 | 230 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Code: 3,
     FromUserName: `自己ID`,
     ToUserName: `自己ID`,
     ClientMsgId: `时间戳`
} | 231 | 232 | 返回数据(JSON): 233 | ``` 234 | { 235 | "BaseResponse": { 236 | "Ret": 0, 237 | "ErrMsg": "" 238 | }, 239 | ... 240 | } 241 | ``` 242 |
243 | 244 | ### 获取联系人信息 245 | 246 | | API | webwxgetcontact | 247 | | --- | --------- | 248 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin//webwxgetcontact?pass_ticket=xxx&skey=xxx&r=xxx | 249 | | method | POST | 250 | | data | JSON | 251 | | header | ContentType: application/json; charset=UTF-8 | 252 | 253 | 返回数据(JSON): 254 | ``` 255 | { 256 | "BaseResponse": { 257 | "Ret": 0, 258 | "ErrMsg": "" 259 | }, 260 | "MemberCount": 334, 261 | "MemberList": [ 262 | { 263 | "Uin": 0, 264 | "UserName": xxx, 265 | "NickName": "Urinx", 266 | "HeadImgUrl": xxx, 267 | "ContactFlag": 3, 268 | "MemberCount": 0, 269 | "MemberList": [], 270 | "RemarkName": "", 271 | "HideInputBarFlag": 0, 272 | "Sex": 0, 273 | "Signature": "你好,我们是地球三体组织。在这里,你将感受到不一样的思维模式,以及颠覆常规的世界观。而我们的目标,就是以三体人的智慧,引领人类未来科学技术500年。", 274 | "VerifyFlag": 8, 275 | "OwnerUin": 0, 276 | "PYInitial": "URINX", 277 | "PYQuanPin": "Urinx", 278 | "RemarkPYInitial": "", 279 | "RemarkPYQuanPin": "", 280 | "StarFriend": 0, 281 | "AppAccountFlag": 0, 282 | "Statues": 0, 283 | "AttrStatus": 0, 284 | "Province": "", 285 | "City": "", 286 | "Alias": "Urinxs", 287 | "SnsFlag": 0, 288 | "UniFriend": 0, 289 | "DisplayName": "", 290 | "ChatRoomId": 0, 291 | "KeyWord": "gh_", 292 | "EncryChatRoomId": "" 293 | }, 294 | ... 295 | ], 296 | "Seq": 0 297 | } 298 | ``` 299 |
300 | 301 | | API | webwxbatchgetcontact | 302 | | --- | --------- | 303 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=xxx&pass_ticket=xxx | 304 | | method | POST | 305 | | data | JSON | 306 | | header | ContentType: application/json; charset=UTF-8 | 307 | | params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     Count: `群数量`,
     List: [
         { UserName: `群ID`, EncryChatRoomId: "" },
         ...
     ],
} | 308 | 309 | 返回数据(JSON)同上 310 |

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

`VerifyFlag`:
         一般公众号/服务号:8
         微信自家的服务号:24
         微信官方账号`微信团队`:56 | 472 | | 特殊账号 | 像文件传输助手之类的账号,有特殊的ID,目前已知的有:
`filehelper`, `newsapp`, `fmessage`, `weibo`, `qqmail`, `fmessage`, `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` | 473 |
474 | 475 | ### 消息类型 476 | 477 | 消息一般格式: 478 | ``` 479 | { 480 | "FromUserName": "", 481 | "ToUserName": "", 482 | "Content": "", 483 | "StatusNotifyUserName": "", 484 | "ImgWidth": 0, 485 | "PlayLength": 0, 486 | "RecommendInfo": {...}, 487 | "StatusNotifyCode": 4, 488 | "NewMsgId": "", 489 | "Status": 3, 490 | "VoiceLength": 0, 491 | "ForwardFlag": 0, 492 | "AppMsgType": 0, 493 | "Ticket": "", 494 | "AppInfo": {...}, 495 | "Url": "", 496 | "ImgStatus": 1, 497 | "MsgType": 1, 498 | "ImgHeight": 0, 499 | "MediaId": "", 500 | "MsgId": "", 501 | "FileName": "", 502 | "HasProductId": 0, 503 | "FileSize": "", 504 | "CreateTime": 1454602196, 505 | "SubMsgType": 0 506 | } 507 | ``` 508 |
509 | 510 | | MsgType | 说明 | 511 | | ------- | --- | 512 | | 1 | 文本消息 | 513 | | 3 | 图片消息 | 514 | | 34 | 语音消息 | 515 | | 37 | VERIFYMSG | 516 | | 40 | POSSIBLEFRIEND_MSG | 517 | | 42 | 共享名片 | 518 | | 43 | 视频通话消息 | 519 | | 47 | 动画表情 | 520 | | 48 | 位置消息 | 521 | | 49 | 分享链接 | 522 | | 50 | VOIPMSG | 523 | | 51 | 微信初始化消息 | 524 | | 52 | VOIPNOTIFY | 525 | | 53 | VOIPINVITE | 526 | | 62 | 小视频 | 527 | | 9999 | SYSNOTICE | 528 | | 10000 | 系统消息 | 529 | | 10002 | 撤回消息 | 530 |
531 | 532 | **微信初始化消息** 533 | ```html 534 | MsgType: 51 535 | FromUserName: 自己ID 536 | ToUserName: 自己ID 537 | StatusNotifyUserName: 最近联系的联系人ID 538 | Content: 539 | 540 | 541 | 542 | // 最近联系的联系人 543 | filehelper,xxx@chatroom,wxid_xxx,xxx,... 544 | 545 | 546 | 547 | 548 | // 朋友圈 549 | MomentsUnreadMsgStatus 550 | 551 | 552 | 1454502365 553 | 554 | 555 | 556 | 557 | // 未读的功能账号消息,群发助手,漂流瓶等 558 | 559 | 560 | 561 | ``` 562 | 563 | **文本消息** 564 | ``` 565 | MsgType: 1 566 | FromUserName: 发送方ID 567 | ToUserName: 接收方ID 568 | Content: 消息内容 569 | ``` 570 | 571 | **图片消息** 572 | ```html 573 | MsgType: 3 574 | FromUserName: 发送方ID 575 | ToUserName: 接收方ID 576 | MsgId: 用于获取图片 577 | Content: 578 | 579 | 580 | 581 | 582 | ``` 583 | 584 | **小视频消息** 585 | ```html 586 | MsgType: 62 587 | FromUserName: 发送方ID 588 | ToUserName: 接收方ID 589 | MsgId: 用于获取小视频 590 | Content: 591 | 592 | 593 | 594 | 595 | ``` 596 | 597 | **地理位置消息** 598 | ``` 599 | MsgType: 1 600 | FromUserName: 发送方ID 601 | ToUserName: 接收方ID 602 | Content: http://weixin.qq.com/cgi-bin/redirectforward?args=xxx 603 | // 属于文本消息,只不过内容是一个跳转到地图的链接 604 | ``` 605 | 606 | **名片消息** 607 | ```js 608 | MsgType: 42 609 | FromUserName: 发送方ID 610 | ToUserName: 接收方ID 611 | Content: 612 | 613 | 614 | 615 | RecommendInfo: 616 | { 617 | "UserName": "xxx", // ID 618 | "Province": "xxx", 619 | "City": "xxx", 620 | "Scene": 17, 621 | "QQNum": 0, 622 | "Content": "", 623 | "Alias": "xxx", // 微信号 624 | "OpCode": 0, 625 | "Signature": "", 626 | "Ticket": "", 627 | "Sex": 0, // 1:男, 2:女 628 | "NickName": "xxx", // 昵称 629 | "AttrStatus": 4293221, 630 | "VerifyFlag": 0 631 | } 632 | ``` 633 | 634 | **语音消息** 635 | ```html 636 | MsgType: 34 637 | FromUserName: 发送方ID 638 | ToUserName: 接收方ID 639 | MsgId: 用于获取语音 640 | Content: 641 | 642 | 643 | 644 | ``` 645 | 646 | **动画表情** 647 | ```html 648 | MsgType: 47 649 | FromUserName: 发送方ID 650 | ToUserName: 接收方ID 651 | Content: 652 | 653 | 654 | 655 | 656 | ``` 657 | 658 | **普通链接或应用分享消息** 659 | ```html 660 | MsgType: 49 661 | AppMsgType: 5 662 | FromUserName: 发送方ID 663 | ToUserName: 接收方ID 664 | Url: 链接地址 665 | FileName: 链接标题 666 | Content: 667 | 668 | 669 | 670 | 671 | 5 672 | 673 | 674 | 675 | ... 676 | 677 | 678 | 679 | 680 | 681 | 682 | ``` 683 | 684 | **音乐链接消息** 685 | ```html 686 | MsgType: 49 687 | AppMsgType: 3 688 | FromUserName: 发送方ID 689 | ToUserName: 接收方ID 690 | Url: 链接地址 691 | FileName: 音乐名 692 | 693 | AppInfo: // 分享链接的应用 694 | { 695 | Type: 0, 696 | AppID: wx485a97c844086dc9 697 | } 698 | 699 | Content: 700 | 701 | 702 | 703 | 704 | 705 | 3 706 | 0 707 | 708 | 709 | 710 | 711 | 0 712 | 713 | 714 | 715 | http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46 716 | 717 | 718 | http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46 719 | 720 | 721 | 0 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | http://imgcache.qq.com/music/photo/album/63/180_albumpic_143163_0.jpg 732 | 733 | 734 | 735 | 736 | 0 737 | 738 | 29 739 | 摇一摇搜歌 740 | 741 | 742 | 743 | ``` 744 | 745 | **群消息** 746 | ``` 747 | MsgType: 1 748 | FromUserName: @@xxx 749 | ToUserName: @xxx 750 | Content: 751 | @xxx:
xxx 752 | ``` 753 | 754 | **红包消息** 755 | ``` 756 | MsgType: 49 757 | AppMsgType: 2001 758 | FromUserName: 发送方ID 759 | ToUserName: 接收方ID 760 | Content: 未知 761 | ``` 762 | 注:根据网页版的代码可以看到未来可能支持查看红包消息,但目前走的是系统消息,见下。 763 | 764 | **系统消息** 765 | ``` 766 | MsgType: 10000 767 | FromUserName: 发送方ID 768 | ToUserName: 自己ID 769 | Content: 770 | "你已添加了 xxx ,现在可以开始聊天了。" 771 | "如果陌生人主动添加你为朋友,请谨慎核实对方身份。" 772 | "收到红包,请在手机上查看" 773 | ``` 774 | 775 | 持续更新中 ... 776 | 777 | ## Todo 778 | - [x] 发送图片或者文件功能 779 | - [ ] 主动给群聊发送消息 780 | - [ ] 建立群聊 781 | - [x] 群发消息 782 | - [ ] 补充更多的接口及完善文档 783 | 784 | ## Related Projets 785 | 786 | * https://github.com/zixia/wechaty 787 | * https://github.com/stonexer/wechatBot 网页版微信机器人 788 | * https://github.com/spacelan/weixin-bot-chrome-extension Chrome插件版 789 | * https://github.com/lu4kyd0y/WeChat-Cloud-Robot 微信云端机器人框架 790 | 791 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | -------------------------------------------------------------------------------- /genReq.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pip freeze |grep -v wheel | gawk -F"==" ' { print $1 } ' > requirements.txt 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama 2 | coloredlogs 3 | humanfriendly 4 | lxml 5 | qrcode 6 | requests 7 | six 8 | requests_toolbelt 9 | -------------------------------------------------------------------------------- /screenshot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/1.png -------------------------------------------------------------------------------- /screenshot/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/2.png -------------------------------------------------------------------------------- /screenshot/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/3.png -------------------------------------------------------------------------------- /screenshot/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/4.png -------------------------------------------------------------------------------- /screenshot/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/5.png -------------------------------------------------------------------------------- /screenshot/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/6.png -------------------------------------------------------------------------------- /screenshot/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/7.png -------------------------------------------------------------------------------- /screenshot/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaagg/WeixinBot/e293db3fa5607ef7906358f33dd8a666af82364a/screenshot/8.jpg -------------------------------------------------------------------------------- /weixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import qrcode 4 | import urllib 5 | import urllib2 6 | import cookielib 7 | import requests 8 | import xml.dom.minidom 9 | import json 10 | import time 11 | import re 12 | import sys 13 | import os 14 | import random 15 | import logging 16 | from collections import defaultdict 17 | from urlparse import urlparse 18 | from lxml import html 19 | import xml.sax.saxutils as saxutils 20 | import traceback 21 | import Queue 22 | import threading 23 | 24 | # for media upload 25 | import mimetypes 26 | from requests_toolbelt.multipart.encoder import MultipartEncoder 27 | 28 | PREFIX = '西码会' 29 | # PREFIX = '群聊同步机器人' 30 | 31 | def catchKeyboardInterrupt(fn): 32 | def wrapper(*args): 33 | try: 34 | return fn(*args) 35 | except KeyboardInterrupt: 36 | logging.debug('[*] 强制退出程序') 37 | return wrapper 38 | 39 | 40 | def _decode_list(data): 41 | rv = [] 42 | for item in data: 43 | if isinstance(item, unicode): 44 | item = item.encode('utf-8') 45 | elif isinstance(item, list): 46 | item = _decode_list(item) 47 | elif isinstance(item, dict): 48 | item = _decode_dict(item) 49 | rv.append(item) 50 | return rv 51 | 52 | def _decode_dict(data): 53 | rv = {} 54 | for key, value in data.iteritems(): 55 | if isinstance(key, unicode): 56 | key = key.encode('utf-8') 57 | if isinstance(value, unicode): 58 | value = value.encode('utf-8') 59 | elif isinstance(value, list): 60 | value = _decode_list(value) 61 | elif isinstance(value, dict): 62 | value = _decode_dict(value) 63 | rv[key] = value 64 | return rv 65 | 66 | def unescape(text): 67 | text = saxutils.unescape(text) 68 | text = re.sub('\s*<\s*/span>', 69 | lambda s: ("\\U%08x" % int(s.group(1), 16)).decode('unicode-escape').encode('utf8'), 70 | text) 71 | return text 72 | 73 | class WebWeixinAPI(object): 74 | def __str__(self): 75 | description = \ 76 | "=========================\n" + \ 77 | "[#] Web Weixin\n" + \ 78 | "[#] Debug Mode: " + str(self.DEBUG) + "\n" + \ 79 | "[#] Uuid: " + self.uuid + "\n" + \ 80 | "[#] Uin: " + str(self.uin) + "\n" + \ 81 | "[#] Sid: " + self.sid + "\n" + \ 82 | "[#] Skey: " + self.skey + "\n" + \ 83 | "[#] DeviceId: " + self.deviceId + "\n" + \ 84 | "[#] PassTicket: " + self.pass_ticket + "\n" + \ 85 | "=========================" 86 | return description 87 | 88 | def __init__(self): 89 | self.DEBUG = 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.PublicUsersList = [] # 公众号/服务号 105 | self.SpecialUsersList = [] # 特殊账号 106 | self.syncHost = '' 107 | 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' 108 | self.saveFolder = os.path.join(os.getcwd(), 'saved') 109 | self.saveSubFolders = {'webwxgeticon': 'icons', 'webwxgetheadimg': 'headimgs', 'webwxgetmsgimg': 'msgimgs', 110 | 'webwxgetvideo': 'videos', 'webwxgetvoice': 'voices', '_showQRCodeImg': 'qrcodes', 111 | 'webwxgetmsgemotion': 'msgemotions', 112 | } 113 | self.appid = 'wx782c26e4c19acffb' 114 | self.lastCheckTs = time.time() 115 | self.SpecialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 116 | 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'] 117 | self.TimeOut = 20 # 同步最短时间间隔(单位:秒) 118 | self.media_count = -1 119 | 120 | self.cookie = cookielib.CookieJar() 121 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookie)) 122 | opener.addheaders = [('User-agent', self.user_agent)] 123 | urllib2.install_opener(opener) 124 | 125 | def getUUID(self): 126 | url = 'https://login.weixin.qq.com/jslogin' 127 | params = { 128 | 'appid': self.appid, 129 | 'fun': 'new', 130 | 'lang': 'zh_CN', 131 | '_': int(time.time()), 132 | } 133 | data = self._post(url, params, False) 134 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' 135 | pm = re.search(regx, data) 136 | if pm: 137 | code = pm.group(1) 138 | self.uuid = pm.group(2) 139 | return code == '200' 140 | return False 141 | 142 | def genQRCode(self): 143 | if sys.platform.startswith('win'): 144 | self._showQRCodeImg() 145 | else: 146 | self._str2qr('https://login.weixin.qq.com/l/' + self.uuid) 147 | 148 | def _showQRCodeImg(self): 149 | url = 'https://login.weixin.qq.com/qrcode/' + self.uuid 150 | params = { 151 | 't': 'webwx', 152 | '_': int(time.time()) 153 | } 154 | 155 | data = self._post(url, params, False) 156 | QRCODE_PATH = self._saveFile('qrcode.jpg', data, '_showQRCodeImg') 157 | os.startfile(QRCODE_PATH) 158 | 159 | def waitForLogin(self, tip=1): 160 | time.sleep(tip) 161 | url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % ( 162 | tip, self.uuid, int(time.time())) 163 | data = self._get(url) 164 | pm = re.search(r'window.code=(\d+);', data) 165 | code = pm.group(1) 166 | 167 | if code == '201': 168 | return True 169 | elif code == '200': 170 | pm = re.search(r'window.redirect_uri="(\S+?)";', data) 171 | r_uri = pm.group(1) + '&fun=new' 172 | self.redirect_uri = r_uri 173 | self.base_uri = r_uri[:r_uri.rfind('/')] 174 | print 'self.base_uri =', self.base_uri 175 | return True 176 | elif code == '408': 177 | self._echo('[登陆超时] \n') 178 | else: 179 | self._echo('[登陆异常] \n') 180 | return False 181 | 182 | def webwxinit(self): 183 | url = self.base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % ( 184 | self.pass_ticket, self.skey, int(time.time())) 185 | params = { 186 | 'BaseRequest': self.BaseRequest 187 | } 188 | dic = self._post(url, params) 189 | self.SyncKey = dic['SyncKey'] 190 | self.User = dic['User'] 191 | # synckey for synccheck 192 | self.synckey = '|'.join( 193 | [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) 194 | 195 | return dic['BaseResponse']['Ret'] == 0 196 | 197 | def webwxstatusnotify(self): 198 | url = self.base_uri + \ 199 | '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket) 200 | params = { 201 | 'BaseRequest': self.BaseRequest, 202 | "Code": 3, 203 | "FromUserName": self.User['UserName'], 204 | "ToUserName": self.User['UserName'], 205 | "ClientMsgId": int(time.time()) 206 | } 207 | dic = self._post(url, params) 208 | 209 | return dic['BaseResponse']['Ret'] == 0 210 | 211 | def webwxgetcontact(self): 212 | print self.base_uri 213 | url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % ( 214 | self.pass_ticket, self.skey, int(time.time())) 215 | dic = self._post(url, {}) 216 | 217 | self.MemberCount = dic['MemberCount'] 218 | self.MemberList = dic['MemberList'] 219 | ContactList = self.MemberList[:] 220 | 221 | for i in xrange(len(ContactList) - 1, -1, -1): 222 | Contact = ContactList[i] 223 | if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 224 | ContactList.remove(Contact) 225 | self.PublicUsersList.append(Contact) 226 | elif Contact['UserName'] in self.SpecialUsers: # 特殊账号 227 | ContactList.remove(Contact) 228 | self.SpecialUsersList.append(Contact) 229 | elif Contact['UserName'].find('@@') != -1: # 群聊 230 | ContactList.remove(Contact) 231 | elif Contact['UserName'] == self.User['UserName']: # 自己 232 | ContactList.remove(Contact) 233 | self.ContactList = ContactList 234 | 235 | return True 236 | 237 | def webwxbatchgetcontact(self, id_list): 238 | url = self.base_uri + \ 239 | '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % ( 240 | int(time.time()), self.pass_ticket) 241 | param_list = [{"UserName": id, "EncryChatRoomId": ""} for id in id_list] 242 | params = { 243 | 'BaseRequest': self.BaseRequest, 244 | "Count": len(param_list), 245 | "List": param_list 246 | } 247 | dic = self._post(url, params) 248 | # blabla ... 249 | return dic['ContactList'] 250 | 251 | def testsynccheck(self): 252 | SyncHost = [ 253 | 'webpush.weixin.qq.com', 254 | 'webpush2.weixin.qq.com', 255 | 'webpush.wechat.com', 256 | 'webpush1.wechat.com', 257 | 'webpush2.wechat.com', 258 | 'webpush1.wechatapp.com', 259 | ] 260 | for host in SyncHost: 261 | self.syncHost = host 262 | [retcode, selector] = self.synccheck() 263 | if retcode == '0': 264 | return True 265 | return False 266 | 267 | def synccheck(self): 268 | params = { 269 | 'r': int(time.time()), 270 | 'sid': self.sid, 271 | 'uin': self.uin, 272 | 'skey': self.skey, 273 | 'deviceid': self.deviceId, 274 | 'synckey': self.synckey, 275 | '_': int(time.time()), 276 | } 277 | url = 'https://' + self.syncHost + \ 278 | '/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params) 279 | data = self._get(url) 280 | pm = re.search( 281 | r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data) 282 | retcode = pm.group(1) 283 | selector = pm.group(2) 284 | return [retcode, selector] 285 | 286 | def webwxsync(self): 287 | url = self.base_uri + \ 288 | '/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( 289 | self.sid, self.skey, self.pass_ticket) 290 | params = { 291 | 'BaseRequest': self.BaseRequest, 292 | 'SyncKey': self.SyncKey, 293 | 'rr': ~int(time.time()) 294 | } 295 | dic = self._post(url, params) 296 | if self.DEBUG: 297 | logging.debug(json.dumps(dic, indent=4)) 298 | 299 | if dic['BaseResponse']['Ret'] == 0: 300 | self.SyncKey = dic['SyncKey'] 301 | self.synckey = '|'.join( 302 | [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) 303 | return dic 304 | 305 | def webwxsendmsg(self, word, to='filehelper'): 306 | url = self.base_uri + \ 307 | '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket) 308 | clientMsgId = str(int(time.time() * 1000)) + \ 309 | str(random.random())[:5].replace('.', '') 310 | params = { 311 | 'BaseRequest': self.BaseRequest, 312 | 'Msg': { 313 | "Type": 1, 314 | "Content": self._transcoding(word), 315 | "FromUserName": self.User['UserName'], 316 | "ToUserName": to, 317 | "LocalID": clientMsgId, 318 | "ClientMsgId": clientMsgId 319 | } 320 | } 321 | headers = {'content-type': 'application/json; charset=UTF-8'} 322 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 323 | r = requests.post(url, data=data, headers=headers) 324 | dic = r.json() 325 | return dic['BaseResponse']['Ret'] == 0 326 | 327 | def webwxuploadmedia(self, image_name): 328 | url = 'https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' 329 | # 计数器 330 | self.media_count = self.media_count + 1 331 | # 文件名 332 | file_name = image_name 333 | # MIME格式 334 | # mime_type = application/pdf, image/jpeg, image/png, etc. 335 | mime_type = mimetypes.guess_type(image_name, strict=False)[0] 336 | # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc 337 | # pic格式,直接显示。doc格式则显示为文件。 338 | media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc' 339 | # 上一次修改日期 340 | lastModifieDate = 'Wed Jun 01 2016 15:27:16 GMT+0800 (China Standard Time)' 341 | # 文件大小 342 | file_size = os.path.getsize(file_name) 343 | # PassTicket 344 | pass_ticket = self.pass_ticket 345 | # clientMediaId 346 | client_media_id = str(int(time.time() * 1000)) + \ 347 | str(random.random())[:5].replace('.', '') 348 | # webwx_data_ticket 349 | webwx_data_ticket = '' 350 | print 'cookie = ', self.cookie 351 | for item in self.cookie: 352 | print item.name + '[' + item.value + ']' 353 | if item.name == 'webwx_data_ticket': 354 | webwx_data_ticket = item.value 355 | break 356 | if (webwx_data_ticket == ''): 357 | return "None Fuck Cookie" 358 | 359 | 360 | uploadmediarequest = json.dumps({ 361 | "BaseRequest": self.BaseRequest, 362 | "ClientMediaId": client_media_id, 363 | "TotalLen": file_size, 364 | "StartPos": 0, 365 | "DataLen": file_size, 366 | "MediaType": 4, 367 | }, ensure_ascii=False).encode('utf8') 368 | 369 | multipart_encoder = MultipartEncoder( 370 | fields={ 371 | 'id': 'WU_FILE_' + str(self.media_count), 372 | 'name': file_name, 373 | 'type': mime_type, 374 | 'lastModifieDate': lastModifieDate, 375 | 'size': str(file_size), 376 | 'mediatype': media_type, 377 | 'uploadmediarequest': uploadmediarequest, 378 | 'webwx_data_ticket': webwx_data_ticket, 379 | 'pass_ticket': pass_ticket, 380 | 'filename': (file_name, open(file_name, 'rb'), mime_type.split('/')[1]) 381 | }, 382 | boundary='-----------------------------1575017231431605357584454111' 383 | ) 384 | 385 | headers = { 386 | 'Host': 'file.wx.qq.com', 387 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0', 388 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 389 | 'Accept-Language': 'en-US,en;q=0.5', 390 | 'Accept-Encoding': 'gzip, deflate', 391 | 'Referer': 'https://wx2.qq.com/', 392 | 'Content-Type': multipart_encoder.content_type, 393 | 'Origin': 'https://wx2.qq.com', 394 | 'Connection': 'keep-alive', 395 | 'Pragma': 'no-cache', 396 | 'Cache-Control': 'no-cache' 397 | } 398 | 399 | r = requests.post(url, data=multipart_encoder, headers=headers) 400 | response_json = r.json() 401 | print 'headers =', headers 402 | print 'multipart_encoder =', multipart_encoder 403 | print 'response_json =', response_json 404 | if response_json['BaseResponse']['Ret'] == 0: 405 | return response_json 406 | 407 | return None 408 | 409 | def webwxsendmsgimg(self, user_id, media_id): 410 | url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s' % self.pass_ticket 411 | clientMsgId = str(int(time.time() * 1000)) + \ 412 | str(random.random())[:5].replace('.', '') 413 | data_json = { 414 | "BaseRequest": self.BaseRequest, 415 | "Msg": { 416 | "Type": 3, 417 | "MediaId": media_id, 418 | "FromUserName": self.User['UserName'], 419 | "ToUserName": user_id, 420 | "LocalID": clientMsgId, 421 | "ClientMsgId": clientMsgId 422 | } 423 | } 424 | headers = {'content-type': 'application/json; charset=UTF-8'} 425 | data = json.dumps(data_json, ensure_ascii=False).encode('utf8') 426 | r = requests.post(url, data=data, headers=headers) 427 | dic = r.json() 428 | return dic['BaseResponse']['Ret'] == 0 429 | 430 | def webwxsendmsgemotion(self, user_id, media_id): 431 | url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket 432 | clientMsgId = str(int(time.time() * 1000)) + \ 433 | str(random.random())[:5].replace('.', '') 434 | data_json = { 435 | "BaseRequest": self.BaseRequest, 436 | "Msg": { 437 | "Type": 47, 438 | "EmojiFlag": 2, 439 | "MediaId": media_id, 440 | "FromUserName": self.User['UserName'], 441 | "ToUserName": user_id, 442 | "LocalID": clientMsgId, 443 | "ClientMsgId": clientMsgId 444 | } 445 | } 446 | headers = {'content-type': 'application/json; charset=UTF-8'} 447 | data = json.dumps(data_json, ensure_ascii=False).encode('utf8') 448 | r = requests.post(url, data=data, headers=headers) 449 | dic = r.json() 450 | if self.DEBUG: 451 | print json.dumps(dic, indent=4) 452 | logging.debug(json.dumps(dic, indent=4)) 453 | return dic['BaseResponse']['Ret'] == 0 454 | 455 | def webwxgeticon(self, id): 456 | url = self.base_uri + \ 457 | '/webwxgeticon?username=%s&skey=%s' % (id, self.skey) 458 | data = self._get(url) 459 | fn = 'img_' + id + '.jpg' 460 | return self._saveFile(fn, data, 'webwxgeticon') 461 | 462 | def webwxgetheadimg(self, id): 463 | url = self.base_uri + \ 464 | '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey) 465 | data = self._get(url) 466 | fn = 'img_' + id + '.jpg' 467 | return self._saveFile(fn, data, 'webwxgetheadimg') 468 | 469 | def webwxgetmsgimg(self, msgid): 470 | url = self.base_uri + \ 471 | '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) 472 | data = self._get(url) 473 | fn = 'img_' + msgid + '.jpg' 474 | return self._saveFile(fn, data, 'webwxgetmsgimg') 475 | 476 | def webwxgetmsgemotion(self, msgid, url): 477 | data = self._get(url) 478 | fn = 'img_' + msgid + '.jpg' 479 | return self._saveFile(fn, data, 'webwxgetmsgemotion') 480 | 481 | # Not work now for weixin haven't support this API 482 | def webwxgetvideo(self, msgid): 483 | url = self.base_uri + \ 484 | '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey) 485 | data = self._get(url, api='webwxgetvideo') 486 | fn = 'video_' + msgid + '.mp4' 487 | return self._saveFile(fn, data, 'webwxgetvideo') 488 | 489 | def webwxgetvoice(self, msgid): 490 | url = self.base_uri + \ 491 | '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) 492 | data = self._get(url) 493 | fn = 'voice_' + msgid + '.mp3' 494 | return self._saveFile(fn, data, 'webwxgetvoice') 495 | 496 | 497 | class WebWeixin(WebWeixinAPI): 498 | def __init__(self): 499 | WebWeixinAPI.__init__(self) 500 | self._group_dict = {} # {group_id, group_info} 501 | self._sync_group_set = set() 502 | self._group_users_queue = Queue.Queue() 503 | 504 | def login(self): 505 | data = self._get(self.redirect_uri) 506 | doc = xml.dom.minidom.parseString(data) 507 | root = doc.documentElement 508 | 509 | for node in root.childNodes: 510 | if node.nodeName == 'skey': 511 | self.skey = node.childNodes[0].data 512 | elif node.nodeName == 'wxsid': 513 | self.sid = node.childNodes[0].data 514 | elif node.nodeName == 'wxuin': 515 | self.uin = node.childNodes[0].data 516 | elif node.nodeName == 'pass_ticket': 517 | self.pass_ticket = node.childNodes[0].data 518 | 519 | if '' in (self.skey, self.sid, self.uin, self.pass_ticket): 520 | return False 521 | 522 | self.BaseRequest = { 523 | 'Uin': int(self.uin), 524 | 'Sid': self.sid, 525 | 'Skey': self.skey, 526 | 'DeviceID': self.deviceId, 527 | } 528 | return True 529 | 530 | def _saveFile(self, filename, data, api=None): 531 | fn = filename 532 | if self.saveSubFolders[api]: 533 | dirName = os.path.join(self.saveFolder, self.saveSubFolders[api]) 534 | if not os.path.exists(dirName): 535 | os.makedirs(dirName) 536 | fn = os.path.join(dirName, filename) 537 | logging.debug('Saved file: %s' % fn) 538 | with open(fn, 'wb') as f: 539 | f.write(data) 540 | f.close() 541 | return fn 542 | 543 | def getGroupName(self, id): 544 | name = '未知群' 545 | GroupList = self.getNameById(id) 546 | for group in GroupList: 547 | if group['UserName'] == id: 548 | name = group['NickName'] 549 | return name 550 | 551 | def getGroupUserRemarkName(self, group_id, user_id): 552 | group_name = '未知群' 553 | user_name = '佚名' 554 | group_list = self.getNameById(group_id) 555 | for group in group_list: 556 | if group['UserName'] == group_id: 557 | group_name = group['NickName'] 558 | member_list = group['MemberList'] 559 | for member in member_list: 560 | if member['UserName'] == user_id: 561 | user_name = member['DisplayName'] if member['DisplayName'] else member['NickName'] 562 | result = unescape('【' + user_name + '】【' + group_name + '】') 563 | return result 564 | 565 | def getNameById(self, id): 566 | return self.webwxbatchgetcontact([id]) 567 | 568 | def getNameByIdList(self, id_list): 569 | return self.webwxbatchgetcontact(id_list) 570 | 571 | def updateGroupDict(self, group_user_list): 572 | for group_id in group_user_list: 573 | assert group_id.startswith('@@') 574 | group_list = self.getNameByIdList(group_user_list) 575 | for group in group_list: 576 | id = group['UserName'] 577 | nick_name = group['NickName'] 578 | if nick_name: 579 | self._group_dict[id] = group 580 | # print id, group['NickName'] 581 | if nick_name.startswith(PREFIX): 582 | self._sync_group_set.add(id) 583 | # member_list = group['MemberList'] 584 | # for member in member_list: 585 | # print member 586 | 587 | def updateGroupDictProcess(self): 588 | while True: 589 | try: 590 | logging.info('[updateGroupDictProcess] entering loop: ') 591 | group_user_list = self._group_users_queue.get(block=True) 592 | logging.info('[updateGroupDictProcess] get list') 593 | self.updateGroupDict(group_user_list) 594 | logging.info('[updateGroupDictProcess] Updated %s groups' % len(group_user_list)) 595 | except Queue.Empty: 596 | time.sleep(2) 597 | 598 | def getUserRemarkName(self, id): 599 | name = '未知群' if id[:2] == '@@' else '陌生人' 600 | if id == self.User['UserName']: 601 | return self.User['NickName'] # 自己 602 | 603 | if id[:2] == '@@': 604 | # 群 605 | name = self.getGroupName(id) 606 | else: 607 | # 特殊账号 608 | for member in self.SpecialUsersList: 609 | if member['UserName'] == id: 610 | name = member['RemarkName'] if member[ 611 | 'RemarkName'] else member['NickName'] 612 | 613 | # 公众号或服务号 614 | for member in self.PublicUsersList: 615 | if member['UserName'] == id: 616 | name = member['RemarkName'] if member[ 617 | 'RemarkName'] else member['NickName'] 618 | 619 | # 直接联系人 620 | for member in self.ContactList: 621 | if member['UserName'] == id: 622 | name = member['RemarkName'] if member[ 623 | 'RemarkName'] else member['NickName'] 624 | 625 | if name == '未知群' or name == '陌生人': 626 | logging.debug(id) 627 | return name 628 | 629 | def getUSerID(self, name): 630 | for member in self.MemberList: 631 | if name == member['RemarkName'] or name == member['NickName']: 632 | return member['UserName'] 633 | return None 634 | 635 | 636 | def _showMsg(self, message, data=None): 637 | src_id = None 638 | srcName = None 639 | dst_id = None 640 | dstName = None 641 | groupName = None 642 | content = None 643 | 644 | msg = message 645 | logging.debug(msg) 646 | 647 | if msg['raw_msg']: 648 | src_id = msg['raw_msg']['FromUserName'] 649 | srcName = self.getUserRemarkName(src_id) 650 | dst_id = msg['raw_msg']['ToUserName'] 651 | dstName = self.getUserRemarkName(dst_id) 652 | content = unescape(msg['raw_msg']['Content']) 653 | # message_id = msg['raw_msg']['MsgId'] 654 | 655 | if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: 656 | # 地理位置消息 657 | data = self._get(content).decode('gbk').encode('utf-8') 658 | pos = self._searchContent('title', data, 'xml') 659 | tree = html.fromstring(self._get(content)) 660 | url = tree.xpath('//html/body/div/img')[0].attrib['src'] 661 | 662 | for item in urlparse(url).query.split('&'): 663 | if item.split('=')[0] == 'center': 664 | loc = item.split('=')[-1:] 665 | 666 | content = '%s 发送了一个 位置消息 - 我在 [%s](%s) @ %s]' % ( 667 | srcName, pos, url, loc) 668 | 669 | if msg['raw_msg']['ToUserName'] == 'filehelper': 670 | # 文件传输助手 671 | dstName = '文件传输助手' 672 | 673 | if msg['raw_msg']['FromUserName'][:2] == '@@': 674 | # 接收到来自群的消息 675 | if re.search(":
", content, re.IGNORECASE): 676 | [people, content] = content.split(':
', 1) 677 | groupName = srcName 678 | src_id = people 679 | srcName = self.getGroupUserRemarkName(msg['raw_msg']['FromUserName'], people); 680 | else: 681 | groupName = srcName 682 | srcName = 'SYSTEM' 683 | elif msg['raw_msg']['ToUserName'][:2] == '@@': 684 | # 自己发给群的消息 685 | groupName = dstName 686 | # dstName = 'GROUP' 687 | 688 | # 收到了红包 689 | if content == '收到红包,请在手机上查看': 690 | msg['message'] = content 691 | 692 | # 指定了消息内容 693 | if 'message' in msg.keys(): 694 | content = msg['message'] 695 | 696 | if groupName != None: 697 | print '%s| %s -> %s: %s' % (groupName.strip(), srcName.strip() + src_id.strip(), dstName.strip() + dst_id.strip(), content.replace('
', '\n')) 698 | logging.info('%s| %s -> %s: %s' % (groupName.strip(), 699 | srcName.strip(), dstName.strip(), content.replace('
', '\n'))) 700 | else: 701 | print '%s -> %s: %s' % (srcName.strip() + src_id.strip(), dstName.strip() + dst_id.strip(), content.replace('
', '\n')) 702 | logging.info('%s -> %s: %s' % (srcName.strip(), 703 | dstName.strip(), content.replace('
', '\n'))) 704 | 705 | msg_type = msg['raw_msg']['MsgType'] 706 | 707 | if groupName == '测试一' and msg['raw_msg']['FromUserName'] == self.User['UserName']: 708 | if msg_type == 47 and data: 709 | self.sendEmotionByUserId(msg['raw_msg']['FromUserName'], data) 710 | if msg_type == 1: 711 | word = srcName.strip() + ':' + content 712 | self.sendMsgById(self.User['UserName'], word) 713 | if msg_type == 49: 714 | word = srcName.strip() + ':\n' + data 715 | print 'word=====', word 716 | self.sendMsgById(self.User['UserName'], word) 717 | 718 | if msg['raw_msg']['FromUserName'] in self._sync_group_set: 719 | if msg_type == 1: 720 | for group_id in self._sync_group_set: 721 | if group_id != msg['raw_msg']['FromUserName']: 722 | word = srcName.strip() + ':\n' + content.replace('
', '\n').strip() 723 | self.sendMsgById(group_id, word) 724 | if msg_type == 3: 725 | for group_id in self._sync_group_set: 726 | if group_id != msg['raw_msg']['FromUserName']: 727 | self.sendMsgById(group_id, srcName.strip() + ':') 728 | self.sendImgByUserId(group_id, data) 729 | if msg_type == 47: 730 | for group_id in self._sync_group_set: 731 | if group_id != msg['raw_msg']['FromUserName'] and data: 732 | self.sendMsgById(group_id, srcName.strip() + ':') 733 | self.sendEmotionByUserId(group_id, data) 734 | if msg_type == 49: 735 | for group_id in self._sync_group_set: 736 | if group_id != msg['raw_msg']['FromUserName'] and data: 737 | word = srcName.strip() + ':\n' + data 738 | self.sendMsgById(group_id, word) 739 | 740 | def handleMsg(self, r): 741 | for msg in r['AddMsgList']: 742 | # logging.debug('[*] 你有新的消息,请注意查收') 743 | 744 | if self.DEBUG: 745 | fn = 'msg' + str(int(random.random() * 1000)) + '.json' 746 | with open(fn, 'w') as f: 747 | f.write(json.dumps(msg)) 748 | logging.debug('[*] 该消息已储存到文件: %s' % (fn)) 749 | 750 | msgType = msg['MsgType'] 751 | name = self.getUserRemarkName(msg['FromUserName']) 752 | content = msg['Content'].replace('<', '<').replace('>', '>') 753 | msgid = msg['MsgId'] 754 | 755 | if msgType == 1: 756 | raw_msg = {'raw_msg': msg} 757 | self._showMsg(raw_msg) 758 | elif msgType == 3: 759 | image = self.webwxgetmsgimg(msgid) 760 | raw_msg = {'raw_msg': msg, 761 | 'message': '%s 发送了一张图片: %s' % (name, image)} 762 | self._showMsg(raw_msg, image) 763 | elif msgType == 34: 764 | voice = self.webwxgetvoice(msgid) 765 | raw_msg = {'raw_msg': msg, 766 | 'message': '%s 发了一段语音: %s' % (name, voice)} 767 | self._showMsg(raw_msg, voice) 768 | elif msgType == 42: 769 | info = msg['RecommendInfo'] 770 | print '%s 发送了一张名片:' % name 771 | print '=========================' 772 | print '= 昵称: %s' % info['NickName'] 773 | print '= 微信号: %s' % info['Alias'] 774 | print '= 地区: %s %s' % (info['Province'], info['City']) 775 | print '= 性别: %s' % ['未知', '男', '女'][info['Sex']] 776 | print '=========================' 777 | raw_msg = {'raw_msg': msg, 'message': '%s 发送了一张名片: %s' % ( 778 | name.strip(), json.dumps(info))} 779 | self._showMsg(raw_msg) 780 | elif msgType == 47: 781 | url = self._searchContent('cdnurl', content) 782 | raw_msg = {'raw_msg': msg, 783 | 'message': '%s 发了一个动画表情,点击下面链接查看: %s' % (name, url)} 784 | if url == '未知': 785 | data = None 786 | else: 787 | data = self.webwxgetmsgemotion(msgid, url) 788 | self._showMsg(raw_msg, data) 789 | elif msgType == 49: 790 | appMsgType = defaultdict(lambda: "") 791 | appMsgType.update({5: '链接', 3: '音乐', 7: '微博'}) 792 | print '%s 分享了一个%s:' % (name, appMsgType[msg['AppMsgType']]) 793 | print '=========================' 794 | print '= 标题: %s' % msg['FileName'] 795 | print '= 描述: %s' % self._searchContent('des', content, 'xml') 796 | print '= 链接: %s' % msg['Url'] 797 | print '= 来自: %s' % self._searchContent('appname', content, 'xml') 798 | print '=========================' 799 | card = { 800 | 'title': msg['FileName'], 801 | 'description': self._searchContent('des', content, 'xml'), 802 | 'url': msg['Url'], 803 | 'appname': self._searchContent('appname', content, 'xml') 804 | } 805 | raw_msg = {'raw_msg': msg, 'message': '%s 分享了一个%s: %s' % ( 806 | name, appMsgType[msg['AppMsgType']], json.dumps(card))} 807 | url = unescape(msg['Url']) 808 | 809 | data = '分享了一个%s:\n' % (appMsgType[msg['AppMsgType']]) 810 | data += '标题: %s\n' % msg['FileName'] 811 | data += '链接: %s' % url 812 | self._showMsg(raw_msg, data) 813 | 814 | elif msgType == 51: 815 | raw_msg = {'raw_msg': msg, 'message': '[*] 成功获取联系人信息'} 816 | self._showMsg(raw_msg) 817 | status_notify_userids = msg['StatusNotifyUserName'] 818 | group_userid_list = [] 819 | for user_id in status_notify_userids.split(','): 820 | if user_id.startswith('@@'): 821 | group_userid_list.append(user_id) 822 | self._group_users_queue.put(group_userid_list) 823 | 824 | elif msgType == 62: 825 | video = self.webwxgetvideo(msgid) 826 | raw_msg = {'raw_msg': msg, 827 | 'message': '%s 发了一段小视频: %s' % (name, video)} 828 | self._showMsg(raw_msg) 829 | elif msgType == 10002: 830 | raw_msg = {'raw_msg': msg, 'message': '%s 撤回了一条消息' % name} 831 | self._showMsg(raw_msg) 832 | else: 833 | pass 834 | # logging.debug('[*] 该消息类型为: %d,可能是表情,图片, 链接或红包: %s' % 835 | # (msg['MsgType'], json.dumps(msg))) 836 | # raw_msg = { 837 | # 'raw_msg': msg, 'message': '[*] 该消息类型为: %d,可能是表情,图片, 链接或红包' % msg['MsgType']} 838 | 839 | def listenMsgMode(self): 840 | updateProcess = threading.Thread(target=self.updateGroupDictProcess) 841 | updateProcess.daemon = True 842 | updateProcess.start() 843 | 844 | logging.debug('[*] 进入消息监听模式 ... 成功') 845 | self._run('[*] 进行同步线路测试 ... ', self.testsynccheck) 846 | while True: 847 | try: 848 | self.lastCheckTs = time.time() 849 | [retcode, selector] = self.synccheck() 850 | logging.debug('retcode: %s, selector: %s' % (retcode, selector)) 851 | if retcode == '1100': 852 | logging.debug('[*] 你在手机上登出了微信,债见') 853 | exit() 854 | if retcode == '1101': 855 | logging.debug('[*] 你在其他地方登录了 WEB 版微信,债见') 856 | exit() 857 | if retcode == '1102': 858 | logging.debug('[*] 你在手机上主动退出啦,债见') 859 | exit() 860 | elif retcode == '0': 861 | if selector == '2': 862 | r = self.webwxsync() 863 | if r is not None: 864 | self.handleMsg(r) 865 | elif selector == '6': 866 | r = self.webwxsync() 867 | # logging.debug('[*] 收到疑似红包消息 %d 次' % redEnvelope) 868 | elif selector == '7': 869 | # logging.debug('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat) 870 | r = self.webwxsync() 871 | elif selector == '0': 872 | time.sleep(1) 873 | else: 874 | r = self.webwxsync() 875 | except KeyboardInterrupt: 876 | raise 877 | except SystemExit: 878 | raise 879 | except: 880 | traceback.print_exc() 881 | 882 | #if (time.time() - self.lastCheckTs) <= 2: 883 | # time.sleep(time.time() - self.lastCheckTs) 884 | 885 | def sendMsgById(self, id, word, isfile=False): 886 | print type(id), id 887 | if isfile: 888 | with open(word, 'r') as f: 889 | for line in f.readlines(): 890 | line = line.replace('\n', '') 891 | if self.webwxsendmsg(line, id): 892 | print ' [成功]' 893 | else: 894 | print ' [失败]' 895 | time.sleep(1) 896 | else: 897 | if self.webwxsendmsg(word, id): 898 | print '[*] 消息发送成功' 899 | logging.debug('[*] 消息发送成功') 900 | else: 901 | print '[*] 消息发送失败' 902 | logging.debug('[*] 消息发送失败') 903 | 904 | def sendMsg(self, name, word, isfile=False): 905 | id = self.getUSerID(name) 906 | print type(id), id 907 | if id: 908 | self.sendMsgById(id, word, isfile) 909 | else: 910 | print '[*] 此用户不存在' 911 | logging.debug('[*] 此用户不存在') 912 | 913 | def sendImgByUserId(self, user_id, file_name): 914 | response = self.webwxuploadmedia(file_name) 915 | media_id = "" 916 | if response is not None: 917 | media_id = response['MediaId'] 918 | response = self.webwxsendmsgimg(user_id, media_id) 919 | 920 | def sendImg(self, name, file_name): 921 | user_id = self.getUSerID(name) 922 | return self.sendImgByUserId(user_id, file_name) 923 | 924 | def sendEmotionByUserId(self, user_id, file_name): 925 | response = self.webwxuploadmedia(file_name) 926 | media_id = "" 927 | if response is not None: 928 | media_id = response['MediaId'] 929 | response = self.webwxsendmsgemotion(user_id, media_id) 930 | 931 | def sendEmotion(self, name, file_name): 932 | user_id = self.getUSerID(name) 933 | return self.sendEmotionByUserId(user_id, file_name) 934 | 935 | @catchKeyboardInterrupt 936 | def start(self): 937 | logging.debug('[*] 微信网页版 ... 开动') 938 | while True: 939 | self._run('[*] 正在获取 uuid ... ', self.getUUID) 940 | logging.debug('[*] 微信网页版 ... 开动') 941 | self.genQRCode() 942 | print '[*] 请使用微信扫描二维码以登录 ... ' 943 | if not self.waitForLogin(): 944 | continue 945 | if not self.waitForLogin(0): 946 | continue 947 | break 948 | 949 | self._run('[*] 正在登录 ... ', self.login) 950 | self._run('[*] 微信初始化 ... ', self.webwxinit) 951 | self._run('[*] 开启状态通知 ... ', self.webwxstatusnotify) 952 | self._run('[*] 获取联系人 ... ', self.webwxgetcontact) 953 | self._echo('[*] 应有 %s 个联系人,读取到联系人 %d 个' % 954 | (self.MemberCount, len(self.MemberList))) 955 | print 956 | self._echo('[*] 共有 %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号' % ( 957 | len(self.ContactList), len(self.SpecialUsersList), len(self.PublicUsersList))) 958 | print 959 | logging.debug('[*] 微信网页版 ... 开动') 960 | if self.DEBUG: 961 | print self 962 | logging.debug(self) 963 | 964 | listenProcess = threading.Thread(target=self.listenMsgMode) 965 | listenProcess.daemon = True 966 | listenProcess.start() 967 | 968 | while True: 969 | text = raw_input('') 970 | if text == 'quit': 971 | listenProcess.terminate() 972 | logging.debug('[*] 退出微信') 973 | exit() 974 | elif text[:2] == '->': 975 | [name, word] = text[2:].split(':', 1) 976 | self.sendMsg(name, word) 977 | elif text[:3] == 'm->': 978 | [name, file] = text[3:].split(':', 1) 979 | self.sendMsg(name, file, True) 980 | elif text[:3] == 'f->': 981 | print '发送文件' 982 | logging.debug('发送文件') 983 | elif text[:3] == 'i->': 984 | print '发送图片' 985 | [name, file_name] = text[3:].split(':', 1) 986 | self.sendImg(name, file_name) 987 | logging.debug('发送图片') 988 | elif text[:3] == 'e->': 989 | print '发送表情' 990 | [name, file_name] = text[3:].split(':', 1) 991 | self.sendEmotion(name, file_name) 992 | logging.debug('发送表情') 993 | elif text.startswith('g->'): 994 | [name, word] = text[3:].split(':', 1) 995 | self.sendMsgById(name, word) 996 | 997 | 998 | def _run(self, str, func, *args): 999 | if func(*args): 1000 | logging.debug('%s... 成功' % (str)) 1001 | else: 1002 | logging.debug('%s... 失败' % (str)) 1003 | logging.debug('[*] 退出程序') 1004 | exit() 1005 | 1006 | def _echo(self, str): 1007 | sys.stdout.write(str) 1008 | sys.stdout.flush() 1009 | 1010 | def _printQR(self, mat): 1011 | for i in mat: 1012 | BLACK = '\033[40m \033[0m' 1013 | WHITE = '\033[47m \033[0m' 1014 | print ''.join([BLACK if j else WHITE for j in i]) 1015 | 1016 | def _str2qr(self, str): 1017 | qr = qrcode.QRCode() 1018 | qr.border = 1 1019 | qr.add_data(str) 1020 | mat = qr.get_matrix() 1021 | self._printQR(mat) # qr.print_tty() or qr.print_ascii() 1022 | 1023 | def _transcoding(self, data): 1024 | if not data: 1025 | return data 1026 | result = None 1027 | if type(data) == unicode: 1028 | result = data 1029 | elif type(data) == str: 1030 | result = data.decode('utf-8') 1031 | return result 1032 | 1033 | def _get(self, url, api=None): 1034 | request = urllib2.Request(url=url) 1035 | request.add_header('Referer', 'https://wx.qq.com/') 1036 | if api == 'webwxgetvoice': 1037 | request.add_header('Range', 'bytes=0-') 1038 | if api == 'webwxgetvideo': 1039 | request.add_header('Range', 'bytes=0-') 1040 | response = urllib2.urlopen(request) 1041 | data = response.read() 1042 | logging.debug(url) 1043 | return data 1044 | 1045 | def _post(self, url, params, jsonfmt=True): 1046 | if jsonfmt: 1047 | request = urllib2.Request(url=url, data=json.dumps(params)) 1048 | request.add_header( 1049 | 'ContentType', 'application/json; charset=UTF-8') 1050 | else: 1051 | request = urllib2.Request(url=url, data=urllib.urlencode(params)) 1052 | response = urllib2.urlopen(request) 1053 | data = response.read() 1054 | if jsonfmt: 1055 | return json.loads(data, object_hook=_decode_dict) 1056 | return data 1057 | 1058 | def _searchContent(self, key, content, fmat='attr'): 1059 | if fmat == 'attr': 1060 | pm = re.search(key + '\s?=\s?"([^"<]+)"', content) 1061 | if pm: 1062 | return pm.group(1) 1063 | elif fmat == 'xml': 1064 | pm = re.search('<{0}>([^<]+)'.format(key), content) 1065 | if not pm: 1066 | pm = re.search( 1067 | '<{0}><\!\[cdata\[(.*?)\]\]>'.format(key), content) 1068 | if pm: 1069 | return pm.group(1) 1070 | return '未知' 1071 | 1072 | 1073 | def main(): 1074 | import coloredlogs 1075 | coloredlogs.install( 1076 | level='DEBUG', 1077 | fmt='%(asctime)s %(programname)s:%(lineno)d [%(process)d] %(levelname)s %(message)s' 1078 | ) 1079 | 1080 | webwx = WebWeixin() 1081 | webwx.start() 1082 | 1083 | if __name__ == '__main__': 1084 | main() 1085 | --------------------------------------------------------------------------------