├── .gitignore
├── LICENSE
├── README.md
├── imgs
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 8.jpg
└── groupQrcode.jpg
├── wxbot_demo_py3
├── requirements.txt
└── weixin.py
└── wxbot_project_py2.7
├── README.md
├── config
├── __init__.py
├── config_manager.py
├── constant.py
├── log.py
├── requirements.txt
└── wechat.conf.bak
├── db
├── __init__.py
├── mysql_db.py
└── sqlite_db.py
├── docker
├── Dockerfile
└── README.md
├── flask_templates
├── index.html
└── upload.html
├── wechat
├── __init__.py
├── utils.py
├── wechat.py
└── wechat_apis.py
├── weixin_bot.py
└── wx_handler
├── __init__.py
├── bot.py
├── sendgrid_mail.py
└── wechat_msg_processor.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Python template
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *,cover
48 | .hypothesis/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 |
58 | # Flask stuff:
59 | instance/
60 | .webassets-cache
61 |
62 | # Scrapy stuff:
63 | .scrapy
64 |
65 | # Sphinx documentation
66 | docs/_build/
67 |
68 | # PyBuilder
69 | target/
70 |
71 | # IPython Notebook
72 | .ipynb_checkpoints
73 |
74 | # pyenv
75 | .python-version
76 |
77 | # celery beat schedule file
78 | celerybeat-schedule
79 |
80 | # dotenv
81 | .env
82 |
83 | # virtualenv
84 | venv/
85 | ENV/
86 |
87 | # Spyder project settings
88 | .spyderproject
89 |
90 | # Rope project settings
91 | .ropeproject
92 |
93 | # pycharm
94 | .idea
95 |
96 | *.pyc
97 | wxbot_demo_py3/saved/
98 | wxbot_project_py2.7/tmp_data/
99 | wxbot_project_py2.7/wechat/wechat_js_backup/
100 | wxbot_project_py2.7/config/wechat.conf
101 | wxbot_project_py2.7/wiki/
102 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeixinBot [](http://github.com/Urinx/WeixinBot) [](http://github.com/Urinx/WeixinBot/fork) 
2 |
3 | 网页版微信API,包含终端版微信及微信机器人
4 |
5 | ## Contents
6 | * [Demo](#Demo)
7 | * [Web Weixin Pipeline](#Web-Weixin-Pipeline)
8 | * [Web Weixin API](#Web-Weixin-API)
9 | * [Discussion Group](#Discussion-Group)
10 | * [Recent Update](#Recent-Update)
11 |
12 | ## Demo
13 | 为了确保能正常运行示例脚本,请安装所需的第三方包。
14 |
15 | ```
16 | pip install -r requirements.txt
17 | ```
18 |
19 | 注:下面演示的图片与功能可能不是最新的,具体请看源码。
20 |
21 |
22 |

23 |
24 |
25 | 按照操作指示在手机微信上扫描二维码然后登录,你可以选择是否开启自动回复模式。
26 |
27 | 
28 |
29 | 开启自动回复模式后,如果接收到的是文字消息就会自动回复,包括群消息。
30 |
31 | 
32 |
33 | 名片,链接,动画表情和地址位置消息。
34 |
35 | 
36 |
37 | 
38 |
39 | 网页版上有的功能目前基本上都能支持。
40 |
41 | ## Web Weixin Pipeline
42 |
43 | ```
44 | +--------------+ +---------------+ +---------------+
45 | | | | | | |
46 | | Get UUID | | Get Contact | | Status Notify |
47 | | | | | | |
48 | +-------+------+ +-------^-------+ +-------^-------+
49 | | | |
50 | | +-------+ +--------+
51 | | | |
52 | +-------v------+ +-----+--+------+ +--------------+
53 | | | | | | |
54 | | Get QRCode | | Weixin Init +------> Sync Check <----+
55 | | | | | | | |
56 | +-------+------+ +-------^-------+ +-------+------+ |
57 | | | | |
58 | | | +-----------+
59 | | | |
60 | +-------v------+ +-------+--------+ +-------v-------+
61 | | | Confirm Login | | | |
62 | +------> Login +---------------> New Login Page | | Weixin Sync |
63 | | | | | | | |
64 | | +------+-------+ +----------------+ +---------------+
65 | | |
66 | |QRCode Scaned|
67 | +-------------+
68 | ```
69 |
70 |
71 | ## Web Weixin API
72 |
73 | ### 登录
74 |
75 | | API | 获取 UUID |
76 | | --- | --------- |
77 | | url | https://login.weixin.qq.com/jslogin |
78 | | method | POST |
79 | | data | URL Encode |
80 | | params | **appid**: `应用ID`
**fun**: new `应用类型`
**lang**: zh\_CN `语言`
**_**: `时间戳` |
81 |
82 | 返回数据(String):
83 | ```
84 | window.QRLogin.code = 200; window.QRLogin.uuid = "xxx"
85 | ```
86 | > 注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是`wx782c26e4c19acffb`,在微信客户端上显示为应用名称为`Web微信`;现在用的是`wxeb7ec651dd0aefa9`,显示名称为`微信网页版`。
87 |
88 |
89 |

90 |
91 |
92 |
93 |
94 | | API | 绑定登陆(webwxpushloginurl) |
95 | | --- | --------- |
96 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxpushloginurl |
97 | | method | GET |
98 | | params | **uin**: xxx |
99 |
100 | 返回数据(String):
101 | ```
102 | {'msg': 'all ok', 'uuid': 'xxx', 'ret': '0'}
103 |
104 | 通过这种方式可以省掉扫二维码这步操作,更加方便
105 | ```
106 |
107 |
108 | | API | 生成二维码 |
109 | | --- | --------- |
110 | | url | https://login.weixin.qq.com/l/ `uuid` |
111 | | method | GET |
112 |
113 |
114 | | API | 二维码扫描登录 |
115 | | --- | --------- |
116 | | url | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login |
117 | | method | GET |
118 | | params | **tip**: 1 `未扫描` 0 `已扫描`
**uuid**: xxx
**_**: `时间戳` |
119 |
120 | 返回数据(String):
121 | ```
122 | window.code=xxx;
123 |
124 | xxx:
125 | 408 登陆超时
126 | 201 扫描成功
127 | 200 确认登录
128 |
129 | 当返回200时,还会有
130 | window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx";
131 | ```
132 |
133 |
134 | | API | webwxnewloginpage |
135 | | --- | --------- |
136 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage |
137 | | method | GET |
138 | | params | **ticket**: xxx
**uuid**: xxx
**lang**: zh_CN `语言`
**scan**: xxx
**fun**: new |
139 |
140 | 返回数据(XML):
141 | ```
142 |
143 | 0
144 | OK
145 | xxx
146 | xxx
147 | xxx
148 | xxx
149 | 1
150 |
151 | ```
152 |
153 |
154 | ### 微信初始化
155 |
156 | | API | webwxinit |
157 | | --- | --------- |
158 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket=xxx&skey=xxx&r=xxx |
159 | | method | POST |
160 | | data | JSON |
161 | | header | ContentType: application/json; charset=UTF-8 |
162 | | params | {
BaseRequest: {
Uin: xxx,
Sid: xxx,
Skey: xxx,
DeviceID: xxx,
}
} |
163 |
164 | 返回数据(JSON):
165 | ```
166 | {
167 | "BaseResponse": {
168 | "Ret": 0,
169 | "ErrMsg": ""
170 | },
171 | "Count": 11,
172 | "ContactList": [...],
173 | "SyncKey": {
174 | "Count": 4,
175 | "List": [
176 | {
177 | "Key": 1,
178 | "Val": 635705559
179 | },
180 | ...
181 | ]
182 | },
183 | "User": {
184 | "Uin": xxx,
185 | "UserName": xxx,
186 | "NickName": xxx,
187 | "HeadImgUrl": xxx,
188 | "RemarkName": "",
189 | "PYInitial": "",
190 | "PYQuanPin": "",
191 | "RemarkPYInitial": "",
192 | "RemarkPYQuanPin": "",
193 | "HideInputBarFlag": 0,
194 | "StarFriend": 0,
195 | "Sex": 1,
196 | "Signature": "Apt-get install B",
197 | "AppAccountFlag": 0,
198 | "VerifyFlag": 0,
199 | "ContactFlag": 0,
200 | "WebWxPluginSwitch": 0,
201 | "HeadImgFlag": 1,
202 | "SnsFlag": 17
203 | },
204 | "ChatSet": xxx,
205 | "SKey": xxx,
206 | "ClientVersion": 369297683,
207 | "SystemTime": 1453124908,
208 | "GrayScale": 1,
209 | "InviteStartCount": 40,
210 | "MPSubscribeMsgCount": 2,
211 | "MPSubscribeMsgList": [...],
212 | "ClickReportInterval": 600000
213 | }
214 | ```
215 |
216 |
217 | | API | webwxstatusnotify |
218 | | --- | --------- |
219 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify?lang=zh_CN&pass_ticket=xxx |
220 | | method | POST |
221 | | data | JSON |
222 | | header | ContentType: application/json; charset=UTF-8 |
223 | | params | {
BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
Code: 3,
FromUserName: `自己ID`,
ToUserName: `自己ID`,
ClientMsgId: `时间戳`
} |
224 |
225 | 返回数据(JSON):
226 | ```
227 | {
228 | "BaseResponse": {
229 | "Ret": 0,
230 | "ErrMsg": ""
231 | },
232 | ...
233 | }
234 | ```
235 |
236 |
237 | ### 获取联系人信息
238 |
239 | | API | webwxgetcontact |
240 | | --- | --------- |
241 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin//webwxgetcontact?pass_ticket=xxx&skey=xxx&r=xxx |
242 | | method | POST |
243 | | data | JSON |
244 | | header | ContentType: application/json; charset=UTF-8 |
245 |
246 | 返回数据(JSON):
247 | ```
248 | {
249 | "BaseResponse": {
250 | "Ret": 0,
251 | "ErrMsg": ""
252 | },
253 | "MemberCount": 334,
254 | "MemberList": [
255 | {
256 | "Uin": 0,
257 | "UserName": xxx,
258 | "NickName": "Urinx",
259 | "HeadImgUrl": xxx,
260 | "ContactFlag": 3,
261 | "MemberCount": 0,
262 | "MemberList": [],
263 | "RemarkName": "",
264 | "HideInputBarFlag": 0,
265 | "Sex": 0,
266 | "Signature": "你好,我们是地球三体组织。在这里,你将感受到不一样的思维模式,以及颠覆常规的世界观。而我们的目标,就是以三体人的智慧,引领人类未来科学技术500年。",
267 | "VerifyFlag": 8,
268 | "OwnerUin": 0,
269 | "PYInitial": "URINX",
270 | "PYQuanPin": "Urinx",
271 | "RemarkPYInitial": "",
272 | "RemarkPYQuanPin": "",
273 | "StarFriend": 0,
274 | "AppAccountFlag": 0,
275 | "Statues": 0,
276 | "AttrStatus": 0,
277 | "Province": "",
278 | "City": "",
279 | "Alias": "Urinxs",
280 | "SnsFlag": 0,
281 | "UniFriend": 0,
282 | "DisplayName": "",
283 | "ChatRoomId": 0,
284 | "KeyWord": "gh_",
285 | "EncryChatRoomId": ""
286 | },
287 | ...
288 | ],
289 | "Seq": 0
290 | }
291 | ```
292 |
293 |
294 | | API | webwxbatchgetcontact |
295 | | --- | --------- |
296 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=xxx&pass_ticket=xxx |
297 | | method | POST |
298 | | data | JSON |
299 | | header | ContentType: application/json; charset=UTF-8 |
300 | | params | {
BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
Count: `群数量`,
List: [
{ UserName: `群ID`, EncryChatRoomId: "" },
...
],
} |
301 |
302 | 返回数据(JSON)同上
303 |
304 |
305 | ### 同步刷新
306 |
307 | | API | synccheck |
308 | | --- | --------- |
309 | | protocol | https |
310 | | host | webpush.weixin.qq.com
webpush.wx2.qq.com
webpush.wx8.qq.com
webpush.wx.qq.com
webpush.web2.wechat.com
webpush.web.wechat.com |
311 | | path | /cgi-bin/mmwebwx-bin/synccheck |
312 | | method | GET |
313 | | data | URL Encode |
314 | | params | **r**: `时间戳`
**sid**: xxx
**uin**: xxx
**skey**: xxx
**deviceid**: xxx
**synckey**: xxx
**_**: `时间戳` |
315 |
316 | 返回数据(String):
317 | ```
318 | window.synccheck={retcode:"xxx",selector:"xxx"}
319 |
320 | retcode:
321 | 0 正常
322 | 1100 失败/登出微信
323 | selector:
324 | 0 正常
325 | 2 新的消息
326 | 7 进入/离开聊天界面
327 | ```
328 |
329 |
330 | | API | webwxsync |
331 | | --- | --------- |
332 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=xxx&skey=xxx&pass_ticket=xxx |
333 | | method | POST |
334 | | data | JSON |
335 | | header | ContentType: application/json; charset=UTF-8 |
336 | | params | {
BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
SyncKey: xxx,
rr: `时间戳取反`
} |
337 |
338 | 返回数据(JSON):
339 | ```
340 | {
341 | 'BaseResponse': {'ErrMsg': '', 'Ret': 0},
342 | 'SyncKey': {
343 | 'Count': 7,
344 | 'List': [
345 | {'Val': 636214192, 'Key': 1},
346 | ...
347 | ]
348 | },
349 | 'ContinueFlag': 0,
350 | 'AddMsgCount': 1,
351 | 'AddMsgList': [
352 | {
353 | 'FromUserName': '',
354 | 'PlayLength': 0,
355 | 'RecommendInfo': {...},
356 | 'Content': "",
357 | 'StatusNotifyUserName': '',
358 | 'StatusNotifyCode': 5,
359 | 'Status': 3,
360 | 'VoiceLength': 0,
361 | 'ToUserName': '',
362 | 'ForwardFlag': 0,
363 | 'AppMsgType': 0,
364 | 'AppInfo': {'Type': 0, 'AppID': ''},
365 | 'Url': '',
366 | 'ImgStatus': 1,
367 | 'MsgType': 51,
368 | 'ImgHeight': 0,
369 | 'MediaId': '',
370 | 'FileName': '',
371 | 'FileSize': '',
372 | ...
373 | },
374 | ...
375 | ],
376 | 'ModChatRoomMemberCount': 0,
377 | 'ModContactList': [],
378 | 'DelContactList': [],
379 | 'ModChatRoomMemberList': [],
380 | 'DelContactCount': 0,
381 | ...
382 | }
383 | ```
384 |
385 |
386 | ### 消息接口
387 |
388 | | API | webwxsendmsg |
389 | | --- | ------------ |
390 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=xxx |
391 | | method | POST |
392 | | data | JSON |
393 | | header | ContentType: application/json; charset=UTF-8 |
394 | | params | {
BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
Msg: {
Type: 1 `文字消息`,
Content: `要发送的消息`,
FromUserName: `自己ID`,
ToUserName: `好友ID`,
LocalID: `与clientMsgId相同`,
ClientMsgId: `时间戳左移4位随后补上4位随机数`
}
} |
395 |
396 | 返回数据(JSON):
397 | ```
398 | {
399 | "BaseResponse": {
400 | "Ret": 0,
401 | "ErrMsg": ""
402 | },
403 | ...
404 | }
405 | ```
406 |
407 | | API | webwxrevokemsg |
408 | | --- | ------------ |
409 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg |
410 | | method | POST |
411 | | data | JSON |
412 | | header | ContentType: application/json; charset=UTF-8 |
413 | | params | {
BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
SvrMsgId: msg_id,
ToUserName: user_id,
ClientMsgId: local_msg_id
} |
414 |
415 | 返回数据(JSON):
416 | ```
417 | {
418 | "BaseResponse": {
419 | "Ret": 0,
420 | "ErrMsg": ""
421 | }
422 | }
423 | ```
424 |
425 | #### 发送表情
426 |
427 | | API | webwxsendmsgemotion |
428 | | --- | ------------ |
429 | | url | https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=xxx |
430 | | method | POST |
431 | | data | JSON |
432 | | header | ContentType: application/json; charset=UTF-8 |
433 | | params | {
BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
Msg: {
Type: 47 `emoji消息`,
EmojiFlag: 2,
MediaId: `表情上传后的媒体ID`,
FromUserName: `自己ID`,
ToUserName: `好友ID`,
LocalID: `与clientMsgId相同`,
ClientMsgId: `时间戳左移4位随后补上4位随机数`
}
} |
434 |
435 |
436 |
437 | ### 图片接口
438 |
439 | | API | webwxgeticon |
440 | | --- | ------------ |
441 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgeticon |
442 | | method | GET |
443 | | params | **seq**: `数字,可为空`
**username**: `ID`
**skey**: xxx |
444 |
445 |
446 | | API | webwxgetheadimg |
447 | | --- | --------------- |
448 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetheadimg |
449 | | method | GET |
450 | | params | **seq**: `数字,可为空`
**username**: `群ID`
**skey**: xxx |
451 |
452 |
453 | | API | webwxgetmsgimg |
454 | | --- | --------------- |
455 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg |
456 | | method | GET |
457 | | params | **MsgID**: `消息ID`
**type**: slave `略缩图` or `为空时加载原图`
**skey**: xxx |
458 |
459 |
460 | ### 多媒体接口
461 |
462 | | API | webwxgetvideo |
463 | | --- | --------------- |
464 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo |
465 | | method | GET |
466 | | params | **msgid**: `消息ID`
**skey**: xxx |
467 |
468 |
469 | | API | webwxgetvoice |
470 | | --- | --------------- |
471 | | url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice |
472 | | method | GET |
473 | | params | **msgid**: `消息ID`
**skey**: xxx |
474 |
475 |
476 | ### 账号类型
477 |
478 | | 类型 | 说明 |
479 | | :--: | --- |
480 | | 个人账号 | 以`@`开头,例如:`@xxx` |
481 | | 群聊 | 以`@@`开头,例如:`@@xxx` |
482 | | 公众号/服务号 | 以`@`开头,但其`VerifyFlag` & 8 != 0
`VerifyFlag`:
一般个人公众号/服务号:8
一般企业的服务号:24
微信官方账号`微信团队`:56 |
483 | | 特殊账号 | 像文件传输助手之类的账号,有特殊的ID,目前已知的有:
`filehelper`, `newsapp`, `fmessage`, `weibo`, `qqmail`, `tmessage`, `qmessage`, `qqsync`, `floatbottle`, `lbsapp`, `shakeapp`, `medianote`, `qqfriend`, `readerapp`, `blogapp`, `facebookapp`, `masssendapp`, `meishiapp`, `feedsapp`, `voip`, `blogappweixin`, `weixin`, `brandsessionholder`, `weixinreminder`, `officialaccounts`, `notification_messages`, `wxitil`, `userexperience_alarm`, `notification_messages` |
484 |
485 |
486 | ### 消息类型
487 |
488 | 消息一般格式:
489 | ```
490 | {
491 | "FromUserName": "",
492 | "ToUserName": "",
493 | "Content": "",
494 | "StatusNotifyUserName": "",
495 | "ImgWidth": 0,
496 | "PlayLength": 0,
497 | "RecommendInfo": {...},
498 | "StatusNotifyCode": 4,
499 | "NewMsgId": "",
500 | "Status": 3,
501 | "VoiceLength": 0,
502 | "ForwardFlag": 0,
503 | "AppMsgType": 0,
504 | "Ticket": "",
505 | "AppInfo": {...},
506 | "Url": "",
507 | "ImgStatus": 1,
508 | "MsgType": 1,
509 | "ImgHeight": 0,
510 | "MediaId": "",
511 | "MsgId": "",
512 | "FileName": "",
513 | "HasProductId": 0,
514 | "FileSize": "",
515 | "CreateTime": 1454602196,
516 | "SubMsgType": 0
517 | }
518 | ```
519 |
520 |
521 | | MsgType | 说明 |
522 | | ------- | --- |
523 | | 1 | 文本消息 |
524 | | 3 | 图片消息 |
525 | | 34 | 语音消息 |
526 | | 37 | 好友确认消息 |
527 | | 40 | POSSIBLEFRIEND_MSG |
528 | | 42 | 共享名片 |
529 | | 43 | 视频消息 |
530 | | 47 | 动画表情 |
531 | | 48 | 位置消息 |
532 | | 49 | 分享链接 |
533 | | 50 | VOIPMSG |
534 | | 51 | 微信初始化消息 |
535 | | 52 | VOIPNOTIFY |
536 | | 53 | VOIPINVITE |
537 | | 62 | 小视频 |
538 | | 9999 | SYSNOTICE |
539 | | 10000 | 系统消息 |
540 | | 10002 | 撤回消息 |
541 |
542 |
543 | **微信初始化消息**
544 | ```html
545 | MsgType: 51
546 | FromUserName: 自己ID
547 | ToUserName: 自己ID
548 | StatusNotifyUserName: 最近联系的联系人ID
549 | Content:
550 |
551 |
552 |
553 | // 最近联系的联系人
554 | filehelper,xxx@chatroom,wxid_xxx,xxx,...
555 |
556 |
557 |
558 |
559 | // 朋友圈
560 | MomentsUnreadMsgStatus
561 |
562 |
563 | 1454502365
564 |
565 |
566 |
567 |
568 | // 未读的功能账号消息,群发助手,漂流瓶等
569 |
570 |
571 |
572 | ```
573 |
574 | **文本消息**
575 | ```
576 | MsgType: 1
577 | FromUserName: 发送方ID
578 | ToUserName: 接收方ID
579 | Content: 消息内容
580 | ```
581 |
582 | **图片消息**
583 | ```html
584 | MsgType: 3
585 | FromUserName: 发送方ID
586 | ToUserName: 接收方ID
587 | MsgId: 用于获取图片
588 | Content:
589 |
590 |
591 |
592 |
593 | ```
594 |
595 | **小视频消息**
596 | ```html
597 | MsgType: 62
598 | FromUserName: 发送方ID
599 | ToUserName: 接收方ID
600 | MsgId: 用于获取小视频
601 | Content:
602 |
603 |
604 |
605 |
606 | ```
607 |
608 | **地理位置消息**
609 | ```
610 | MsgType: 1
611 | FromUserName: 发送方ID
612 | ToUserName: 接收方ID
613 | Content: http://weixin.qq.com/cgi-bin/redirectforward?args=xxx
614 | // 属于文本消息,只不过内容是一个跳转到地图的链接
615 | ```
616 |
617 | **名片消息**
618 | ```js
619 | MsgType: 42
620 | FromUserName: 发送方ID
621 | ToUserName: 接收方ID
622 | Content:
623 |
624 |
625 |
626 | RecommendInfo:
627 | {
628 | "UserName": "xxx", // ID
629 | "Province": "xxx",
630 | "City": "xxx",
631 | "Scene": 17,
632 | "QQNum": 0,
633 | "Content": "",
634 | "Alias": "xxx", // 微信号
635 | "OpCode": 0,
636 | "Signature": "",
637 | "Ticket": "",
638 | "Sex": 0, // 1:男, 2:女
639 | "NickName": "xxx", // 昵称
640 | "AttrStatus": 4293221,
641 | "VerifyFlag": 0
642 | }
643 | ```
644 |
645 | **语音消息**
646 | ```html
647 | MsgType: 34
648 | FromUserName: 发送方ID
649 | ToUserName: 接收方ID
650 | MsgId: 用于获取语音
651 | Content:
652 |
653 |
654 |
655 | ```
656 |
657 | **动画表情**
658 | ```html
659 | MsgType: 47
660 | FromUserName: 发送方ID
661 | ToUserName: 接收方ID
662 | Content:
663 |
664 |
665 |
666 |
667 | ```
668 |
669 | **普通链接或应用分享消息**
670 | ```html
671 | MsgType: 49
672 | AppMsgType: 5
673 | FromUserName: 发送方ID
674 | ToUserName: 接收方ID
675 | Url: 链接地址
676 | FileName: 链接标题
677 | Content:
678 |
679 |
680 |
681 |
682 | 5
683 |
684 |
685 |
686 | ...
687 |
688 |
689 |
690 |
691 |
692 |
693 | ```
694 |
695 | **音乐链接消息**
696 | ```html
697 | MsgType: 49
698 | AppMsgType: 3
699 | FromUserName: 发送方ID
700 | ToUserName: 接收方ID
701 | Url: 链接地址
702 | FileName: 音乐名
703 |
704 | AppInfo: // 分享链接的应用
705 | {
706 | Type: 0,
707 | AppID: wx485a97c844086dc9
708 | }
709 |
710 | Content:
711 |
712 |
713 |
714 |
715 |
716 | 3
717 | 0
718 |
719 |
720 |
721 |
722 | 0
723 |
724 |
725 |
726 | http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46
727 |
728 |
729 | http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46
730 |
731 |
732 | 0
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 | http://imgcache.qq.com/music/photo/album/63/180_albumpic_143163_0.jpg
743 |
744 |
745 |
746 |
747 | 0
748 |
749 | 29
750 | 摇一摇搜歌
751 |
752 |
753 |
754 | ```
755 |
756 | **群消息**
757 | ```
758 | MsgType: 1
759 | FromUserName: @@xxx
760 | ToUserName: @xxx
761 | Content:
762 | @xxx:
xxx
763 | ```
764 |
765 | **红包消息**
766 | ```
767 | MsgType: 49
768 | AppMsgType: 2001
769 | FromUserName: 发送方ID
770 | ToUserName: 接收方ID
771 | Content: 未知
772 | ```
773 | 注:根据网页版的代码可以看到未来可能支持查看红包消息,但目前走的是系统消息,见下。
774 |
775 | **系统消息**
776 | ```
777 | MsgType: 10000
778 | FromUserName: 发送方ID
779 | ToUserName: 自己ID
780 | Content:
781 | "你已添加了 xxx ,现在可以开始聊天了。"
782 | "如果陌生人主动添加你为朋友,请谨慎核实对方身份。"
783 | "收到红包,请在手机上查看"
784 | ```
785 |
786 |
787 | ## Discussion Group
788 | 如果你希望和 WeixinBot 的其他开发者交流,或者有什么问题和建议,欢迎大家加入微信群【Youth fed the dog】一起讨论。扫描下面的二维码添加机器人为好友,并回复【Aidog】获取入群链接。
789 |
790 |
791 |

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