Full Code of Urinx/WeixinBot for AI

master d9edcd2c9203 cached
28 files
197.5 KB
52.3k tokens
211 symbols
1 requests
Download .txt
Showing preview only (207K chars total). Download the full file or copy to clipboard to get everything.
Repository: Urinx/WeixinBot
Branch: master
Commit: d9edcd2c9203
Files: 28
Total size: 197.5 KB

Directory structure:
gitextract_stqn26qf/

├── .gitignore
├── LICENSE
├── README.md
├── wxbot_demo_py3/
│   ├── requirements.txt
│   └── weixin.py
└── wxbot_project_py2.7/
    ├── README.md
    ├── config/
    │   ├── __init__.py
    │   ├── config_manager.py
    │   ├── constant.py
    │   ├── log.py
    │   ├── requirements.txt
    │   └── wechat.conf.bak
    ├── db/
    │   ├── __init__.py
    │   ├── mysql_db.py
    │   └── sqlite_db.py
    ├── docker/
    │   ├── Dockerfile
    │   └── README.md
    ├── flask_templates/
    │   ├── index.html
    │   └── upload.html
    ├── wechat/
    │   ├── __init__.py
    │   ├── utils.py
    │   ├── wechat.py
    │   └── wechat_apis.py
    ├── weixin_bot.py
    └── wx_handler/
        ├── __init__.py
        ├── bot.py
        ├── sendgrid_mail.py
        └── wechat_msg_processor.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# IPython Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# dotenv
.env

# virtualenv
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

# pycharm
.idea

*.pyc
wxbot_demo_py3/saved/
wxbot_project_py2.7/tmp_data/
wxbot_project_py2.7/wechat/wechat_js_backup/
wxbot_project_py2.7/config/wechat.conf
wxbot_project_py2.7/wiki/


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# WeixinBot [![star this repo](http://github-svg-buttons.herokuapp.com/star.svg?user=Urinx&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/Urinx/WeixinBot) [![fork this repo](http://github-svg-buttons.herokuapp.com/fork.svg?user=Urinx&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/Urinx/WeixinBot/fork) ![python](https://img.shields.io/badge/python-2.7%20&%203.6-ff69b4.svg)

网页版微信API,包含终端版微信及微信机器人

## Contents
* [Demo](#Demo)
* [Web Weixin Pipeline](#Web-Weixin-Pipeline)
* [Web Weixin API](#Web-Weixin-API)
* [Discussion Group](#Discussion-Group)
* [Recent Update](#Recent-Update)

## <a name="Demo">Demo</a>
为了确保能正常运行示例脚本,请安装所需的第三方包。

```
pip install -r requirements.txt
```

注:下面演示的图片与功能可能不是最新的,具体请看源码。

<div align=center>
<img src="imgs/1.png" width="500" height="550"/>
</div>

按照操作指示在手机微信上扫描二维码然后登录,你可以选择是否开启自动回复模式。

![2](imgs/2.png)

开启自动回复模式后,如果接收到的是文字消息就会自动回复,包括群消息。

![3](imgs/3.png)

名片,链接,动画表情和地址位置消息。

![4](imgs/4.png)

![5](imgs/5.png)

网页版上有的功能目前基本上都能支持。

## <a name="Web-Weixin-Pipeline">Web Weixin Pipeline</a>

```
       +--------------+     +---------------+   +---------------+
       |              |     |               |   |               |
       |   Get UUID   |     |  Get Contact  |   | Status Notify |
       |              |     |               |   |               |
       +-------+------+     +-------^-------+   +-------^-------+
               |                    |                   |
               |                    +-------+  +--------+
               |                            |  |
       +-------v------+               +-----+--+------+      +--------------+
       |              |               |               |      |              |
       |  Get QRCode  |               |  Weixin Init  +------>  Sync Check  <----+
       |              |               |               |      |              |    |
       +-------+------+               +-------^-------+      +-------+------+    |
               |                              |                      |           |
               |                              |                      +-----------+
               |                              |                      |
       +-------v------+               +-------+--------+     +-------v-------+
       |              | Confirm Login |                |     |               |
+------>    Login     +---------------> New Login Page |     |  Weixin Sync  |
|      |              |               |                |     |               |
|      +------+-------+               +----------------+     +---------------+
|             |
|QRCode Scaned|
+-------------+
```


## <a name="Web-Weixin-API">Web Weixin API</a>

### 登录

| API | 获取 UUID |
| --- | --------- |
| url | https://login.weixin.qq.com/jslogin |
| method | POST |
| data | URL Encode |
| params | **appid**: `应用ID` <br> **fun**: new `应用类型` <br> **lang**: zh\_CN `语言` <br> **_**: `时间戳` |

返回数据(String):
```
window.QRLogin.code = 200; window.QRLogin.uuid = "xxx"
```
> 注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是`wx782c26e4c19acffb`,在微信客户端上显示为应用名称为`Web微信`;现在用的是`wxeb7ec651dd0aefa9`,显示名称为`微信网页版`。

<div align=center>
<img src="imgs/8.jpg" width="320" height="211"/>
</div>

<br>

| API | 绑定登陆(webwxpushloginurl) |
| --- | --------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxpushloginurl |
| method | GET |
| params | **uin**: xxx |

返回数据(String):
```
{'msg': 'all ok', 'uuid': 'xxx', 'ret': '0'}

通过这种方式可以省掉扫二维码这步操作,更加方便
```
<br>

| API | 生成二维码 |
| --- | --------- |
| url | https://login.weixin.qq.com/l/ `uuid` |
| method | GET |
<br>

| API | 二维码扫描登录 |
| --- | --------- |
| url | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login |
| method | GET |
| params | **tip**: 1 `未扫描` 0 `已扫描` <br> **uuid**: xxx <br> **_**: `时间戳` |

返回数据(String):
```
window.code=xxx;

xxx:
	408 登陆超时
	201 扫描成功
	200 确认登录

当返回200时,还会有
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx";
```
<br>

| API | webwxnewloginpage |
| --- | --------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage |
| method | GET |
| params | **ticket**: xxx <br> **uuid**: xxx <br> **lang**: zh_CN `语言` <br> **scan**: xxx <br> **fun**: new |

返回数据(XML):
```
<error>
	<ret>0</ret>
	<message>OK</message>
	<skey>xxx</skey>
	<wxsid>xxx</wxsid>
	<wxuin>xxx</wxuin>
	<pass_ticket>xxx</pass_ticket>
	<isgrayscale>1</isgrayscale>
</error>
```
<br>

### 微信初始化

| API | webwxinit |
| --- | --------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket=xxx&skey=xxx&r=xxx |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |
| params | { <br> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	Uin: xxx, <br>	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Sid: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;	Skey: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DeviceID: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp; } <br> } |

返回数据(JSON):
```
{
	"BaseResponse": {
		"Ret": 0,
		"ErrMsg": ""
	},
	"Count": 11,
	"ContactList": [...],
	"SyncKey": {
		"Count": 4,
		"List": [
			{
				"Key": 1,
				"Val": 635705559
			},
			...
		]
	},
	"User": {
		"Uin": xxx,
		"UserName": xxx,
		"NickName": xxx,
		"HeadImgUrl": xxx,
		"RemarkName": "",
		"PYInitial": "",
		"PYQuanPin": "",
		"RemarkPYInitial": "",
		"RemarkPYQuanPin": "",
		"HideInputBarFlag": 0,
		"StarFriend": 0,
		"Sex": 1,
		"Signature": "Apt-get install B",
		"AppAccountFlag": 0,
		"VerifyFlag": 0,
		"ContactFlag": 0,
		"WebWxPluginSwitch": 0,
		"HeadImgFlag": 1,
		"SnsFlag": 17
	},
	"ChatSet": xxx,
	"SKey": xxx,
	"ClientVersion": 369297683,
	"SystemTime": 1453124908,
	"GrayScale": 1,
	"InviteStartCount": 40,
	"MPSubscribeMsgCount": 2,
	"MPSubscribeMsgList": [...],
	"ClickReportInterval": 600000
}
```
<br>

| API | webwxstatusnotify |
| --- | --------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify?lang=zh_CN&pass_ticket=xxx |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |
| params | { <br> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Code: 3, <br> &nbsp;&nbsp;&nbsp;&nbsp; FromUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp; ToUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp; ClientMsgId: `时间戳` <br> } |

返回数据(JSON):
```
{
	"BaseResponse": {
		"Ret": 0,
		"ErrMsg": ""
	},
	...
}
```
<br>

### 获取联系人信息

| API | webwxgetcontact |
| --- | --------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin//webwxgetcontact?pass_ticket=xxx&skey=xxx&r=xxx |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |

返回数据(JSON):
```
{
	"BaseResponse": {
		"Ret": 0,
		"ErrMsg": ""
	},
	"MemberCount": 334,
	"MemberList": [
		{
			"Uin": 0,
			"UserName": xxx,
			"NickName": "Urinx",
			"HeadImgUrl": xxx,
			"ContactFlag": 3,
			"MemberCount": 0,
			"MemberList": [],
			"RemarkName": "",
			"HideInputBarFlag": 0,
			"Sex": 0,
			"Signature": "你好,我们是地球三体组织。在这里,你将感受到不一样的思维模式,以及颠覆常规的世界观。而我们的目标,就是以三体人的智慧,引领人类未来科学技术500年。",
			"VerifyFlag": 8,
			"OwnerUin": 0,
			"PYInitial": "URINX",
			"PYQuanPin": "Urinx",
			"RemarkPYInitial": "",
			"RemarkPYQuanPin": "",
			"StarFriend": 0,
			"AppAccountFlag": 0,
			"Statues": 0,
			"AttrStatus": 0,
			"Province": "",
			"City": "",
			"Alias": "Urinxs",
			"SnsFlag": 0,
			"UniFriend": 0,
			"DisplayName": "",
			"ChatRoomId": 0,
			"KeyWord": "gh_",
			"EncryChatRoomId": ""
		},
		...
	],
	"Seq": 0
}
```
<br>

| API | webwxbatchgetcontact |
| --- | --------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=xxx&pass_ticket=xxx |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |
| params | { <br> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Count: `群数量`, <br> &nbsp;&nbsp;&nbsp;&nbsp; List: [ <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { UserName: `群ID`, EncryChatRoomId: "" }, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ... <br> &nbsp;&nbsp;&nbsp;&nbsp; ], <br> } |

返回数据(JSON)同上
<br><br>

### 同步刷新

| API | synccheck |
| --- | --------- |
| protocol | https |
| host | webpush.weixin.qq.com <br> webpush.wx2.qq.com <br> webpush.wx8.qq.com <br> webpush.wx.qq.com <br> webpush.web2.wechat.com <br> webpush.web.wechat.com |
| path | /cgi-bin/mmwebwx-bin/synccheck |
| method | GET |
| data | URL Encode |
| params | **r**: `时间戳` <br> **sid**: xxx <br> **uin**: xxx <br> **skey**: xxx <br> **deviceid**: xxx <br> **synckey**: xxx <br> **_**: `时间戳` |

返回数据(String):
```
window.synccheck={retcode:"xxx",selector:"xxx"}

retcode:
	0 正常
	1100 失败/登出微信
selector:
	0 正常
	2 新的消息
	7 进入/离开聊天界面
```
<br>

| API | webwxsync |
| --- | --------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=xxx&skey=xxx&pass_ticket=xxx |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |
| params | { <br> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; SyncKey: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp; rr: `时间戳取反` <br> } |

返回数据(JSON):
```
{
	'BaseResponse': {'ErrMsg': '', 'Ret': 0},
	'SyncKey': {
		'Count': 7,
		'List': [
			{'Val': 636214192, 'Key': 1},
			...
		]
	},
	'ContinueFlag': 0,
	'AddMsgCount': 1,
	'AddMsgList': [
		{
			'FromUserName': '',
			'PlayLength': 0,
			'RecommendInfo': {...},
			'Content': "", 
			'StatusNotifyUserName': '',
			'StatusNotifyCode': 5,
			'Status': 3,
			'VoiceLength': 0,
			'ToUserName': '',
			'ForwardFlag': 0,
			'AppMsgType': 0,
			'AppInfo': {'Type': 0, 'AppID': ''},
			'Url': '',
			'ImgStatus': 1,
			'MsgType': 51,
			'ImgHeight': 0,
			'MediaId': '', 
			'FileName': '',
			'FileSize': '',
			...
		},
		...
	],
	'ModChatRoomMemberCount': 0,
	'ModContactList': [],
	'DelContactList': [],
	'ModChatRoomMemberList': [],
	'DelContactCount': 0,
	...
}
```
<br>

### 消息接口

| API | webwxsendmsg |
| --- | ------------ |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=xxx |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |
| params | { <br> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Msg: { <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: 1 `文字消息`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Content: `要发送的消息`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FromUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName: `好友ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LocalID: `与clientMsgId相同`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientMsgId: `时间戳左移4位随后补上4位随机数` <br> &nbsp;&nbsp;&nbsp;&nbsp; } <br> } |

返回数据(JSON):
```
{
	"BaseResponse": {
		"Ret": 0,
		"ErrMsg": ""
	},
	...
}
```

| API | webwxrevokemsg |
| --- | ------------ |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |
| params | { <br> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; SvrMsgId: msg_id, <br> &nbsp;&nbsp;&nbsp;&nbsp; ToUserName: user_id, <br> &nbsp;&nbsp;&nbsp;&nbsp; ClientMsgId: local_msg_id <br>  } |

返回数据(JSON):
```
{
	"BaseResponse": {
		"Ret": 0,
		"ErrMsg": ""
	}
}
```

#### 发送表情

| API | webwxsendmsgemotion |
| --- | ------------ |
| url | https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=xxx |
| method | POST |
| data | JSON |
| header | ContentType: application/json; charset=UTF-8 |
| params | { <br> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Msg: { <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: 47 `emoji消息`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; EmojiFlag: 2, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MediaId: `表情上传后的媒体ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FromUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName: `好友ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LocalID: `与clientMsgId相同`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientMsgId: `时间戳左移4位随后补上4位随机数` <br> &nbsp;&nbsp;&nbsp;&nbsp; } <br> } |

<br>

### 图片接口

| API | webwxgeticon |
| --- | ------------ |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgeticon |
| method | GET |
| params | **seq**: `数字,可为空` <br> **username**: `ID` <br> **skey**: xxx |
<br>

| API | webwxgetheadimg |
| --- | --------------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetheadimg |
| method | GET |
| params | **seq**: `数字,可为空` <br> **username**: `群ID` <br> **skey**: xxx |
<br>

| API | webwxgetmsgimg |
| --- | --------------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg |
| method | GET |
| params | **MsgID**: `消息ID` <br> **type**: slave `略缩图` or `为空时加载原图` <br> **skey**: xxx |
<br>

### 多媒体接口

| API | webwxgetvideo |
| --- | --------------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo |
| method | GET |
| params | **msgid**: `消息ID` <br> **skey**: xxx |
<br>

| API | webwxgetvoice |
| --- | --------------- |
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice |
| method | GET |
| params | **msgid**: `消息ID` <br> **skey**: xxx |
<br>

### 账号类型

| 类型 | 说明 |
| :--: | --- |
| 个人账号 | 以`@`开头,例如:`@xxx` |
| 群聊 | 以`@@`开头,例如:`@@xxx` |
| 公众号/服务号 | 以`@`开头,但其`VerifyFlag` & 8 != 0 <br><br> `VerifyFlag`: <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 一般个人公众号/服务号:8 <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 一般企业的服务号:24 <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 微信官方账号`微信团队`:56 |
| 特殊账号 | 像文件传输助手之类的账号,有特殊的ID,目前已知的有:<br> `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` |
<br>

### 消息类型

消息一般格式:
```
{
	"FromUserName": "",
	"ToUserName": "",
	"Content": "",
	"StatusNotifyUserName": "",
	"ImgWidth": 0,
	"PlayLength": 0,
	"RecommendInfo": {...},
	"StatusNotifyCode": 4,
	"NewMsgId": "",
	"Status": 3,
	"VoiceLength": 0,
	"ForwardFlag": 0,
	"AppMsgType": 0,
	"Ticket": "",
	"AppInfo": {...},
	"Url": "",
	"ImgStatus": 1,
	"MsgType": 1,
	"ImgHeight": 0,
	"MediaId": "",
	"MsgId": "",
	"FileName": "",
	"HasProductId": 0,
	"FileSize": "",
	"CreateTime": 1454602196,
	"SubMsgType": 0
}
```
<br>

| MsgType | 说明 |
| ------- | --- |
| 1  | 文本消息 |
| 3  | 图片消息 |
| 34 | 语音消息 |
| 37 | 好友确认消息 |
| 40 | POSSIBLEFRIEND_MSG |
| 42 | 共享名片 |
| 43 | 视频消息 |
| 47 | 动画表情 |
| 48 | 位置消息 |
| 49 | 分享链接 |
| 50 | VOIPMSG |
| 51 | 微信初始化消息 |
| 52 | VOIPNOTIFY |
| 53 | VOIPINVITE |
| 62 | 小视频 |
| 9999 | SYSNOTICE |
| 10000 | 系统消息 |
| 10002 | 撤回消息 |
<br>

**微信初始化消息**
```html
MsgType: 51
FromUserName: 自己ID
ToUserName: 自己ID
StatusNotifyUserName: 最近联系的联系人ID
Content:
	<msg>
	    <op id='4'>
	        <username>
	        	// 最近联系的联系人
	            filehelper,xxx@chatroom,wxid_xxx,xxx,...
	        </username>
	        <unreadchatlist>
	            <chat>
	                <username>
	                	// 朋友圈
	                    MomentsUnreadMsgStatus
	                </username>
	                <lastreadtime>
	                    1454502365
	                </lastreadtime>
	            </chat>
	        </unreadchatlist>
	        <unreadfunctionlist>
	        	// 未读的功能账号消息,群发助手,漂流瓶等
	        </unreadfunctionlist>
	    </op>
	</msg>
```

**文本消息**
```
MsgType: 1
FromUserName: 发送方ID
ToUserName: 接收方ID
Content: 消息内容
```

**图片消息**
```html
MsgType: 3
FromUserName: 发送方ID
ToUserName: 接收方ID
MsgId: 用于获取图片
Content:
	<msg>
		<img length="6503" hdlength="0" />
		<commenturl></commenturl>
	</msg>
```

**小视频消息**
```html
MsgType: 62
FromUserName: 发送方ID
ToUserName: 接收方ID
MsgId: 用于获取小视频
Content:
	<msg>
		<img length="6503" hdlength="0" />
		<commenturl></commenturl>
	</msg>
```

**地理位置消息**
```
MsgType: 1
FromUserName: 发送方ID
ToUserName: 接收方ID
Content: http://weixin.qq.com/cgi-bin/redirectforward?args=xxx
// 属于文本消息,只不过内容是一个跳转到地图的链接
```

**名片消息**
```js
MsgType: 42
FromUserName: 发送方ID
ToUserName: 接收方ID
Content:
	<?xml version="1.0"?>
	<msg bigheadimgurl="" smallheadimgurl="" username="" nickname=""  shortpy="" alias="" imagestatus="3" scene="17" province="" city="" sign="" sex="1" certflag="0" certinfo="" brandIconUrl="" brandHomeUrl="" brandSubscriptConfigUrl="" brandFlags="0" regionCode="" />

RecommendInfo:
	{
		"UserName": "xxx", // ID
		"Province": "xxx", 
		"City": "xxx", 
		"Scene": 17, 
		"QQNum": 0, 
		"Content": "", 
		"Alias": "xxx", // 微信号
		"OpCode": 0, 
		"Signature": "", 
		"Ticket": "", 
		"Sex": 0, // 1:男, 2:女
		"NickName": "xxx", // 昵称
		"AttrStatus": 4293221, 
		"VerifyFlag": 0
	}
```

**语音消息**
```html
MsgType: 34
FromUserName: 发送方ID
ToUserName: 接收方ID
MsgId: 用于获取语音
Content:
	<msg>
		<voicemsg endflag="1" cancelflag="0" forwardflag="0" voiceformat="4" voicelength="1580" length="2026" bufid="216825389722501519" clientmsgid="49efec63a9774a65a932a4e5fcd4e923filehelper174_1454602489" fromusername="" />
	</msg>
```

**动画表情**
```html
MsgType: 47
FromUserName: 发送方ID
ToUserName: 接收方ID
Content:
	<msg>
		<emoji fromusername = "" tousername = "" type="2" idbuffer="media:0_0" md5="e68363487d8f0519c4e1047de403b2e7" len = "86235" productid="com.tencent.xin.emoticon.bilibili" androidmd5="e68363487d8f0519c4e1047de403b2e7" androidlen="86235" s60v3md5 = "e68363487d8f0519c4e1047de403b2e7" s60v3len="86235" s60v5md5 = "e68363487d8f0519c4e1047de403b2e7" s60v5len="86235" cdnurl = "http://emoji.qpic.cn/wx_emoji/eFygWtxcoMF8M0oCCsksMA0gplXAFQNpiaqsmOicbXl1OC4Tyx18SGsQ/" designerid = "" thumburl = "http://mmbiz.qpic.cn/mmemoticon/dx4Y70y9XctRJf6tKsy7FwWosxd4DAtItSfhKS0Czr56A70p8U5O8g/0" encrypturl = "http://emoji.qpic.cn/wx_emoji/UyYVK8GMlq5VnJ56a4GkKHAiaC266Y0me0KtW6JN2FAZcXiaFKccRevA/" aeskey= "a911cc2ec96ddb781b5ca85d24143642" ></emoji> 
		<gameext type="0" content="0" ></gameext>
	</msg>
```

**普通链接或应用分享消息**
```html
MsgType: 49
AppMsgType: 5
FromUserName: 发送方ID
ToUserName: 接收方ID
Url: 链接地址
FileName: 链接标题
Content:
	<msg>
		<appmsg appid=""  sdkver="0">
			<title></title>
			<des></des>
			<type>5</type>
			<content></content>
			<url></url>
			<thumburl></thumburl>
			...
		</appmsg>
		<appinfo>
			<version></version>
			<appname></appname>
		</appinfo>
	</msg>
```

**音乐链接消息**
```html
MsgType: 49
AppMsgType: 3
FromUserName: 发送方ID
ToUserName: 接收方ID
Url: 链接地址
FileName: 音乐名

AppInfo: // 分享链接的应用
	{
		Type: 0, 
		AppID: wx485a97c844086dc9
	}

Content:
	<msg>
		<appmsg appid="wx485a97c844086dc9"  sdkver="0">
			<title></title>
			<des></des>
			<action></action>
			<type>3</type>
			<showtype>0</showtype>
			<mediatagname></mediatagname>
			<messageext></messageext>
			<messageaction></messageaction>
			<content></content>
			<contentattr>0</contentattr>
			<url></url>
			<lowurl></lowurl>
			<dataurl>
				http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&amp;guid=ffffffffc104ea2964a111cf3ff3edaf&amp;fromtag=46
			</dataurl>
			<lowdataurl>
				http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&amp;guid=ffffffffc104ea2964a111cf3ff3edaf&amp;fromtag=46
			</lowdataurl>
			<appattach>
				<totallen>0</totallen>
				<attachid></attachid>
				<emoticonmd5></emoticonmd5>
				<fileext></fileext>
			</appattach>
			<extinfo></extinfo>
			<sourceusername></sourceusername>
			<sourcedisplayname></sourcedisplayname>
			<commenturl></commenturl>
			<thumburl>
				http://imgcache.qq.com/music/photo/album/63/180_albumpic_143163_0.jpg
			</thumburl>
			<md5></md5>
		</appmsg>
		<fromusername></fromusername>
		<scene>0</scene>
		<appinfo>
			<version>29</version>
			<appname>摇一摇搜歌</appname>
		</appinfo>
		<commenturl></commenturl>
	</msg>
```

**群消息**
```
MsgType: 1
FromUserName: @@xxx
ToUserName: @xxx
Content:
	@xxx:<br/>xxx
```

**红包消息**
```
MsgType: 49
AppMsgType: 2001
FromUserName: 发送方ID
ToUserName: 接收方ID
Content: 未知
```
注:根据网页版的代码可以看到未来可能支持查看红包消息,但目前走的是系统消息,见下。

**系统消息**
```
MsgType: 10000
FromUserName: 发送方ID
ToUserName: 自己ID
Content:
	"你已添加了 xxx ,现在可以开始聊天了。"
	"如果陌生人主动添加你为朋友,请谨慎核实对方身份。"
	"收到红包,请在手机上查看"
```


## <a name="Discussion-Group">Discussion Group</a>
如果你希望和 WeixinBot 的其他开发者交流,或者有什么问题和建议,欢迎大家加入微信群【Youth fed the dog】一起讨论。扫描下面的二维码添加机器人为好友,并回复【Aidog】获取入群链接。

<div align=center>
<img src="imgs/groupQrcode.jpg" width="220" height="220" alt="join us"/>
</div>

注:这个不是群的二维码,是机器人拉你入群,记得回复机器人【Aidog】哦~ (secret code: Aidog)

## <a name="Recent-Update">Recent Update</a>

- association_login
	目前网页版微信已经可以脱离扫码,但是依然需要在客户端进行确认登录。


================================================
FILE: wxbot_demo_py3/requirements.txt
================================================
colorama
coloredlogs
humanfriendly
lxml
qrcode
requests
six
requests_toolbelt
pyqrcode
certifi


================================================
FILE: wxbot_demo_py3/weixin.py
================================================
#!/usr/bin/env python
# coding: utf-8
import qrcode
from pyqrcode import QRCode
import urllib.request, urllib.parse, urllib.error
import urllib.request, urllib.error, urllib.parse
import http.cookiejar
import requests
import xml.dom.minidom
import json
import time
import ssl
import re
import sys
import os
import subprocess
import random
import multiprocessing
import platform
import logging
import http.client
from collections import defaultdict
from urllib.parse import urlparse
from lxml import html
from socket import timeout as timeout_error
#import pdb

# for media upload
import mimetypes
from requests_toolbelt.multipart.encoder import MultipartEncoder


def catchKeyboardInterrupt(fn):
    def wrapper(*args):
        try:
            return fn(*args)
        except KeyboardInterrupt:
            print('\n[*] 强制退出程序')
            logging.debug('[*] 强制退出程序')
    return wrapper


def _decode_list(data):
    rv = []
    for item in data:
        if isinstance(item, str):
            item = item.encode('utf-8')
        elif isinstance(item, list):
            item = _decode_list(item)
        elif isinstance(item, dict):
            item = _decode_dict(item)
        rv.append(item)
    return rv


def _decode_dict(data):
    rv = {}
    for key, value in data.items():
        if isinstance(key, str):
            key = key.encode('utf-8')
        if isinstance(value, str):
            value = value.encode('utf-8')
        elif isinstance(value, list):
            value = _decode_list(value)
        elif isinstance(value, dict):
            value = _decode_dict(value)
        rv[key] = value
    return rv


class WebWeixin(object):

    def __str__(self):
        description = \
            "=========================\n" + \
            "[#] Web Weixin\n" + \
            "[#] Debug Mode: " + str(self.DEBUG) + "\n" + \
            "[#] Uuid: " + self.uuid + "\n" + \
            "[#] Uin: " + str(self.uin) + "\n" + \
            "[#] Sid: " + self.sid + "\n" + \
            "[#] Skey: " + self.skey + "\n" + \
            "[#] DeviceId: " + self.deviceId + "\n" + \
            "[#] PassTicket: " + self.pass_ticket + "\n" + \
            "========================="
        return description

    def __init__(self):
        self.DEBUG = False
        self.commandLineQRCode = False
        self.uuid = ''
        self.base_uri = ''
        self.redirect_uri = ''
        self.uin = ''
        self.sid = ''
        self.skey = ''
        self.pass_ticket = ''
        self.deviceId = 'e' + repr(random.random())[2:17]
        self.BaseRequest = {}
        self.synckey = ''
        self.SyncKey = []
        self.User = []
        self.MemberList = []
        self.ContactList = []  # 好友
        self.GroupList = []  # 群
        self.GroupMemeberList = []  # 群友
        self.PublicUsersList = []  # 公众号/服务号
        self.SpecialUsersList = []  # 特殊账号
        self.autoReplyMode = False
        self.syncHost = ''
        self.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36'
        self.interactive = False
        self.autoOpen = False
        self.saveFolder = os.path.join(os.getcwd(), 'saved')
        self.saveSubFolders = {'webwxgeticon': 'icons', 'webwxgetheadimg': 'headimgs', 'webwxgetmsgimg': 'msgimgs',
                               'webwxgetvideo': 'videos', 'webwxgetvoice': 'voices', '_showQRCodeImg': 'qrcodes'}
        self.appid = 'wx782c26e4c19acffb'
        self.lang = 'zh_CN'
        self.lastCheckTs = time.time()
        self.memberCount = 0
        self.SpecialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp',
                             'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages']
        self.TimeOut = 20  # 同步最短时间间隔(单位:秒)
        self.media_count = -1

        self.cookie = http.cookiejar.CookieJar()
        opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookie))
        opener.addheaders = [('User-agent', self.user_agent)]
        urllib.request.install_opener(opener)

    def loadConfig(self, config):
        if config['DEBUG']:
            self.DEBUG = config['DEBUG']
        if config['autoReplyMode']:
            self.autoReplyMode = config['autoReplyMode']
        if config['user_agent']:
            self.user_agent = config['user_agent']
        if config['interactive']:
            self.interactive = config['interactive']
        if config['autoOpen']:
            self.autoOpen = config['autoOpen']

    def getUUID(self):
        url = 'https://login.weixin.qq.com/jslogin'
        params = {
            'appid': self.appid,
            'fun': 'new',
            'lang': self.lang,
            '_': int(time.time()),
        }
        #r = requests.get(url=url, params=params)
        #r.encoding = 'utf-8'
        #data = r.text
        data = self._post(url, params, False).decode("utf-8")
        if data == '':
            return False
        regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
        pm = re.search(regx, data)
        if pm:
            code = pm.group(1)
            self.uuid = pm.group(2)
            return code == '200'
        return False

    def genQRCode(self):
        #return self._showQRCodeImg()
        if sys.platform.startswith('win'):
            self._showQRCodeImg('win')
        elif sys.platform.find('darwin') >= 0:
            self._showQRCodeImg('macos')
        else:
            self._str2qr('https://login.weixin.qq.com/l/' + self.uuid)

    def _showQRCodeImg(self, str):
        if self.commandLineQRCode:
            qrCode = QRCode('https://login.weixin.qq.com/l/' + self.uuid)
            self._showCommandLineQRCode(qrCode.text(1))
        else:
            url = 'https://login.weixin.qq.com/qrcode/' + self.uuid
            params = {
                't': 'webwx',
                '_': int(time.time())
            }

            data = self._post(url, params, False)
            if data == '':
                return
            QRCODE_PATH = self._saveFile('qrcode.jpg', data, '_showQRCodeImg')
            if str == 'win':
                os.startfile(QRCODE_PATH)
            elif str == 'macos':
                subprocess.call(["open", QRCODE_PATH])
            else:
                return

    def _showCommandLineQRCode(self, qr_data, enableCmdQR=2):
        try:
            b = u'\u2588'
            sys.stdout.write(b + '\r')
            sys.stdout.flush()
        except UnicodeEncodeError:
            white = 'MM'
        else:
            white = b
        black = '  '
        blockCount = int(enableCmdQR)
        if abs(blockCount) == 0:
            blockCount = 1
        white *= abs(blockCount)
        if blockCount < 0:
            white, black = black, white
        sys.stdout.write(' ' * 50 + '\r')
        sys.stdout.flush()
        qr = qr_data.replace('0', white).replace('1', black)
        sys.stdout.write(qr)
        sys.stdout.flush()

    def waitForLogin(self, tip=1):
        time.sleep(tip)
        url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % (
            tip, self.uuid, int(time.time()))
        data = self._get(url)
        if data == '':
            return False
        pm = re.search(r"window.code=(\d+);", data)
        code = pm.group(1)

        if code == '201':
            return True
        elif code == '200':
            pm = re.search(r'window.redirect_uri="(\S+?)";', data)
            r_uri = pm.group(1) + '&fun=new'
            self.redirect_uri = r_uri
            self.base_uri = r_uri[:r_uri.rfind('/')]
            return True
        elif code == '408':
            self._echo('[登陆超时] \n')
        else:
            self._echo('[登陆异常] \n')
        return False

    def login(self):
        data = self._get(self.redirect_uri)
        if data == '':
            return False
        doc = xml.dom.minidom.parseString(data)
        root = doc.documentElement

        for node in root.childNodes:
            if node.nodeName == 'skey':
                self.skey = node.childNodes[0].data
            elif node.nodeName == 'wxsid':
                self.sid = node.childNodes[0].data
            elif node.nodeName == 'wxuin':
                self.uin = node.childNodes[0].data
            elif node.nodeName == 'pass_ticket':
                self.pass_ticket = node.childNodes[0].data

        if '' in (self.skey, self.sid, self.uin, self.pass_ticket):
            return False

        self.BaseRequest = {
            'Uin': int(self.uin),
            'Sid': self.sid,
            'Skey': self.skey,
            'DeviceID': self.deviceId,
        }
        return True

    def webwxinit(self):
        url = self.base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % (
            self.pass_ticket, self.skey, int(time.time()))
        params = {
            'BaseRequest': self.BaseRequest
        }
        dic = self._post(url, params)
        if dic == '':
            return False
        self.SyncKey = dic['SyncKey']
        self.User = dic['User']
        # synckey for synccheck
        self.synckey = '|'.join(
            [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']])

        return dic['BaseResponse']['Ret'] == 0

    def webwxstatusnotify(self):
        url = self.base_uri + \
            '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket)
        params = {
            'BaseRequest': self.BaseRequest,
            "Code": 3,
            "FromUserName": self.User['UserName'],
            "ToUserName": self.User['UserName'],
            "ClientMsgId": int(time.time())
        }
        dic = self._post(url, params)
        if dic == '':
            return False

        return dic['BaseResponse']['Ret'] == 0

    def webwxgetcontact(self):
        SpecialUsers = self.SpecialUsers
        url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (
            self.pass_ticket, self.skey, int(time.time()))
        dic = self._post(url, {})
        if dic == '':
            return False

        self.MemberCount = dic['MemberCount']
        self.MemberList = dic['MemberList']
        ContactList = self.MemberList[:]
        GroupList = self.GroupList[:]
        PublicUsersList = self.PublicUsersList[:]
        SpecialUsersList = self.SpecialUsersList[:]

        for i in range(len(ContactList) - 1, -1, -1):
            Contact = ContactList[i]
            if Contact['VerifyFlag'] & 8 != 0:  # 公众号/服务号
                ContactList.remove(Contact)
                self.PublicUsersList.append(Contact)
            elif Contact['UserName'] in SpecialUsers:  # 特殊账号
                ContactList.remove(Contact)
                self.SpecialUsersList.append(Contact)
            elif '@@' in Contact['UserName']:  # 群聊
                ContactList.remove(Contact)
                self.GroupList.append(Contact)
            elif Contact['UserName'] == self.User['UserName']:  # 自己
                ContactList.remove(Contact)
        self.ContactList = ContactList

        return True

    def webwxbatchgetcontact(self):
        url = self.base_uri + \
            '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (
                int(time.time()), self.pass_ticket)
        params = {
            'BaseRequest': self.BaseRequest,
            "Count": len(self.GroupList),
            "List": [{"UserName": g['UserName'], "EncryChatRoomId":""} for g in self.GroupList]
        }
        dic = self._post(url, params)
        if dic == '':
            return False

        # blabla ...
        ContactList = dic['ContactList']
        ContactCount = dic['Count']
        self.GroupList = ContactList

        for i in range(len(ContactList) - 1, -1, -1):
            Contact = ContactList[i]
            MemberList = Contact['MemberList']
            for member in MemberList:
                self.GroupMemeberList.append(member)
        return True

    def getNameById(self, id):
        url = self.base_uri + \
            '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (
                int(time.time()), self.pass_ticket)
        params = {
            'BaseRequest': self.BaseRequest,
            "Count": 1,
            "List": [{"UserName": id, "EncryChatRoomId": ""}]
        }
        dic = self._post(url, params)
        if dic == '':
            return None

        # blabla ...
        return dic['ContactList']

    def testsynccheck(self):
        SyncHost = ['wx2.qq.com',
                    'webpush.wx2.qq.com',
                    'wx8.qq.com',
                    'webpush.wx8.qq.com',
                    'qq.com',
                    'webpush.wx.qq.com',
                    'web2.wechat.com',
                    'webpush.web2.wechat.com',
                    'wechat.com',
                    'webpush.web.wechat.com',
                    'webpush.weixin.qq.com',
                    'webpush.wechat.com',
                    'webpush1.wechat.com',
                    'webpush2.wechat.com',
                    'webpush.wx.qq.com',
                    'webpush2.wx.qq.com']
        for host in SyncHost:
            self.syncHost = host
            [retcode, selector] = self.synccheck()
            if retcode == '0':
                return True
        return False

    def synccheck(self):
        params = {
            'r': int(time.time()),
            'sid': self.sid,
            'uin': self.uin,
            'skey': self.skey,
            'deviceid': self.deviceId,
            'synckey': self.synckey,
            '_': int(time.time()),
        }
        url = 'https://' + self.syncHost + '/cgi-bin/mmwebwx-bin/synccheck?' + urllib.parse.urlencode(params)
        data = self._get(url, timeout=5)
        if data == '':
            return [-1,-1]

        pm = re.search(
            r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data)
        retcode = pm.group(1)
        selector = pm.group(2)
        return [retcode, selector]

    def webwxsync(self):
        url = self.base_uri + \
            '/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (
                self.sid, self.skey, self.pass_ticket)
        params = {
            'BaseRequest': self.BaseRequest,
            'SyncKey': self.SyncKey,
            'rr': ~int(time.time())
        }
        dic = self._post(url, params)
        if dic == '':
            return None
        if self.DEBUG:
            print(json.dumps(dic, indent=4))
            (json.dumps(dic, indent=4))

        if dic['BaseResponse']['Ret'] == 0:
            self.SyncKey = dic['SyncKey']
            self.synckey = '|'.join(
                [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']])
        return dic

    def webwxsendmsg(self, word, to='filehelper'):
        url = self.base_uri + \
            '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket)
        clientMsgId = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        params = {
            'BaseRequest': self.BaseRequest,
            'Msg': {
                "Type": 1,
                "Content": self._transcoding(word),
                "FromUserName": self.User['UserName'],
                "ToUserName": to,
                "LocalID": clientMsgId,
                "ClientMsgId": clientMsgId
            }
        }
        headers = {'content-type': 'application/json; charset=UTF-8'}
        data = json.dumps(params, ensure_ascii=False).encode('utf8')
        r = requests.post(url, data=data, headers=headers)
        dic = r.json()
        return dic['BaseResponse']['Ret'] == 0

    def webwxuploadmedia(self, image_name):
        url = 'https://file2.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json'
        # 计数器
        self.media_count = self.media_count + 1
        # 文件名
        file_name = image_name
        # MIME格式
        # mime_type = application/pdf, image/jpeg, image/png, etc.
        mime_type = mimetypes.guess_type(image_name, strict=False)[0]
        # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc
        # pic格式,直接显示。doc格式则显示为文件。
        media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc'
        # 上一次修改日期
        lastModifieDate = 'Thu Mar 17 2016 00:55:10 GMT+0800 (CST)'
        # 文件大小
        file_size = os.path.getsize(file_name)
        # PassTicket
        pass_ticket = self.pass_ticket
        # clientMediaId
        client_media_id = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        # webwx_data_ticket
        webwx_data_ticket = ''
        for item in self.cookie:
            if item.name == 'webwx_data_ticket':
                webwx_data_ticket = item.value
                break
        if (webwx_data_ticket == ''):
            return "None Fuck Cookie"

        uploadmediarequest = json.dumps({
            "BaseRequest": self.BaseRequest,
            "ClientMediaId": client_media_id,
            "TotalLen": file_size,
            "StartPos": 0,
            "DataLen": file_size,
            "MediaType": 4
        }, ensure_ascii=False).encode('utf8')

        multipart_encoder = MultipartEncoder(
            fields={
                'id': 'WU_FILE_' + str(self.media_count),
                'name': file_name,
                'type': mime_type,
                'lastModifieDate': lastModifieDate,
                'size': str(file_size),
                'mediatype': media_type,
                'uploadmediarequest': uploadmediarequest,
                'webwx_data_ticket': webwx_data_ticket,
                'pass_ticket': pass_ticket,
                'filename': (file_name, open(file_name, 'rb'), mime_type.split('/')[1])
            },
            boundary='-----------------------------1575017231431605357584454111'
        )

        headers = {
            'Host': 'file2.wx.qq.com',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'en-US,en;q=0.5',
            'Accept-Encoding': 'gzip, deflate',
            'Referer': 'https://wx2.qq.com/',
            'Content-Type': multipart_encoder.content_type,
            'Origin': 'https://wx2.qq.com',
            'Connection': 'keep-alive',
            'Pragma': 'no-cache',
            'Cache-Control': 'no-cache'
        }

        r = requests.post(url, data=multipart_encoder, headers=headers)
        response_json = r.json()
        if response_json['BaseResponse']['Ret'] == 0:
            return response_json
        return None

    def webwxsendmsgimg(self, user_id, media_id):
        url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s' % self.pass_ticket
        clientMsgId = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        data_json = {
            "BaseRequest": self.BaseRequest,
            "Msg": {
                "Type": 3,
                "MediaId": media_id,
                "FromUserName": self.User['UserName'],
                "ToUserName": user_id,
                "LocalID": clientMsgId,
                "ClientMsgId": clientMsgId
            }
        }
        headers = {'content-type': 'application/json; charset=UTF-8'}
        data = json.dumps(data_json, ensure_ascii=False).encode('utf8')
        r = requests.post(url, data=data, headers=headers)
        dic = r.json()
        return dic['BaseResponse']['Ret'] == 0

    def webwxsendmsgemotion(self, user_id, media_id):
        url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket
        clientMsgId = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        data_json = {
            "BaseRequest": self.BaseRequest,
            "Msg": {
                "Type": 47,
                "EmojiFlag": 2,
                "MediaId": media_id,
                "FromUserName": self.User['UserName'],
                "ToUserName": user_id,
                "LocalID": clientMsgId,
                "ClientMsgId": clientMsgId
            }
        }
        headers = {'content-type': 'application/json; charset=UTF-8'}
        data = json.dumps(data_json, ensure_ascii=False).encode('utf8')
        r = requests.post(url, data=data, headers=headers)
        dic = r.json()
        if self.DEBUG:
            print(json.dumps(dic, indent=4))
            logging.debug(json.dumps(dic, indent=4))
        return dic['BaseResponse']['Ret'] == 0

    def _saveFile(self, filename, data, api=None):
        fn = filename
        if self.saveSubFolders[api]:
            dirName = os.path.join(self.saveFolder, self.saveSubFolders[api])
            if not os.path.exists(dirName):
                os.makedirs(dirName)
            fn = os.path.join(dirName, filename)
            logging.debug('Saved file: %s' % fn)
            with open(fn, 'wb') as f:
                f.write(data)
                f.close()
        return fn

    def webwxgeticon(self, id):
        url = self.base_uri + \
            '/webwxgeticon?username=%s&skey=%s' % (id, self.skey)
        data = self._get(url)
        if data == '':
            return ''
        fn = 'img_' + id + '.jpg'
        return self._saveFile(fn, data, 'webwxgeticon')

    def webwxgetheadimg(self, id):
        url = self.base_uri + \
            '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey)
        data = self._get(url)
        if data == '':
            return ''
        fn = 'img_' + id + '.jpg'
        return self._saveFile(fn, data, 'webwxgetheadimg')

    def webwxgetmsgimg(self, msgid):
        url = self.base_uri + \
            '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
        data = self._get(url)
        if data == '':
            return ''
        fn = 'img_' + msgid + '.jpg'
        return self._saveFile(fn, data, 'webwxgetmsgimg')

    # Not work now for weixin haven't support this API
    def webwxgetvideo(self, msgid):
        url = self.base_uri + \
            '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey)
        data = self._get(url, api='webwxgetvideo')
        if data == '':
            return ''
        fn = 'video_' + msgid + '.mp4'
        return self._saveFile(fn, data, 'webwxgetvideo')

    def webwxgetvoice(self, msgid):
        url = self.base_uri + \
            '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
        data = self._get(url, api='webwxgetvoice')
        if data == '':
            return ''
        fn = 'voice_' + msgid + '.mp3'
        return self._saveFile(fn, data, 'webwxgetvoice')

    def getGroupName(self, id):
        name = '未知群'
        for member in self.GroupList:
            if member['UserName'] == id:
                name = member['NickName']
        if name == '未知群':
            # 现有群里面查不到
            GroupList = self.getNameById(id)
            for group in GroupList:
                self.GroupList.append(group)
                if group['UserName'] == id:
                    name = group['NickName']
                    MemberList = group['MemberList']
                    for member in MemberList:
                        self.GroupMemeberList.append(member)
        return name

    def getUserRemarkName(self, id):
        name = '未知群' if id[:2] == '@@' else '陌生人'
        if id == self.User['UserName']:
            return self.User['NickName']  # 自己

        if id[:2] == '@@':
            # 群
            name = self.getGroupName(id)
        else:
            # 特殊账号
            for member in self.SpecialUsersList:
                if member['UserName'] == id:
                    name = member['RemarkName'] if member[
                        'RemarkName'] else member['NickName']

            # 公众号或服务号
            for member in self.PublicUsersList:
                if member['UserName'] == id:
                    name = member['RemarkName'] if member[
                        'RemarkName'] else member['NickName']

            # 直接联系人
            for member in self.ContactList:
                if member['UserName'] == id:
                    name = member['RemarkName'] if member[
                        'RemarkName'] else member['NickName']
            # 群友
            for member in self.GroupMemeberList:
                if member['UserName'] == id:
                    name = member['DisplayName'] if member[
                        'DisplayName'] else member['NickName']

        if name == '未知群' or name == '陌生人':
            logging.debug(id)
        return name

    def getUSerID(self, name):
        for member in self.MemberList:
            if name == member['RemarkName'] or name == member['NickName']:
                return member['UserName']
        return None

    def _showMsg(self, message):

        srcName = None
        dstName = None
        groupName = None
        content = None

        msg = message
        logging.debug(msg)

        if msg['raw_msg']:
            srcName = self.getUserRemarkName(msg['raw_msg']['FromUserName'])
            dstName = self.getUserRemarkName(msg['raw_msg']['ToUserName'])
            content = msg['raw_msg']['Content'].replace(
                '&lt;', '<').replace('&gt;', '>')
            message_id = msg['raw_msg']['MsgId']

            if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1:
                # 地理位置消息
                data = self._get(content)
                if data == '':
                    return
                data.decode('gbk').encode('utf-8')
                pos = self._searchContent('title', data, 'xml')
                temp = self._get(content)
                if temp == '':
                    return
                tree = html.fromstring(temp)
                url = tree.xpath('//html/body/div/img')[0].attrib['src']

                for item in urlparse(url).query.split('&'):
                    if item.split('=')[0] == 'center':
                        loc = item.split('=')[-1:]

                content = '%s 发送了一个 位置消息 - 我在 [%s](%s) @ %s]' % (
                    srcName, pos, url, loc)

            if msg['raw_msg']['ToUserName'] == 'filehelper':
                # 文件传输助手
                dstName = '文件传输助手'

            if msg['raw_msg']['FromUserName'][:2] == '@@':
                # 接收到来自群的消息
                if ":<br/>" in content:
                    [people, content] = content.split(':<br/>', 1)
                    groupName = srcName
                    srcName = self.getUserRemarkName(people)
                    dstName = 'GROUP'
                else:
                    groupName = srcName
                    srcName = 'SYSTEM'
            elif msg['raw_msg']['ToUserName'][:2] == '@@':
                # 自己发给群的消息
                groupName = dstName
                dstName = 'GROUP'

            # 收到了红包
            if content == '收到红包,请在手机上查看':
                msg['message'] = content

            # 指定了消息内容
            if 'message' in list(msg.keys()):
                content = msg['message']

        if groupName != None:
            print('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), srcName.strip(), dstName.strip(), content.replace('<br/>', '\n')))
            logging.info('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(),
                                                   srcName.strip(), dstName.strip(), content.replace('<br/>', '\n')))
        else:
            print('%s %s -> %s: %s' % (message_id, srcName.strip(), dstName.strip(), content.replace('<br/>', '\n')))
            logging.info('%s %s -> %s: %s' % (message_id, srcName.strip(),
                                              dstName.strip(), content.replace('<br/>', '\n')))

    def handleMsg(self, r):
        for msg in r['AddMsgList']:
            print('[*] 你有新的消息,请注意查收')
            logging.debug('[*] 你有新的消息,请注意查收')

            if self.DEBUG:
                fn = 'msg' + str(int(random.random() * 1000)) + '.json'
                with open(fn, 'w') as f:
                    f.write(json.dumps(msg))
                print('[*] 该消息已储存到文件: ' + fn)
                logging.debug('[*] 该消息已储存到文件: %s' % (fn))

            msgType = msg['MsgType']
            name = self.getUserRemarkName(msg['FromUserName'])
            content = msg['Content'].replace('&lt;', '<').replace('&gt;', '>')
            msgid = msg['MsgId']

            if msgType == 1:
                raw_msg = {'raw_msg': msg}
                self._showMsg(raw_msg)
#自己加的代码-------------------------------------------#
                #if self.autoReplyRevokeMode:
                #    store
#自己加的代码-------------------------------------------#
                if self.autoReplyMode:
                    ans = self._xiaodoubi(content) + '\n[微信机器人自动回复]'
                    if self.webwxsendmsg(ans, msg['FromUserName']):
                        print('自动回复: ' + ans)
                        logging.info('自动回复: ' + ans)
                    else:
                        print('自动回复失败')
                        logging.info('自动回复失败')
            elif msgType == 3:
                image = self.webwxgetmsgimg(msgid)
                raw_msg = {'raw_msg': msg,
                           'message': '%s 发送了一张图片: %s' % (name, image)}
                self._showMsg(raw_msg)
                self._safe_open(image)
            elif msgType == 34:
                voice = self.webwxgetvoice(msgid)
                raw_msg = {'raw_msg': msg,
                           'message': '%s 发了一段语音: %s' % (name, voice)}
                self._showMsg(raw_msg)
                self._safe_open(voice)
            elif msgType == 42:
                info = msg['RecommendInfo']
                print('%s 发送了一张名片:' % name)
                print('=========================')
                print('= 昵称: %s' % info['NickName'])
                print('= 微信号: %s' % info['Alias'])
                print('= 地区: %s %s' % (info['Province'], info['City']))
                print('= 性别: %s' % ['未知', '男', '女'][info['Sex']])
                print('=========================')
                raw_msg = {'raw_msg': msg, 'message': '%s 发送了一张名片: %s' % (
                    name.strip(), json.dumps(info))}
                self._showMsg(raw_msg)
            elif msgType == 47:
                url = self._searchContent('cdnurl', content)
                raw_msg = {'raw_msg': msg,
                           'message': '%s 发了一个动画表情,点击下面链接查看: %s' % (name, url)}
                self._showMsg(raw_msg)
                self._safe_open(url)
            elif msgType == 49:
                appMsgType = defaultdict(lambda: "")
                appMsgType.update({5: '链接', 3: '音乐', 7: '微博'})
                print('%s 分享了一个%s:' % (name, appMsgType[msg['AppMsgType']]))
                print('=========================')
                print('= 标题: %s' % msg['FileName'])
                print('= 描述: %s' % self._searchContent('des', content, 'xml'))
                print('= 链接: %s' % msg['Url'])
                print('= 来自: %s' % self._searchContent('appname', content, 'xml'))
                print('=========================')
                card = {
                    'title': msg['FileName'],
                    'description': self._searchContent('des', content, 'xml'),
                    'url': msg['Url'],
                    'appname': self._searchContent('appname', content, 'xml')
                }
                raw_msg = {'raw_msg': msg, 'message': '%s 分享了一个%s: %s' % (
                    name, appMsgType[msg['AppMsgType']], json.dumps(card))}
                self._showMsg(raw_msg)
            elif msgType == 51:
                raw_msg = {'raw_msg': msg, 'message': '[*] 成功获取联系人信息'}
                self._showMsg(raw_msg)
            elif msgType == 62:
                video = self.webwxgetvideo(msgid)
                raw_msg = {'raw_msg': msg,
                           'message': '%s 发了一段小视频: %s' % (name, video)}
                self._showMsg(raw_msg)
                self._safe_open(video)
            elif msgType == 10002:
                raw_msg = {'raw_msg': msg, 'message': '%s 撤回了一条消息' % name}
                self._showMsg(raw_msg)
            else:
                logging.debug('[*] 该消息类型为: %d,可能是表情,图片, 链接或红包: %s' %
                              (msg['MsgType'], json.dumps(msg)))
                raw_msg = {
                    'raw_msg': msg, 'message': '[*] 该消息类型为: %d,可能是表情,图片, 链接或红包' % msg['MsgType']}
                self._showMsg(raw_msg)

    def listenMsgMode(self):
        print('[*] 进入消息监听模式 ... 成功')
        logging.debug('[*] 进入消息监听模式 ... 成功')
        self._run('[*] 进行同步线路测试 ... ', self.testsynccheck)
        playWeChat = 0
        redEnvelope = 0
        while True:
            self.lastCheckTs = time.time()
            [retcode, selector] = self.synccheck()
            if self.DEBUG:
                print('retcode: %s, selector: %s' % (retcode, selector))
            logging.debug('retcode: %s, selector: %s' % (retcode, selector))
            if retcode == '1100':
                print('[*] 你在手机上登出了微信,债见')
                logging.debug('[*] 你在手机上登出了微信,债见')
                break
            if retcode == '1101':
                print('[*] 你在其他地方登录了 WEB 版微信,债见')
                logging.debug('[*] 你在其他地方登录了 WEB 版微信,债见')
                break
            elif retcode == '0':
                if selector == '2':
                    r = self.webwxsync()
                    if r is not None:
                        self.handleMsg(r)
                elif selector == '6':
                    # TODO
                    redEnvelope += 1
                    print('[*] 收到疑似红包消息 %d 次' % redEnvelope)
                    logging.debug('[*] 收到疑似红包消息 %d 次' % redEnvelope)
                elif selector == '7':
                    playWeChat += 1
                    print('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat)
                    logging.debug('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat)
                    r = self.webwxsync()
                elif selector == '0':
                    time.sleep(1)
            if (time.time() - self.lastCheckTs) <= 20:
                time.sleep(time.time() - self.lastCheckTs)

    def sendMsg(self, name, word, isfile=False):
        id = self.getUSerID(name)
        if id:
            if isfile:
                with open(word, 'r') as f:
                    for line in f.readlines():
                        line = line.replace('\n', '')
                        self._echo('-> ' + name + ': ' + line)
                        if self.webwxsendmsg(line, id):
                            print(' [成功]')
                        else:
                            print(' [失败]')
                        time.sleep(1)
            else:
                if self.webwxsendmsg(word, id):
                    print('[*] 消息发送成功')
                    logging.debug('[*] 消息发送成功')
                else:
                    print('[*] 消息发送失败')
                    logging.debug('[*] 消息发送失败')
        else:
            print('[*] 此用户不存在')
            logging.debug('[*] 此用户不存在')

    def sendMsgToAll(self, word):
        for contact in self.ContactList:
            name = contact['RemarkName'] if contact[
                'RemarkName'] else contact['NickName']
            id = contact['UserName']
            self._echo('-> ' + name + ': ' + word)
            if self.webwxsendmsg(word, id):
                print(' [成功]')
            else:
                print(' [失败]')
            time.sleep(1)

    def sendImg(self, name, file_name):
        response = self.webwxuploadmedia(file_name)
        media_id = ""
        if response is not None:
            media_id = response['MediaId']
        user_id = self.getUSerID(name)
        response = self.webwxsendmsgimg(user_id, media_id)

    def sendEmotion(self, name, file_name):
        response = self.webwxuploadmedia(file_name)
        media_id = ""
        if response is not None:
            media_id = response['MediaId']
        user_id = self.getUSerID(name)
        response = self.webwxsendmsgemotion(user_id, media_id)

    @catchKeyboardInterrupt
    def start(self):
        self._echo('[*] 微信网页版 ... 开动')
        print()
        logging.debug('[*] 微信网页版 ... 开动')
        while True:
            self._run('[*] 正在获取 uuid ... ', self.getUUID)
            self._echo('[*] 正在获取二维码 ... 成功')
            print()
            logging.debug('[*] 微信网页版 ... 开动')
            self.genQRCode()
            print('[*] 请使用微信扫描二维码以登录 ... ')
            if not self.waitForLogin():
                continue
                print('[*] 请在手机上点击确认以登录 ... ')
            if not self.waitForLogin(0):
                continue
            break

        self._run('[*] 正在登录 ... ', self.login)
        self._run('[*] 微信初始化 ... ', self.webwxinit)
        self._run('[*] 开启状态通知 ... ', self.webwxstatusnotify)
        self._run('[*] 获取联系人 ... ', self.webwxgetcontact)
        self._echo('[*] 应有 %s 个联系人,读取到联系人 %d 个' %
                   (self.MemberCount, len(self.MemberList)))
        print()
        self._echo('[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号' % (len(self.GroupList),
                                                                         len(self.ContactList), len(self.SpecialUsersList), len(self.PublicUsersList)))
        print()
        self._run('[*] 获取群 ... ', self.webwxbatchgetcontact)
        logging.debug('[*] 微信网页版 ... 开动')
        if self.DEBUG:
            print(self)
        logging.debug(self)

        if self.interactive and input('[*] 是否开启自动回复模式(y/n): ') == 'y':
            self.autoReplyMode = True
            print('[*] 自动回复模式 ... 开启')
            logging.debug('[*] 自动回复模式 ... 开启')
        else:
            print('[*] 自动回复模式 ... 关闭')
            logging.debug('[*] 自动回复模式 ... 关闭')

        if sys.platform.startswith('win'):
            import _thread
            _thread.start_new_thread(self.listenMsgMode())
        else:
            listenProcess = multiprocessing.Process(target=self.listenMsgMode)
            listenProcess.start()

        while True:
            text = input('')
            if text == 'quit':
                listenProcess.terminate()
                print('[*] 退出微信')
                logging.debug('[*] 退出微信')
                exit()
            elif text[:2] == '->':
                [name, word] = text[2:].split(':')
                if name == 'all':
                    self.sendMsgToAll(word)
                else:
                    self.sendMsg(name, word)
            elif text[:3] == 'm->':
                [name, file] = text[3:].split(':')
                self.sendMsg(name, file, True)
            elif text[:3] == 'f->':
                print('发送文件')
                logging.debug('发送文件')
            elif text[:3] == 'i->':
                print('发送图片')
                [name, file_name] = text[3:].split(':')
                self.sendImg(name, file_name)
                logging.debug('发送图片')
            elif text[:3] == 'e->':
                print('发送表情')
                [name, file_name] = text[3:].split(':')
                self.sendEmotion(name, file_name)
                logging.debug('发送表情')

    def _safe_open(self, path):
        if self.autoOpen:
            if platform.system() == "Linux":
                os.system("xdg-open %s &" % path)
            else:
                os.system('open %s &' % path)

    def _run(self, str, func, *args):
        self._echo(str)
        if func(*args):
            print('成功')
            logging.debug('%s... 成功' % (str))
        else:
            print('失败\n[*] 退出程序')
            logging.debug('%s... 失败' % (str))
            logging.debug('[*] 退出程序')
            exit()

    def _echo(self, str):
        sys.stdout.write(str)
        sys.stdout.flush()

    def _printQR(self, mat):
        for i in mat:
            BLACK = '\033[40m  \033[0m'
            WHITE = '\033[47m  \033[0m'
            print(''.join([BLACK if j else WHITE for j in i]))

    def _str2qr(self, str):
        print(str)
        qr = qrcode.QRCode()
        qr.border = 1
        qr.add_data(str)
        qr.make()
        # img = qr.make_image()
        # img.save("qrcode.png")
        #mat = qr.get_matrix()
        #self._printQR(mat)  # qr.print_tty() or qr.print_ascii()
        qr.print_ascii(invert=True)

    def _transcoding(self, data):
        if not data:
            return data
        result = None
        if type(data) == str:
            result = data
        elif type(data) == str:
            result = data.decode('utf-8')
        return result

    def _get(self, url: object, api: object = None, timeout: object = None) -> object:
        request = urllib.request.Request(url=url)
        request.add_header('Referer', 'https://wx.qq.com/')
        if api == 'webwxgetvoice':
            request.add_header('Range', 'bytes=0-')
        if api == 'webwxgetvideo':
            request.add_header('Range', 'bytes=0-')
        try:
            response = urllib.request.urlopen(request, timeout=timeout) if timeout else urllib.request.urlopen(request)
            if api == 'webwxgetvoice' or api == 'webwxgetvideo':
                data = response.read()
            else:
                data = response.read().decode('utf-8')
            logging.debug(url)
            return data
        except urllib.error.HTTPError as e:
            logging.error('HTTPError = ' + str(e.code))
        except urllib.error.URLError as e:
            logging.error('URLError = ' + str(e.reason))
        except http.client.HTTPException as e:
            logging.error('HTTPException')
        except timeout_error as e:
            pass
        except ssl.CertificateError as e:
            pass
        except Exception:
            import traceback
            logging.error('generic exception: ' + traceback.format_exc())
        return ''

    def _post(self, url: object, params: object, jsonfmt: object = True) -> object:
        if jsonfmt:
            data = (json.dumps(params)).encode()
            
            request = urllib.request.Request(url=url, data=data)
            request.add_header(
                'ContentType', 'application/json; charset=UTF-8')
        else:
            request = urllib.request.Request(url=url, data=urllib.parse.urlencode(params).encode(encoding='utf-8'))


        try:
            response = urllib.request.urlopen(request)
            data = response.read()
            if jsonfmt:
                return json.loads(data.decode('utf-8') )#object_hook=_decode_dict)
            return data
        except urllib.error.HTTPError as e:
            logging.error('HTTPError = ' + str(e.code))
        except urllib.error.URLError as e:
            logging.error('URLError = ' + str(e.reason))
        except http.client.HTTPException as e:
            logging.error('HTTPException')
        except Exception:
            import traceback
            logging.error('generic exception: ' + traceback.format_exc())

        return ''

    def _xiaodoubi(self, word):
        url = 'http://www.xiaodoubi.com/bot/chat.php'
        try:
            r = requests.post(url, data={'chat': word})
            return r.content
        except:
            return "让我一个人静静 T_T..."

    def _simsimi(self, word):
        key = ''
        url = 'http://sandbox.api.simsimi.com/request.p?key=%s&lc=ch&ft=0.0&text=%s' % (
            key, word)
        r = requests.get(url)
        ans = r.json()
        if ans['result'] == '100':
            return ans['response']
        else:
            return '你在说什么,风太大听不清列'

    def _searchContent(self, key, content, fmat='attr'):
        if fmat == 'attr':
            pm = re.search(key + '\s?=\s?"([^"<]+)"', content)
            if pm:
                return pm.group(1)
        elif fmat == 'xml':
            pm = re.search('<{0}>([^<]+)</{0}>'.format(key), content)
            if not pm:
                pm = re.search(
                    '<{0}><\!\[CDATA\[(.*?)\]\]></{0}>'.format(key), content)
            if pm:
                return pm.group(1)
        return '未知'


class UnicodeStreamFilter:

    def __init__(self, target):
        self.target = target
        self.encoding = 'utf-8'
        self.errors = 'replace'
        self.encode_to = self.target.encoding

    def write(self, s):
        if type(s) == str:
            s = s.encode().decode('utf-8')
        s = s.encode(self.encode_to, self.errors).decode(self.encode_to)
        self.target.write(s)

    def flush(self):
        self.target.flush()

if sys.stdout.encoding == 'cp936':
    sys.stdout = UnicodeStreamFilter(sys.stdout)


if __name__ == '__main__':
    logger = logging.getLogger(__name__)
    if not sys.platform.startswith('win'):
        import coloredlogs
        coloredlogs.install(level='DEBUG')

    webwx = WebWeixin()
    webwx.start()


================================================
FILE: wxbot_project_py2.7/README.md
================================================
# wxbot_project_py2.7

目录结构:
```bash
.
├── README.md
├── config
│   ├── __init__.py
│   ├── config_manager.py
│   ├── constant.py
│   ├── log.py
│   ├── requirements.txt
│   └── wechat.conf.bak
├── db
│   ├── __init__.py
│   ├── mysql_db.py
│   └── sqlite_db.py
├── docker
│   ├── Dockerfile
│   └── README.md
├── flask_templates
│   ├── index.html
│   └── upload.html
├── wechat
│   ├── __init__.py
│   ├── utils.py
│   ├── wechat_apis.py
│   └── wechat.py
├── weixin_bot.py
└── wx_handler
    ├── __init__.py
    ├── bot.py
    ├── sendgrid_mail.py
    └── wechat_msg_processor.py
```


================================================
FILE: wxbot_project_py2.7/config/__init__.py
================================================
#!/usr/bin/env python
# coding: utf-8

from config_manager import ConfigManager
from constant import Constant
from log import Log

================================================
FILE: wxbot_project_py2.7/config/config_manager.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from constant import Constant
#---------------------------------------------------
import ConfigParser
import os
#===================================================


class ConfigManager(object):

    def __init__(self):
        self.config = Constant.WECHAT_CONFIG_FILE
        self.cp = ConfigParser.ConfigParser()
        self.cp.read(self.config)

        data_dir = self.get('setting', 'prefix')
        upload_dir = self.getpath('uploaddir')
        if not os.path.exists(data_dir):
            os.makedirs(data_dir)
        if not os.path.exists(upload_dir):
            os.makedirs(upload_dir)

    def get(self, section, option):
        return self.cp.get(section, option)

    def set(self, section, option, value):
        self.cp.set(section, option, value)
        self.cp.write(open(self.config, 'w'))

    def getpath(self, dir):
        prefix = self.get('setting', 'prefix')
        return prefix + self.get('setting', dir)

    def setup_database(self):
        path = self.get('setting', 'prefix')
        conf = [
            path + self.get('setting', 'uploaddir'),
            path + self.get('setting', 'datadir'),
            path + self.get('setting', 'logdir'),
        ]
        return conf

    def set_wechat_config(self, conf):
        for [key, value] in conf.items():
            self.cp.set('wechat', key, value)
        self.cp.write(open(self.config, 'w'))

    def get_wechat_config(self):
        uin = self.cp.get('wechat', 'uin')
        last_login = self.cp.get('wechat', 'last_login')
        conf = [
            self.cp.get('wechat', 'uuid'),
            self.cp.get('wechat', 'redirect_uri'),
            int(uin if uin else 0),
            self.cp.get('wechat', 'sid'),
            self.cp.get('wechat', 'skey'),
            self.cp.get('wechat', 'pass_ticket'),
            self.cp.get('wechat', 'synckey'),
            self.cp.get('wechat', 'device_id'),
            float(last_login if last_login else 0),
        ]
        return conf

    def get_wechat_media_dir(self):
        prefix = self.get('setting', 'prefix')
        path = prefix + self.cp.get('setting', 'mediapath')
        return {
            'webwxgeticon': path + '/icons',
            'webwxgetheadimg': path + '/headimgs',
            'webwxgetmsgimg': path + '/msgimgs',
            'webwxgetvideo': path + '/videos',
            'webwxgetvoice': path + '/voices',
            '_showQRCodeImg': path + '/qrcodes',
        }

    def get_pickle_files(self):
        prefix = self.get('setting', 'prefix')
        return {
            'User': prefix + self.get('setting', 'contact_user'),
            'MemberList': prefix + self.get('setting', 'contact_member_list'),
            'GroupList': prefix + self.get('setting', 'contact_group_list'),
            'GroupMemeberList': prefix + self.get('setting', 'contact_group_memeber_list'),
            'SpecialUsersList': prefix + self.get('setting', 'contact_special_users_list'),
        }

    def get_cookie(self):
        prefix = self.get('setting', 'prefix')
        path = prefix + self.get('setting', 'cookie')
        basedir = os.path.dirname(path)
        if not os.path.exists(basedir):
            os.makedirs(basedir)
        return path

    def mysql(self):
        mysql = {
            'host': self.get('mysql', 'host'),
            'port': self.cp.getint('mysql', 'port'),
            'user': self.get('mysql', 'user'),
            'passwd': self.get('mysql', 'passwd'),
            'database': self.get('mysql', 'database'),
        }
        return mysql


================================================
FILE: wxbot_project_py2.7/config/constant.py
================================================
#!/usr/bin/env python
# coding: utf-8
import time

class Constant(object):
    """
    @brief      All used constants are listed here
    """

    WECHAT_CONFIG_FILE = 'config/wechat.conf'
    LOGGING_LOGGER_NAME = 'WeChat'

    QRCODE_BLACK = '\033[40m  \033[0m'
    QRCODE_WHITE = '\033[47m  \033[0m'

    HTTP_HEADER_USERAGENT = [('User-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36')]
    HTTP_HEADER_CONTENTTYPE = ['ContentType', 'application/json; charset=UTF-8']
    HTTP_HEADER_CONNECTION = ['Connection', 'keep-alive']
    HTTP_HEADER_REFERER = ['Referer', 'https://wx.qq.com/']
    HTTP_HEADER_RANGE = ['Range', 'bytes=0-']

    REGEX_EMOJI = r'<span class="emoji emoji(\w+)"></span>'
    
    SERVER_LOG_FORMAT = '%(asctime)s - %(pathname)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s'
    SERVER_UPLOAD_ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
    SERVER_PAGE_UPLOAD = 'upload.html'
    SERVER_PAGE_INDEX = 'index.html'

    RUN_RESULT_SUCCESS = '成功 %ds\n'
    RUN_RESULT_FAIL = '失败\n[*] 退出程序\n'
    MAIN_RESTART = '[*] wait for restart'
    LOG_MSG_FILE = 'WeChat-Msgs-%Y-%m-%d.json'
    LOG_MSG_GROUP_LIST_FILE = 'group_list.json'
    LOG_MSG_QUIT = '\n[*] Force quit.\n'
    LOG_MSG_FAIL = '失败\n'
    LOG_MSG_SUCCESS = '成功\n'
    LOG_MSG_START = '[*] 微信网页版 ... 开动\n'
    LOG_MSG_RECOVER = '[*] 从配置文件中恢复 ... '
    LOG_MSG_RECOVER_CONTACT = '[*] 从文件中恢复联系人数据 ... '
    LOG_MSG_TRY_INIT = '[*] 尝试初始化 ... '
    LOG_MSG_ASSOCIATION_LOGIN = '[*] 通过关联登录 ... '
    LOG_MSG_GET_UUID = '[*] 正在获取 uuid ... '
    LOG_MSG_GET_QRCODE = '[*] 正在获取二维码 ... 成功\n'
    LOG_MSG_SCAN_QRCODE = '[*] 请使用微信扫描二维码以登录 ... \n'
    LOG_MSG_CONFIRM_LOGIN = '[*] 请在手机上点击确认以登录 ... \n'
    LOG_MSG_WAIT_LOGIN_ERR1 = '[登陆超时] \n'
    LOG_MSG_WAIT_LOGIN_ERR2 = '[登陆异常] \n'
    LOG_MSG_LOGIN = '[*] 正在登录 ... '
    LOG_MSG_INIT = '[*] 微信初始化 ... '
    LOG_MSG_STATUS_NOTIFY = '[*] 开启状态通知 ... '
    LOG_MSG_GET_CONTACT = '[*] 获取联系人 ... '
    LOG_MSG_CONTACT_COUNT = '[*] 应有 %s 个联系人,读取到联系人 %d 个\n'
    LOG_MSG_OTHER_CONTACT_COUNT = '[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号\n'
    LOG_MSG_GET_GROUP_MEMBER = '[*] 拉取群聊成员 ... '
    LOG_MSG_SNAPSHOT = '[*] 保存配置 ... '
    LOG_MSG_LOGOUT = '[*] 你在手机上登出了微信\n'
    LOG_MSG_LOGIN_OTHERWHERE = '[*] 你在其他地方登录了 WEB 版微信\n'
    LOG_MSG_QUIT_ON_PHONE = '[*] 你在手机上主动退出了\n'
    LOG_MSG_RUNTIME = '[*] Total run: %s\n'
    LOG_MSG_KILL_PROCESS = 'kill %d'
    LOG_MSG_NEW_MSG = '>>> %d 条新消息\n'
    LOG_MSG_LOCATION = '[位置] %s'
    LOG_MSG_PICTURE = '[图片] %s'
    LOG_MSG_VOICE = '[语音] %s'
    LOG_MSG_RECALL = '撤回了一条消息'
    LOG_MSG_ADD_FRIEND = '%s 请求添加你为好友'
    LOG_MSG_UNKNOWN_MSG = '[*] 该消息类型为: %d,内容: %s'
    LOG_MSG_VIDEO = '[小视频] %s'
    LOG_MSG_NOTIFY_PHONE = '[*] 提示手机网页版微信登录状态\n'
    LOG_MSG_EMOTION = '[表情] %s'
    LOG_MSG_NAME_CARD = (
        '[名片]\n'
        '=========================\n'
        '= 昵称: %s\n'
        '= 微信号: %s\n'
        '= 地区: %s %s\n'
        '= 性别: %s\n'
        '========================='
    )
    LOG_MSG_SEX_OPTION = ['未知', '男', '女']
    LOG_MSG_APP_LINK = (
        '[%s]\n'
        '=========================\n'
        '= 标题: %s\n'
        '= 描述: %s\n'
        '= 链接: %s\n'
        '= 来自: %s\n'
        '========================='
    )
    LOG_MSG_APP_LINK_TYPE = {5: '链接', 3: '音乐', 7: '微博'}
    LOG_MSG_APP_IMG = (
        '[图片]\n'
        '=========================\n'
        '= 文件: %s\n'
        '= 来自: %s\n'
        '========================='
    )
    LOG_MSG_SYSTEM = '系统消息'
    LOG_MSG_UNKNOWN_NAME = '未知_'
    LOG_MSG_UNKNOWN_GROUP_NAME = '未知群_'

    TABLE_GROUP_MSG_LOG = 'WeChatRoomMessage'
    TABLE_GROUP_MSG_LOG_COL = """
        MsgID text,
        RoomOwnerID text,
        RoomName text,
        UserCount text,
        FromUserName text,
        ToUserName text,
        AttrStatus text,
        DisplayName text,
        Name text, 
        MsgType text,
        FaceMsg text,
        TextMsg text,
        ImageMsg text,
        VideoMsg text,
        SoundMsg text,
        LinkMsg text,
        NameCardMsg text,
        LocationMsg text,
        RecallMsgID text,
        SysMsg text,
        MsgTime text,
        MsgTimestamp text
    """

    @staticmethod
    def TABLE_GROUP_LIST():
        return 'WeChatRoom_' + time.strftime('%Y%m%d', time.localtime())
    
    TABLE_GROUP_LIST_COL = """
        RoomName text,
        RoomID text,
        RoomOwnerID text,
        UserCount text,
        RoomIcon text
    """

    @staticmethod
    def TABLE_GROUP_USER_LIST():
        return 'WeChatRoomMember_' + time.strftime('%Y%m%d', time.localtime())

    TABLE_GROUP_USER_LIST_COL = """
        RoomID text,
        MemberID text,
        MemberNickName text,
        MemberDisplayName text,
        MemberAttrStatus text
    """
    TABLE_RECORD_ENTER_GROUP = 'WeChatEnterGroupRecord'
    TABLE_RECORD_ENTER_GROUP_COL = """
        MsgID text,
        RoomName text,
        FromUserName text,
        ToUserName text,
        Name text,
        EnterTime text
    """
    TABLE_RECORD_RENAME_GROUP = 'WeChatRenameGroupRecord'
    TABLE_RECORD_RENAME_GROUP_COL = """
        MsgID text,
        FromName text,
        ToName text,
        ModifyPeople text,
        ModifyTime text
    """

    API_APPID = 'wx782c26e4c19acffb'
    API_WXAPPID = 'wx299208e619de7026' # Weibo
                # 'wxeb7ec651dd0aefa9' # Weixin
    API_LANG = 'zh_CN'
    API_USER_AGENT = (
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) '
        'AppleWebKit/537.36 (KHTML, like Gecko) '
        'Chrome/48.0.2564.109 Safari/537.36'
    )
    API_SPECIAL_USER = [
        'newsapp', 'filehelper', 'weibo', 'qqmail',
        'fmessage', 'tmessage', 'qmessage', 'qqsync',
        'floatbottle', 'lbsapp', 'shakeapp', 'medianote',
        'qqfriend', 'readerapp', 'blogapp', 'facebookapp',
        'masssendapp', 'meishiapp', 'feedsapp', 'voip',
        'blogappweixin', 'brandsessionholder', 'weixin',
        'weixinreminder', 'officialaccounts', 'wxitil',
        'notification_messages', 'wxid_novlwrv3lqwv11',
        'gh_22b87fa7cb3c', 'userexperience_alarm',
    ]

    EMOTICON = [
        '[Smile]', '[Grimace]', '[Drool]', '[Scowl]', '[CoolGuy]', '[Sob]', '[Shy]',
        '[Silent]', '[Sleep]', '[Cry]', '[Awkward]', '[Angry]', '[Tongue]', '[Grin]',
        '[Surprise]', '[Frown]', '[Ruthless]', '[Blush]', '[Scream]', '[Puke]',
        '[Chuckle]', '[Joyful]', '[Slight]', '[Smug]', '[Hungry]', '[Drowsy]', '[Panic]',
        '[Sweat]', '[Laugh]', '[Commando]', '[Determined]', '[Scold]', '[Shocked]', '[Shhh]',
        '[Dizzy]', '[Tormented]', '[Toasted]', '[Skull]', '[Hammer]', '[Wave]',
        '[Relief]', '[DigNose]', '[Clap]', '[Shame]', '[Trick]',' [Bah!L]','[Bah!R]',
        '[Yawn]', '[Lookdown]', '[Wronged]', '[Puling]', '[Sly]', '[Kiss]', '[Uh-oh]',
        '[Whimper]', '[Cleaver]', '[Melon]', '[Beer]', '[Basketball]', '[PingPong]',
        '[Coffee]', '[Rice]', '[Pig]', '[Rose]', '[Wilt]', '[Lip]', '[Heart]',
        '[BrokenHeart]', '[Cake]', '[Lightning]', '[Bomb]', '[Dagger]', '[Soccer]', '[Ladybug]',
        '[Poop]', '[Moon]', '[Sun]', '[Gift]', '[Hug]', '[Strong]',
        '[Weak]', '[Shake]', '[Victory]', '[Admire]', '[Beckon]', '[Fist]', '[Pinky]',
        '[Love]', '[No]', '[OK]', '[InLove]', '[Blowkiss]', '[Waddle]', '[Tremble]',
        '[Aaagh!]', '[Twirl]', '[Kotow]', '[Lookback]', '[Jump]', '[Give-in]',
        u'\U0001f604', u'\U0001f637', u'\U0001f639', u'\U0001f61d', u'\U0001f632', u'\U0001f633',
        u'\U0001f631', u'\U0001f64d', u'\U0001f609', u'\U0001f60c', u'\U0001f612', u'\U0001f47f',
        u'\U0001f47b', u'\U0001f49d', u'\U0001f64f', u'\U0001f4aa', u'\U0001f4b5', u'\U0001f382',
        u'\U0001f388', u'\U0001f4e6',
    ]
    BOT_ZHIHU_URL_LATEST = 'http://news-at.zhihu.com/api/4/news/latest'
    BOT_ZHIHU_URL_DAILY = 'http://daily.zhihu.com/story/'
    BOT_TULING_API_KEY = '55e7f30895a0a10535984bae5ad294d1'
    BOT_TULING_API_URL = 'http://www.tuling123.com/openapi/api?key=%s&info=%s&userid=%s'
    BOT_TULING_BOT_REPLY = u'麻烦说的清楚一点,我听不懂你在说什么'


================================================
FILE: wxbot_project_py2.7/config/log.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from constant import Constant
from config import ConfigManager
#---------------------------------------------------
import logging
import logging.config
#===================================================

cm = ConfigManager()

logging.config.fileConfig(Constant.WECHAT_CONFIG_FILE)
# create logger
Log = logging.getLogger(Constant.LOGGING_LOGGER_NAME)

# 'application' code
# Log.debug('debug message')
# Log.info('info message')
# Log.warn('warn message')
# Log.error('error message')
# Log.critical('critical message')


================================================
FILE: wxbot_project_py2.7/config/requirements.txt
================================================
qrcode
flask
requests
requests_toolbelt
pymysql
sendgrid


================================================
FILE: wxbot_project_py2.7/config/wechat.conf.bak
================================================
[wechat]
host = wx.qq.com
uuid = 
redirect_uri = 
uin = 
sid = 
skey = 
pass_ticket = 
device_id = 
last_login = 

[setting]
prefix = tmp_data/
database = WeChat.db
datadir = Data/infos/
logdir = Logs
mediapath = Data
uploaddir = Data/upload
qrcodedir = Data/qrcode
server_port = 8080
cookie = Cookie/WeChat.cookie
contact_user = Pickle/User.pkl
contact_member_list = Pickle/MemberList.pkl
contact_group_list = Pickle/GroupList.pkl
contact_group_memeber_list = Pickle/GroupMemeberList.pkl
contact_special_users_list = Pickle/SpecialUsersList.pkl
server_mode = False
server_log_file = server.log
log_mode = False

[mysql]
host = localhost
port = 3306
user = root
passwd = root
database = wechat

[sendgrid]
api_key = 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
from_email = wxbot@wechat.com
to_email = xxx@example.com

[loggers]
keys = root,WeChat

[handlers]
keys = consoleHandler,fileHandler

[formatters]
keys = simpleFormatter

[logger_root]
level = DEBUG
handlers = consoleHandler

[logger_WeChat]
level = DEBUG
handlers = fileHandler
qualname = WeChat
propagate = 0

[handler_consoleHandler]
class = StreamHandler
level = DEBUG
formatter = simpleFormatter
args = (sys.stdout,)

[handler_fileHandler]
class = FileHandler
level = DEBUG
formatter = simpleFormatter
args = ('tmp_data/wechat.log',)

[formatter_simpleFormatter]
format = %(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt = 



================================================
FILE: wxbot_project_py2.7/db/__init__.py
================================================
#!/usr/bin/env python
# coding: utf-8

from sqlite_db import SqliteDB
from mysql_db import MysqlDB

================================================
FILE: wxbot_project_py2.7/db/mysql_db.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from config import Log
#---------------------------------------------------
import pymysql
import threading
import traceback
#===================================================

def array_join(arr, c):
    t = ''
    for a in arr:
        t += "'%s'" % str(a).replace("'","\\\'") + c
    return t[:-len(c)]

class MysqlDB(object):
    """
    修改服务器上的配置文件/etc/my.cnf,在对应位置添加以下设置:
    [client]
    default-character-set = utf8mb4

    [mysql]
    default-character-set = utf8mb4

    [mysqld]
    character-set-client-handshake = FALSE
    character-set-server = utf8mb4
    collation-server = utf8mb4_unicode_ci
    init_connect='SET NAMES utf8mb4'
    """

    def __init__(self, conf):
        self.conf = conf
        config = {
            'host': conf['host'],
            'port': conf['port'],
            'user': conf['user'],
            'passwd': conf['passwd'],
            'charset':'utf8mb4', # 支持1-4个字节字符
            'cursorclass': pymysql.cursors.DictCursor
        }
        self.conn = pymysql.connect(**config)
        self.conn.autocommit(1)
        # for thread-save
        self.lock = threading.Lock()

        self.create_db(conf['database'])
        self.conn.select_db(conf['database'])

        # cache table cols
        self.table_cols = {}
        for t in self.show_tables():
            self.table_cols[t] = self.get_table_column_name(t)

    def show_database(self):
        c = self.conn.cursor()
        sql = 'SHOW DATABASES'
        Log.debug('DB -> %s' % sql)
        c.execute(sql)
        return [r['Database'] for r in c.fetchall()]

    def show_tables(self):
        c = self.conn.cursor()
        sql = 'SHOW TABLES'
        Log.debug('DB -> %s' % sql)
        c.execute(sql)
        return [r['Tables_in_'+self.conf['database']] for r in c.fetchall()]

    def create_db(self, db_name):
        """
        @brief      Creates a database
        @param      db_name  String
        """
        if self.conf['database'] not in self.show_database():
            sql = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % db_name
            Log.debug('DB -> %s' % sql)
            self.execute(sql)

    def create_table(self, table, cols):
        """
        @brief      Creates a table in database
        @param      table  String
        @param      cols   String, the cols in table
        """
        if table not in self.table_cols:
            sql = 'CREATE TABLE IF NOT EXISTS %s(id int primary key auto_increment, %s) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % (table, cols)
            Log.debug('DB -> %s' % sql)
            self.execute(sql)
            self.table_cols[table] = ['id'] + [c.strip().split(' ')[0] for c in cols.split(',')]

    def delete_table(self, table):
        """
        @brief      Delete a table in database
        @param      table  String
        """
        if table in self.table_cols:
            sql = "DROP TABLE IF EXISTS %s" % table
            Log.debug('DB -> %s' % sql)
            self.execute(sql)
            self.table_cols.pop(table)

    def insert(self, table, value):
        """
        @brief      Insert a row in table
        @param      table  String
        @param      value  Tuple
        """
        col_name = self.table_cols[table][1:]
        sql = "INSERT INTO %s(%s) VALUES (%s)" % (table, str(','.join(col_name)), array_join(value, ','))
        Log.debug('DB -> %s' % sql)
        self.execute(sql)

    def insertmany(self, table, values):
        """
        @brief      Insert many rows in table
        @param      table  String
        @param      values  Array of tuple
        """
        col_name = self.table_cols[table][1:]
        sql = 'INSERT INTO %s(%s) VALUES (%s)' % (table, ','.join(col_name), ','.join(['%s'] * len(values[0])))
        Log.debug('DB -> %s' % sql)
        self.execute(sql, values)

    def select(self, table, field='', condition=''):
        """
        @brief      select all result from table
        @param      table  String
        @param      field  String
        @param      condition  String
        @return     result  Tuple
        """
        sql = "SELECT * FROM %s" % table
        if field and condition:
            sql += " WHERE %s='%s'" % (field, condition)
        Log.debug('DB -> %s' % sql)
        return self.execute(sql)

    def get_table_column_name(self, table):
        """
        @brief      select all result from table
        @param      table  String
        @return     result  Array
        """
        c = self.conn.cursor()
        c.execute("SELECT * FROM %s" % table)
        names = list(map(lambda x: x[0], c.description))
        return names

    def execute(self, sql, values=None):
        """
        @brief      execute sql commands, return result if it has
        @param      sql  String
        @param      value  Tuple
        @return     result  Array
        """
        c = self.conn.cursor()
        self.lock.acquire()
        hasReturn = sql.lstrip().upper().startswith("SELECT")

        result = []
        try:
            if values:
                c.executemany(sql, values)
            else:
                c.execute(sql)

            if hasReturn:
                result = c.fetchall()

        except Exception, e:
            Log.error(traceback.format_exc())
            self.conn.rollback()
        finally:
            self.lock.release()

        if hasReturn:
            return result

    def delete(self, table, field='', condition=''):
        """
        @brief      execute sql commands, return result if it has
        @param      table  String
        @param      field  String
        @param      condition  String
        """
        sql = "DELETE FROM %s WHERE %s=%s" % (table, field, condition)
        Log.debug('DB -> %s' % sql)
        self.execute(sql)

    def close(self):
        """
        @brief      close connection to database
        """
        Log.debug('DB -> close')
        # 关闭数据库连接
        self.conn.close()

        

================================================
FILE: wxbot_project_py2.7/db/sqlite_db.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from config import Log
#---------------------------------------------------
import sqlite3
import threading
import traceback
#===================================================


def _dict_factory(cursor, row):
    aDict = {}
    for iField, field in enumerate (cursor.description):
        aDict [field [0]] = row [iField]
    return aDict


class SqliteDB(object):

    def __init__(self, db_file):
        self.db_file = db_file
        # self.conn = sqlite3.connect(db_file, check_same_thread=False)
        # use 8-bit strings instead of unicode string
        self.conn.text_factory = str
        # not return a tuple but a dict with column name as key
        self.conn.row_factory = _dict_factory
        # for thread-save
        self.lock = threading.Lock()

    def set_conn(self):
        self._conn = sqlite3.connect(self.db_file,check_same_thread=False)

    @property
    def conn(self):
        try:
            self._conn.execute('select 1;')
            # check out conn
        except (sqlite3.ProgrammingError,AttributeError):
            # Cannot operate on a closed database
            self.set_conn()

        finally:
            return self._conn


    def create_table(self, table, cols):
        """
        @brief      Creates a table in database
        @param      table  String
        @param      cols   String, the cols in table
        """
        sql = "CREATE TABLE if not exists %s (%s);" % (table, cols)
        Log.debug('DB -> %s' % sql)
        self.execute(sql)

    def delete_table(self, table):
        """
        @brief      Delete a table in database
        @param      table  String
        """
        sql = "DROP TABLE if exists %s;" % table
        Log.debug('DB -> %s' % sql)
        self.execute(sql)

    def insert(self, table, value):
        """
        @brief      Insert a row in table
        @param      table  String
        @param      value  Tuple
        """
        sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * len(value)) + ");") % table
        Log.debug('DB -> %s' % sql)
        self.execute(sql, value)

    def insertmany(self, table, values):
        """
        @brief      Insert many rows in table
        @param      table  String
        @param      values  Array of tuple
        """
        c = self.conn.cursor()
        self.lock.acquire()
        n = len(values[0])
        sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * n) + ");") % table
        Log.debug('DB -> %s' % sql)

        try:
            c.executemany(sql, values)
        except Exception, e:
            Log.error(traceback.format_exc())
        finally:
            self.lock.release()

        self.conn.commit()

    def select(self, table, field='', condition=''):
        """
        @brief      select all result from table
        @param      table  String
        @param      field  String
        @param      condition  String
        @return     result  Tuple
        """
        result = []
        if field and condition:
            cond = (condition,)
            sql = "SELECT * FROM %s WHERE %s=?" % (table, field)
            Log.debug('DB -> %s' % sql)
            result = self.execute(sql, cond)
        else:
            sql = "SELECT * FROM %s" % table
            Log.debug('DB -> %s' % sql)
            result = self.execute(sql)
        return result

    def update(self, table, dic, condition=''):
        k_arr = []
        v_arr = []
        for (k, v) in dic.items():
            k_arr.append('%s=?' % k)
            v_arr.append(v)

        sql = "UPDATE %s SET %s" % (table, ','.join(k_arr))
        if condition:
            sql += " WHERE %s" % condition

        Log.debug('DB -> %s' % sql)
        self.execute(sql, tuple(v_arr))

    def get_table_column_name(self, table):
        """
        @brief      select all result from table
        @param      table  String
        @return     result  Array
        """
        c = self.conn.cursor()
        c.execute("SELECT * FROM %s" % table)
        names = list(map(lambda x: x[0], c.description))
        return names

    def execute(self, sql, value=None):
        """
        @brief      execute sql commands, return result if it has
        @param      sql  String
        @param      value  Tuple
        @return     result  Array
        """
        c = self.conn.cursor()
        self.lock.acquire()
        hasReturn = sql.lstrip().upper().startswith("SELECT")

        try:
            if value:
                c.execute(sql, value)
            else:
                c.execute(sql)

            if hasReturn:
                result = c.fetchall()
        except Exception, e:
            Log.error(traceback.format_exc())
        finally:
            self.lock.release()

        self.conn.commit()

        if hasReturn:
            return result

    def delete(self, table, field='', condition=''):
        """
        @brief      execute sql commands, return result if it has
        @param      table  String
        @param      field  String
        @param      condition  String
        """
        sql = "DELETE FROM %s WHERE %s=?" % (table, field)
        Log.debug('DB -> %s' % sql)
        cond = (condition,)
        self.execute(sql, cond)

    def close(self):
        """
        @brief      close connection to database
        """
        Log.debug('DB -> close')
        self.conn.close()


================================================
FILE: wxbot_project_py2.7/docker/Dockerfile
================================================
FROM ubuntu:16.04
MAINTAINER Urinx <uri.lqy@gmail.com>

RUN apt-get update && \
    apt-get install -y python \
                      python-dev \
                      python-pip && \
    apt-get clean && \
    apt-get autoclean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

ADD weixin_bot.tar.gz /
WORKDIR /weixin_bot

RUN pip install -r config/requirements.txt
EXPOSE 80
ENTRYPOINT ["./weixin_bot.py"]
CMD [""]


================================================
FILE: wxbot_project_py2.7/docker/README.md
================================================
# 本地构建 wechat_bot docker 镜像

拉下镜像:

```bash
docker pull ubuntu:16.04
```

打包本项目,将压缩包放到`docker`目录下:

```
tar -czf weixin_bot.tar.gz wxbot_project_py2.7/
```

切换到`docker`目录,执行`build`命令:

```bash
docker build -t wechat-bot .
```

导出镜像:

```bash
docker save wechat-bot > wechat.tar
```

导入镜像:

```bash
docker load < wechat.tar
```

运行:

```bash
docker run -d -P --name xxx -v /src/data/dir:/Wechat_bot/test wechat-bot
```

删除镜像:

```bash
docker rmi -f wechat-bot
```

查看log:

```bash
docker log wechat-bot
```


================================================
FILE: wxbot_project_py2.7/flask_templates/index.html
================================================
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>WeChat Bot Server</title>
	<style type="text/css">
	.container {
		display: flex;
	}
	div.doc{
		margin: auto;
		text-align: left;
	}
	</style>
</head>
<body class="container">

<div class="doc">
<pre>
	#####################
	# WeChat Bot Server #
	#####################


## APIs

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

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

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

- /group_member_list/&lt;g_id&gt;
  -----------------------------
  @brief      list group member
  @method     get
  @param      g_id String
  -----------------------------

- /group_chat_log/&lt;g_name&gt;
  -------------------------------
  @brief      list group chat log
  @method     get
  @param      g_name String
  -------------------------------

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

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

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

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

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

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

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

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

- /mass_send_file
  -------------------------------------------
  @brief      send file to mass users or gourps
  @method     post
  @param      application/json
              {
                'to_list': [
                  'group_id',
                  ...
                ],
                'msg': '',
              }
  @return     application/json
              {
                'ret': 0,
                'unsend_list': [],
              }
  -------------------------------------------
</pre>
</div>

</body>
</html>



================================================
FILE: wxbot_project_py2.7/flask_templates/upload.html
================================================
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form action="upload" method=post enctype=multipart/form-data>
  <p><input type=file name=file>
     <input type=submit value=Upload>
</form>

================================================
FILE: wxbot_project_py2.7/wechat/__init__.py
================================================
#!/usr/bin/env python
# coding: utf-8

from wechat import WeChat

================================================
FILE: wxbot_project_py2.7/wechat/utils.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from config import Log
from config import Constant
#---------------------------------------------------
import qrcode
import re
import os
import sys
import json
import urllib
import urllib2
import cookielib
import cPickle as pickle
import traceback
import time
import hashlib
#===================================================


def _decode_data(data):
    """
    @brief      decode array or dict to utf-8
    @param      data   array or dict
    @return     utf-8
    """
    if isinstance(data, dict):
        rv = {}
        for key, value in data.iteritems():
            if isinstance(key, unicode):
                key = key.encode('utf-8')
            rv[key] = _decode_data(value)
        return rv
    elif isinstance(data, list):
        rv = []
        for item in data:
            item = _decode_data(item)
            rv.append(item)
        return rv
    elif isinstance(data, unicode):
        return data.encode('utf-8')
    else:
        return data


def str2qr_terminal(text):
    """
    @brief      convert string to qrcode matrix and outprint
    @param      text   The string
    """
    Log.debug(text)
    qr = qrcode.QRCode()
    qr.border = 1
    qr.add_data(text)
    mat = qr.get_matrix()
    print_qr(mat)


def str2qr_image(text, image_path):
    """
    @brief      convert string to qrcode image & save
    @param      text         The string
    @param      image_path   Save image to the path
    """
    qr = qrcode.QRCode()
    qr.border = 1
    qr.add_data(text)
    qr.make(fit=True)
    img = qr.make_image()
    img.save(image_path)


def print_qr(mat):
    for i in mat:
        BLACK = Constant.QRCODE_BLACK
        WHITE = Constant.QRCODE_WHITE
        echo(''.join([BLACK if j else WHITE for j in i])+'\n')


def echo(str):
    Log.info(str[:-1])
    sys.stdout.write(str)
    sys.stdout.flush()


def run(str, func, *args):
    t = time.time()
    echo(str)
    r = False
    try:
        r = func(*args)
    except:
        Log.error(traceback.format_exc())
    if r:
        totalTime = int(time.time() - t)
        echo(Constant.RUN_RESULT_SUCCESS % totalTime)
    else:
        echo(Constant.RUN_RESULT_FAIL)
        exit()


def get(url, api=None):
    """
    @brief      http get request
    @param      url   String
    @param      api   wechat api
    @return     http response
    """
    Log.debug('GET -> ' + url)
    request = urllib2.Request(url=url)
    request.add_header(*Constant.HTTP_HEADER_CONNECTION)
    request.add_header(*Constant.HTTP_HEADER_REFERER)
    if api in ['webwxgetvoice', 'webwxgetvideo']:
        request.add_header(*Constant.HTTP_HEADER_RANGE)

    while True:
        try:
            response = urllib2.urlopen(request, timeout=30)
            data = response.read()
            response.close()
            if api == None:
                Log.debug(data)
            return data
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            Log.error(traceback.format_exc())

        time.sleep(1)


def post(url, params, jsonfmt=True):
    """
    @brief      http post request
    @param      url      String
    @param      params   Dict, post params
    @param      jsonfmt  Bool, whether is json format
    @return     http response
    """
    Log.debug('POST -> '+url)
    Log.debug(params)
    if jsonfmt:
        request = urllib2.Request(url=url, data=json.dumps(params, ensure_ascii=False).encode('utf8'))
        request.add_header(*Constant.HTTP_HEADER_CONTENTTYPE)
    else:
        request = urllib2.Request(url=url, data=urllib.urlencode(params))

    while True:
        try:
            response = urllib2.urlopen(request, timeout=30)
            data = response.read()
            response.close()

            if jsonfmt:
                Log.debug(data)
                return json.loads(data, object_hook=_decode_data)
            return data
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            Log.error(traceback.format_exc())

        time.sleep(1)


def set_cookie(cookie_file):
    """
    @brief      Load cookie from file
    @param      cookie_file
    @param      user_agent
    @return     cookie, LWPCookieJar
    """
    cookie = cookielib.LWPCookieJar(cookie_file)
    try:
        cookie.load(ignore_discard=True)
    except:
        Log.error(traceback.format_exc())
    opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
    opener.addheaders = Constant.HTTP_HEADER_USERAGENT
    urllib2.install_opener(opener)
    return cookie


def generate_file_name(filename):
    """
    @brief      generate file name
    @return     new file name
    """
    i = filename.rfind('.')
    ext = filename[i:]
    tmp = filename + str(int(time.time()))
    hash_md5 = hashlib.md5(tmp)
    return hash_md5.hexdigest() + ext


def save_file(filename, data, dirName):
    """
    @brief      Saves raw data to file.
    @param      filename  String
    @param      data      Binary data
    @param      dirName   String
    @return     file path
    """
    Log.debug('save file: ' + filename)
    fn = filename
    if not os.path.exists(dirName):
        os.makedirs(dirName)
    fn = os.path.join(dirName, filename)
    with open(fn, 'wb') as f:
        f.write(data)
    return fn


def save_json(filename, data, dirName, mode='w+'):
    """
    @brief      Saves dict to json file.
    @param      filename  String
    @param      data      Dict
    @param      dirName   String
    @return     file path
    """
    Log.debug('save json: ' + filename)
    fn = filename
    if not os.path.exists(dirName):
        os.makedirs(dirName)
    fn = os.path.join(dirName, filename)
    with open(fn, mode) as f:
        f.write(json.dumps(data, indent=4)+'\n')
    return fn


def load_json(filepath):
    Log.debug('load json: ' + filepath)
    with open(filepath, 'r') as f:
        return _decode_data(json.loads(f.read()))


def pickle_save(data, file):
    """
    @brief      Use pickle to save python object into file
    @param      data  The pyhton data
    @param      file  The file
    """
    basedir = os.path.dirname(file)
    if not os.path.exists(basedir):
        os.makedirs(basedir)
    with open(file, 'wb') as f:
        pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)


def pickle_load(file):
    """
    @brief      Use pickle to load python object from file
    @param      file  The file
    @return     python data
    """
    if os.path.isfile(file):
        with open(file, 'rb') as f:
            return pickle.load(f)
    return None


def search_content(key, content, fmat='attr'):
    """
    @brief      Search content from xml or html format
    @param      key      String
    @param      content  String
    @param      fmat     attr
                         xml
    @return     String
    """
    if fmat == 'attr':
        pm = re.search(key + '\s?=\s?"([^"<]+)"', content)
        if pm:
            return pm.group(1)
    elif fmat == 'xml':
        pm = re.search('<{0}>([^<]+)</{0}>'.format(key), content)
        if not pm:
            pm = re.search(
                '<{0}><\!\[CDATA\[(.*?)\]\]></{0}>'.format(key), content)
        if pm:
            return pm.group(1)
    return 'unknown'


def is_str(s):
    """
    @brief      Determines if string.
    @param      s     String
    @return     True if string, False otherwise.
    """
    return isinstance(s, basestring)


def trans_coding(data):
    """
    @brief      Transform string to unicode
    @param      data  String
    @return     unicode
    """
    if not data:
        return data
    result = None
    if type(data) == unicode:
        result = data
    elif type(data) == str:
        result = data.decode('utf-8')
    return result


def trans_emoji(text):
    """
    @brief      Transform emoji html text to unicode
    @param      text  String
    @return     emoji unicode
    """
    def _emoji(matched):
        hex = matched.group(1)
        return ('\\U%08x' % int(hex, 16)).decode('unicode-escape').encode('utf-8')

    replace_t = re.sub(Constant.REGEX_EMOJI, _emoji, text)
    return replace_t


def auto_reload(mod):
    """
    @brief      reload modules
    @param      mod: the need reload modules
    """
    try:
        module = sys.modules[mod]
    except:
        Log.error(traceback.format_exc())
        return False

    filename = module.__file__
    # .pyc 修改时间不会变
    # 所以就用 .py 的修改时间
    if filename.endswith(".pyc"):
        filename = filename.replace(".pyc", ".py")
    mod_time = os.path.getmtime(filename)
    if not "loadtime" in module.__dict__:
        module.loadtime = 0

    try:
        if mod_time > module.loadtime:
            reload(module)
        else:
            return False
    except:
        Log.error(traceback.format_exc())
        return False

    module.loadtime = mod_time

    echo('[*] load \'%s\' successful.\n' % mod)
    return True


def split_array(arr, n):
    for i in xrange(0, len(arr), n):
        yield arr[i:i+n]



================================================
FILE: wxbot_project_py2.7/wechat/wechat.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from utils import *
from wechat_apis import WXAPI
from config import ConfigManager
from config import Constant
from config import Log
#---------------------------------------------------
import json
import re
import sys
import os
import time
import random
from collections import defaultdict
from datetime import timedelta
import traceback
import Queue
import threading
#===================================================


class WeChat(WXAPI):

    def __str__(self):
        description = \
            "=========================\n" + \
            "[#] Web WeChat\n" + \
            "[#] UUID: " + self.uuid + "\n" + \
            "[#] Uin: " + str(self.uin) + "\n" + \
            "[#] Sid: " + self.sid + "\n" + \
            "[#] Skey: " + self.skey + "\n" + \
            "[#] DeviceId: " + self.device_id + "\n" + \
            "[#] PassTicket: " + self.pass_ticket + "\n" + \
            "[#] Run Time: " + self.get_run_time() + '\n' + \
            "========================="
        return description

    def __init__(self, host='wx.qq.com'):
        super(WeChat, self).__init__(host)

        self.db = None
        self.save_data_folder = ''  # 保存图片,语音,小视频的文件夹
        self.last_login = 0  # 上次退出的时间
        self.time_out = 5  # 同步时间间隔(单位:秒)
        self.msg_handler = None
        self.start_time = time.time()
        self.bot = None

        cm = ConfigManager()
        self.save_data_folders = cm.get_wechat_media_dir()
        self.cookie_file = cm.get_cookie()
        self.pickle_file = cm.get_pickle_files()
        self.log_mode = cm.get('setting', 'log_mode') == 'True'
        self.exit_code = 0

    def start(self):
        echo(Constant.LOG_MSG_START)
        run(Constant.LOG_MSG_RECOVER, self.recover)

        timeOut = time.time() - self.last_login
        echo(Constant.LOG_MSG_TRY_INIT)
        if self.webwxinit():
            echo(Constant.LOG_MSG_SUCCESS)
            run(Constant.LOG_MSG_RECOVER_CONTACT, self.recover_contacts)
        else:
            echo(Constant.LOG_MSG_FAIL)

            while True:
                # first try to login by uin without qrcode
                echo(Constant.LOG_MSG_ASSOCIATION_LOGIN)
                if self.association_login():
                    echo(Constant.LOG_MSG_SUCCESS)
                else:
                    echo(Constant.LOG_MSG_FAIL)
                    # scan qrcode to login
                    run(Constant.LOG_MSG_GET_UUID, self.getuuid)
                    echo(Constant.LOG_MSG_GET_QRCODE)
                    self.genqrcode()
                    echo(Constant.LOG_MSG_SCAN_QRCODE)

                if not self.waitforlogin():
                    continue
                echo(Constant.LOG_MSG_CONFIRM_LOGIN)
                if not self.waitforlogin(0):
                    continue
                break

            run(Constant.LOG_MSG_LOGIN, self.login)
            run(Constant.LOG_MSG_INIT, self.webwxinit)
            run(Constant.LOG_MSG_STATUS_NOTIFY, self.webwxstatusnotify)
            run(Constant.LOG_MSG_GET_CONTACT, self.webwxgetcontact)
            echo(Constant.LOG_MSG_CONTACT_COUNT % (
                    self.MemberCount, len(self.MemberList)
                ))
            echo(Constant.LOG_MSG_OTHER_CONTACT_COUNT % (
                    len(self.GroupList), len(self.ContactList),
                    len(self.SpecialUsersList), len(self.PublicUsersList)
                ))
            run(Constant.LOG_MSG_GET_GROUP_MEMBER, self.fetch_group_contacts)

        run(Constant.LOG_MSG_SNAPSHOT, self.snapshot)

        while True:
            [retcode, selector] = self.synccheck()
            Log.debug('retcode: %s, selector: %s' % (retcode, selector))
            self.exit_code = int(retcode)

            if retcode == '1100':
                echo(Constant.LOG_MSG_LOGOUT)
                break
            if retcode == '1101':
                echo(Constant.LOG_MSG_LOGIN_OTHERWHERE)
                break
            if retcode == '1102':
                echo(Constant.LOG_MSG_QUIT_ON_PHONE)
                break
            elif retcode == '0':
                if selector == '2':
                    r = self.webwxsync()
                    if r is not None:
                        try:
                            self.handle_msg(r)
                        except:
                            Log.error(traceback.format_exc())
                elif selector == '7':
                    r = self.webwxsync()
                elif selector == '0':
                    time.sleep(self.time_out)
                elif selector == '4':
                    # 保存群聊到通讯录
                    # 修改群名称
                    # 新增或删除联系人
                    # 群聊成员数目变化
                    r = self.webwxsync()
                    if r is not None:
                        try:
                            self.handle_mod(r)
                        except:
                            Log.error(traceback.format_exc())
                elif selector == '3' or selector == '6':
                    break
            else:
                r = self.webwxsync()
                Log.debug('webwxsync: %s\n' % json.dumps(r))

            # 执行定时任务
            if self.msg_handler:
                self.msg_handler.check_schedule_task()

            # if self.bot:
            #     r = self.bot.time_schedule()
            #     if r:
            #         for g in self.GroupList:
            #             echo('[*] 推送 -> %s: %s' % (g['NickName'], r))
            #             g_id = g['UserName']
            #             self.webwxsendmsg(r, g_id)

    def get_run_time(self):
        """
        @brief      get how long this run
        @return     String
        """
        totalTime = int(time.time() - self.start_time)
        t = timedelta(seconds=totalTime)
        return '%s Day %s' % (t.days, t)

    def stop(self):
        """
        @brief      Save some data and use shell to kill this process
        """
        run(Constant.LOG_MSG_SNAPSHOT, self.snapshot)
        echo(Constant.LOG_MSG_RUNTIME % self.get_run_time())
        # close database connect
        self.db.close()

    def fetch_group_contacts(self):
        """
        @brief      Fetches all groups contacts.
        @return     Bool: whether operation succeed.
        @note       This function must be finished in 180s
        """
        Log.debug('fetch_group_contacts')
        # clean database
        if self.msg_handler:
            self.msg_handler.clean_db()

        # sqlite
        # ----------------------------------------------------
        # group max_thread_num  max_fetch_group_num    time(s)
        # 197      10                 10               108
        # 197      10                 15               95
        # 197      20                 10               103
        # 197      10                 20               55
        # 197       5                 30               39
        # 197       4                 50               35
        # ----------------------------------------------------
        # mysql
        # ----------------------------------------------------
        # group max_thread_num  max_fetch_group_num    time(s)
        # 197       4                 50               20
        # ----------------------------------------------------

        max_thread_num = 4
        max_fetch_group_num = 50
        group_list_queue = Queue.Queue()

        class GroupListThread(threading.Thread):

            def __init__(self, group_list_queue, wechat):
                threading.Thread.__init__(self)
                self.group_list_queue = group_list_queue
                self.wechat = wechat

            def run(self):
                while not self.group_list_queue.empty():
                    g_list = self.group_list_queue.get()
                    gid_list = []
                    g_dict = {}
                    for g in g_list:
                        gid = g['UserName']
                        gid_list.append(gid)
                        g_dict[gid] = g

                    group_member_list = self.wechat.webwxbatchgetcontact(gid_list)

                    for member_list in group_member_list:
                        gid = member_list['UserName']
                        g = g_dict[gid]
                        g['MemberCount'] = member_list['MemberCount']
                        g['OwnerUin'] = member_list['OwnerUin']
                        self.wechat.GroupMemeberList[gid] = member_list['MemberList']

                        # 如果使用 Mysql 则可以在多线程里操作数据库
                        # 否则请注释下列代码在主线程里更新群列表
                        # -----------------------------------
                        # 处理群成员
                        # if self.wechat.msg_handler:
                        #     self.wechat.msg_handler.handle_group_member_list(gid, member_list['MemberList'])
                        # -----------------------------------

                    self.group_list_queue.task_done()

        for g_list in split_array(self.GroupList, max_fetch_group_num):
            group_list_queue.put(g_list)

        for i in range(max_thread_num):
            t = GroupListThread(group_list_queue, self)
            t.setDaemon(True)
            t.start()

        group_list_queue.join()

        if self.msg_handler:
            # 处理群
            if self.GroupList:
                self.msg_handler.handle_group_list(self.GroupList)

            # 这个是用 sqlite 来存储群列表,sqlite 对多线程的支持不太好
            # ----------------------------------------------------
            # 处理群成员
            for (gid, member_list) in self.GroupMemeberList.items():
                self.msg_handler.handle_group_member_list(gid, member_list)
            # ----------------------------------------------------

        return True

    def snapshot(self):
        """
        @brief      Save basic infos for next login.
        @return     Bool: whether operation succeed.
        """
        try:
            conf = {
                'uuid': self.uuid,
                'redirect_uri': self.redirect_uri,
                'uin': self.uin,
                'sid': self.sid,
                'skey': self.skey,
                'pass_ticket': self.pass_ticket,
                'synckey': self.synckey,
                'device_id': self.device_id,
                'last_login': time.time(),
            }
            cm = ConfigManager()
            Log.debug('save wechat config')
            cm.set_wechat_config(conf)

            # save cookie
            Log.debug('save cookie')
            if self.cookie:
                self.cookie.save(ignore_discard=True)

            # save contacts
            Log.debug('save contacts')
            self.save_contacts()
        except Exception, e:
            Log.error(traceback.format_exc())
            return False
        return True

    def recover(self):
        """
        @brief      Recover from snapshot data.
        @return     Bool: whether operation succeed.
        """
        cm = ConfigManager()
        [self.uuid, self.redirect_uri, self.uin,
        self.sid, self.skey, self.pass_ticket,
        self.synckey, device_id, self.last_login] = cm.get_wechat_config()

        if device_id:
            self.device_id = device_id

        self.base_request = {
            'Uin': int(self.uin),
            'Sid': self.sid,
            'Skey': self.skey,
            'DeviceID': self.device_id,
        }

        # set cookie
        Log.debug('set cookie')
        self.cookie = set_cookie(self.cookie_file)

        return True

    def save_contacts(self):
        """
        @brief      Save contacts.
        """
        pickle_save(self.User, self.pickle_file['User'])
        pickle_save(self.MemberList, self.pickle_file['MemberList'])
        pickle_save(self.GroupList, self.pickle_file['GroupList'])
        pickle_save(self.GroupMemeberList, self.pickle_file['GroupMemeberList'])
        pickle_save(self.SpecialUsersList, self.pickle_file['SpecialUsersList'])

    def recover_contacts(self):
        """
        @brief      recover contacts.
        @return     Bool: whether operation succeed.
        """
        try:
            self.User = pickle_load(self.pickle_file['User'])
            self.MemberList = pickle_load(self.pickle_file['MemberList'])
            self.GroupList = pickle_load(self.pickle_file['GroupList'])
            self.GroupMemeberList = pickle_load(self.pickle_file['GroupMemeberList'])
            self.SpecialUsersList = pickle_load(self.pickle_file['SpecialUsersList'])
            return True
        except Exception, e:
            Log.error(traceback.format_exc())
        return False

    def handle_mod(self, r):
        # ModContactCount: 变更联系人或群聊成员数目
        # ModContactList: 变更联系人或群聊列表,或群名称改变
        Log.debug('handle modify')
        self.handle_msg(r)
        for m in r['ModContactList']:
            if m['UserName'][:2] == '@@':
                # group
                in_list = False
                g_id = m['UserName']
                for g in self.GroupList:
                    # group member change
                    if g_id == g['UserName']:
                        g['MemberCount'] = m['MemberCount']
                        g['NickName'] = m['NickName']
                        self.GroupMemeberList[g_id] = m['MemberList']
                        in_list = True
                        if self.msg_handler:
                            self.msg_handler.handle_group_member_change(g_id, m['MemberList'])
                        break
                if not in_list:
                    # a new group
                    self.GroupList.append(m)
                    self.GroupMemeberList[g_id] = m['MemberList']
                    if self.msg_handler:
                        self.msg_handler.handle_group_list_change(m)
                        self.msg_handler.handle_group_member_change(g_id, m['MemberList'])

            elif m['UserName'][0] == '@':
                # user
                in_list = False
                for u in self.MemberList:
                    u_id = m['UserName']
                    if u_id == u['UserName']:
                        u = m
                        in_list = True
                        break
                # if don't have then add it
                if not in_list:
                    self.MemberList.append(m)

    def handle_msg(self, r):
        """
        @brief      Recover from snapshot data.
        @param      r  Dict: message json
        """
        Log.debug('handle message')
        if self.msg_handler:
            self.msg_handler.handle_wxsync(r)

        n = len(r['AddMsgList'])
        if n == 0:
            return

        if self.log_mode:
            echo(Constant.LOG_MSG_NEW_MSG % n)

        for msg in r['AddMsgList']:

            msgType = msg['MsgType']
            msgId = msg['MsgId']
            content = msg['Content'].replace('&lt;', '<').replace('&gt;', '>')
            raw_msg = None

            if msgType == self.wx_conf['MSGTYPE_TEXT']:
                # 地理位置消息
                if content.find('pictype=location') != -1:
                    location = content.split('<br/>')[1][:-1]
                    raw_msg = {
                        'raw_msg': msg,
                        'location': location,
                        'log': Constant.LOG_MSG_LOCATION % location
                    }
                # 普通文本消息
                else:
                    text = content.split(':<br/>')[-1]
                    raw_msg = {
                        'raw_msg': msg,
                        'text': text,
                        'log': text.replace('<br/>', '\n')
                    }
            elif msgType == self.wx_conf['MSGTYPE_IMAGE']:
                data = self.webwxgetmsgimg(msgId)
                fn = 'img_' + msgId + '.jpg'
                dir = self.save_data_folders['webwxgetmsgimg']
                path = save_file(fn, data, dir)
                raw_msg = {'raw_msg': msg,
                           'image': path,
                           'log': Constant.LOG_MSG_PICTURE % path}
            elif msgType == self.wx_conf['MSGTYPE_VOICE']:
                data = self.webwxgetvoice(msgId)
                fn = 'voice_' + msgId + '.mp3'
                dir = self.save_data_folders['webwxgetvoice']
                path = save_file(fn, data, dir)
                raw_msg = {'raw_msg': msg,
                           'voice': path,
                           'log': Constant.LOG_MSG_VOICE % path}
            elif msgType == self.wx_conf['MSGTYPE_SHARECARD']:
                info = msg['RecommendInfo']
                card = Constant.LOG_MSG_NAME_CARD % (
                    info['NickName'],
                    info['Alias'],
                    info['Province'], info['City'],
                    Constant.LOG_MSG_SEX_OPTION[info['Sex']]
                )
                namecard = '%s %s %s %s %s' % (
                    info['NickName'], info['Alias'], info['Province'],
                    info['City'], Constant.LOG_MSG_SEX_OPTION[info['Sex']]
                )
                raw_msg = {
                    'raw_msg': msg,
                    'namecard': namecard,
                    'log': card
                }
            elif msgType == self.wx_conf['MSGTYPE_EMOTICON']:
                url = search_content('cdnurl', content)
                raw_msg = {'raw_msg': msg,
                           'emoticon': url,
                           'log': Constant.LOG_MSG_EMOTION % url}
            elif msgType == self.wx_conf['MSGTYPE_APP']:
                card = ''
                # 链接, 音乐, 微博
                if msg['AppMsgType'] in [
                    self.wx_conf['APPMSGTYPE_AUDIO'],
                    self.wx_conf['APPMSGTYPE_URL'],
                    self.wx_conf['APPMSGTYPE_OPEN']
                ]:
                    card = Constant.LOG_MSG_APP_LINK % (
                        Constant.LOG_MSG_APP_LINK_TYPE[msg['AppMsgType']],
                        msg['FileName'],
                        search_content('des', content, 'xml'),
                        msg['Url'],
                        search_content('appname', content, 'xml')
                    )
                    raw_msg = {
                        'raw_msg': msg,
                        'link': msg['Url'],
                        'log': card
                    }
                # 图片
                elif msg['AppMsgType'] == self.wx_conf['APPMSGTYPE_IMG']:
                    data = self.webwxgetmsgimg(msgId)
                    fn = 'img_' + msgId + '.jpg'
                    dir = self.save_data_folders['webwxgetmsgimg']
                    path = save_file(fn, data, dir)
                    card = Constant.LOG_MSG_APP_IMG % (
                        path,
                        search_content('appname', content, 'xml')
                    )
                    raw_msg = {
                        'raw_msg': msg,
                        'image': path,
                        'log': card
                    }
                else:
                    raw_msg = {
                        'raw_msg': msg,
                        'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content)
                    }
            elif msgType == self.wx_conf['MSGTYPE_STATUSNOTIFY']:
                Log.info(Constant.LOG_MSG_NOTIFY_PHONE)
            elif msgType == self.wx_conf['MSGTYPE_MICROVIDEO']:
                data = self.webwxgetvideo(msgId)
                fn = 'video_' + msgId + '.mp4'
                dir = self.save_data_folders['webwxgetvideo']
                path = save_file(fn, data, dir)
                raw_msg = {'raw_msg': msg,
                           'video': path,
                           'log': Constant.LOG_MSG_VIDEO % path}
            elif msgType == self.wx_conf['MSGTYPE_RECALLED']:
                recall_id = search_content('msgid', content, 'xml')
                text = Constant.LOG_MSG_RECALL
                raw_msg = {
                    'raw_msg': msg,
                    'text': text,
                    'recall_msg_id': recall_id,
                    'log': text
                }
            elif msgType == self.wx_conf['MSGTYPE_SYS']:
                raw_msg = {
                    'raw_msg': msg,
                    'sys_notif': content,
                    'log': content
                }
            elif msgType == self.wx_conf['MSGTYPE_VERIFYMSG']:
                name = search_content('fromnickname', content)
                raw_msg = {
                    'raw_msg': msg,
                    'log': Constant.LOG_MSG_ADD_FRIEND % name
                }
            else:
                raw_msg = {
                    'raw_msg': msg,
                    'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content)
                }

            isGroupMsg = '@@' in msg['FromUserName']+msg['ToUserName']
            if self.msg_handler and raw_msg:
                if isGroupMsg:
                    # handle group messages
                    g_msg = self.make_group_msg(raw_msg)
                    self.msg_handler.handle_group_msg(g_msg)
                else:
                    # handle personal messages
                    self.msg_handler.handle_user_msg(raw_msg)

            if self.log_mode:
                self.show_msg(raw_msg)

    def make_group_msg(self, msg):
        """
        @brief      Package the group message for storage.
        @param      msg  Dict: raw msg
        @return     raw_msg Dict: packged msg
        """
        Log.debug('make group message')
        raw_msg = {
            'raw_msg': msg['raw_msg'],
            'msg_id': msg['raw_msg']['MsgId'],
            'group_owner_uin': '',
            'group_name': '',
            'group_count': '',
            'from_user_name': msg['raw_msg']['FromUserName'],
            'to_user_name': msg['raw_msg']['ToUserName'],
            'user_attrstatus': '',
            'user_display_name': '',
            'user_nickname': '',
            'msg_type': msg['raw_msg']['MsgType'],
            'text': '',
            'link': '',
            'image': '',
            'video': '',
            'voice': '',
            'emoticon': '',
            'namecard': '',
            'location': '',
            'recall_msg_id': '',
            'sys_notif': '',
            'time': '',
            'timestamp': '',
            'log': '',
        }
        content = msg['raw_msg']['Content'].replace(
            '&lt;', '<').replace('&gt;', '>')

        group = None
        src = None

        if msg['raw_msg']['FromUserName'][:2] == '@@':
            # 接收到来自群的消息
            g_id = msg['raw_msg']['FromUserName']
            group = self.get_group_by_id(g_id)

            if re.search(":<br/>", content, re.IGNORECASE):
                u_id = content.split(':<br/>')[0]
                src = self.get_group_user_by_id(u_id, g_id)

        elif msg['raw_msg']['ToUserName'][:2] == '@@':
            # 自己发给群的消息
            g_id = msg['raw_msg']['ToUserName']
            u_id = msg['raw_msg']['FromUserName']
            src = self.get_group_user_by_id(u_id, g_id)
            group = self.get_group_by_id(g_id)

        if src:
            raw_msg['user_attrstatus'] = src['AttrStatus']
            raw_msg['user_display_name'] = src['DisplayName']
            raw_msg['user_nickname'] = src['NickName']
        if group:
            raw_msg['group_count'] = group['MemberCount']
            raw_msg['group_owner_uin'] = group['OwnerUin']
            raw_msg['group_name'] = group['ShowName']

        raw_msg['timestamp'] = msg['raw_msg']['CreateTime']
        t = time.localtime(float(raw_msg['timestamp']))
        raw_msg['time'] = time.strftime("%Y-%m-%d %T", t)

        for key in [
            'text', 'link', 'image', 'video', 'voice',
            'emoticon', 'namecard', 'location', 'log',
            'recall_msg_id', 'sys_notif'
        ]:
            if key in msg:
                raw_msg[key] = msg[key]

        return raw_msg

    def show_msg(self, message):
        """
        @brief      Log the message to stdout
        @param      message  Dict
        """
        msg = message
        src = None
        dst = None
        group = None

        if msg and msg['raw_msg']:

            content = msg['raw_msg']['Content']
            content = content.replace('&lt;', '<').replace('&gt;', '>')
            msg_id = msg['raw_msg']['MsgId']

            if msg['raw_msg']['FromUserName'][:2] == '@@':
                # 接收到来自群的消息
                g_id = msg['raw_msg']['FromUserName']
                group = self.get_group_by_id(g_id)

                if re.search(":<br/>", content, re.IGNORECASE):
                    u_id = content.split(':<br/>')[0]
                    src = self.get_group_user_by_id(u_id, g_id)
                    dst = {'ShowName': 'GROUP'}
                else:
                    u_id = msg['raw_msg']['ToUserName']
                    src = {'ShowName': 'SYSTEM'}
                    dst = self.get_group_user_by_id(u_id, g_id)
            elif msg['raw_msg']['ToUserName'][:2] == '@@':
                # 自己发给群的消息
                g_id = msg['raw_msg']['ToUserName']
                u_id = msg['raw_msg']['FromUserName']
                group = self.get_group_by_id(g_id)
                src = self.get_group_user_by_id(u_id, g_id)
                dst = {'ShowName': 'GROUP'}
            else:
                # 非群聊消息
                src = self.get_user_by_id(msg['raw_msg']['FromUserName'])
                dst = self.get_user_by_id(msg['raw_msg']['ToUserName'])

            if group:
                echo('%s |%s| %s -> %s: %s\n' % (
                    msg_id,
                    trans_emoji(group['ShowName']),
                    trans_emoji(src['ShowName']),
                    dst['ShowName'],
                    trans_emoji(msg['log'])
                ))
            else:
                echo('%s %s -> %s: %s\n' % (
                    msg_id,
                    trans_emoji(src['ShowName']),
                    trans_emoji(dst['ShowName']),
                    trans_emoji(msg['log'])
                ))


================================================
FILE: wxbot_project_py2.7/wechat/wechat_apis.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from utils import *
from config import Constant
from config import Log
#---------------------------------------------------
import sys
import os
import cookielib
import random
import requests
import time
import xml.dom.minidom
# for media upload
import mimetypes
from requests_toolbelt.multipart.encoder import MultipartEncoder
#===================================================


class WXAPI(object):

    def __init__(self, host):
        self.wx_host = host
        self.wx_filehost = ''
        self.wx_conf = {}
        # jsLogin时这个appid只能使用: wx782c26e4c19acffb
        self.appid = Constant.API_APPID
        self.uuid = ''
        self.redirect_uri = ''
        self.skey = ''
        self.sid = ''
        self.uin = ''
        self.pass_ticket = ''
        self.base_request = {}
        self.synckey_dic = {}
        self.synckey = ''
        self.device_id = 'e' + repr(random.random())[2:17]
        # device_id: 登录手机设备
        # web wechat 的格式为: e123456789012345 (e+15位随机数)
        # mobile wechat 的格式为: A1234567890abcde (A+15位随机数字或字母)
        self.user_agent = Constant.API_USER_AGENT
        self.cookie = None

        self.conf_factory()

        self.User = []  # 登陆账号信息
        self.MemberList = []  # 好友+群聊+公众号+特殊账号
        self.MemberCount = 0
        self.ContactList = []  # 好友
        self.GroupList = []  # 群
        self.GroupMemeberList = {}  # 群聊成员字典
                                    # "group_id": [
                                    #       {member}, ...
                                    # ]
        self.PublicUsersList = []  # 公众号/服务号
        self.SpecialUsersList = []  # 特殊账号

        self.media_count = 0

    def conf_factory(self):
        e = self.wx_host  # wx.qq.com
        t, o, n = "login.weixin.qq.com", "file.wx.qq.com", "webpush.weixin.qq.com"

        if e.find("wx2.qq.com") > -1:
            t, o, n = "login.wx2.qq.com", "file.wx2.qq.com", "webpush.wx2.qq.com"
        elif e.find("wx8.qq.com") > -1:
            t, o, n = "login.wx8.qq.com", "file.wx8.qq.com", "webpush.wx8.qq.com"
        elif e.find("qq.com") > -1:
            t, o, n = "login.wx.qq.com", "file.wx.qq.com", "webpush.wx.qq.com"
        elif e.find("web2.wechat.com") > -1:
            t, o, n = "login.web2.wechat.com", "file.web2.wechat.com", "webpush.web2.wechat.com"
        elif e.find("wechat.com") > -1:
            t, o, n = "login.web.wechat.com", "file.web.wechat.com", "webpush.web.wechat.com"

        self.wx_filehost = o
        conf = {
            'LANG': Constant.API_LANG,
            'SpecialUsers': Constant.API_SPECIAL_USER,
            'API_jsLogin': "https://" + t + "/jslogin",
            'API_qrcode': "https://login.weixin.qq.com/l/",
            'API_qrcode_img': "https://login.weixin.qq.com/qrcode/",
            'API_login': "https://" + t + "/cgi-bin/mmwebwx-bin/login",
            'API_synccheck': "https://" + n + "/cgi-bin/mmwebwx-bin/synccheck",
            'API_webwxdownloadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxgetmedia",
            'API_webwxuploadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxuploadmedia",
            'API_webwxpreview': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpreview",
            'API_webwxinit': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxinit",
            'API_webwxgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetcontact",
            'API_webwxsync': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsync",
            'API_webwxbatchgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxbatchgetcontact",
            'API_webwxgeticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgeticon",
            'API_webwxsendmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsg",
            'API_webwxsendmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsgimg",
            'API_webwxsendmsgvedio': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendvideomsg",
            'API_webwxsendemoticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendemoticon",
            'API_webwxsendappmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendappmsg",
            'API_webwxgetheadimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetheadimg",
            'API_webwxgetmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmsgimg",
            'API_webwxgetmedia': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmedia",
            'API_webwxgetvideo': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvideo",
            'API_webwxlogout': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxlogout",
            'API_webwxgetvoice': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvoice",
            'API_webwxupdatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxupdatechatroom",
            'API_webwxcreatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcreatechatroom",
            'API_webwxstatusnotify': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatusnotify",
            'API_webwxcheckurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckurl",
            'API_webwxverifyuser': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxverifyuser",
            'API_webwxfeedback': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendfeedback",
            'API_webwxreport': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatreport",
            'API_webwxsearch': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsearchcontact",
            'API_webwxoplog': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxoplog",
            'API_checkupload': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckupload",
            'API_webwxrevokemsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxrevokemsg",
            'API_webwxpushloginurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpushloginurl",
            'CONTACTFLAG_CONTACT': 1,
            'CONTACTFLAG_CHATCONTACT': 2,
            'CONTACTFLAG_CHATROOMCONTACT': 4,
            'CONTACTFLAG_BLACKLISTCONTACT': 8,
            'CONTACTFLAG_DOMAINCONTACT': 16,
            'CONTACTFLAG_HIDECONTACT': 32,
            'CONTACTFLAG_FAVOURCONTACT': 64,
            'CONTACTFLAG_3RDAPPCONTACT': 128,
            'CONTACTFLAG_SNSBLACKLISTCONTACT': 256,
            'CONTACTFLAG_NOTIFYCLOSECONTACT': 512,
            'CONTACTFLAG_TOPCONTACT': 2048,
            'MSGTYPE_TEXT': 1,
            'MSGTYPE_IMAGE': 3,
            'MSGTYPE_VOICE': 34,
            'MSGTYPE_VIDEO': 43,
            'MSGTYPE_MICROVIDEO': 62,
            'MSGTYPE_EMOTICON': 47,
            'MSGTYPE_APP': 49,
            'MSGTYPE_VOIPMSG': 50,
            'MSGTYPE_VOIPNOTIFY': 52,
            'MSGTYPE_VOIPINVITE': 53,
            'MSGTYPE_LOCATION': 48,
            'MSGTYPE_STATUSNOTIFY': 51,
            'MSGTYPE_SYSNOTICE': 9999,
            'MSGTYPE_POSSIBLEFRIEND_MSG': 40,
            'MSGTYPE_VERIFYMSG': 37,
            'MSGTYPE_SHARECARD': 42,
            'MSGTYPE_SYS': 10000,
            'MSGTYPE_RECALLED': 10002,
            'APPMSGTYPE_TEXT': 1,
            'APPMSGTYPE_IMG': 2,
            'APPMSGTYPE_AUDIO': 3,
            'APPMSGTYPE_VIDEO': 4,
            'APPMSGTYPE_URL': 5,
            'APPMSGTYPE_ATTACH': 6,
            'APPMSGTYPE_OPEN': 7,
            'APPMSGTYPE_EMOJI': 8,
            'APPMSGTYPE_VOICE_REMIND': 9,
            'APPMSGTYPE_SCAN_GOOD': 10,
            'APPMSGTYPE_GOOD': 13,
            'APPMSGTYPE_EMOTION': 15,
            'APPMSGTYPE_CARD_TICKET': 16,
            'APPMSGTYPE_REALTIME_SHARE_LOCATION': 17,
            'APPMSGTYPE_TRANSFERS': 2e3,
            'APPMSGTYPE_RED_ENVELOPES': 2001,
            'APPMSGTYPE_READER_TYPE': 100001,
            'UPLOAD_MEDIA_TYPE_IMAGE': 1,
            'UPLOAD_MEDIA_TYPE_VIDEO': 2,
            'UPLOAD_MEDIA_TYPE_AUDIO': 3,
            'UPLOAD_MEDIA_TYPE_ATTACHMENT': 4,
        }
        self.wx_conf = conf

    def getuuid(self):
        """
        @brief      Gets the uuid just used for login.
        @return     Bool: whether operation succeed.
        """
        url = self.wx_conf['API_jsLogin']
        params = {
            'appid': self.appid,
            'fun': 'new',
            'lang': self.wx_conf['LANG'],
            '_': int(time.time()),
        }
        data = post(url, params, False)
        regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
        pm = re.search(regx, data)
        if pm:
            code = pm.group(1)
            self.uuid = pm.group(2)
            return code == '200'
        return False

    def genqrcode(self):
        """
        @brief      outprint the qrcode to stdout on macos/linux
                    or open image on windows
        """
        if sys.platform.startswith('win'):
            url = self.wx_conf['API_qrcode_img'] + self.uuid
            params = {
                't': 'webwx',
                '_': int(time.time())
            }
            data = post(url, params, False)
            if data == '':
                return
            qrcode_path = save_file('qrcode.jpg', data, './')
            os.startfile(qrcode_path)
        else:
            str2qr_terminal(self.wx_conf['API_qrcode'] + self.uuid)

    def waitforlogin(self, tip=1):
        """
        @brief      wait for scaning qrcode to login
        @param      tip   1: wait for scan qrcode
                          0: wait for confirm
        @return     Bool: whether operation succeed
        """
        time.sleep(tip)
        url = self.wx_conf['API_login'] + '?tip=%s&uuid=%s&_=%s' % (
            tip, self.uuid, int(time.time()))
        data = get(url)
        pm = re.search(r'window.code=(\d+);', data)
        code = pm.group(1)

        if code == '201':
            return True
        elif code == '200':
            pm = re.search(r'window.redirect_uri="(\S+?)";', data)
            r_uri = pm.group(1) + '&fun=new'
            self.redirect_uri = r_uri
            self.wx_host = r_uri.split('://')[1].split('/')[0]
            self.conf_factory()
            return True
        elif code == '408':
            echo(Constant.LOG_MSG_WAIT_LOGIN_ERR1)
        else:
            echo(Constant.LOG_MSG_WAIT_LOGIN_ERR2)
        return False

    def login(self):
        """
        @brief      login
                    redirect_uri 有效时间是从扫码成功后算起,
                    大概是 300 秒,在此期间可以重新登录,但获取的联系人和群ID会改变
        @return     Bool: whether operation succeed
        """
        data = get(self.redirect_uri)
        doc = xml.dom.minidom.parseString(data)
        root = doc.documentElement

        for node in root.childNodes:
            if node.nodeName == 'ret':
                if node.childNodes[0].data != "0":
                    return False
            elif node.nodeName == 'skey':
                self.skey = node.childNodes[0].data
            elif node.nodeName == 'wxsid':
                self.sid = node.childNodes[0].data
            elif node.nodeName == 'wxuin':
                self.uin = node.childNodes[0].data
            elif node.nodeName == 'pass_ticket':
                self.pass_ticket = node.childNodes[0].data

        if '' in (self.skey, self.sid, self.uin, self.pass_ticket):
            return False

        self.base_request = {
            'Uin': int(self.uin),
            'Sid': self.sid,
            'Skey': self.skey,
            'DeviceID': self.device_id,
        }

        return True

    def webwxinit(self):
        """
        @brief      wechat initial
                    掉线后 300 秒可以重新使用此 api 登录
                    获取的联系人和群ID保持不变
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxinit'] + \
            '?pass_ticket=%s&skey=%s&r=%s' % (
            self.pass_ticket, self.skey, int(time.time())
        )
        params = {
            'BaseRequest': self.base_request
        }
        dic = post(url, params)
        self.User = dic['User']
        self.make_synckey(dic)

        return dic['BaseResponse']['Ret'] == 0

    def webwxstatusnotify(self):
        """
        @brief      notify the mobile phone, this not necessary
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxstatusnotify'] + \
            '?lang=%s&pass_ticket=%s' % (
                self.wx_conf['LANG'], self.pass_ticket
            )
        params = {
            'BaseRequest': self.base_request,
            "Code": 3,
            "FromUserName": self.User['UserName'],
            "ToUserName": self.User['UserName'],
            "ClientMsgId": int(time.time())
        }
        dic = post(url, params)

        return dic['BaseResponse']['Ret'] == 0

    def webwxgetcontact(self):
        """
        @brief      get all contacts: people, group, public user, special user
        @return     Bool: whether operation succeed
        """
        SpecialUsers = self.wx_conf['SpecialUsers']
        url = self.wx_conf['API_webwxgetcontact'] + \
            '?pass_ticket=%s&skey=%s&r=%s' % (
                self.pass_ticket, self.skey, int(time.time())
            )
        dic = post(url, {})

        self.MemberCount = dic['MemberCount']
        self.MemberList = dic['MemberList']
        ContactList = self.MemberList[:]
        GroupList = self.GroupList[:]
        PublicUsersList = self.PublicUsersList[:]
        SpecialUsersList = self.SpecialUsersList[:]

        for i in xrange(len(ContactList) - 1, -1, -1):
            Contact = ContactList[i]
            if Contact['VerifyFlag'] & 8 != 0:  # 公众号/服务号
                ContactList.remove(Contact)
                self.PublicUsersList.append(Contact)
            elif Contact['UserName'] in SpecialUsers:  # 特殊账号
                ContactList.remove(Contact)
                self.SpecialUsersList.append(Contact)
            elif Contact['UserName'].find('@@') != -1:  # 群聊
                ContactList.remove(Contact)
                self.GroupList.append(Contact)
            elif Contact['UserName'] == self.User['UserName']:  # 自己
                ContactList.remove(Contact)
        self.ContactList = ContactList

        return True

    def webwxbatchgetcontact(self, gid_list):
        """
        @brief      get group contacts
        @param      gid_list, The list of group id
        @return     List, list of group contacts
        """
        url = self.wx_conf['API_webwxbatchgetcontact'] + \
            '?type=ex&r=%s&pass_ticket=%s' % (
                int(time.time()), self.pass_ticket
            )
        params = {
            'BaseRequest': self.base_request,
            "Count": len(gid_list),
            "List": [{"UserName": gid, "EncryChatRoomId": ""} for gid in gid_list]
        }
        dic = post(url, params)
        return dic['ContactList']

    def synccheck(self):
        """
        @brief      check whether there's a message
        @return     [retcode, selector]
                    retcode: 0    successful
                             1100 logout
                             1101 login otherwhere
                    selector: 0 nothing
                              2 message
                              6 unkonwn
                              7 webwxsync
        """
        params = {
            'r': int(time.time()),
            'sid': self.sid,
            'uin': self.uin,
            'skey': self.skey,
            'deviceid': self.device_id,
            'synckey': self.synckey,
            '_': int(time.time()),
        }
        url = self.wx_conf['API_synccheck'] + '?' + urllib.urlencode(params)
        data = get(url)
        reg = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
        pm = re.search(reg, data)
        retcode = pm.group(1)
        selector = pm.group(2)
        return [retcode, selector]

    def webwxsync(self):
        """
        @brief      sync the messages
        @return     Dict{}
        """
        url = self.wx_conf['API_webwxsync'] + \
            '?sid=%s&skey=%s&pass_ticket=%s' % (
                self.sid, self.skey, self.pass_ticket
            )
        params = {
            'BaseRequest': self.base_request,
            'SyncKey': self.synckey_dic,
            'rr': ~int(time.time())
        }
        dic = post(url, params)

        if dic['BaseResponse']['Ret'] == 0:
            self.make_synckey(dic)
        return dic

    def webwxgetmsgimg(self, msgid):
        """
        @brief      get image in message
        @param      msgid  The id of message
        @return     binary data of image
        """
        url = self.wx_conf['API_webwxgetmsgimg'] + \
            '?MsgID=%s&skey=%s' % (msgid, self.skey)
        data = get(url, api='webwxgetmsgimg')
        return data

    def webwxgetvoice(self, msgid):
        """
        @brief      get voice in message
        @param      msgid  The id of message
        @return     binary data of voice
        """
        url = self.wx_conf['API_webwxgetvoice'] + \
            '?msgid=%s&skey=%s' % (msgid, self.skey)
        data = get(url, api='webwxgetvoice')
        return data

    def webwxgetvideo(self, msgid):
        """
        @brief      get video in message
        @param      msgid  The id of message
        @return     binary data of video
        """
        url = self.wx_conf['API_webwxgetvideo'] + \
            '?msgid=%s&skey=%s' % (msgid, self.skey)
        data = get(url, api='webwxgetvideo')
        return data

    def webwxgeticon(self, id):
        """
        @brief      get user small icon
        @param      id  String
        @return     binary data of icon
        """
        url = self.wx_conf['API_webwxgeticon'] + \
            '?username=%s&skey=%s' % (id, self.skey)
        data = get(url, api='webwxgeticon')
        return data

    def webwxgetheadimg(self, id):
        """
        @brief      get user head image
        @param      id  String
        @return     binary data of image
        """
        url = self.wx_conf['API_webwxgetheadimg'] + \
            '?username=%s&skey=%s' % (id, self.skey)
        data = get(url, api='webwxgetheadimg')
        return data

    def webwxsendmsg(self, word, to='filehelper'):
        """
        @brief      send text message
        @param      word  String
        @param      to    User id
        @return     dic   Dict
        """
        url = self.wx_conf['API_webwxsendmsg'] + \
            '?pass_ticket=%s' % (self.pass_ticket)
        clientMsgId = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        params = {
            'BaseRequest': self.base_request,
            'Msg': {
                "Type": 1,
                "Content": trans_coding(word),
                "FromUserName": self.User['UserName'],
                "ToUserName": to,
                "LocalID": clientMsgId,
                "ClientMsgId": clientMsgId
            }
        }
        dic = post(url, params)
        return dic

    def webwxuploadmedia(self, file_path):
        """
        @brief      upload image
        @param      file_path  String
        @return     Dict: json
        """
        url = self.wx_conf['API_webwxuploadmedia'] + '?f=json'
        # 计数器
        self.media_count = self.media_count + 1
        fn = file_path
        # mime_type: 
        #   'application/pdf'
        #   'image/jpeg'
        #   'image/png'
        #   ...
        mime_type = mimetypes.guess_type(fn, strict=False)[0]
        if not mime_type:
            mime_type = 'text/plain'
        # 文档格式
        # 微信服务器目前应该支持3种类型:
        #   pic     直接显示,包含图片,表情
        #   video   不清楚
        #   doc     显示为文件,包含PDF等
        media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc'
        time_format = "%a %b %d %Y %T GMT%z (%Z)"
        last_modifie_date = time.strftime(time_format, time.localtime())
        file_size = os.path.getsize(fn)
        pass_ticket = self.pass_ticket
        client_media_id = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')

        webwx_data_ticket = ''
        for item in self.cookie:
            if item.name == 'webwx_data_ticket':
                webwx_data_ticket = item.value
                break
        if (webwx_data_ticket == ''):
            Log.error("No Cookie\n")
            return None

        uploadmediarequest = json.dumps({
            "BaseRequest": self.base_request,
            "ClientMediaId": client_media_id,
            "TotalLen": file_size,
            "StartPos": 0,
            "DataLen": file_size,
            "MediaType": 4
        }, ensure_ascii=False).encode('utf8')

        multipart_encoder = MultipartEncoder(
            fields={
                'id': 'WU_FILE_' + str(self.media_count),
                'name': fn,
                'type': mime_type,
                'lastModifieDate': last_modifie_date,
                'size': str(file_size),
                'mediatype': media_type,
                'uploadmediarequest': uploadmediarequest,
                'webwx_data_ticket': webwx_data_ticket,
                'pass_ticket': pass_ticket,
                'filename': (
                    fn,
                    open(fn, 'rb'),
                    mime_type.split('/')[1]
                )
            },
            boundary=(
                '-----------------------------'
                '1575017231431605357584454111'
            )
        )

        headers = {
            'Host': self.wx_filehost,
            'User-Agent': self.user_agent,
            'Accept': (
                'text/html,application/xhtml+xml,'
                'application/xml;q=0.9,*/*;q=0.8'
            ),
            'Accept-Language': 'en-US,en;q=0.5',
            'Accept-Encoding': 'gzip, deflate',
            'Referer': 'https://' + self.wx_host,
            'Content-Type': multipart_encoder.content_type,
            'Origin': 'https://' + self.wx_host,
            'Connection': 'keep-alive',
            'Pragma': 'no-cache',
            'Cache-Control': 'no-cache',
        }

        r = requests.post(url, data=multipart_encoder, headers=headers)
        dic = json.loads(r.text)  #修复无法发送Media消息BUG
        if dic['BaseResponse']['Ret'] == 0:
            return dic
        return None

    def webwxsendmsgimg(self, user_id, media_id):
        """
        @brief      send image
        @param      user_id  String
        @param      media_id  String
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxsendmsgimg'] + \
            '?fun=async&f=json&pass_ticket=%s' % self.pass_ticket
        clientMsgId = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        data_json = {
            "BaseRequest": self.base_request,
            "Msg": {
                "Type": 3,
                "MediaId": media_id,
                "FromUserName": self.User['UserName'],
                "ToUserName": user_id,
                "LocalID": clientMsgId,
                "ClientMsgId": clientMsgId
            }
        }
        r = post(url, data_json)
        return dic['BaseResponse']['Ret'] == 0

    def webwxsendemoticon(self, user_id, media_id):
        """
        @brief      send image
        @param      user_id  String
        @param      media_id  String
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxsendemoticon'] + \
            '?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket
        clientMsgId = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        data_json = {
            "BaseRequest": self.base_request,
            "Msg": {
                "Type": 47,
                "EmojiFlag": 2,
                "MediaId": media_id,
                "FromUserName": self.User['UserName'],
                "ToUserName": user_id,
                "LocalID": clientMsgId,
                "ClientMsgId": clientMsgId
            }
        }
        r = post(url, data_json)
        return dic['BaseResponse']['Ret'] == 0

    def webwxsendappmsg(self, user_id, data):
        """
        @brief      send app msg
        @param      user_id  String
        @param      data     Dict
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxsendappmsg'] + \
            '?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket
        clientMsgId = str(int(time.time() * 1000)) + \
            str(random.random())[:5].replace('.', '')
        content = ''.join([
            "<appmsg appid='%s' sdkver=''>" % data['appid'], # 可使用其它AppID
                "<title>%s</title>" % data['title'],
                "<des></des>",
                "<action></action>",
                "<type>%d</type>" % data['type'],
                "<content></content>",
                "<url></url>",
                "<lowurl></lowurl>",
                "<appattach>",
                    "<totallen>%d</totallen>" % data['totallen'],
                    "<attachid>%s</attachid>" % data['attachid'],
                    "<fileext>%s</fileext>" % data['fileext'],
                "</appattach>",
                "<extinfo></extinfo>",
            "</appmsg>",
        ])
        data_json = {
            "BaseRequest": self.base_request,
            "Msg": {
                "Type": data['type'],
                "Content": content,
                "FromUserName": self.User['UserName'],
                "ToUserName": user_id,
                "LocalID": clientMsgId,
                "ClientMsgId": clientMsgId
            },
            "Scene": 0
        }
        r = post(url, data_json)
        return dic['BaseResponse']['Ret'] == 0

    def webwxcreatechatroom(self, uid_arr):
        """
        @brief      create a chat group
        @param      uid_arr  [String]
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxcreatechatroom'] + '?r=%s' % int(time.time())
        params = {
            'BaseRequest': self.base_request,
            'Topic': '',
            'MemberCount': len(uid_arr),
            'MemberList': [{'UserName': uid} for uid in uid_arr],
        }
        dic = post(url, params)
        return dic['BaseResponse']['Ret'] == 0

    def webwxupdatechatroom(self, add_arr, del_arr, invite_arr):
        """
        @brief      add/delete/invite member in chat group
        @param      add_arr     [uid: String]
        @param      del_arr     [uid: String]
        @param      invite_arr  [uid: String]
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxupdatechatroom'] + '?r=%s' % int(time.time())
        params = {
            'BaseRequest': self.base_request,
            'ChatRoomName': '',
            'NewTopic': '',
            'AddMemberList': add_arr,
            'DelMemberList': del_arr,
            'InviteMemberList': invite_arr,
        }
        dic = post(url, params)
        return dic['BaseResponse']['Ret'] == 0

    def webwxrevokemsg(self, msgid, user_id, client_msgid):
        """
        @brief      revoke a message
        @param      msgid           String
        @param      user_id         String
        @param      client_msgid    String
        @return     Bool: whether operation succeed
        """
        url = self.wx_conf['API_webwxrevokemsg'] + '?r=%s' % int(time.time())
        params = {
            'BaseRequest': self.base_request,
            'SvrMsgId': msgid,
            'ToUserName': user_id,
            'ClientMsgId': client_msgid
        }
        dic = post(url, params)
        return dic['BaseResponse']['Ret'] == 0

    def webwxpushloginurl(self, uin):
        """
        @brief      push a login confirm alert to mobile device
        @param      uin   String
        @return     dic   Dict
        """
        url = self.wx_conf['API_webwxpushloginurl'] + '?uin=%s' % uin
        dic = eval(get(url))
        return dic

    def association_login(self):
        """
        @brief      login without scan qrcode
        @return     Bool: whether operation succeed
        """
        if self.uin != '':
            dic = self.webwxpushloginurl(self.uin)
            if dic['ret'] == '0':
                self.uuid = dic['uuid']
                return True
        return False

    def send_text(self, user_id, text):
        """
        @brief      send text
        @param      user_id  String
        @param      text  String
        @return     Bool: whether operation succeed
        """
        try:
            dic = self.webwxsendmsg(text, user_id)
            return dic['BaseResponse']['Ret'] == 0
        except:
            return False

    def send_img(self, user_id, file_path):
        """
        @brief      send image
        @param      user_id  String
        @param      file_path  String
        @return     Bool: whether operation succeed
        """
        response = self.webwxuploadmedia(file_path)
        media_id = ""
        if response is not None:
            media_id = response['MediaId']
        return self.webwxsendmsgimg(user_id, media_id)

    def send_emot(self, user_id, file_path):
        """
        @brief      send emotion
        @param      user_id  String
        @param      file_path  String
        @return     Bool: whether operation succeed
        """
        response = self.webwxuploadmedia(file_path)
        media_id = ""
        if response is not None:
            media_id = response['MediaId']
        return self.webwxsendemoticon(user_id, media_id)

    def send_file(self, user_id, file_path):
        """
        @brief      send file
        @param      user_id  String
        @param      file_path  String
        @return     Bool: whether operation succeed
        """
        title = file_path.split('/')[-1]
        data = {
            'appid': Constant.API_WXAPPID,
            'title': title,
            'totallen': '',
            'attachid': '',
            'type': self.wx_conf['APPMSGTYPE_ATTACH'],
            'fileext': title.split('.')[-1],
        }

        response = self.webwxuploadmedia(file_path)
        if response is not None:
            data['totallen'] = response['StartPos']
            data['attachid'] = response['MediaId']
        else:
            Log.error('File upload error')
        
        return self.webwxsendappmsg(user_id, data)

    def make_synckey(self, dic):
        self.synckey_dic = dic['SyncKey']

        def foo(x):
            return str(x['Key']) + '_' + str(x['Val'])

        # synckey for synccheck
        self.synckey = '|'.join(
            [foo(keyVal) for keyVal in self.synckey_dic['List']])

    def revoke_msg(self, msgid, user_id, client_msgid):
        """
        @brief      revoke a message
        @param      msgid           String
        @param      user_id         String
        @param      client_msgid    String
        @return     Bool: whether operation succeed
        """
        return self.webwxrevokemsg(msgid, user_id, client_msgid)

    # -----------------------------------------------------
    # The following is getting user & group infomation apis
    def get_user_by_id(self, user_id):
        """
        @brief      get all type of name by user id
        @param      user_id  The id of user
        @return     Dict: {
                        'UserName'      # 微信动态ID
                        'RemarkName'    # 备注
                        'NickName'      # 微信昵称
                        'ShowName'      # Log显示用的
                    }
        """
        UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id
        name = {
            'UserName': user_id,
            'RemarkName': '',
            'NickName': '',
            'ShowName': '',
        }
        name['ShowName'] = UnknownPeople

        # yourself
        if user_id == self.User['UserName']:
            name['RemarkName'] = self.User['RemarkName']
            name['NickName'] = self.User['NickName']
            name['ShowName'] = name['NickName']
        else:
            # 联系人
            for member in self.MemberList:
                if member['UserName'] == user_id:
                    r, n = member['RemarkName'], member['NickName']
                    name['RemarkName'] = r
                    name['NickName'] = n
                    name['ShowName'] = r if r else n
                    break
            # 特殊帐号
            for member in self.SpecialUsersList:
                if member['UserName'] == user_id:
                    name['RemarkName'] = user_id
                    name['NickName'] = user_id
                    name['ShowName'] = user_id
                    break

        return name

    def get_group_user_by_id(self, user_id, group_id):
        """
        @brief      get group user by user id
        @param      user_id  The id of user
        @param      group_id  The id of group
        @return     Dict: {
                        'UserName'      # 微信动态ID
                        'NickName'      # 微信昵称
                        'DisplayName'   # 群聊显示名称
                        'ShowName'      # Log显示用的
                        'AttrStatus'    # 群用户id
                    }
        """
        UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id
        name = {
            'UserName': user_id,
            'NickName': '',
            'DisplayName': '',
            'ShowName': '',
            'AttrStatus': '',
        }
        name['ShowName'] = UnknownPeople

        # 群友
        if group_id in self.GroupMemeberList:
            for member in self.GroupMemeberList[group_id]:
                if member['UserName'] == user_id:
                    n, d = member['NickName'], member['DisplayName']
                    name['NickName'] = n
                    name['DisplayName'] = d
                    name['AttrStatus'] = member['AttrStatus']
                    name['ShowName'] = d if d else n
                    break

        return name

    def get_group_by_id(self, group_id):
        """
        @brief      get basic info by group id
        @param      group_id  The id of group
        @return     Dict: {
                        'UserName'      # 微信动态ID
                        'NickName'      # 微信昵称
                        'DisplayName'   # 群聊显示名称
                        'ShowName'      # Log显示用的
                        'OwnerUin'      # 群主ID
                        'MemberCount'   # 群人数
                    }
        """
        UnknownGroup = Constant.LOG_MSG_UNKNOWN_GROUP_NAME + group_id
        group = {
            'UserName': group_id,
            'NickName': '',
            'DisplayName': '',
            'ShowName': '',
            'OwnerUin': '',
            'MemberCount': '',
        }

        for member in self.GroupList:
            if member['UserName'] == group_id:
                group['NickName'] = member['NickName']
                group['DisplayName'] = member.get('DisplayName', '')
                group['ShowName'] = member.get('NickName', UnknownGroup)
                group['OwnerUin'] = member.get('OwnerUin', '')
                group['MemberCount'] = member['MemberCount']
                break

        return group

    def get_user_id(self, name):
        """
        @brief      Gets the user id.
        @param      name  The user nickname or remarkname
        @return     The user id.
        """
        for member in self.MemberList:
            if name == member['RemarkName'] or name == member['NickName']:
                return member['UserName']
        return None


================================================
FILE: wxbot_project_py2.7/weixin_bot.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from wechat import WeChat
from wechat.utils import *
from wx_handler import WeChatMsgProcessor
from wx_handler import Bot
from wx_handler import SGMail
from db import SqliteDB
from db import MysqlDB
from config import ConfigManager
from config import Constant
from config import Log
#---------------------------------------------------
from flask import Flask, render_template, send_file, jsonify, request
import threading
import traceback
import os
import logging
import time
#===================================================


cm = ConfigManager()
db = SqliteDB(cm.getpath('database'))
# db = MysqlDB(cm.mysql())
wechat_msg_processor = WeChatMsgProcessor(db)
wechat = WeChat(cm.get('wechat', 'host'))
wechat.db = db
wechat.bot = Bot()
wechat.msg_handler = wechat_msg_processor
wechat_msg_processor.wechat = wechat

PORT = int(cm.get('setting', 'server_port'))
app = Flask(__name__, template_folder='flask_templates')
app.config['UPLOAD_FOLDER'] = cm.getpath('uploaddir')

logger = logging.getLogger('werkzeug')
log_format_str = Constant.SERVER_LOG_FORMAT
formatter = logging.Formatter(log_format_str)
flask_log_handler = logging.FileHandler(cm.getpath('server_log_file'))
flask_log_handler.setLevel(logging.INFO)
flask_log_handler.setFormatter(formatter)
logger.addHandler(flask_log_handler)
app.logger.addHandler(flask_log_handler)

# sendgrid mail
sg_apikey = cm.get('sendgrid', 'api_key')
from_email = cm.get('sendgrid', 'from_email')
to_email = cm.get('sendgrid', 'to_email')
sg = SGMail(sg_apikey, from_email, to_email)

@app.route('/')
def index():
    return render_template(Constant.SERVER_PAGE_INDEX)


@app.route('/qrcode')
def qrcode():
    qdir = cm.getpath('qrcodedir')
    if not os.path.exists(qdir):
        os.makedirs(qdir)
    image_path = '%s/%s_%d.png' % (qdir, wechat.uuid, int(time.time()*100))
    s = wechat.wx_conf['API_qrcode'] + wechat.uuid
    str2qr_image(s, image_path)
    return send_file(image_path, mimetype='image/png')


@app.route("/group_list")
def group_list():
    """
    @brief      list groups
    """
    result = wechat.db.select(Constant.TABLE_GROUP_LIST())
    return jsonify({'count': len(result), 'group': result})


@app.route('/group_member_list/<g_id>')
def group_member_list(g_id):
    """
    @brief      list group member
    @param      g_id String
    """
    result = wechat.db.select(Constant.TABLE_GROUP_USER_LIST(), 'RoomID', g_id)
    return jsonify({'count': len(result), 'member': result})


@app.route('/group_chat_log/<g_name>')
def group_chat_log(g_name):
    """
    @brief      list group chat log
    @param      g_name String
    """
    result = wechat.db.select(Constant.TABLE_GROUP_MSG_LOG, 'RoomName', g_name)
    return jsonify({'count': len(result), 'chats': result})


@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        def allowed_file(filename):
            return '.' in filename and \
                filename.rsplit('.', 1)[1] in Constant.SERVER_UPLOAD_ALLOWED_EXTENSIONS

        j = {'ret': 1, 'msg': ''}

        # check if the post request has the file part
        if 'file' not in request.files:
            j['msg'] = 'No file part'
            return jsonify(j)

        # if user does not select file, browser also
        # submit a empty part without filename
        file = request.files['file']
        if file.filename == '':
            j['msg'] = 'No selected file'
        elif file and allowed_file(file.filename):
            filename = generate_file_name(file.filename)
            file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            file.save(file_path)
            j['ret'] = 0
            j['msg'] = filename
        else:
            j['msg'] = 'File type not support'
        return jsonify(j)
    else:
        return render_template(Constant.SERVER_PAGE_UPLOAD)


@app.route('/send_msg/<to>/<msg>')
def send_msg(to, msg):
    """
    @brief      send message to user or gourp
    @param      to: String, user id or group id
    @param      msg: String, words
    """
    return jsonify({'ret': 0 if wechat.send_text(to, msg) else 1})


@app.route('/send_img/<to>/<img>')
def send_img(to, img):
    """
    @brief      send image to user or gourp
    @param      to: String, user id or group id
    @param      img: String, image file name
    """
    img_path = os.path.join(app.config['UPLOAD_FOLDER'], img)
    return jsonify({'ret': 0 if wechat.send_img(to, img_path) else 1})


@app.route('/send_emot/<to>/<emot>')
def send_emot(to, emot):
    """
    @brief      send emotion to user or gourp
    @param      to: String, user id or group id
    @param      emot: String, emotion file name
    """
    emot_path = os.path.join(app.config['UPLOAD_FOLDER'], emot)
    return jsonify({'ret': 0 if wechat.send_emot(to, emot_path) else 1})


@app.route('/send_file/<to>/<file>')
def send_file(to, file):
    """
    @brief      send file to user or gourp
    @param      to: String, user id or group id
    @param      file: String, file name
    """
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], file)
    return jsonify({'ret': 0 if wechat.send_file(to, file_path) else 1})


def mass_send(method, data, func):
    j = {'ret': -1, 'unsend_list':[]}
    if method == 'POST' and data:
        to_list = data['to_list']
        msg = data['msg']
        media_type = data.get('media_type', '')

        if media_type in ['img', 'emot']:
            file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg)
            response = wechat.webwxuploadmedia(file_path)
            if response is not None:
                msg = response['MediaId']
        elif media_type == 'file':
            file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg)
            data = {
                'appid': Constant.API_WXAPPID,
                'title': msg,
                'totallen': '',
                'attachid': '',
                'type': wechat.wx_conf['APPMSGTYPE_ATTACH'],
                'fileext': msg.split('.')[-1],
            }
            response = wechat.webwxuploadmedia(file_path)
            if response is not None:
                data['totallen'] = response['StartPos']
                data['attachid'] = response['MediaId']
            else:
                Log.error('File upload error')
            msg = data

        for groups in split_array(to_list, 20):
            for g in groups:
                r = func(g, msg)
                if not r:
                    j['unsend_list'].append(g)
            time.sleep(1)

        j['ret'] = len(j['unsend_list'])

    return j


@app.route('/mass_send_msg/', methods=["GET", "POST"])
def mass_send_msg():
    """
    @brief      send text to mass users or gourps
    """
    j = mass_send(request.method, request.json, wechat.send_text)
    return jsonify(j)


@app.route('/mass_send_img', methods=["GET", "POST"])
def mass_send_img():
    """
    @brief      send iamge to mass users or gourps
    """
    j = mass_send(request.method, request.json, wechat.webwxsendmsgimg)
    return jsonify(j)


@app.route('/mass_send_emot', methods=["GET", "POST"])
def mass_send_emot():
    """
    @brief      send emoticon to mass users or gourps
    """
    j = mass_send(request.method, request.json, wechat.webwxsendemoticon)
    return jsonify(j)


@app.route('/mass_send_file', methods=["GET", "POST"])
def mass_send_file():
    """
    @brief      send file to mass users or gourps
    """
    j = mass_send(request.method, request.json, wechat.webwxsendappmsg)
    return jsonify(j)


def run_server():
    app.run(port=PORT)

if cm.get('setting', 'server_mode') == 'True':
    serverProcess = threading.Thread(target=run_server)
    serverProcess.start()

while True:
    try:
        wechat.start()
    except KeyboardInterrupt:
        echo(Constant.LOG_MSG_QUIT)
        wechat.exit_code = 1
    else:
        Log.error(traceback.format_exc())
    finally:
        wechat.stop()
    
    # send a mail to tell the wxbot is failing
    subject = 'wxbot stop message'
    log_file = open(eval(cm.get('handler_fileHandler', 'args'))[0], 'r')
    mail_content = '<pre>' + str(wechat) + '\n\n-----\nLogs:\n-----\n\n' + ''.join(log_file.readlines()[-100:]) + '</pre>'
    sg.send_mail(subject, mail_content, 'text/html')
    log_file.close()

    if wechat.exit_code == 0:
        echo(Constant.MAIN_RESTART)
    else:
        # kill process
        os.system(Constant.LOG_MSG_KILL_PROCESS % os.getpid())


================================================
FILE: wxbot_project_py2.7/wx_handler/__init__.py
================================================
#!/usr/bin/env python
# coding: utf-8

from wechat_msg_processor import WeChatMsgProcessor
from bot import Bot
from sendgrid_mail import SGMail 

================================================
FILE: wxbot_project_py2.7/wx_handler/bot.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from wechat.utils import *
from config import Constant
#---------------------------------------------------
import random, time, json
#===================================================


class Bot(object):

    def __init__(self):
        self.emoticons = Constant.EMOTICON
        self.gifs = []
        self.last_time = time.time()
    
    def time_schedule(self):
        r = ''
        now = time.time()
        if int(now - self.last_time) > 3600:
            self.last_time = now
            url_latest = Constant.BOT_ZHIHU_URL_LATEST
            url_daily = Constant.BOT_ZHIHU_URL_DAILY
            data = get(url_latest)
            j = json.loads(data)
            story = j['stories'][random.randint(0, len(j['stories'])-1)]
            r = story['title'] + '\n' + url_daily + str(story['id'])
        return r.encode('utf-8')

    def reply(self, text):
        APIKEY = Constant.BOT_TULING_API_KEY
        api_url = Constant.BOT_TULING_API_URL % (APIKEY, text, '12345678')
        r = json.loads(get(api_url))
        if r.get('code') == 100000 and r.get('text') != Constant.BOT_TULING_BOT_REPLY:
            p = random.randint(1, 10)
            if p > 3:
                return r['text']
            elif p > 1:
                # send emoji
                if random.randint(1, 10) > 5:
                    n = random.randint(0, len(self.emoticons)-1)
                    m = random.randint(1, 3)
                    reply = self.emoticons[n].encode('utf-8') * m
                    return reply
        return ''


================================================
FILE: wxbot_project_py2.7/wx_handler/sendgrid_mail.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
import sendgrid
from sendgrid.helpers.mail import *
#===================================================

class SGMail(object):

    def __init__(self, apikey, from_email, to_email):
        self.sg = sendgrid.SendGridAPIClient(apikey=apikey)
        self.from_email = Email(from_email)
        self.to_email = Email(to_email)

    def send_mail(self, subject, plain_content, type='text/plain'):
        content = Content(type, plain_content)
        mail = Mail(self.from_email, subject, self.to_email, content)
        response = self.sg.client.mail.send.post(request_body=mail.get())
        return response.status_code == 202


================================================
FILE: wxbot_project_py2.7/wx_handler/wechat_msg_processor.py
================================================
#!/usr/bin/env python
# coding: utf-8

#===================================================
from wechat.utils import *
from config import ConfigManager
from config import Constant
from config import Log
#---------------------------------------------------
import os
import time
import json
import re
#===================================================


class WeChatMsgProcessor(object):
    """
    Process fetched data
    """

    def __init__(self, db):
        self.db = db
        self.wechat = None  # recieve `WeChat` class instance
                            # for call some wechat apis

        # read config
        cm = ConfigManager()
        [self.upload_dir, self.data_dir, self.log_dir] = cm.setup_database()

    def clean_db(self):
        """
        @brief clean database, delete table & create table
        """
        self.db.delete_table(Constant.TABLE_GROUP_LIST())
        self.db.delete_table(Constant.TABLE_GROUP_USER_LIST())
        self.db.create_table(Constant.TABLE_GROUP_MSG_LOG, Constant.TABLE_GROUP_MSG_LOG_COL)
        self.db.create_table(Constant.TABLE_GROUP_LIST(), Constant.TABLE_GROUP_LIST_COL)
        self.db.create_table(Constant.TABLE_GROUP_USER_LIST(), Constant.TABLE_GROUP_USER_LIST_COL)
        self.db.create_table(Constant.TABLE_RECORD_ENTER_GROUP, Constant.TABLE_RECORD_ENTER_GROUP_COL)
        self.db.create_table(Constant.TABLE_RECORD_RENAME_GROUP, Constant.TABLE_RECORD_RENAME_GROUP_COL)

    def handle_wxsync(self, msg):
        """
        @brief      Recieve webwxsync message, saved into json
        @param      msg  Dict: webwxsync msg
        """
        fn = time.strftime(Constant.LOG_MSG_FILE, time.localtime())
        save_json(fn, msg, self.log_dir, 'a+')

    def handle_group_list(self, group_list):
        """
        @brief      handle group list & saved in DB
        @param      group_list  Array
        """
        fn = Constant.LOG_MSG_GROUP_LIST_FILE
        save_json(fn, group_list, self.data_dir)
        cols = [(
            g['NickName'],
            g['UserName'],
            g['OwnerUin'],
            g['MemberCount'],
            g['HeadImgUrl']
        ) for g in group_list]
        self.db.insertmany(Constant.TABLE_GROUP_LIST(), cols)

    def handle_group_member_list(self, group_id, member_list):
        """
        @brief      handle group member list & saved in DB
        @param      member_list  Array
        """
        fn = group_id + '.json'
        save_json(fn, member_list, self.data_dir)
        cols = [(
            group_id,
            m['UserName'],
            m['NickName'],
            m['DisplayName'],
            m['AttrStatus']
  
Download .txt
gitextract_stqn26qf/

├── .gitignore
├── LICENSE
├── README.md
├── wxbot_demo_py3/
│   ├── requirements.txt
│   └── weixin.py
└── wxbot_project_py2.7/
    ├── README.md
    ├── config/
    │   ├── __init__.py
    │   ├── config_manager.py
    │   ├── constant.py
    │   ├── log.py
    │   ├── requirements.txt
    │   └── wechat.conf.bak
    ├── db/
    │   ├── __init__.py
    │   ├── mysql_db.py
    │   └── sqlite_db.py
    ├── docker/
    │   ├── Dockerfile
    │   └── README.md
    ├── flask_templates/
    │   ├── index.html
    │   └── upload.html
    ├── wechat/
    │   ├── __init__.py
    │   ├── utils.py
    │   ├── wechat.py
    │   └── wechat_apis.py
    ├── weixin_bot.py
    └── wx_handler/
        ├── __init__.py
        ├── bot.py
        ├── sendgrid_mail.py
        └── wechat_msg_processor.py
Download .txt
SYMBOL INDEX (211 symbols across 12 files)

FILE: wxbot_demo_py3/weixin.py
  function catchKeyboardInterrupt (line 33) | def catchKeyboardInterrupt(fn):
  function _decode_list (line 43) | def _decode_list(data):
  function _decode_dict (line 56) | def _decode_dict(data):
  class WebWeixin (line 71) | class WebWeixin(object):
    method __str__ (line 73) | def __str__(self):
    method __init__ (line 87) | def __init__(self):
    method loadConfig (line 130) | def loadConfig(self, config):
    method getUUID (line 142) | def getUUID(self):
    method genQRCode (line 164) | def genQRCode(self):
    method _showQRCodeImg (line 173) | def _showQRCodeImg(self, str):
    method _showCommandLineQRCode (line 195) | def _showCommandLineQRCode(self, qr_data, enableCmdQR=2):
    method waitForLogin (line 217) | def waitForLogin(self, tip=1):
    method login (line 241) | def login(self):
    method webwxinit (line 269) | def webwxinit(self):
    method webwxstatusnotify (line 286) | def webwxstatusnotify(self):
    method webwxgetcontact (line 302) | def webwxgetcontact(self):
    method webwxbatchgetcontact (line 334) | def webwxbatchgetcontact(self):
    method getNameById (line 359) | def getNameById(self, id):
    method testsynccheck (line 375) | def testsynccheck(self):
    method synccheck (line 399) | def synccheck(self):
    method webwxsync (line 420) | def webwxsync(self):
    method webwxsendmsg (line 442) | def webwxsendmsg(self, word, to='filehelper'):
    method webwxuploadmedia (line 464) | def webwxuploadmedia(self, image_name):
    method webwxsendmsgimg (line 539) | def webwxsendmsgimg(self, user_id, media_id):
    method webwxsendmsgemotion (line 560) | def webwxsendmsgemotion(self, user_id, media_id):
    method _saveFile (line 585) | def _saveFile(self, filename, data, api=None):
    method webwxgeticon (line 598) | def webwxgeticon(self, id):
    method webwxgetheadimg (line 607) | def webwxgetheadimg(self, id):
    method webwxgetmsgimg (line 616) | def webwxgetmsgimg(self, msgid):
    method webwxgetvideo (line 626) | def webwxgetvideo(self, msgid):
    method webwxgetvoice (line 635) | def webwxgetvoice(self, msgid):
    method getGroupName (line 644) | def getGroupName(self, id):
    method getUserRemarkName (line 661) | def getUserRemarkName(self, id):
    method getUSerID (line 697) | def getUSerID(self, name):
    method _showMsg (line 703) | def _showMsg(self, message):
    method handleMsg (line 776) | def handleMsg(self, r):
    method listenMsgMode (line 876) | def listenMsgMode(self):
    method sendMsg (line 916) | def sendMsg(self, name, word, isfile=False):
    method sendMsgToAll (line 940) | def sendMsgToAll(self, word):
    method sendImg (line 952) | def sendImg(self, name, file_name):
    method sendEmotion (line 960) | def sendEmotion(self, name, file_name):
    method start (line 969) | def start(self):
    method _safe_open (line 1048) | def _safe_open(self, path):
    method _run (line 1055) | def _run(self, str, func, *args):
    method _echo (line 1066) | def _echo(self, str):
    method _printQR (line 1070) | def _printQR(self, mat):
    method _str2qr (line 1076) | def _str2qr(self, str):
    method _transcoding (line 1088) | def _transcoding(self, data):
    method _get (line 1098) | def _get(self, url: object, api: object = None, timeout: object = None...
    method _post (line 1128) | def _post(self, url: object, params: object, jsonfmt: object = True) -...
    method _xiaodoubi (line 1157) | def _xiaodoubi(self, word):
    method _simsimi (line 1165) | def _simsimi(self, word):
    method _searchContent (line 1176) | def _searchContent(self, key, content, fmat='attr'):
  class UnicodeStreamFilter (line 1191) | class UnicodeStreamFilter:
    method __init__ (line 1193) | def __init__(self, target):
    method write (line 1199) | def write(self, s):
    method flush (line 1205) | def flush(self):

FILE: wxbot_project_py2.7/config/config_manager.py
  class ConfigManager (line 12) | class ConfigManager(object):
    method __init__ (line 14) | def __init__(self):
    method get (line 26) | def get(self, section, option):
    method set (line 29) | def set(self, section, option, value):
    method getpath (line 33) | def getpath(self, dir):
    method setup_database (line 37) | def setup_database(self):
    method set_wechat_config (line 46) | def set_wechat_config(self, conf):
    method get_wechat_config (line 51) | def get_wechat_config(self):
    method get_wechat_media_dir (line 67) | def get_wechat_media_dir(self):
    method get_pickle_files (line 79) | def get_pickle_files(self):
    method get_cookie (line 89) | def get_cookie(self):
    method mysql (line 97) | def mysql(self):

FILE: wxbot_project_py2.7/config/constant.py
  class Constant (line 5) | class Constant(object):
    method TABLE_GROUP_LIST (line 129) | def TABLE_GROUP_LIST():
    method TABLE_GROUP_USER_LIST (line 141) | def TABLE_GROUP_USER_LIST():

FILE: wxbot_project_py2.7/db/mysql_db.py
  function array_join (line 12) | def array_join(arr, c):
  class MysqlDB (line 18) | class MysqlDB(object):
    method __init__ (line 34) | def __init__(self, conf):
    method show_database (line 57) | def show_database(self):
    method show_tables (line 64) | def show_tables(self):
    method create_db (line 71) | def create_db(self, db_name):
    method create_table (line 81) | def create_table(self, table, cols):
    method delete_table (line 93) | def delete_table(self, table):
    method insert (line 104) | def insert(self, table, value):
    method insertmany (line 115) | def insertmany(self, table, values):
    method select (line 126) | def select(self, table, field='', condition=''):
    method get_table_column_name (line 140) | def get_table_column_name(self, table):
    method execute (line 151) | def execute(self, sql, values=None):
    method delete (line 181) | def delete(self, table, field='', condition=''):
    method close (line 192) | def close(self):

FILE: wxbot_project_py2.7/db/sqlite_db.py
  function _dict_factory (line 13) | def _dict_factory(cursor, row):
  class SqliteDB (line 20) | class SqliteDB(object):
    method __init__ (line 22) | def __init__(self, db_file):
    method set_conn (line 32) | def set_conn(self):
    method conn (line 36) | def conn(self):
    method create_table (line 48) | def create_table(self, table, cols):
    method delete_table (line 58) | def delete_table(self, table):
    method insert (line 67) | def insert(self, table, value):
    method insertmany (line 77) | def insertmany(self, table, values):
    method select (line 98) | def select(self, table, field='', condition=''):
    method update (line 118) | def update(self, table, dic, condition=''):
    method get_table_column_name (line 132) | def get_table_column_name(self, table):
    method execute (line 143) | def execute(self, sql, value=None):
    method delete (line 172) | def delete(self, table, field='', condition=''):
    method close (line 184) | def close(self):

FILE: wxbot_project_py2.7/wechat/utils.py
  function _decode_data (line 23) | def _decode_data(data):
  function str2qr_terminal (line 48) | def str2qr_terminal(text):
  function str2qr_image (line 61) | def str2qr_image(text, image_path):
  function print_qr (line 75) | def print_qr(mat):
  function echo (line 82) | def echo(str):
  function run (line 88) | def run(str, func, *args):
  function get (line 104) | def get(url, api=None):
  function post (line 134) | def post(url, params, jsonfmt=True):
  function set_cookie (line 168) | def set_cookie(cookie_file):
  function generate_file_name (line 186) | def generate_file_name(filename):
  function save_file (line 198) | def save_file(filename, data, dirName):
  function save_json (line 216) | def save_json(filename, data, dirName, mode='w+'):
  function load_json (line 234) | def load_json(filepath):
  function pickle_save (line 240) | def pickle_save(data, file):
  function pickle_load (line 253) | def pickle_load(file):
  function search_content (line 265) | def search_content(key, content, fmat='attr'):
  function is_str (line 288) | def is_str(s):
  function trans_coding (line 297) | def trans_coding(data):
  function trans_emoji (line 313) | def trans_emoji(text):
  function auto_reload (line 327) | def auto_reload(mod):
  function split_array (line 362) | def split_array(arr, n):

FILE: wxbot_project_py2.7/wechat/wechat.py
  class WeChat (line 25) | class WeChat(WXAPI):
    method __str__ (line 27) | def __str__(self):
    method __init__ (line 41) | def __init__(self, host='wx.qq.com'):
    method start (line 59) | def start(self):
    method get_run_time (line 161) | def get_run_time(self):
    method stop (line 170) | def stop(self):
    method fetch_group_contacts (line 179) | def fetch_group_contacts(self):
    method snapshot (line 270) | def snapshot(self):
    method recover (line 304) | def recover(self):
    method save_contacts (line 330) | def save_contacts(self):
    method recover_contacts (line 340) | def recover_contacts(self):
    method handle_mod (line 356) | def handle_mod(self, r):
    method handle_msg (line 397) | def handle_msg(self, r):
    method make_group_msg (line 565) | def make_group_msg(self, msg):
    method show_msg (line 643) | def show_msg(self, message):

FILE: wxbot_project_py2.7/wechat/wechat_apis.py
  class WXAPI (line 22) | class WXAPI(object):
    method __init__ (line 24) | def __init__(self, host):
    method conf_factory (line 62) | def conf_factory(self):
    method getuuid (line 170) | def getuuid(self):
    method genqrcode (line 191) | def genqrcode(self):
    method waitforlogin (line 210) | def waitforlogin(self, tip=1):
    method login (line 239) | def login(self):
    method webwxinit (line 275) | def webwxinit(self):
    method webwxstatusnotify (line 295) | def webwxstatusnotify(self):
    method webwxgetcontact (line 315) | def webwxgetcontact(self):
    method webwxbatchgetcontact (line 351) | def webwxbatchgetcontact(self, gid_list):
    method synccheck (line 369) | def synccheck(self):
    method webwxsync (line 398) | def webwxsync(self):
    method webwxgetmsgimg (line 418) | def webwxgetmsgimg(self, msgid):
    method webwxgetvoice (line 429) | def webwxgetvoice(self, msgid):
    method webwxgetvideo (line 440) | def webwxgetvideo(self, msgid):
    method webwxgeticon (line 451) | def webwxgeticon(self, id):
    method webwxgetheadimg (line 462) | def webwxgetheadimg(self, id):
    method webwxsendmsg (line 473) | def webwxsendmsg(self, word, to='filehelper'):
    method webwxuploadmedia (line 498) | def webwxuploadmedia(self, file_path):
    method webwxsendmsgimg (line 593) | def webwxsendmsgimg(self, user_id, media_id):
    method webwxsendemoticon (line 618) | def webwxsendemoticon(self, user_id, media_id):
    method webwxsendappmsg (line 644) | def webwxsendappmsg(self, user_id, data):
    method webwxcreatechatroom (line 687) | def webwxcreatechatroom(self, uid_arr):
    method webwxupdatechatroom (line 703) | def webwxupdatechatroom(self, add_arr, del_arr, invite_arr):
    method webwxrevokemsg (line 723) | def webwxrevokemsg(self, msgid, user_id, client_msgid):
    method webwxpushloginurl (line 741) | def webwxpushloginurl(self, uin):
    method association_login (line 751) | def association_login(self):
    method send_text (line 763) | def send_text(self, user_id, text):
    method send_img (line 776) | def send_img(self, user_id, file_path):
    method send_emot (line 789) | def send_emot(self, user_id, file_path):
    method send_file (line 802) | def send_file(self, user_id, file_path):
    method make_synckey (line 828) | def make_synckey(self, dic):
    method revoke_msg (line 838) | def revoke_msg(self, msgid, user_id, client_msgid):
    method get_user_by_id (line 850) | def get_user_by_id(self, user_id):
    method get_group_user_by_id (line 894) | def get_group_user_by_id(self, user_id, group_id):
    method get_group_by_id (line 930) | def get_group_by_id(self, group_id):
    method get_user_id (line 964) | def get_user_id(self, name):

FILE: wxbot_project_py2.7/weixin_bot.py
  function index (line 55) | def index():
  function qrcode (line 60) | def qrcode():
  function group_list (line 71) | def group_list():
  function group_member_list (line 80) | def group_member_list(g_id):
  function group_chat_log (line 90) | def group_chat_log(g_name):
  function upload_file (line 100) | def upload_file():
  function send_msg (line 132) | def send_msg(to, msg):
  function send_img (line 142) | def send_img(to, img):
  function send_emot (line 153) | def send_emot(to, emot):
  function send_file (line 164) | def send_file(to, file):
  function mass_send (line 174) | def mass_send(method, data, func):
  function mass_send_msg (line 217) | def mass_send_msg():
  function mass_send_img (line 226) | def mass_send_img():
  function mass_send_emot (line 235) | def mass_send_emot():
  function mass_send_file (line 244) | def mass_send_file():
  function run_server (line 252) | def run_server():

FILE: wxbot_project_py2.7/wx_handler/bot.py
  class Bot (line 12) | class Bot(object):
    method __init__ (line 14) | def __init__(self):
    method time_schedule (line 19) | def time_schedule(self):
    method reply (line 32) | def reply(self, text):

FILE: wxbot_project_py2.7/wx_handler/sendgrid_mail.py
  class SGMail (line 9) | class SGMail(object):
    method __init__ (line 11) | def __init__(self, apikey, from_email, to_email):
    method send_mail (line 16) | def send_mail(self, subject, plain_content, type='text/plain'):

FILE: wxbot_project_py2.7/wx_handler/wechat_msg_processor.py
  class WeChatMsgProcessor (line 17) | class WeChatMsgProcessor(object):
    method __init__ (line 22) | def __init__(self, db):
    method clean_db (line 31) | def clean_db(self):
    method handle_wxsync (line 43) | def handle_wxsync(self, msg):
    method handle_group_list (line 51) | def handle_group_list(self, group_list):
    method handle_group_member_list (line 67) | def handle_group_member_list(self, group_id, member_list):
    method handle_group_list_change (line 83) | def handle_group_list_change(self, new_group):
    method handle_group_member_change (line 90) | def handle_group_member_change(self, group_id, member_list):
    method handle_group_msg (line 99) | def handle_group_msg(self, msg):
    method handle_user_msg (line 185) | def handle_user_msg(self, msg):
    method handle_command (line 202) | def handle_command(self, cmd, msg):
    method check_schedule_task (line 241) | def check_schedule_task(self):
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (213K chars).
[
  {
    "path": ".gitignore",
    "chars": 1298,
    "preview": "# Created by .ignore support plugin (hsz.mobi)\n### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 21031,
    "preview": "# WeixinBot [![star this repo](http://github-svg-buttons.herokuapp.com/star.svg?user=Urinx&repo=WeixinBot&style=flat&bac"
  },
  {
    "path": "wxbot_demo_py3/requirements.txt",
    "chars": 95,
    "preview": "colorama\ncoloredlogs\nhumanfriendly\nlxml\nqrcode\nrequests\nsix\nrequests_toolbelt\npyqrcode\ncertifi\n"
  },
  {
    "path": "wxbot_demo_py3/weixin.py",
    "chars": 45218,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\nimport qrcode\nfrom pyqrcode import QRCode\nimport urllib.request, urllib.parse, url"
  },
  {
    "path": "wxbot_project_py2.7/README.md",
    "chars": 587,
    "preview": "# wxbot_project_py2.7\n\n目录结构:\n```bash\n.\n├── README.md\n├── config\n│   ├── __init__.py\n│   ├── config_manager.py\n│   ├── co"
  },
  {
    "path": "wxbot_project_py2.7/config/__init__.py",
    "chars": 129,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\nfrom config_manager import ConfigManager\nfrom constant import Constant\nfrom log i"
  },
  {
    "path": "wxbot_project_py2.7/config/config_manager.py",
    "chars": 3635,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom constant import Constan"
  },
  {
    "path": "wxbot_project_py2.7/config/constant.py",
    "chars": 8118,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\nimport time\n\nclass Constant(object):\n    \"\"\"\n    @brief      All used constants ar"
  },
  {
    "path": "wxbot_project_py2.7/config/log.py",
    "chars": 615,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom constant import Constan"
  },
  {
    "path": "wxbot_project_py2.7/config/requirements.txt",
    "chars": 57,
    "preview": "qrcode\nflask\nrequests\nrequests_toolbelt\npymysql\nsendgrid\n"
  },
  {
    "path": "wxbot_project_py2.7/config/wechat.conf.bak",
    "chars": 1428,
    "preview": "[wechat]\nhost = wx.qq.com\nuuid = \nredirect_uri = \nuin = \nsid = \nskey = \npass_ticket = \ndevice_id = \nlast_login = \n\n[sett"
  },
  {
    "path": "wxbot_project_py2.7/db/__init__.py",
    "chars": 98,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\nfrom sqlite_db import SqliteDB\nfrom mysql_db import MysqlDB"
  },
  {
    "path": "wxbot_project_py2.7/db/mysql_db.py",
    "chars": 6112,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom config import Log\n#----"
  },
  {
    "path": "wxbot_project_py2.7/db/sqlite_db.py",
    "chars": 5463,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom config import Log\n#----"
  },
  {
    "path": "wxbot_project_py2.7/docker/Dockerfile",
    "chars": 424,
    "preview": "FROM ubuntu:16.04\nMAINTAINER Urinx <uri.lqy@gmail.com>\n\nRUN apt-get update && \\\n    apt-get install -y python \\\n        "
  },
  {
    "path": "wxbot_project_py2.7/docker/README.md",
    "chars": 506,
    "preview": "# 本地构建 wechat_bot docker 镜像\n\n拉下镜像:\n\n```bash\ndocker pull ubuntu:16.04\n```\n\n打包本项目,将压缩包放到`docker`目录下:\n\n```\ntar -czf weixin_"
  },
  {
    "path": "wxbot_project_py2.7/flask_templates/index.html",
    "chars": 5100,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>WeChat Bot Server</title>\n\t<style type=\"text/css\">\n\t.conta"
  },
  {
    "path": "wxbot_project_py2.7/flask_templates/upload.html",
    "chars": 213,
    "preview": "<!doctype html>\n<title>Upload new File</title>\n<h1>Upload new File</h1>\n<form action=\"upload\" method=post enctype=multip"
  },
  {
    "path": "wxbot_project_py2.7/wechat/__init__.py",
    "chars": 64,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\nfrom wechat import WeChat"
  },
  {
    "path": "wxbot_project_py2.7/wechat/utils.py",
    "chars": 9097,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom config import Log\nfrom "
  },
  {
    "path": "wxbot_project_py2.7/wechat/wechat.py",
    "chars": 26186,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom utils import *\nfrom wec"
  },
  {
    "path": "wxbot_project_py2.7/wechat/wechat_apis.py",
    "chars": 35465,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom utils import *\nfrom con"
  },
  {
    "path": "wxbot_project_py2.7/weixin_bot.py",
    "chars": 8567,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom wechat import WeChat\nfr"
  },
  {
    "path": "wxbot_project_py2.7/wx_handler/__init__.py",
    "chars": 144,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\nfrom wechat_msg_processor import WeChatMsgProcessor\nfrom bot import Bot\nfrom send"
  },
  {
    "path": "wxbot_project_py2.7/wx_handler/bot.py",
    "chars": 1623,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom wechat.utils import *\nf"
  },
  {
    "path": "wxbot_project_py2.7/wx_handler/sendgrid_mail.py",
    "chars": 742,
    "preview": "#!/usr/bin/env python\r\n# coding: utf-8\r\n\r\n#===================================================\r\nimport sendgrid\r\nfrom se"
  },
  {
    "path": "wxbot_project_py2.7/wx_handler/wechat_msg_processor.py",
    "chars": 8890,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\n\n#===================================================\nfrom wechat.utils import *\nf"
  }
]

About this extraction

This page contains the full source code of the Urinx/WeixinBot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (197.5 KB), approximately 52.3k tokens, and a symbol index with 211 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!