├── .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 [](http://github.com/buaagg/WeixinBot) [](http://github.com/buaagg/WeixinBot/fork) 
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 | 
39 |
40 | 按照操作指示在手机微信上扫描二维码然后登录。
41 |
42 | 现在,名片,链接,动画表情和地址位置消息都可以正常接收。
43 |
44 | 
45 |
46 | 
47 |
48 | **目前支持的命令**:
49 |
50 | `->[昵称或ID]:[内容]` 给好友发送消息
51 |
52 | `m->[昵称或ID]:[文件路径]` 给好友发送文件中的内容
53 |
54 | 
55 |
56 | `f->[昵称或ID]:[文件路径]` 给好友发送文件
57 |
58 | `i->[昵称或ID]:[图片路径]` 给好友发送图片
59 |
60 | `e->[昵称或ID]:[文件路径]` 给好友发送表情(jpg/gif)
61 |
62 | `quit` 退出程序
63 |
64 | 
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}>([^<]+){0}>'.format(key), content)
1065 | if not pm:
1066 | pm = re.search(
1067 | '<{0}><\!\[cdata\[(.*?)\]\]>{0}>'.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 |
--------------------------------------------------------------------------------