Browse Source

Merge remote-tracking branch 'upstream/master' remote-git=776cae

gjwang 9 years ago
parent
commit
03a29ffc4b
8 changed files with 1121 additions and 714 deletions
  1. 5 0
      .gitignore
  2. 167 73
      README.md
  3. 116 0
      bot.py
  4. BIN
      img/backfront.jpg
  5. BIN
      img/group_chat.png
  6. BIN
      img/group_chat_backend.jpg
  7. 28 24
      test.py
  8. 805 617
      wxbot.py

+ 5 - 0
.gitignore

@@ -63,3 +63,8 @@ target/
 
 
 *.json
+.idea/
+
+qr.png
+/*.jpg
+*.ini

+ 167 - 73
README.md

@@ -1,11 +1,49 @@
 # wxBot [![star this repo](http://github-svg-buttons.herokuapp.com/star.svg?user=liuwons&repo=wxBot&style=flat&background=1081C1)](http://github.com/liuwons/wxBot) [![fork this repo](http://github-svg-buttons.herokuapp.com/fork.svg?user=liuwons&repo=wxBot&style=flat&background=1081C1)](http://github.com/liuwons/wxBot/fork) ![python](https://img.shields.io/badge/python-2.7-ff69b4.svg)
 
-Python包装Web微信实现的微信机器人框架。可以很容易地实现微信机器人。
+**wxBot** 是用Python包装Web微信协议实现的微信机器人框架。
 
-## 环境与依赖
+目前的消息支持情况:
+
+- [ ] 群消息
+  - [x] 文本
+  - [x] 图片
+  - [x] 地理位置
+  - [x] 个人名片
+  - [x] 语音
+  - [x] 动画
+  - [ ] 语音电话
+  - [ ] 红包
+
+- [ ] 联系人消息
+  - [x] 文本
+  - [x] 图片
+  - [x] 地理位置
+  - [x] 个人名片
+  - [x] 语音
+  - [x] 小视频
+  - [x] 动画
+  - [ ] 视频电话
+  - [ ] 红包
+  - [ ] 转账
+
+
+
+Web微信协议参考资料:
+
+[挖掘微信Web版通信的全过程](http://www.tanhao.me/talk/1466.html/)
+
+[微信协议简单调研笔记](http://www.blogjava.net/yongboy/archive/2015/11/05/410636.html)
+
+[qwx: WeChat Qt frontend 微信Qt前端](https://github.com/xiangzhai/qwx)
+
+
+## 1 环境与依赖
 
 目前只能运行于Python 2环境 。
-**wxBot** 用到了Python **requests** 和 **pyqrcode** 库,使用之前需要安装这两个库:
+
+**wxBot** 用到了Python **requests** , **pypng** , 以及 **pyqrcode** 库。
+
+使用之前需要所依赖的库:
 
 ```bash
 pip install requests
@@ -13,11 +51,15 @@ pip install pyqrcode
 pip install pypng
 ```
 
-## 快速开发
-### 代码
+## 2 快速开发
 
-利用 **wxBot** 最简单的方法就是继承WXBot类并实现handle_msg_all或者schedule函数,然后实例化子类并run,如下的代码对所有来自好友的文本消息回复 "hi", 并不断向好友tb发送"schedule"。
-handle_msg_all函数用于处理收到的每条消息,而schedule函数可以做一些任务性的事情(例如不断向好友推送信息或者一些定时任务)。
+利用 **wxBot** 最简单的方法就是继承WXBot类并实现 `handle_msg_all` 或者 `schedule` 函数,然后实例化子类并调用 `run` 方法 。
+
+### 2.1 代码
+
+以下的代码对所有来自好友的文本消息回复 *hi* , 并不断向好友 *tb* 发送 *schedule* 。
+
+`handle_msg_all` 函数用于处理收到的每条消息,而 `schedule` 函数可以做一些任务性的事情(例如不断向好友推送信息或者一些定时任务)。
 
 ```python
 #!/usr/bin/env python
@@ -28,8 +70,8 @@ from wxbot import *
 
 class MyWXBot(WXBot):
     def handle_msg_all(self, msg):
-        if msg['msg_type_id'] == 5 and msg['user_type'] == 'contact':
-            self.send_msg_by_uid('hi', msg['user_id'])
+        if msg['msg_type_id'] == 4 and msg['content']['type'] == 0:
+            self.send_msg_by_uid('hi', msg['user']['id'])
 
     def schedule(self):
         self.send_msg('tb', 'schedule')
@@ -45,94 +87,146 @@ if __name__ == '__main__':
 
 ```
 
-### 运行
+### 2.2 运行
 
-直接用python运行代码(如运行测试代码test.py):
+直接用 `python` 运行代码(如运行测试代码 ***test.py*** ):
 
 ``` python
 python test.py
 ```
 
-### 登录微信
+### 2.3 登录微信
 
-程序运行之后,会在当前目录下生成二维码图片文件 qr.png ,用微信扫描此二维码并按操作指示确认登录网页微信。
+程序运行之后,会在当前目录下生成二维码图片文件 ***qr.png*** ,用微信扫描此二维码并按操作指示确认登录网页微信。
 
-![1](img/1.png)
-
-如果运行在Linux下,还可以通过设置WXBot对象的conf['qr']为'tty'的方式直接在终端打印二维码(此方法只能在Linux终端下使用),效果如下:
+如果运行在Linux下,还可以通过设置 **WXBot** 对象的 `conf['qr']` 为 `tty` 的方式直接在终端打印二维码(此方法只能在Linux终端下使用),效果如下:
 
 ![login_on_ubuntu](img/login_on_ubuntu.png)
 
-## 效果展示
+## 3 效果展示
 
-测试代码test.py的运行效果:
+测试代码 ***test.py*** 的运行效果:
 
 ![向机器人发送消息](img/send_msg.png)
 
 ![后台](img/backfront.jpg)
 
-## 接口
-### handle_msg_all
+## 4 接口
+### 4.1 `handle_msg_all`
 
-handle_msg_all函数的参数msg是代表一条消息的字典。一般包含以下字段:
+`handle_msg_all` 函数的参数 `msg` 是代表一条消息的字典。字段的内容为
 
-| 字段名 | 意义 |
+| 字段名 | 字段内容 |
 | ----- | --- |
-| user_type | 用户类型,具体见用户类型表 |
-| msg_id | 消息id,微信内部数据 |
-| msg_type_id | 消息类型,具体见消息类型表 |
-| user_id | 发送消息的用户的id,微信内部数据 |
-| user_name | 发送消息的用户的名字,为备注名或者微信用户名 |
-| content | 消息体,不同类型消息的此字段内容不同,具体见消息类型表 |
-
-用户类型表:
-
-| 类型名 | 意义 |
-| ----- | ---- |
-| contact | 好友 |
-| public | 公众号 |
-| group | 群 |
-| special | 特殊账号 |
-| unknown | 未知 |
-
-消息类型表:
-
-| 类型号 | 类型名称 | 类型描述 | content |
-| ------ | ------- | --- | -------- |
-| 1 | Location | 位置 | 字典,包含location(位置的文本描述)和xml(原始未解析的xml格式文本)字段 |
-| 2 | FileHelper | 文件 | 字符串,文本文件内容 |
-| 3 | Self | 自己发送的消息 | 字符串,文本消息 |
-| 4 | Group | 群消息 | 字典,包含group_id(微信内部id), group_name(群名), user(微信内部id), user_name(用户名称), msg(字符串消息)字段 |
-| 5 | Text | 普通文本消息 | 字符串, 文本消息 |
-| 6 | Image | 图片 | 字符串, 图片url |
-| 7 | Voice | 语音 | 字符串, 语音url |
-| 8 | Recommend | 微信名片 | 字典, 包含nickname(昵称),alias(别名),province(省份),city(城市), gender(性别)字段 |
-| 9 | Animation | 动画 | 字符串, 动画url |
-| 10 | Share | 分享 | 字典,包含type(类型),title(标题),desc(描述),url(链接),from(来自)字段 |
-| 11 | Video | 视频 | 字符串,未解析的xml字符串 |
-| 12 | VideoCall | 视频电话 | 字符串,未解析的xml字符串 |
-| 13 | Redraw | 撤回消息 | 字符串,未解析的xml字符串 |
-| 14 | Init | 微信初始化系统消息,可以忽略 | 字符串,未解析的xml字符串 |
-| 99 | Unknown | 未知类型 | 字符串,未解析的xml字符串 |
-
-### WXBot对象属性
-
-WXBot对象在登录并初始化之后,含有以下的可用数据:
+| `msg_type_id` | 整数,消息类型,具体解释可以查看 **消息类型表** |
+| `msg_id` | 字符串,消息id |
+| `content` | 字典,消息内容,具体含有的字段请参考 **消息类型表** ,一般含有 `type`(数据类型)与 `data`(数据内容)字段,`type` 与 `data`的对应关系可以参考 **数据类型表**  |
+| `user` | 字典,消息来源,字典包含 `name`(发送者名称,如果是群则为群名称,如果为微信号,有备注则为备注名,否则为微信号或者群昵称)字段与 `id`(发送者id)字段,都是字符串  |
+
+
+### 4.2 消息类型表
+
+| 类型号 | 消息类型 | `content` |
+| ----- | --- | ------ |
+| 0 | 初始化消息,内部数据 | 无意义,可以忽略 |
+| 1 | 自己发送的消息 | 无意义,可以忽略 |
+| 2 | 文件消息 | 字典,包含 `type` 与 `data` 字段 |
+| 3 | 群消息 | 字典, 包含 `user` (字典,包含 `id` 与 `name`字段,都是字符串,表示发送此消息的群用户)与 `type` 、 `data` 字段,红包消息除外(只有 `type` 字段) |
+| 4 | 联系人消息 | 字典,包含 `type` 与 `data` 字段 |
+| 5 | 公众号消息 | 字典,包含 `type` 与 `data` 字段 |
+| 6 | 特殊账号消息 | 字典,包含 `type` 与 `data` 字段 |
+| 99 | 未知账号消息 | 无意义,可以忽略 |
+
+
+### 4.3 数据类型表
+
+| `type` | 数据类型 | `data` |
+| ---- | ---- | ------ |
+| 0 | 文本 | 字符串,表示文本消息的具体内容 |
+| 1 | 地理位置 | 字符串,表示地理位置 |
+| 3 | 图片 | 字符串,图片数据的url,HTTP POST请求此url可以得到jpg文件格式的数据 |
+| 4 | 语音 | 字符串,语音数据的url,HTTP POST请求此url可以得到mp3文件格式的数据 |
+| 5 | 名片 | 字典,包含 `nickname` (昵称), `alias` (别名),`province` (省份),`city` (城市), `gender` (性别)字段 |
+| 6 | 动画 | 字符串, 动画url, HTTP POST请求此url可以得到gif文件格式的数据 |
+| 7 | 分享 | 字典,包含 `type` (类型),`title` (标题),`desc` (描述),`url` (链接),`from` (源网站)字段 |
+| 8 | 视频 | 不可用 |
+| 9 | 视频电话 | 不可用 |
+| 10 | 撤回消息 | 不可用 |
+| 11 | 空内容 | 空字符串 |
+| 12 | 红包 | 不可用 |
+| 99 | 未知类型 | 不可用 |
+
+
+### 4.4 WXBot对象属性
+
+**WXBot** 对象在登录并初始化之后,含有以下的可用数据:
 
 | 属性 | 描述 |
 | ---- | ---- |
-| contact_list | 当前用户的微信联系人列表 |
-| group_list | 当前用户的微信群列表 |
-| session | WXBot与WEB微信服务器端交互所用的requests Session对象 |
+| `contact_list` | 当前用户的微信联系人列表 |
+| `group_list` | 当前用户的微信群列表 |
+| `public_list` | 当前用户关注的公众号列表 |
+| `special_list` | 特殊账号列表 |
+| `session` | **WXBot** 与WEB微信服务器端交互所用的 **Requests** `Session` 对象 |
+
+### 4.5 WXBot对象方法
 
-WXBot对象还含有一些可以利用的方法:
+**WXBot** 对象还含有一些可以利用的方法
 
 | 方法 | 描述 |
 | ---- | --- |
-| get_icon(id) | 获取用户icon并保存到本地文件 img_[id].jpg ,id为用户id(Web微信数据) |
-| get_head_img(id) | 获取用户头像并保存到本地文件 img_[id].jpg,id为用户id(Web微信数据) |
-| get_msg_img(msgid) | 获取图像消息并保存到本地文件 img_[msgid].jpg, msgid为消息id(Web微信数据) |
-| get_voice(msgid) | 获取语音消息并保存到本地文件 voice_[msgid].mp3, msgid为消息id(Web微信数据) |
-| get_user_remark_name(uid) | 获取好友的备注名,没有备注名则获取好友微信号, uid为好友的用户id(Web微信数据) |
-| send_msg_by_uid(word, dst) | 向好友发送消息,word为消息字符串,dst为好友用户id(Web微信数据) |
-| send_msg(name, word, isfile) | 向好友发送消息,name为好友的备注名或者好友微信号,isfile为False时word为消息,isfile为True时word为文件路径(此时向好友发送文件里的每一行) |
+| `get_icon(id)` | 获取用户icon并保存到本地文件 ***img_[id].jpg***  , `id` 为用户id(Web微信数据) |
+| `get_head_img(id)` | 获取用户头像并保存到本地文件 ***img_[id].jpg*** ,`id` 为用户id(Web微信数据) |
+| `get_msg_img(msgid)` | 获取图像消息并保存到本地文件 ***img_[msgid].jpg*** , `msgid` 为消息id(Web微信数据) |
+| `get_voice(msgid)` | 获取语音消息并保存到本地文件 ***voice_[msgid].mp3*** , `msgid` 为消息id(Web微信数据) |
+| `get_account_name(uid)` | 获取微信id对应的名称,返回一个可能包含 `remark_name` (备注名), `nickname` (昵称), `display_name` (群名称)的字典|
+| `send_msg_by_uid(word, dst)` | 向好友发送消息,`word` 为消息字符串,`dst` 为好友用户id(Web微信数据) |
+| `send_msg(name, word, isfile)` | 向好友发送消息,`name` 为好友的备注名或者好友微信号, `isfile`为 `False` 时 `word` 为消息,`isfile` 为 `True` 时 `word` 为文件路径(此时向好友发送文件里的每一行) |
+| `is_contact(uid)` | 判断id为 `uid` 的账号是否是本帐号的好友,返回 `True` (是)或 `False` (不是) |
+| `is_public(uid)` | 判断id为 `uid` 的账号是否是本帐号所关注的公众号,返回 `True` (是)或 `False` (不是) |
+
+
+## 5 Example
+
+***bot.py*** 用 **[图灵机器人](http://www.tuling123.com/)** API 以及 **wxBot** 实现了一个自动回复机器人.
+
+此机器人会回复来自联系人的消息,以及群里@此账号的消息。
+
+并且本帐号可以通过发送 *退下* 、 *走开* 、 *关闭* 、 *关掉* 、 *休息* 、 *滚开* 来关闭机器人的自动回复。
+
+也可以通过发送 *出来* 、 *启动* 、 *工作* 来再次开启机器人的自动回复。
+
+群聊时需要将对应的群保存到联系人列表。
+
+群聊实现效果:
+
+![群聊](img/group_chat.png)
+
+![群聊后台](img/group_chat_backend.jpg)
+
+
+***bot.py*** 的运行方法:
+
+- 要接入图灵机器人API时:
+
+  1. 在[图灵机器人官网](http://www.tuling123.com/)注册账号,申请图灵key: [图灵key申请地址](http://www.tuling123.com/html/doc/apikey.html)
+
+  2. 在 ***bot.py*** 文件所在目录下新建 ***conf.ini*** 文件,内容为:(key字段内容为申请到的图灵key)
+
+    ```txt
+    [main]    
+    key=1d2678900f734aa0a23734ace8aec5b1
+    ```
+
+  3. 运行 ***bot.py***
+
+    ```python
+    python bot.py
+    ```
+
+- 不接入图灵机器人API时(此时机器人对联系人消息以及群里@自己的消息统一回复 *知道了* ):
+  1. 运行 ***bot.py***
+
+    ```python
+    python bot.py
+    ```

+ 116 - 0
bot.py

@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+from wxbot import *
+import ConfigParser
+import json
+
+
+class TulingWXBot(WXBot):
+    def __init__(self):
+        WXBot.__init__(self)
+
+        self.tuling_key = ""
+        self.robot_switch = True
+
+        try:
+            cf = ConfigParser.ConfigParser()
+            cf.read('conf.ini')
+            self.tuling_key = cf.get('main', 'key')
+        except Exception:
+            pass
+        print 'tuling_key:', self.tuling_key
+
+    def tuling_auto_reply(self, uid, msg):
+        if self.tuling_key:
+            url = "http://www.tuling123.com/openapi/api"
+            user_id = uid.replace('@', '')[:30]
+            body = {'key': self.tuling_key, 'info': msg.encode('utf8'), 'userid': user_id}
+            r = requests.post(url, data=body)
+            respond = json.loads(r.text)
+            result = ''
+            if respond['code'] == 100000:
+                result = respond['text'].replace('<br>', '  ')
+            elif respond['code'] == 200000:
+                result = respond['url']
+            else:
+                result = respond['text'].replace('<br>', '  ')
+
+            print '    ROBOT:', result
+            return result
+        else:
+            return u"知道啦"
+
+    def auto_switch(self, msg):
+        msg_data = msg['content']['data']
+        STOP = [u'退下', u'走开', u'关闭', u'关掉', u'休息', u'滚开']
+        START = [u'出来', u'启动', u'工作']
+        if self.robot_switch:
+            for i in STOP:
+                if i == msg_data:
+                    self.robot_switch = False
+                    self.send_msg_by_uid(u'[Robot]' + u'机器人已关闭!', msg['to_user_id'])
+        else:
+            for i in START:
+                if i == msg_data:
+                    self.robot_switch = True
+                    self.send_msg_by_uid(u'[Robot]' + u'机器人已开启!', msg['to_user_id'])
+
+    def handle_msg_all(self, msg):
+        if not self.robot_switch and msg['msg_type_id'] != 1:
+            return
+        if msg['msg_type_id'] == 1 and msg['content']['type'] == 0:  # reply to self
+            self.auto_switch(msg)
+        elif msg['msg_type_id'] == 4 and msg['content']['type'] == 0:  # text message from contact
+            self.send_msg_by_uid(self.tuling_auto_reply(msg['user']['id'], msg['content']['data']), msg['user']['id'])
+        elif msg['msg_type_id'] == 3:  # group message
+            if msg['content']['data'].find('@') >= 0:  # someone @ another
+                my_names = self.get_group_member_name(msg['user']['id'], self.user['UserName'])
+                if my_names is None:
+                    my_names = {}
+                if 'NickName' in self.user and len(self.user['NickName']) > 0:
+                    my_names['nickname2'] = self.user['NickName']
+                if 'RemarkName' in self.user and len(self.user['RemarkName']) > 0:
+                    my_names['remark_name2'] = self.user['RemarkName']
+                is_at_me = False
+                text_msg = ''
+                for _ in my_names:
+                    if msg['content']['data'].find('@'+my_names[_]) >= 0:
+                        is_at_me = True
+                        text_msg = msg['content']['data'].replace('@'+my_names[_], '').strip()
+                        break
+                if is_at_me:  # someone @ me
+                    snames = self.get_group_member_name(msg['user']['id'], msg['content']['user']['id'])
+                    if snames is None:
+                        snames = self.get_account_name(msg['content']['user']['id'])
+                    src_name = ''
+                    if snames is not None:
+                        if 'display_name' in snames and len(snames['display_name']) > 0:
+                            src_name = snames['display_name']
+                        elif 'nickname' in snames and len(snames['nickname']) > 0:
+                            src_name = snames['nickname']
+                        elif 'remark_name' in snames and len(snames['remark_name']) > 0:
+                            src_name = snames['remark_name']
+                    else:
+                        return
+
+                    if src_name != '':
+                        reply = '@' + src_name + ' '
+                        if msg['content']['type'] == 0:  # text message
+                            reply += self.tuling_auto_reply(msg['content']['user']['id'], text_msg)
+                        else:
+                            reply += u"对不起,只认字,其他杂七杂八的我都不认识,,,Ծ‸Ծ,,"
+                        self.send_msg_by_uid(reply, msg['user']['id'])
+
+
+def main():
+    bot = TulingWXBot()
+    bot.DEBUG = True
+    bot.conf['qr'] = 'png'
+
+    bot.run()
+
+
+if __name__ == '__main__':
+    main()
+

BIN
img/backfront.jpg


BIN
img/group_chat.png


BIN
img/group_chat_backend.jpg


+ 28 - 24
test.py

@@ -1,24 +1,28 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-import time
-from wxbot import *
-
-class MyWXBot(WXBot):
-    def handle_msg_all(self, msg):
-        if msg['msg_type_id'] == 5 and msg['user_type'] == 'contact':
-            self.send_msg_by_uid('hi', msg['user_id'])
-'''
-    def schedule(self):
-        self.send_msg('tb', 'schedule')
-        time.sleep(1)
-'''
-
-def main():
-    bot = MyWXBot()
-    bot.DEBUG = True
-    bot.conf['qr'] = 'png'
-    bot.run()
-
-if __name__ == '__main__':
-    main()
+#!/usr/bin/env python
+# coding: utf-8
+
+from wxbot import *
+
+
+class MyWXBot(WXBot):
+    def handle_msg_all(self, msg):
+        if msg['msg_type_id'] == 4 and msg['content']['type'] == 0:
+            self.send_msg_by_uid('hi', msg['user']['id'])
+
+
+'''
+    def schedule(self):
+        self.send_msg('tb', 'schedule')
+        time.sleep(1)
+'''
+
+
+def main():
+    bot = MyWXBot()
+    bot.DEBUG = True
+    bot.conf['qr'] = 'png'
+    bot.run()
+
+
+if __name__ == '__main__':
+    main()

+ 805 - 617
wxbot.py

@@ -1,617 +1,805 @@
-#!/usr/bin/env python
-# coding: utf-8
-from collections import defaultdict
-import pyqrcode
-import requests
-import json
-import xml.dom.minidom
-import multiprocessing
-import urllib
-import time, re, sys, os, random
-
-UNKONWN = 'unkonwn'
-SUCCESS = '200'
-SCANED  = '201'
-TIMEOUT = '408'
-
-def utf82gbk(string):
-    return string.decode('utf8').encode('gbk')
-
-def make_unicode(data):
-    if not data:
-        return data
-    result = None
-    if type(data) == unicode:
-        result = data
-    elif type(data) == str:
-        result = data.decode('utf-8')
-    return result
-
-class WXBot:
-    def __init__(self):
-        self.DEBUG = False
-        self.uuid = ''
-        self.base_uri = ''
-        self.redirect_uri= ''
-        self.uin = ''
-        self.sid = ''
-        self.skey = ''
-        self.pass_ticket = ''
-        self.device_id = 'e' + repr(random.random())[2:17]
-        self.base_request = {}
-        self.sync_key_str = ''
-        self.sync_key = []
-        self.user = []
-        self.member_list = []
-        self.contact_list = []  # contact list
-        self.public_list = []   # public account list
-        self.group_list = []    # group chat list
-        self.special_list = []  # special list account
-        self.sync_host = ''
-        self.session = requests.Session()
-        self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'})
-
-        self.conf = {'qr': 'png',}
-
-    def get_uuid(self):
-        url = 'https://login.weixin.qq.com/jslogin'
-        params = {
-            'appid': 'wx782c26e4c19acffb',
-            'fun': 'new',
-            'lang': 'zh_CN',
-            '_': int(time.time())*1000 + random.randint(1,999),
-        }
-        r = self.session.get(url, params=params)
-        r.encoding = 'utf-8'
-        data = r.text
-        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 gen_qr_code(self, qr_file_path):
-        string = 'https://login.weixin.qq.com/l/' + self.uuid
-        qr = pyqrcode.create(string)
-        if self.conf['qr'] == 'png':
-            qr.png(qr_file_path)
-        elif self.conf['qr'] == 'tty':
-            print 'Not support tty'
-            pass
-            #qr.print_tty()
-
-    def do_request(self, url):
-        r = self.session.get(url)
-        r.encoding = 'utf-8'
-        data = r.text
-        param = re.search(r'window.code=(\d+);', data)
-        code = param.group(1)
-        return code, data
-
-    def wait4login(self):
-        '''
-        http comet:
-        tip=1, the request wait for user to scan the qr, 
-               201: scaned
-               408: timeout
-        tip=0, the request wait for user confirm, 
-               200: confirmed
-        '''
-        LOGIN_TEMPLATE = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s'
-        tip = 1
-
-        try_later_secs = 1
-        MAX_RETRY_TIMES = 10
-        
-        code = UNKONWN
-        
-        retry_time = MAX_RETRY_TIMES
-        while retry_time > 0:
-            url = LOGIN_TEMPLATE % (tip, self.uuid, int(time.time()))
-            code, data = self.do_request(url)
-            if code == SCANED:
-                print '[INFO] Please confirm to login .'
-                tip = 0
-            elif code == SUCCESS: #confirmed sucess
-                param = re.search(r'window.redirect_uri="(\S+?)";', data)
-                redirect_uri = param.group(1) + '&fun=new'
-                self.redirect_uri = redirect_uri
-                self.base_uri = redirect_uri[:redirect_uri.rfind('/')]
-                return code
-            elif code == TIMEOUT:
-                print '[ERROR] WeChat login timeout. retry in %s secs later...'%(try_later_secs, )
-
-                tip = 1 #need to reset tip, because the server will reset the peer connection
-                retry_time -= 1
-                time.sleep(try_later_secs)
-            else:
-                print ('[ERROR] WeChat login exception return_code=%s. retry in %s secs later...' % 
-                        (code, try_later_secs))
-                tip = 1
-                retry_time -= 1
-                time.sleep(try_later_secs)
-            
-        return code
-
-    def login(self):
-        r = self.session.get(self.redirect_uri)
-        r.encoding = 'utf-8'
-        data = r.text
-        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.base_request = {
-            'Uin': self.uin,
-            'Sid': self.sid,
-            'Skey': self.skey,
-            'DeviceID': self.device_id,
-            }
-        return True
-
-    def init(self):
-        url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
-        params = {
-            'BaseRequest': self.base_request
-        }
-        r = self.session.post(url, data=json.dumps(params))
-        r.encoding = 'utf-8'
-        dic = json.loads(r.text)
-        self.sync_key = dic['SyncKey']
-        self.user = dic['User']
-        self.sync_key_str = '|'.join([ str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.sync_key['List'] ])
-        return dic['BaseResponse']['Ret'] == 0
-
-    def status_notify(self):
-        url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket)
-        self.base_request['Uin'] = int(self.base_request['Uin'])
-        params = {
-            'BaseRequest': self.base_request,
-            "Code": 3,
-            "FromUserName": self.user['UserName'],
-            "ToUserName": self.user['UserName'],
-            "ClientMsgId": int(time.time())
-        }
-        r = self.session.post(url, data=json.dumps(params))
-        r.encoding = 'utf-8'
-        dic = json.loads(r.text)
-        return dic['BaseResponse']['Ret'] == 0
-
-    def get_contact(self):
-        url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (self.pass_ticket, self.skey, int(time.time()))
-        r = self.session.post(url, data='{}')
-        r.encoding = 'utf-8'
-        if self.DEBUG:
-            with open('contacts.json', 'w') as f:
-                f.write(r.text.encode('utf-8'))
-        dic = json.loads(r.text)
-        self.member_list = dic['MemberList']
-
-        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.contact_list = []
-        self.public_list = []
-        self.special_list = []
-        self.group_list = []
-        for contact in self.member_list:
-            if contact['VerifyFlag'] & 8 != 0: # public account
-                self.public_list.append(contact)
-            elif contact['UserName'] in SpecialUsers: # special account
-                self.special_list.append(contact)
-            elif contact['UserName'].find('@@') != -1: # group
-                self.group_list.append(contact)
-            elif contact['UserName'] == self.user['UserName']: # self
-                pass
-            else:
-                self.contact_list.append(contact)
-
-        if self.DEBUG:
-            with open('contact_list.json', 'w') as f:
-                f.write(json.dumps(self.contact_list))
-            with open('special_list.json', 'w') as f:
-                f.write(json.dumps(self.special_list))
-            with open('group_list.json', 'w') as f:
-                f.write(json.dumps(self.group_list))
-            with open('public_list.json', 'w') as f:
-                f.write(json.dumps(self.public_list))
-
-        return True
-
-    def batch_get_contact(self):
-        url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
-        params = {
-            'BaseRequest': self.base_request,
-            "Count": len(self.group_list),
-            "List": [ {"UserName": g['UserName'], "EncryChatRoomId":""} for g in self.group_list ]
-        }
-        r = self.session.post(url, data=params)
-        r.encoding = 'utf-8'
-        dic = json.loads(r.text)
-        return True
-
-    def test_sync_check(self):
-        for host in ['webpush', 'webpush2']:
-            self.sync_host = host
-            [retcode, selector] = self.sync_check()
-            if retcode == '0':
-                return True
-        return False
-
-    def sync_check(self):
-        params = {
-            'r': int(time.time()),
-            'sid': self.sid,
-            'uin': self.uin,
-            'skey': self.skey,
-            'deviceid': self.device_id,
-            'synckey': self.sync_key_str,
-            '_': int(time.time()),
-        }
-        url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params)
-        r = self.session.get(url)
-        r.encoding = 'utf-8'
-        data = r.text
-        pm = re.search(r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data)
-        retcode = pm.group(1)
-        selector = pm.group(2)
-        return [retcode, selector]
-
-    def sync(self):
-        url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' % (self.sid, self.skey, self.pass_ticket)
-        params = {
-            'BaseRequest': self.base_request,
-            'SyncKey': self.sync_key,
-            'rr': ~int(time.time())
-        }
-        r = self.session.post(url, data=json.dumps(params))
-        r.encoding = 'utf-8'
-        dic = json.loads(r.text)
-        if dic['BaseResponse']['Ret'] == 0:
-            self.sync_key = dic['SyncKey']
-            self.sync_key_str = '|'.join([ str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.sync_key['List'] ])
-        return dic
-
-    def get_icon(self, id):
-        url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (id, self.skey)
-        r = self.session.get(url)
-        data = r.content
-        fn = 'img_'+id+'.jpg'
-        with open(fn, 'wb') as f:
-            f.write(data)
-        return fn
-
-    def get_head_img(self, id):
-        url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey)
-        r = self.session.get(url)
-        data = r.content
-        fn = 'img_'+id+'.jpg'
-        with open(fn, 'wb') as f:
-            f.write(data)
-        return fn
-
-    def get_msg_img_url(self, msgid):
-        return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
-
-    def get_msg_img(self, msgid):
-        url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
-        r = self.session.get(url)
-        data = r.content
-        fn = 'img_'+msgid+'.jpg'
-        with open(fn, 'wb') as f:
-            f.write(data)
-        return fn
-
-    def get_voice_url(self, msgid):
-        return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
-
-    def get_voice(self, msgid):
-        url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
-        r = self.session.get(url)
-        data = r.content
-        fn = 'voice_'+msgid+'.mp3'
-        with open(fn, 'wb') as f:
-            f.write(data)
-        return fn
-
-    #Get the NickName or RemarkName of an user by user id
-    def get_user_remark_name(self, uid):
-        name = 'unknown group' if uid[:2] == '@@' else 'stranger'
-        for member in self.member_list:
-            if member['UserName'] == uid:
-                name = member['RemarkName'] if member['RemarkName'] else member['NickName']
-        return name
-
-    #Get user id of an user
-    def get_user_id(self, name):
-        for member in self.member_list:
-            if name == member['RemarkName'] or name == member['NickName'] or name == member['UserName']:
-                return member['UserName']
-        return None
-
-    def get_user_type(self, wx_user_id):
-        for account in self.contact_list:
-            if wx_user_id == account['UserName']:
-                return 'contact'
-        for account in self.public_list:
-            if wx_user_id == account['UserName']:
-                return 'public'
-        for account in self.special_list:
-            if wx_user_id == account['UserName']:
-                return 'special'
-        for account in self.group_list:
-            if wx_user_id == account['UserName']:
-                return 'group'
-        return 'unknown'
-
-    '''
-    msg:
-        user_type
-        msg_id
-        msg_type_id
-        user_id
-        user_name
-        content
-    '''
-    def handle_msg_all(self, msg):
-        pass
-
-    '''
-    msg_type_id:
-        1 -> Location
-        2 -> FileHelper
-        3 -> Self
-        4 -> Group
-        5 -> User Text Message
-        6 -> Image
-        7 -> Voice
-        8 -> Recommend
-        9 -> Animation
-        10 -> Share
-        11 -> Video
-        12 -> Video Call
-        13 -> Redraw
-        14 -> Init Message
-        99 -> Unknown
-    '''
-    def handle_msg(self, r):
-        for msg in r['AddMsgList']:
-            mtype = msg['MsgType']
-
-            wx_user_id = msg['FromUserName']
-            user_type = self.get_user_type(wx_user_id)
-
-            name = self.get_user_remark_name(wx_user_id)
-            content = msg['Content'].replace('&lt;','<').replace('&gt;','>')
-            msg_id = msg['MsgId']
-            msg_type_id = 99
-
-
-            if mtype == 51: #init message
-                msg_type_id = 14
-            elif mtype == 1:
-                if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1:
-                    r = self.session.get(content)
-                    r.encoding = 'gbk'
-                    data = r.text
-                    pos = self.search_content('title', data, 'xml')
-                    msg_type_id = 1
-                    content = {'location': pos, 'xml': data}
-                    if self.DEBUG:
-                        print '[Location] %s : I am at %s ' % (name, pos)
-
-                elif msg['ToUserName'] == 'filehelper':
-                    msg_type_id = 2
-                    content = content.replace('<br/>','\n')
-                    if self.DEBUG:
-                        print '[File] %s : %s' % (name, content)
-
-                elif msg['FromUserName'] == self.user['UserName']: #self
-                    msg_type_id = 3
-
-                elif msg['FromUserName'][:2] == '@@':
-                    [people, content] = content.split(':<br/>')
-                    group = self.get_user_remark_name(msg['FromUserName'])
-                    name = self.get_user_remark_name(people)
-                    msg_type_id = 4
-                    content = {'group_id': msg['FromUserName'], 'group_name': group, 'user': people, 'user_name': name, 'msg': content}
-                    if self.DEBUG:
-                        print '[Group] |%s| %s: %s' % (group, name, content.replace('<br/>','\n'))
-
-                else:
-                    msg_type_id = 5
-                    if self.DEBUG:
-                        print '[Text] ', name, ' : ', content
-
-            elif mtype == 3:
-                msg_type_id = 6
-                content = self.get_msg_img_url(msg_id)
-                if self.DEBUG:
-                    image = self.get_msg_img(msg_id)
-                    print '[Image] %s : %s' % (name, image)
-
-            elif mtype == 34:
-                msg_type_id = 7
-                content = self.get_voice_url(msg_id)
-                if self.DEBUG:
-                    voice = self.get_voice(msg_id)
-                    print '[Voice] %s : %s' % (name, voice)
-
-            elif mtype == 42:
-                msg_type_id = 8
-
-                info = msg['RecommendInfo']
-                content = {}
-                content['nickname'] = info['NickName']
-                content['alias'] = info['Alias']
-                content['province'] = info['Province']
-                content['city'] = info['City']
-                content['gender'] = ['unknown', 'male', 'female'][info['Sex']]
-                if self.DEBUG:
-                    print '[Recommend] %s : ' % name
-                    print '========================='
-                    print '= NickName: %s' % info['NickName']
-                    print '= Alias: %s' % info['Alias']
-                    print '= Local: %s %s' % (info['Province'], info['City'])
-                    print '= Gender: %s' % ['unknown', 'male', 'female'][info['Sex']]
-                    print '========================='
-
-            elif mtype == 47:
-                msg_type_id = 9
-                url = self.search_content('cdnurl', content)
-                content = url
-                if self.DEBUG:
-                    print '[Animation] %s : %s' % (name, url)
-
-            elif mtype == 49:
-                msg_type_id = 10
-                appMsgType = defaultdict(lambda : "")
-                appMsgType.update({5:'link', 3:'music', 7:'weibo'})
-                content = {'type': appMsgType[msg['AppMsgType']], 'title': msg['FileName'], 'desc': self.search_content('des', content, 'xml'), 'url': msg['Url'], 'from': self.search_content('appname', content, 'xml')}
-                if self.DEBUG:
-                    print '[Share] %s : %s' % (name, appMsgType[msg['AppMsgType']])
-                    print '========================='
-                    print '= title: %s' % msg['FileName']
-                    print '= desc: %s' % self.search_content('des', content, 'xml')
-                    print '= link: %s' % msg['Url']
-                    print '= from: %s' % self.search_content('appname', content, 'xml')
-                    print '========================='
-
-            elif mtype == 62:
-                msg_type_id = 11
-                if self.DEBUG:
-                    print '[Video] ', name, ' sent you a video, please check on mobiles'
-
-            elif mtype == 53:
-                msg_type_id = 12
-                if self.DEBUG:
-                    print '[Video Call] ', name, ' call you'
-            elif mtype == 10002:
-                msg_type_id = 13
-                if self.DEBUG:
-                    print '[Redraw] ', name, ' redraw back a message'
-            else:
-                msg_type_id = 99
-                if self.DEBUG:
-                    print '[Unknown] : %s' % str(mtype)
-                    print msg
-            message = {'user_type': user_type, 'msg_id':msg_id, 'msg_type_id': msg_type_id, 'content': content, 'user_id': msg['FromUserName'], 'user_name': name}
-            self.handle_msg_all(message)
-
-    def schedule(self):
-        pass
-
-    def proc_msg(self):
-        self.test_sync_check()
-        while True:
-            [retcode, selector] = self.sync_check()
-            if retcode == '1100': # User have login on mobile
-                pass
-            elif retcode == '0':
-                if selector == '2':
-                    r = self.sync()
-                    if r is not None:
-                        self.handle_msg(r)
-                elif selector == '7': # Play WeChat on mobile
-                    r = self.sync()
-                    if r is not None:
-                        self.handle_msg(r)
-                elif selector == '0':
-                    time.sleep(1)
-            self.schedule()
-
-    def send_msg_by_uid(self, word, dst = 'filehelper'):
-        url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket)
-        msg_id = str(int(time.time()*1000)) + str(random.random())[:5].replace('.','')
-        params = {
-            'BaseRequest': self.base_request,
-            'Msg': {
-                "Type": 1,
-                "Content": make_unicode(word),
-                "FromUserName": self.user['UserName'],
-                "ToUserName": dst,
-                "LocalID": msg_id,
-                "ClientMsgId": msg_id
-            }
-        }
-        headers = {'content-type': 'application/json; charset=UTF-8'}
-        data = json.dumps(params, ensure_ascii=False).encode('utf8')
-        r = self.session.post(url, data = data, headers = headers)
-        dic = r.json()
-        return dic['BaseResponse']['Ret'] == 0
-
-    def send_msg(self, name, word, isfile = False):
-        uid = self.get_user_id(name)
-        if uid:
-            if isfile:
-                with open(word, 'r') as f:
-                    result = True
-                    for line in f.readlines():
-                        line = line.replace('\n','')
-                        print '-> '+name+': '+line
-                        if self.send_msg_by_uid(line, uid):
-                            pass
-                        else:
-                            result = False
-                        time.sleep(1)
-                    return result
-            else:
-                if self.send_msg_by_uid(word, uid):
-                    return True
-                else:
-                    return False
-        else:
-            if self.DEBUG:
-                print '[ERROR] This user does not exist .'
-            return True
-
-    def search_content(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 pm: return pm.group(1)
-        return 'unknown'
-
-    def run(self):
-        self.get_uuid()
-        self.gen_qr_code('qr.png')
-        print '[INFO] Please use WeCaht to scan the QR code .'
-        
-        result = self.wait4login()
-        if result != SUCCESS:
-            print '[ERROR] Web WeChat login failed. failed code=%s'%(result, )
-            return
-        
-        if self.login():
-            print '[INFO] Web WeChat login succeed .'
-        else:
-            print '[ERROR] Web WeChat login failed.'
-            return
-
-        if self.init():
-            print '[INFO] Web WeChat init succeed .'
-        else:
-            print '[INFO] Web WeChat init failed'
-            return
-        self.status_notify()
-        self.get_contact()
-        print '[INFO] Get %d contacts' % len(self.contact_list)
-        print '[INFO] Start to process messages .'
-        self.proc_msg()
+#!/usr/bin/env python
+# coding: utf-8
+
+import pyqrcode
+import requests
+import json
+import xml.dom.minidom
+import urllib
+import time
+import re
+import random
+from requests.exceptions import *
+import os,subprocess,sys
+
+UNKONWN = 'unkonwn'
+SUCCESS = '200'
+SCANED  = '201'
+TIMEOUT = '408'
+
+class WXBot:
+    """WXBot, a framework to process WeChat messages"""
+
+    def __init__(self):
+        self.DEBUG = False
+        self.uuid = ''
+        self.base_uri = ''
+        self.redirect_uri = ''
+        self.uin = ''
+        self.sid = ''
+        self.skey = ''
+        self.pass_ticket = ''
+        self.device_id = 'e' + repr(random.random())[2:17]
+        self.base_request = {}
+        self.sync_key_str = ''
+        self.sync_key = []
+        self.user = {}
+        self.account_info = {}
+        self.member_list = []  # all kind of accounts: contacts, public accounts, groups, special accounts
+        self.contact_list = []  # contact list
+        self.public_list = []  # public account list
+        self.group_list = []  # group chat list
+        self.special_list = []  # special list account
+        self.group_members = {}  # members of all groups
+        self.sync_host = ''
+        self.session = requests.Session()
+        self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'})
+        self.conf = {'qr': 'png'}
+
+    def get_contact(self):
+        """Get information of all contacts of current account."""
+        url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' \
+                              % (self.pass_ticket, self.skey, int(time.time()))
+        r = self.session.post(url, data='{}')
+        r.encoding = 'utf-8'
+        if self.DEBUG:
+            with open('contacts.json', 'w') as f:
+                f.write(r.text.encode('utf-8'))
+        dic = json.loads(r.text)
+        self.member_list = dic['MemberList']
+
+        special_users = ['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.contact_list = []
+        self.public_list = []
+        self.special_list = []
+        self.group_list = []
+        for contact in self.member_list:
+            if contact['VerifyFlag'] & 8 != 0:  # public account
+                self.public_list.append(contact)
+                self.account_info[contact['UserName']] = {'type': 'public', 'info': contact}
+            elif contact['UserName'] in special_users:  # special account
+                self.special_list.append(contact)
+                self.account_info[contact['UserName']] = {'type': 'special', 'info': contact}
+            elif contact['UserName'].find('@@') != -1:  # group
+                self.group_list.append(contact)
+                self.account_info[contact['UserName']] = {'type': 'group', 'info': contact}
+            elif contact['UserName'] == self.user['UserName']:  # self
+                self.account_info[contact['UserName']] = {'type': 'self', 'info': contact}
+                pass
+            else:
+                self.contact_list.append(contact)
+
+        self.group_members = self.batch_get_group_members()
+
+        for group in self.group_members:
+            for member in self.group_members[group]:
+                if member['UserName'] not in self.account_info:
+                    self.account_info[member['UserName']] = {'type': 'group_member', 'info': member, 'group': group}
+
+        if self.DEBUG:
+            with open('contact_list.json', 'w') as f:
+                f.write(json.dumps(self.contact_list))
+            with open('special_list.json', 'w') as f:
+                f.write(json.dumps(self.special_list))
+            with open('group_list.json', 'w') as f:
+                f.write(json.dumps(self.group_list))
+            with open('public_list.json', 'w') as f:
+                f.write(json.dumps(self.public_list))
+            with open('member_list.json', 'w') as f:
+                f.write(json.dumps(self.member_list))
+            with open('group_users.json', 'w') as f:
+                f.write(json.dumps(self.group_members))
+            with open('account_info.json', 'w') as f:
+                f.write(json.dumps(self.account_info))
+        return True
+
+    def batch_get_group_members(self):
+        """Get information of accounts in all groups at once."""
+        url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
+        params = {
+            'BaseRequest': self.base_request,
+            "Count": len(self.group_list),
+            "List": [{"UserName": group['UserName'], "EncryChatRoomId": ""} for group in self.group_list]
+        }
+        r = self.session.post(url, data=json.dumps(params))
+        r.encoding = 'utf-8'
+        dic = json.loads(r.text)
+        group_members = {}
+        for group in dic['ContactList']:
+            gid = group['UserName']
+            members = group['MemberList']
+            group_members[gid] = members
+        return group_members
+
+    def get_group_member_name(self, gid, uid):
+        """
+        Get name of a member in a group.
+        :param gid: group id
+        :param uid: group member id
+        :return: names like {"display_name": "test_user", "nickname": "test", "remark_name": "for_test" }
+        """
+        if gid not in self.group_members:
+            return None
+        group = self.group_members[gid]
+        for member in group:
+            if member['UserName'] == uid:
+                names = {}
+                if 'RemarkName' in member:
+                    names['remark_name'] = member['RemarkName']
+                if 'NickName' in member:
+                    names['nickname'] = member['NickName']
+                if 'DisplayName' in member:
+                    names['display_name'] = member['DisplayName']
+                return names
+        return None
+
+    def get_account_info(self, uid):
+        if uid in self.account_info:
+            return self.account_info[uid]
+        else:
+            return None
+
+    def get_account_name(self, uid):
+        info = self.get_account_info(uid)
+        if info is None:
+            return 'unknown'
+        info = info['info']
+        name = {}
+        if 'RemarkName' in info and info['RemarkName']:
+            name['remark_name'] = info['RemarkName']
+        if 'NickName' in info and info['NickName']:
+            name['nickname'] = info['NickName']
+        if 'DisplayName' in info and info['DisplayName']:
+            name['display_name'] = info['DisplayName']
+        return name
+
+    @staticmethod
+    def get_prefer_name(name):
+        if 'remark_name' in name:
+            return name['remark_name']
+        if 'display_name' in name:
+            return name['display_name']
+        if 'nickname' in name:
+            return name['nickname']
+        return 'unknown'
+
+    def get_user_type(self, wx_user_id):
+        """
+        Get the relationship of a account and current user.
+        :param wx_user_id:
+        :return: The type of the account.
+        """
+        for account in self.contact_list:
+            if wx_user_id == account['UserName']:
+                return 'contact'
+        for account in self.public_list:
+            if wx_user_id == account['UserName']:
+                return 'public'
+        for account in self.special_list:
+            if wx_user_id == account['UserName']:
+                return 'special'
+        for account in self.group_list:
+            if wx_user_id == account['UserName']:
+                return 'group'
+        for group in self.group_members:
+            for member in self.group_members[group]:
+                if member['UserName'] == wx_user_id:
+                    return 'group_member'
+        return 'unknown'
+
+    def is_contact(self, uid):
+        for account in self.contact_list:
+            if uid == account['UserName']:
+                return True
+        return False
+
+    def is_public(self, uid):
+        for account in self.public_list:
+            if uid == account['UserName']:
+                return True
+        return False
+
+    def is_special(self, uid):
+        for account in self.special_list:
+            if uid == account['UserName']:
+                return True
+        return False
+
+    def handle_msg_all(self, msg):
+        """
+        The function to process all WeChat messages, please override this function.
+        msg:
+            msg_id  ->  id of the received WeChat message
+            msg_type_id  ->  the type of the message
+            user  ->  the account that the message if sent from
+            content  ->  content of the message
+        :param msg: The received message.
+        :return: None
+        """
+        pass
+
+    def extract_msg_content(self, msg_type_id, msg):
+        """
+        content_type_id:
+            0 -> Text
+            1 -> Location
+            3 -> Image
+            4 -> Voice
+            5 -> Recommend
+            6 -> Animation
+            7 -> Share
+            8 -> Video
+            9 -> VideoCall
+            10 -> Redraw
+            11 -> Empty
+            99 -> Unknown
+        :param msg_type_id: The type of the received message.
+        :param msg: The received message.
+        :return: The extracted content of the message.
+        """
+        mtype = msg['MsgType']
+        content = msg['Content'].replace('&lt;', '<').replace('&gt;', '>')
+        msg_id = msg['MsgId']
+
+        msg_content = {}
+        if msg_type_id == 0:
+            return {'type': 11, 'data': ''}
+        elif msg_type_id == 2:  # File Helper
+            return {'type': 0, 'data': content.replace('<br/>', '\n')}
+        elif msg_type_id == 3:  # Group
+            sp = content.find('<br/>')
+            uid = content[:sp]
+            content = content[sp:]
+            content = content.replace('<br/>', '')
+            uid = uid[:-1]
+            msg_content['user'] = {'id': uid, 'name': self.get_prefer_name(self.get_account_name(uid))}
+        else:  # Self, Contact, Special, Public, Unknown
+            pass
+
+        msg_prefix = (msg_content['user']['name'] + ':') if 'user' in msg_content else ''
+
+        if mtype == 1:
+            if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1:
+                r = self.session.get(content)
+                r.encoding = 'gbk'
+                data = r.text
+                pos = self.search_content('title', data, 'xml')
+                msg_content['type'] = 1
+                msg_content['data'] = pos
+                msg_content['detail'] = data
+                if self.DEBUG:
+                    print '    %s[Location] %s ' % (msg_prefix, pos)
+            else:
+                msg_content['type'] = 0
+                msg_content['data'] = content.replace(u'\u2005', '')
+                if self.DEBUG:
+                    print '    %s[Text] %s' % (msg_prefix, msg_content['data'])
+        elif mtype == 3:
+            msg_content['type'] = 3
+            msg_content['data'] = self.get_msg_img_url(msg_id)
+            if self.DEBUG:
+                image = self.get_msg_img(msg_id)
+                print '    %s[Image] %s' % (msg_prefix, image)
+        elif mtype == 34:
+            msg_content['type'] = 4
+            msg_content['data'] = self.get_voice_url(msg_id)
+            if self.DEBUG:
+                voice = self.get_voice(msg_id)
+                print '    %s[Voice] %s' % (msg_prefix, voice)
+        elif mtype == 42:
+            msg_content['type'] = 5
+            info = msg['RecommendInfo']
+            msg_content['data'] = {'nickname': info['NickName'],
+                                   'alias': info['Alias'],
+                                   'province': info['Province'],
+                                   'city': info['City'],
+                                   'gender': ['unknown', 'male', 'female'][info['Sex']]}
+            if self.DEBUG:
+                print '    %s[Recommend]' % msg_prefix
+                print '    -----------------------------'
+                print '    | NickName: %s' % info['NickName']
+                print '    | Alias: %s' % info['Alias']
+                print '    | Local: %s %s' % (info['Province'], info['City'])
+                print '    | Gender: %s' % ['unknown', 'male', 'female'][info['Sex']]
+                print '    -----------------------------'
+        elif mtype == 47:
+            msg_content['type'] = 6
+            msg_content['data'] = self.search_content('cdnurl', content)
+            if self.DEBUG:
+                print '    %s[Animation] %s' % (msg_prefix, msg_content['data'])
+        elif mtype == 49:
+            msg_content['type'] = 7
+            app_msg_type = ''
+            if msg['AppMsgType'] == 3:
+                app_msg_type = 'music'
+            elif msg['AppMsgType'] == 5:
+                app_msg_type = 'link'
+            elif msg['AppMsgType'] == 7:
+                app_msg_type = 'weibo'
+            else:
+                app_msg_type = 'unknown'
+            msg_content['data'] = {'type': app_msg_type,
+                                   'title': msg['FileName'],
+                                   'desc': self.search_content('des', content, 'xml'),
+                                   'url': msg['Url'],
+                                   'from': self.search_content('appname', content, 'xml')}
+            if self.DEBUG:
+                print '    %s[Share] %s' % (msg_prefix, app_msg_type)
+                print '    --------------------------'
+                print '    | title: %s' % msg['FileName']
+                print '    | desc: %s' % self.search_content('des', content, 'xml')
+                print '    | link: %s' % msg['Url']
+                print '    | from: %s' % self.search_content('appname', content, 'xml')
+                print '    --------------------------'
+
+        elif mtype == 62:
+            msg_content['type'] = 8
+            msg_content['data'] = content
+            if self.DEBUG:
+                print '    %s[Video] Please check on mobiles' % msg_prefix
+        elif mtype == 53:
+            msg_content['type'] = 9
+            msg_content['data'] = content
+            if self.DEBUG:
+                print '    %s[Video Call]' % msg_prefix
+        elif mtype == 10002:
+            msg_content['type'] = 10
+            msg_content['data'] = content
+            if self.DEBUG:
+                print '    %s[Redraw]' % msg_prefix
+        elif mtype == 10000:
+            msg_content['type'] = 12
+            msg_content['data'] = msg['Content']
+            if self.DEBUG:
+                print '    [Red Packet]'
+        else:
+            msg_content['type'] = 99
+            msg_content['data'] = content
+            if self.DEBUG:
+                print '    %s[Unknown]' % msg_prefix
+        return msg_content
+
+    def handle_msg(self, r):
+        """
+        The inner function that processes raw WeChat messages.
+        msg_type_id:
+            0 -> Init
+            1 -> Self
+            2 -> FileHelper
+            3 -> Group
+            4 -> Contact
+            5 -> Public
+            6 -> Special
+            99 -> Unknown
+        :param r: The raw data of the messages.
+        :return: None
+        """
+        for msg in r['AddMsgList']:
+            msg_type_id = 99
+            user = {'id': msg['FromUserName'], 'name': 'unknown'}
+            if msg['MsgType'] == 51:  # init message
+                msg_type_id = 0
+                user['name'] = 'system'
+            elif msg['FromUserName'] == self.user['UserName']:  # Self
+                msg_type_id = 1
+                user['name'] = 'self'
+            elif msg['ToUserName'] == 'filehelper':  # File Helper
+                msg_type_id = 2
+                user['name'] = 'file_helper'
+            elif msg['FromUserName'][:2] == '@@':  # Group
+                msg_type_id = 3
+                user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
+            elif self.is_contact(msg['FromUserName']):  # Contact
+                msg_type_id = 4
+                user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
+            elif self.is_public(msg['FromUserName']):  # Public
+                msg_type_id = 5
+                user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
+            elif self.is_special(msg['FromUserName']):  # Special
+                msg_type_id = 6
+                user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
+            else:
+                msg_type_id = 99
+                user['name'] = 'unknown'
+
+            if self.DEBUG and msg_type_id != 0:
+                print '[MSG] %s:' % user['name']
+            content = self.extract_msg_content(msg_type_id, msg)
+            message = {'msg_type_id': msg_type_id,
+                       'msg_id': msg['MsgId'],
+                       'content': content,
+                       'to_user_id': msg['ToUserName'],
+                       'user': user}
+            self.handle_msg_all(message)
+
+    def schedule(self):
+        """
+        The function to do schedule works.
+        This function will be called a lot of times.
+        Please override this if needed.
+        :return: None
+        """
+        pass
+
+    def proc_msg(self):
+        self.test_sync_check()
+        while True:
+            check_time = time.time()
+            [retcode, selector] = self.sync_check()
+            if retcode == '1100':  # logout from mobile
+                break
+            elif retcode == '1101':  # login web WeChat from other devide
+                break
+            elif retcode == '0':
+                if selector == '2':  # new message
+                    r = self.sync()
+                    if r is not None:
+                        self.handle_msg(r)
+                elif selector == '7':  # Play WeChat on mobile
+                    r = self.sync()
+                    if r is not None:
+                        self.handle_msg(r)
+                elif selector == '0':  # nothing
+                    pass
+                else:
+                    pass
+            self.schedule()
+            check_time = time.time() - check_time
+            if check_time < 0.5:
+                time.sleep(0.5 - check_time)
+
+    def send_msg_by_uid(self, word, dst='filehelper'):
+        url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % self.pass_ticket
+        msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '')
+        if type(word) == 'str':
+            word = word.decode('utf-8')
+        params = {
+            'BaseRequest': self.base_request,
+            'Msg': {
+                "Type": 1,
+                "Content": word,
+                "FromUserName": self.user['UserName'],
+                "ToUserName": dst,
+                "LocalID": msg_id,
+                "ClientMsgId": msg_id
+            }
+        }
+        headers = {'content-type': 'application/json; charset=UTF-8'}
+        data = json.dumps(params, ensure_ascii=False).encode('utf8')
+        try:
+            r = self.session.post(url, data=data, headers=headers)
+        except (ConnectionError, ReadTimeout):
+            return False
+        dic = r.json()
+        return dic['BaseResponse']['Ret'] == 0
+
+    def get_user_id(self, name):
+        for contact in self.contact_list:
+            if 'RemarkName' in contact and contact['RemarkName'] == name:
+                return contact['UserName']
+            elif 'NickName' in contact and contact['NickName'] == name:
+                return contact['UserName']
+            elif 'DisplayName' in contact and contact['DisplayName'] == name:
+                return contact['UserName']
+        return ''
+
+    def send_msg(self, name, word, isfile=False):
+        uid = self.get_user_id(name)
+        if uid:
+            if isfile:
+                with open(word, 'r') as f:
+                    result = True
+                    for line in f.readlines():
+                        line = line.replace('\n', '')
+                        print '-> ' + name + ': ' + line
+                        if self.send_msg_by_uid(line, uid):
+                            pass
+                        else:
+                            result = False
+                        time.sleep(1)
+                    return result
+            else:
+                if self.send_msg_by_uid(word, uid):
+                    return True
+                else:
+                    return False
+        else:
+            if self.DEBUG:
+                print '[ERROR] This user does not exist .'
+            return True
+
+    @staticmethod
+    def search_content(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 pm:
+                return pm.group(1)
+        return 'unknown'
+
+    def run(self):
+        self.get_uuid()
+        self.gen_qr_code('qr.png')
+        print '[INFO] Please use WeCaht to scan the QR code .'
+        
+        result = self.wait4login()
+        if result != SUCCESS:
+            print '[ERROR] Web WeChat login failed. failed code=%s'%(result, )
+            return
+        
+        if self.login():
+            print '[INFO] Web WeChat login succeed .'
+        else:
+            print '[ERROR] Web WeChat login failed.'
+            return
+
+        if self.init():
+            print '[INFO] Web WeChat init succeed .'
+        else:
+            print '[INFO] Web WeChat init failed'
+            return
+        self.status_notify()
+        self.get_contact()
+        print '[INFO] Get %d contacts' % len(self.contact_list)
+        print '[INFO] Start to process messages .'
+        self.proc_msg()
+
+    def get_uuid(self):
+        url = 'https://login.weixin.qq.com/jslogin'
+        params = {
+            'appid': 'wx782c26e4c19acffb',
+            'fun': 'new',
+            'lang': 'zh_CN',
+            '_': int(time.time()) * 1000 + random.randint(1, 999),
+        }
+        r = self.session.get(url, params=params)
+        r.encoding = 'utf-8'
+        data = r.text
+        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 gen_qr_code(self, qr_file_path):
+        string = 'https://login.weixin.qq.com/l/' + self.uuid
+        qr = pyqrcode.create(string)
+        if self.conf['qr'] == 'png':
+            qr.png(qr_file_path, scale=8)
+            if sys.platform.find('darwin') >= 0:
+                subprocess.call(['open', qr_file_path])
+            elif sys.platform.find('linux') >= 0:
+                subprocess.call(['xdg-open', qr_file_path])
+            else:
+                os.startfile(qr_file_path)
+
+        elif self.conf['qr'] == 'tty':
+            print(qr.terminal(quiet_zone=1))
+
+    def do_request(self, url):
+        r = self.session.get(url)
+        r.encoding = 'utf-8'
+        data = r.text
+        param = re.search(r'window.code=(\d+);', data)
+        code = param.group(1)
+        return code, data
+
+    def wait4login(self):
+        '''
+        http comet:
+        tip=1, the request wait for user to scan the qr, 
+               201: scaned
+               408: timeout
+        tip=0, the request wait for user confirm, 
+               200: confirmed
+        '''
+        LOGIN_TEMPLATE = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s'
+        tip = 1
+
+        try_later_secs = 1
+        MAX_RETRY_TIMES = 10
+        
+        code = UNKONWN
+        
+        retry_time = MAX_RETRY_TIMES
+        while retry_time > 0:
+            url = LOGIN_TEMPLATE % (tip, self.uuid, int(time.time()))
+            code, data = self.do_request(url)
+            if code == SCANED:
+                print '[INFO] Please confirm to login .'
+                tip = 0
+            elif code == SUCCESS: #confirmed sucess
+                param = re.search(r'window.redirect_uri="(\S+?)";', data)
+                redirect_uri = param.group(1) + '&fun=new'
+                self.redirect_uri = redirect_uri
+                self.base_uri = redirect_uri[:redirect_uri.rfind('/')]
+                return code
+            elif code == TIMEOUT:
+                print '[ERROR] WeChat login timeout. retry in %s secs later...'%(try_later_secs, )
+
+                tip = 1 #need to reset tip, because the server will reset the peer connection
+                retry_time -= 1
+                time.sleep(try_later_secs)
+            else:
+                print ('[ERROR] WeChat login exception return_code=%s. retry in %s secs later...' % 
+                        (code, try_later_secs))
+                tip = 1
+                retry_time -= 1
+                time.sleep(try_later_secs)
+            
+        return code
+
+    def login(self):
+        if len(self.redirect_uri) < 4:
+            print '[ERROR] Login failed due to network problem, please try again.'
+            return False
+        r = self.session.get(self.redirect_uri)
+        r.encoding = 'utf-8'
+        data = r.text
+        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.base_request = {
+            'Uin': self.uin,
+            'Sid': self.sid,
+            'Skey': self.skey,
+            'DeviceID': self.device_id,
+        }
+        return True
+
+    def init(self):
+        url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
+        params = {
+            'BaseRequest': self.base_request
+        }
+        r = self.session.post(url, data=json.dumps(params))
+        r.encoding = 'utf-8'
+        dic = json.loads(r.text)
+        self.sync_key = dic['SyncKey']
+        self.user = dic['User']
+        self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
+                                      for keyVal in self.sync_key['List']])
+        return dic['BaseResponse']['Ret'] == 0
+
+    def status_notify(self):
+        url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % self.pass_ticket
+        self.base_request['Uin'] = int(self.base_request['Uin'])
+        params = {
+            'BaseRequest': self.base_request,
+            "Code": 3,
+            "FromUserName": self.user['UserName'],
+            "ToUserName": self.user['UserName'],
+            "ClientMsgId": int(time.time())
+        }
+        r = self.session.post(url, data=json.dumps(params))
+        r.encoding = 'utf-8'
+        dic = json.loads(r.text)
+        return dic['BaseResponse']['Ret'] == 0
+
+    def test_sync_check(self):
+        for host in ['webpush', 'webpush2']:
+            self.sync_host = host
+            retcode = self.sync_check()[0]
+            if retcode == '0':
+                return True
+        return False
+
+    def sync_check(self):
+        params = {
+            'r': int(time.time()),
+            'sid': self.sid,
+            'uin': self.uin,
+            'skey': self.skey,
+            'deviceid': self.device_id,
+            'synckey': self.sync_key_str,
+            '_': int(time.time()),
+        }
+        url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params)
+        try:
+            r = self.session.get(url)
+        except (ConnectionError, ReadTimeout):
+            return [-1, -1]
+        r.encoding = 'utf-8'
+        data = r.text
+        pm = re.search(r'window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}', data)
+        retcode = pm.group(1)
+        selector = pm.group(2)
+        return [retcode, selector]
+
+    def sync(self):
+        url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' \
+                              % (self.sid, self.skey, self.pass_ticket)
+        params = {
+            'BaseRequest': self.base_request,
+            'SyncKey': self.sync_key,
+            'rr': ~int(time.time())
+        }
+        try:
+            r = self.session.post(url, data=json.dumps(params))
+        except (ConnectionError, ReadTimeout):
+            return None
+        r.encoding = 'utf-8'
+        dic = json.loads(r.text)
+        if dic['BaseResponse']['Ret'] == 0:
+            self.sync_key = dic['SyncKey']
+            self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
+                                          for keyVal in self.sync_key['List']])
+        return dic
+
+    def get_icon(self, uid):
+        url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (uid, self.skey)
+        r = self.session.get(url)
+        data = r.content
+        fn = 'img_' + uid + '.jpg'
+        with open(fn, 'wb') as f:
+            f.write(data)
+        return fn
+
+    def get_head_img(self, uid):
+        url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (uid, self.skey)
+        r = self.session.get(url)
+        data = r.content
+        fn = 'img_' + uid + '.jpg'
+        with open(fn, 'wb') as f:
+            f.write(data)
+        return fn
+
+    def get_msg_img_url(self, msgid):
+        return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
+
+    def get_msg_img(self, msgid):
+        url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
+        r = self.session.get(url)
+        data = r.content
+        fn = 'img_' + msgid + '.jpg'
+        with open(fn, 'wb') as f:
+            f.write(data)
+        return fn
+
+    def get_voice_url(self, msgid):
+        return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
+
+    def get_voice(self, msgid):
+        url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
+        r = self.session.get(url)
+        data = r.content
+        fn = 'voice_' + msgid + '.mp3'
+        with open(fn, 'wb') as f:
+            f.write(data)
+        return fn