├── README.md ├── led_server.py ├── led_websocket.py ├── wechat_pi.py └── wxbot.py /README.md: -------------------------------------------------------------------------------- 1 | # 提醒 2 | 项目不再维护,推荐使用[Remote GPIO Recipes](https://gpiozero.readthedocs.io/en/stable/recipes_remote_gpio.html) 3 | 4 | # raspberrypi_api 5 | 把树莓派的硬件功能作为web api 6 | 7 | ![](https://raw.githubusercontent.com/wwj718/gif_bed/master/ledf96a0f7d.png) 8 | 9 | # 原因 10 | * 近期公司有一个有趣的项目,希望用乐高玩具式的可视化编程工具来操控硬件 11 | * 树莓派操控硬件需要有root权限,作为服务之后则没有限制 12 | * 解耦 13 | 14 | # 架构 15 | * 初期效用flask作为web框架 16 | * 把led_server视为下位机,api视为指令集 17 | 18 | # 使用 19 | 我的树莓派当前ip为:192.168.0.106 20 | 21 | ### 启动服务 22 | sudo python led_server.py 23 | 24 | ### 控制 25 | 可以在浏览器或命令行里打开api接口(动作) 26 | 27 | * 点亮红灯: curl 192.168.0.106/led_up 28 | * 熄灭红灯: curl 192.168.0.106/led_down 29 | * 闪啊闪 : curl 192.168.0.106/led_up_down 30 | 31 | #### 在网页中用js控制 32 | ```javascript 33 | xmlhttp=new XMLHttpRequest(); 34 | xmlhttp.open("GET","http://192.168.0.106/led_up_down",true); 35 | xmlhttp.send(); 36 | ``` 37 | 38 | 39 | # 微信控制 40 | 和此前的[wechat_bot](https://github.com/wwj718/wechat_bot)关联即可 41 | 42 | # todo 43 | * 权限 44 | * 先用`?key=xxx` 45 | * websocket 46 | * 长连接 47 | * 双向通信 48 | * 浏览器中js可操作 49 | * python实现: 50 | * [WebSocket-for-Python](https://github.com/Lawouach/WebSocket-for-Python) 51 | * [Flask-SocketIO](https://github.com/miguelgrinberg/Flask-SocketIO) 52 | * [flask-sockets](https://github.com/kennethreitz/flask-sockets) (暂时选择这个) 53 | 54 | # done 55 | * cors 56 | * 可以用js控制硬件 57 | -------------------------------------------------------------------------------- /led_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import RPi.GPIO 5 | import time 6 | from flask import Flask 7 | from flask_cors import CORS, cross_origin 8 | app = Flask(__name__) 9 | CORS(app) 10 | # 对硬件的操作参考:http://blog.mangolovecarrot.net/2015/04/20/raspi-study01/ , 感谢 mango 同学 11 | # 指定GPIO口的选定模式为GPIO引脚编号模式(而非主板编号模式) 12 | RPi.GPIO.setmode(RPi.GPIO.BCM) 13 | 14 | # 指定GPIO14(就是LED长针连接的GPIO针脚)的模式为输出模式 15 | # 如果上面GPIO口的选定模式指定为主板模式的话,这里就应该指定8号而不是14号。 16 | RPi.GPIO.setup(14, RPi.GPIO.OUT) 17 | 18 | # 循环10次 19 | @app.route('/led_up') 20 | def led_up(): 21 | RPi.GPIO.output(14, True) 22 | return 'ok' 23 | 24 | @app.route('/led_down') 25 | def led_down(): 26 | RPi.GPIO.output(14, False) 27 | return 'ok' 28 | 29 | # 闪啊闪 30 | @app.route('/led_up_down') 31 | def led_up_down(): 32 | for i in range(0, 5): 33 | # 让GPIO14输出高电平(LED灯亮) 34 | RPi.GPIO.output(14, True) 35 | # 持续一段时间 36 | time.sleep(0.5) 37 | # 让GPIO14输出低电平(LED灯灭) 38 | RPi.GPIO.output(14, False) 39 | # 持续一段时间 40 | time.sleep(0.5) 41 | return 'ok' 42 | 43 | 44 | # 最后清理GPIO口(不做也可以,建议每次程序结束时清理一下,好习惯) 45 | #RPi.GPIO.cleanup() 46 | 47 | 48 | if __name__ == '__main__': 49 | app.run(host='0.0.0.0',port='5000') 50 | -------------------------------------------------------------------------------- /led_websocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import RPi.GPIO 5 | import time 6 | from flask import Flask 7 | from flask_cors import CORS, cross_origin 8 | 9 | #from flask_socketio import SocketIO, emit 10 | from flask_sockets import Sockets # pip install flask-sockets 11 | 12 | app = Flask(__name__) 13 | CORS(app) 14 | #app.config['HOST'] = '0.0.0.0' 15 | #socketio = SocketIO(app) 16 | sockets = Sockets(app) 17 | 18 | # 对硬件的操作参考:http://blog.mangolovecarrot.net/2015/04/20/raspi-study01/ , 感谢 mango 同学 19 | # 指定GPIO口的选定模式为GPIO引脚编号模式(而非主板编号模式) 20 | #RPi.GPIO.setmode(RPi.GPIO.BCM) 21 | 22 | # 指定GPIO14(就是LED长针连接的GPIO针脚)的模式为输出模式 23 | # 如果上面GPIO口的选定模式指定为主板模式的话,这里就应该指定8号而不是14号。 24 | #RPi.GPIO.setup(14, RPi.GPIO.OUT) 25 | 26 | # 循环10次 27 | @app.route('/led_up') 28 | def led_up(): 29 | RPi.GPIO.output(14, True) 30 | return 'ok' 31 | 32 | @app.route('/led_down') 33 | def led_down(): 34 | RPi.GPIO.output(14, False) 35 | return 'ok' 36 | # 闪啊闪 37 | @app.route('/led_up_down') 38 | def led_up_down(): 39 | for i in range(0, 5): 40 | # 让GPIO14输出高电平(LED灯亮) 41 | RPi.GPIO.output(14, True) 42 | # 持续一段时间 43 | time.sleep(0.5) 44 | # 让GPIO14输出低电平(LED灯灭) 45 | RPi.GPIO.output(14, False) 46 | # 持续一段时间 47 | time.sleep(0.5) 48 | return 'ok' 49 | 50 | # 闪啊闪 51 | @app.route('/led_up_down') 52 | def led_up_down(): 53 | for i in range(0, 5): 54 | # 让GPIO14输出高电平(LED灯亮) 55 | RPi.GPIO.output(14, True) 56 | # 持续一段时间 57 | time.sleep(0.5) 58 | # 让GPIO14输出低电平(LED灯灭) 59 | RPi.GPIO.output(14, False) 60 | # 持续一段时间 61 | time.sleep(0.5) 62 | return 'ok' 63 | 64 | @sockets.route('/echo') 65 | def echo_socket(ws): 66 | while not ws.closed: 67 | message = ws.receive() 68 | print(message) 69 | ws.send(message) 70 | 71 | 72 | # 最后清理GPIO口(不做也可以,建议每次程序结束时清理一下,好习惯) 73 | #RPi.GPIO.cleanup() 74 | 75 | 76 | #if __name__ == '__main__': 77 | # #app.run(host='0.0.0.0',port='5000') 78 | # socketio.run(app,host="0.0.0.0") 79 | if __name__ == "__main__": 80 | from gevent import pywsgi 81 | from geventwebsocket.handler import WebSocketHandler 82 | server = pywsgi.WSGIServer(('0.0.0.0', 5000), app, handler_class=WebSocketHandler) 83 | server.serve_forever() 84 | -------------------------------------------------------------------------------- /wechat_pi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | from wxbot import WXBot 5 | import requests 6 | #bot_api="http://192.168.0.108:8000/get_response" 7 | led_server = 'http://127.0.0.1:5000/' 8 | 9 | #import BaiduYuyin as pby 10 | #YOUR_APP_KEY = "BElGG5nsGL8oevAa3gMzMk4Y" 11 | #YOUR_SECRET_KEY = "uVla1FdpQ2HgmojeY9e6pobrS3lRGaeY" 12 | #tts = pby.TTS(app_key=YOUR_APP_KEY, secret_key=YOUR_SECRET_KEY) 13 | 14 | 15 | class MyWXBot(WXBot): 16 | def _led(self,msg,user_input,action): 17 | response = '正在{}'.format(user_input) 18 | self.send_msg_by_uid(response, msg['user']['id']) 19 | url = led_server+action 20 | requests.get(url) 21 | response = '完成{}'.format(user_input) 22 | self.send_msg_by_uid(response, msg['user']['id']) 23 | 24 | 25 | def handle_msg_all(self, msg): 26 | if msg['msg_type_id'] == 4 and msg['content']['type'] == 0: 27 | user_input = msg["content"]["data"] 28 | #payload={"user_input":user_input} 29 | # 读出来 30 | #print(user_input) 31 | #print(type(user_input)) # unicode 32 | #tts.say(user_input.encode("utf-8")) # encode decode 33 | #response = requests.get(bot_api,params=payload).json()["response"] 34 | if '关' in user_input: 35 | self._led(msg,user_input,'led_down') 36 | if '开' in user_input: 37 | self._led(msg,user_input,'led_up') 38 | if '闪' in user_input: 39 | self._led(msg,user_input,'led_up_down') 40 | #print response 41 | #print(type(response)) # unicode 42 | 43 | def main(): 44 | bot = MyWXBot() 45 | bot.DEBUG = True 46 | bot.conf['qr'] = 'png' 47 | bot.run() 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /wxbot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import os 5 | import sys 6 | import traceback 7 | import webbrowser 8 | import pyqrcode 9 | import requests 10 | import mimetypes 11 | import json 12 | import xml.dom.minidom 13 | import urllib 14 | import time 15 | import re 16 | import random 17 | from traceback import format_exc 18 | from requests.exceptions import ConnectionError, ReadTimeout 19 | import HTMLParser 20 | 21 | UNKONWN = 'unkonwn' 22 | SUCCESS = '200' 23 | SCANED = '201' 24 | TIMEOUT = '408' 25 | 26 | 27 | def show_image(file_path): 28 | """ 29 | 跨平台显示图片文件 30 | :param file_path: 图片文件路径 31 | """ 32 | if sys.version_info >= (3, 3): 33 | from shlex import quote 34 | else: 35 | from pipes import quote 36 | 37 | if sys.platform == "darwin": 38 | command = "open -a /Applications/Preview.app %s&" % quote(file_path) 39 | os.system(command) 40 | else: 41 | webbrowser.open(os.path.join(os.getcwd(),'temp',file_path)) 42 | 43 | 44 | class SafeSession(requests.Session): 45 | def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, 46 | timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None, 47 | json=None): 48 | for i in range(3): 49 | try: 50 | return super(SafeSession, self).request(method, url, params, data, headers, cookies, files, auth, 51 | timeout, 52 | allow_redirects, proxies, hooks, stream, verify, cert, json) 53 | except Exception as e: 54 | print e.message, traceback.format_exc() 55 | continue 56 | 57 | 58 | class WXBot: 59 | """WXBot功能类""" 60 | 61 | def __init__(self): 62 | self.DEBUG = False 63 | self.uuid = '' 64 | self.base_uri = '' 65 | self.redirect_uri = '' 66 | self.uin = '' 67 | self.sid = '' 68 | self.skey = '' 69 | self.pass_ticket = '' 70 | self.device_id = 'e' + repr(random.random())[2:17] 71 | self.base_request = {} 72 | self.sync_key_str = '' 73 | self.sync_key = [] 74 | self.sync_host = '' 75 | 76 | #文件缓存目录 77 | self.temp_pwd = os.path.join(os.getcwd(),'temp') 78 | if os.path.exists(self.temp_pwd) == False: 79 | os.makedirs(self.temp_pwd) 80 | 81 | self.session = SafeSession() 82 | self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'}) 83 | self.conf = {'qr': 'png'} 84 | 85 | self.my_account = {} # 当前账户 86 | 87 | # 所有相关账号: 联系人, 公众号, 群组, 特殊账号 88 | self.member_list = [] 89 | 90 | # 所有群组的成员, {'group_id1': [member1, member2, ...], ...} 91 | self.group_members = {} 92 | 93 | # 所有账户, {'group_member':{'id':{'type':'group_member', 'info':{}}, ...}, 'normal_member':{'id':{}, ...}} 94 | self.account_info = {'group_member': {}, 'normal_member': {}} 95 | 96 | self.contact_list = [] # 联系人列表 97 | self.public_list = [] # 公众账号列表 98 | self.group_list = [] # 群聊列表 99 | self.special_list = [] # 特殊账号列表 100 | self.encry_chat_room_id_list = [] # 存储群聊的EncryChatRoomId,获取群内成员头像时需要用到 101 | 102 | self.file_index = 0 103 | 104 | @staticmethod 105 | def to_unicode(string, encoding='utf-8'): 106 | """ 107 | 将字符串转换为Unicode 108 | :param string: 待转换字符串 109 | :param encoding: 字符串解码方式 110 | :return: 转换后的Unicode字符串 111 | """ 112 | if isinstance(string, str): 113 | return string.decode(encoding) 114 | elif isinstance(string, unicode): 115 | return string 116 | else: 117 | raise Exception('Unknown Type') 118 | 119 | def get_contact(self): 120 | """获取当前账户的所有相关账号(包括联系人、公众号、群聊、特殊账号)""" 121 | url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' \ 122 | % (self.pass_ticket, self.skey, int(time.time())) 123 | r = self.session.post(url, data='{}') 124 | r.encoding = 'utf-8' 125 | if self.DEBUG: 126 | with open(os.path.join(self.temp_pwd,'contacts.json'), 'w') as f: 127 | f.write(r.text.encode('utf-8')) 128 | dic = json.loads(r.text) 129 | self.member_list = dic['MemberList'] 130 | 131 | special_users = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 132 | 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 133 | 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 134 | 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 135 | 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 136 | 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 137 | 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 138 | 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'] 139 | 140 | self.contact_list = [] 141 | self.public_list = [] 142 | self.special_list = [] 143 | self.group_list = [] 144 | 145 | for contact in self.member_list: 146 | if contact['VerifyFlag'] & 8 != 0: # 公众号 147 | self.public_list.append(contact) 148 | self.account_info['normal_member'][contact['UserName']] = {'type': 'public', 'info': contact} 149 | elif contact['UserName'] in special_users: # 特殊账户 150 | self.special_list.append(contact) 151 | self.account_info['normal_member'][contact['UserName']] = {'type': 'special', 'info': contact} 152 | elif contact['UserName'].find('@@') != -1: # 群聊 153 | self.group_list.append(contact) 154 | self.account_info['normal_member'][contact['UserName']] = {'type': 'group', 'info': contact} 155 | elif contact['UserName'] == self.my_account['UserName']: # 自己 156 | self.account_info['normal_member'][contact['UserName']] = {'type': 'self', 'info': contact} 157 | else: 158 | self.contact_list.append(contact) 159 | self.account_info['normal_member'][contact['UserName']] = {'type': 'contact', 'info': contact} 160 | 161 | self.batch_get_group_members() 162 | 163 | for group in self.group_members: 164 | for member in self.group_members[group]: 165 | if member['UserName'] not in self.account_info: 166 | self.account_info['group_member'][member['UserName']] = \ 167 | {'type': 'group_member', 'info': member, 'group': group} 168 | 169 | if self.DEBUG: 170 | with open(os.path.join(self.temp_pwd,'contact_list.json'), 'w') as f: 171 | f.write(json.dumps(self.contact_list)) 172 | with open(os.path.join(self.temp_pwd,'special_list.json'), 'w') as f: 173 | f.write(json.dumps(self.special_list)) 174 | with open(os.path.join(self.temp_pwd,'group_list.json'), 'w') as f: 175 | f.write(json.dumps(self.group_list)) 176 | with open(os.path.join(self.temp_pwd,'public_list.json'), 'w') as f: 177 | f.write(json.dumps(self.public_list)) 178 | with open(os.path.join(self.temp_pwd,'member_list.json'), 'w') as f: 179 | f.write(json.dumps(self.member_list)) 180 | with open(os.path.join(self.temp_pwd,'group_users.json'), 'w') as f: 181 | f.write(json.dumps(self.group_members)) 182 | with open(os.path.join(self.temp_pwd,'account_info.json'), 'w') as f: 183 | f.write(json.dumps(self.account_info)) 184 | return True 185 | 186 | def batch_get_group_members(self): 187 | """批量获取所有群聊成员信息""" 188 | url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket) 189 | params = { 190 | 'BaseRequest': self.base_request, 191 | "Count": len(self.group_list), 192 | "List": [{"UserName": group['UserName'], "EncryChatRoomId": ""} for group in self.group_list] 193 | } 194 | r = self.session.post(url, data=json.dumps(params)) 195 | r.encoding = 'utf-8' 196 | dic = json.loads(r.text) 197 | group_members = {} 198 | encry_chat_room_id = {} 199 | for group in dic['ContactList']: 200 | gid = group['UserName'] 201 | members = group['MemberList'] 202 | group_members[gid] = members 203 | encry_chat_room_id[gid] = group['EncryChatRoomId'] 204 | self.group_members = group_members 205 | self.encry_chat_room_id_list = encry_chat_room_id 206 | 207 | def get_group_member_name(self, gid, uid): 208 | """ 209 | 获取群聊中指定成员的名称信息 210 | :param gid: 群id 211 | :param uid: 群聊成员id 212 | :return: 名称信息,类似 {"display_name": "test_user", "nickname": "test", "remark_name": "for_test" } 213 | """ 214 | if gid not in self.group_members: 215 | return None 216 | group = self.group_members[gid] 217 | for member in group: 218 | if member['UserName'] == uid: 219 | names = {} 220 | if 'RemarkName' in member and member['RemarkName']: 221 | names['remark_name'] = member['RemarkName'] 222 | if 'NickName' in member and member['NickName']: 223 | names['nickname'] = member['NickName'] 224 | if 'DisplayName' in member and member['DisplayName']: 225 | names['display_name'] = member['DisplayName'] 226 | return names 227 | return None 228 | 229 | def get_contact_info(self, uid): 230 | return self.account_info['normal_member'].get(uid) 231 | 232 | 233 | def get_group_member_info(self, uid): 234 | return self.account_info['group_member'].get(uid) 235 | 236 | def get_contact_name(self, uid): 237 | info = self.get_contact_info(uid) 238 | if info is None: 239 | return None 240 | info = info['info'] 241 | name = {} 242 | if 'RemarkName' in info and info['RemarkName']: 243 | name['remark_name'] = info['RemarkName'] 244 | if 'NickName' in info and info['NickName']: 245 | name['nickname'] = info['NickName'] 246 | if 'DisplayName' in info and info['DisplayName']: 247 | name['display_name'] = info['DisplayName'] 248 | if len(name) == 0: 249 | return None 250 | else: 251 | return name 252 | 253 | @staticmethod 254 | def get_contact_prefer_name(name): 255 | if name is None: 256 | return None 257 | if 'remark_name' in name: 258 | return name['remark_name'] 259 | if 'nickname' in name: 260 | return name['nickname'] 261 | if 'display_name' in name: 262 | return name['display_name'] 263 | return None 264 | 265 | @staticmethod 266 | def get_group_member_prefer_name(name): 267 | if name is None: 268 | return None 269 | if 'remark_name' in name: 270 | return name['remark_name'] 271 | if 'display_name' in name: 272 | return name['display_name'] 273 | if 'nickname' in name: 274 | return name['nickname'] 275 | return None 276 | 277 | def get_user_type(self, wx_user_id): 278 | """ 279 | 获取特定账号与自己的关系 280 | :param wx_user_id: 账号id: 281 | :return: 与当前账号的关系 282 | """ 283 | for account in self.contact_list: 284 | if wx_user_id == account['UserName']: 285 | return 'contact' 286 | for account in self.public_list: 287 | if wx_user_id == account['UserName']: 288 | return 'public' 289 | for account in self.special_list: 290 | if wx_user_id == account['UserName']: 291 | return 'special' 292 | for account in self.group_list: 293 | if wx_user_id == account['UserName']: 294 | return 'group' 295 | for group in self.group_members: 296 | for member in self.group_members[group]: 297 | if member['UserName'] == wx_user_id: 298 | return 'group_member' 299 | return 'unknown' 300 | 301 | def is_contact(self, uid): 302 | for account in self.contact_list: 303 | if uid == account['UserName']: 304 | return True 305 | return False 306 | 307 | def is_public(self, uid): 308 | for account in self.public_list: 309 | if uid == account['UserName']: 310 | return True 311 | return False 312 | 313 | def is_special(self, uid): 314 | for account in self.special_list: 315 | if uid == account['UserName']: 316 | return True 317 | return False 318 | 319 | def handle_msg_all(self, msg): 320 | """ 321 | 处理所有消息,请子类化后覆盖此函数 322 | msg: 323 | msg_id -> 消息id 324 | msg_type_id -> 消息类型id 325 | user -> 发送消息的账号id 326 | content -> 消息内容 327 | :param msg: 收到的消息 328 | """ 329 | pass 330 | 331 | @staticmethod 332 | def proc_at_info(msg): 333 | if not msg: 334 | return '', [] 335 | segs = msg.split(u'\u2005') 336 | str_msg_all = '' 337 | str_msg = '' 338 | infos = [] 339 | if len(segs) > 1: 340 | for i in range(0, len(segs) - 1): 341 | segs[i] += u'\u2005' 342 | pm = re.search(u'@.*\u2005', segs[i]).group() 343 | if pm: 344 | name = pm[1:-1] 345 | string = segs[i].replace(pm, '') 346 | str_msg_all += string + '@' + name + ' ' 347 | str_msg += string 348 | if string: 349 | infos.append({'type': 'str', 'value': string}) 350 | infos.append({'type': 'at', 'value': name}) 351 | else: 352 | infos.append({'type': 'str', 'value': segs[i]}) 353 | str_msg_all += segs[i] 354 | str_msg += segs[i] 355 | str_msg_all += segs[-1] 356 | str_msg += segs[-1] 357 | infos.append({'type': 'str', 'value': segs[-1]}) 358 | else: 359 | infos.append({'type': 'str', 'value': segs[-1]}) 360 | str_msg_all = msg 361 | str_msg = msg 362 | return str_msg_all.replace(u'\u2005', ''), str_msg.replace(u'\u2005', ''), infos 363 | 364 | def extract_msg_content(self, msg_type_id, msg): 365 | """ 366 | content_type_id: 367 | 0 -> Text 368 | 1 -> Location 369 | 3 -> Image 370 | 4 -> Voice 371 | 5 -> Recommend 372 | 6 -> Animation 373 | 7 -> Share 374 | 8 -> Video 375 | 9 -> VideoCall 376 | 10 -> Redraw 377 | 11 -> Empty 378 | 99 -> Unknown 379 | :param msg_type_id: 消息类型id 380 | :param msg: 消息结构体 381 | :return: 解析的消息 382 | """ 383 | mtype = msg['MsgType'] 384 | content = HTMLParser.HTMLParser().unescape(msg['Content']) 385 | msg_id = msg['MsgId'] 386 | 387 | msg_content = {} 388 | if msg_type_id == 0: 389 | return {'type': 11, 'data': ''} 390 | elif msg_type_id == 2: # File Helper 391 | return {'type': 0, 'data': content.replace('
', '\n')} 392 | elif msg_type_id == 3: # 群聊 393 | sp = content.find('
') 394 | uid = content[:sp] 395 | content = content[sp:] 396 | content = content.replace('
', '') 397 | uid = uid[:-1] 398 | name = self.get_contact_prefer_name(self.get_contact_name(uid)) 399 | if not name: 400 | name = self.get_group_member_prefer_name(self.get_group_member_name(msg['FromUserName'], uid)) 401 | if not name: 402 | name = 'unknown' 403 | msg_content['user'] = {'id': uid, 'name': name} 404 | else: # Self, Contact, Special, Public, Unknown 405 | pass 406 | 407 | msg_prefix = (msg_content['user']['name'] + ':') if 'user' in msg_content else '' 408 | 409 | if mtype == 1: 410 | if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: 411 | r = self.session.get(content) 412 | r.encoding = 'gbk' 413 | data = r.text 414 | pos = self.search_content('title', data, 'xml') 415 | msg_content['type'] = 1 416 | msg_content['data'] = pos 417 | msg_content['detail'] = data 418 | if self.DEBUG: 419 | print ' %s[Location] %s ' % (msg_prefix, pos) 420 | else: 421 | msg_content['type'] = 0 422 | if msg_type_id == 3 or (msg_type_id == 1 and msg['ToUserName'][:2] == '@@'): # Group text message 423 | msg_infos = self.proc_at_info(content) 424 | str_msg_all = msg_infos[0] 425 | str_msg = msg_infos[1] 426 | detail = msg_infos[2] 427 | msg_content['data'] = str_msg_all 428 | msg_content['detail'] = detail 429 | msg_content['desc'] = str_msg 430 | else: 431 | msg_content['data'] = content 432 | if self.DEBUG: 433 | try: 434 | print ' %s[Text] %s' % (msg_prefix, msg_content['data']) 435 | except UnicodeEncodeError: 436 | print ' %s[Text] (illegal text).' % msg_prefix 437 | elif mtype == 3: 438 | msg_content['type'] = 3 439 | msg_content['data'] = self.get_msg_img_url(msg_id) 440 | msg_content['img'] = self.session.get(msg_content['data']).content.encode('hex') 441 | if self.DEBUG: 442 | image = self.get_msg_img(msg_id) 443 | print ' %s[Image] %s' % (msg_prefix, image) 444 | elif mtype == 34: 445 | msg_content['type'] = 4 446 | msg_content['data'] = self.get_voice_url(msg_id) 447 | msg_content['voice'] = self.session.get(msg_content['data']).content.encode('hex') 448 | if self.DEBUG: 449 | voice = self.get_voice(msg_id) 450 | print ' %s[Voice] %s' % (msg_prefix, voice) 451 | elif mtype == 42: 452 | msg_content['type'] = 5 453 | info = msg['RecommendInfo'] 454 | msg_content['data'] = {'nickname': info['NickName'], 455 | 'alias': info['Alias'], 456 | 'province': info['Province'], 457 | 'city': info['City'], 458 | 'gender': ['unknown', 'male', 'female'][info['Sex']]} 459 | if self.DEBUG: 460 | print ' %s[Recommend]' % msg_prefix 461 | print ' -----------------------------' 462 | print ' | NickName: %s' % info['NickName'] 463 | print ' | Alias: %s' % info['Alias'] 464 | print ' | Local: %s %s' % (info['Province'], info['City']) 465 | print ' | Gender: %s' % ['unknown', 'male', 'female'][info['Sex']] 466 | print ' -----------------------------' 467 | elif mtype == 47: 468 | msg_content['type'] = 6 469 | msg_content['data'] = self.search_content('cdnurl', content) 470 | if self.DEBUG: 471 | print ' %s[Animation] %s' % (msg_prefix, msg_content['data']) 472 | elif mtype == 49: 473 | msg_content['type'] = 7 474 | if msg['AppMsgType'] == 3: 475 | app_msg_type = 'music' 476 | elif msg['AppMsgType'] == 5: 477 | app_msg_type = 'link' 478 | elif msg['AppMsgType'] == 7: 479 | app_msg_type = 'weibo' 480 | else: 481 | app_msg_type = 'unknown' 482 | msg_content['data'] = {'type': app_msg_type, 483 | 'title': msg['FileName'], 484 | 'desc': self.search_content('des', content, 'xml'), 485 | 'url': msg['Url'], 486 | 'from': self.search_content('appname', content, 'xml'), 487 | 'content': msg.get('Content') # 有的公众号会发一次性3 4条链接一个大图,如果只url那只能获取第一条,content里面有所有的链接 488 | } 489 | if self.DEBUG: 490 | print ' %s[Share] %s' % (msg_prefix, app_msg_type) 491 | print ' --------------------------' 492 | print ' | title: %s' % msg['FileName'] 493 | print ' | desc: %s' % self.search_content('des', content, 'xml') 494 | print ' | link: %s' % msg['Url'] 495 | print ' | from: %s' % self.search_content('appname', content, 'xml') 496 | print ' | content: %s' % msg.get('content')[:20] 497 | print ' --------------------------' 498 | 499 | elif mtype == 62: 500 | msg_content['type'] = 8 501 | msg_content['data'] = content 502 | if self.DEBUG: 503 | print ' %s[Video] Please check on mobiles' % msg_prefix 504 | elif mtype == 53: 505 | msg_content['type'] = 9 506 | msg_content['data'] = content 507 | if self.DEBUG: 508 | print ' %s[Video Call]' % msg_prefix 509 | elif mtype == 10002: 510 | msg_content['type'] = 10 511 | msg_content['data'] = content 512 | if self.DEBUG: 513 | print ' %s[Redraw]' % msg_prefix 514 | elif mtype == 10000: # unknown, maybe red packet, or group invite 515 | msg_content['type'] = 12 516 | msg_content['data'] = msg['Content'] 517 | if self.DEBUG: 518 | print ' [Unknown]' 519 | else: 520 | msg_content['type'] = 99 521 | msg_content['data'] = content 522 | if self.DEBUG: 523 | print ' %s[Unknown]' % msg_prefix 524 | return msg_content 525 | 526 | def handle_msg(self, r): 527 | """ 528 | 处理原始微信消息的内部函数 529 | msg_type_id: 530 | 0 -> Init 531 | 1 -> Self 532 | 2 -> FileHelper 533 | 3 -> Group 534 | 4 -> Contact 535 | 5 -> Public 536 | 6 -> Special 537 | 99 -> Unknown 538 | :param r: 原始微信消息 539 | """ 540 | for msg in r['AddMsgList']: 541 | user = {'id': msg['FromUserName'], 'name': 'unknown'} 542 | if msg['MsgType'] == 51: # init message 543 | msg_type_id = 0 544 | user['name'] = 'system' 545 | elif msg['FromUserName'] == self.my_account['UserName']: # Self 546 | msg_type_id = 1 547 | user['name'] = 'self' 548 | elif msg['ToUserName'] == 'filehelper': # File Helper 549 | msg_type_id = 2 550 | user['name'] = 'file_helper' 551 | elif msg['FromUserName'][:2] == '@@': # Group 552 | msg_type_id = 3 553 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 554 | elif self.is_contact(msg['FromUserName']): # Contact 555 | msg_type_id = 4 556 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 557 | elif self.is_public(msg['FromUserName']): # Public 558 | msg_type_id = 5 559 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 560 | elif self.is_special(msg['FromUserName']): # Special 561 | msg_type_id = 6 562 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 563 | else: 564 | msg_type_id = 99 565 | user['name'] = 'unknown' 566 | if not user['name']: 567 | user['name'] = 'unknown' 568 | user['name'] = HTMLParser.HTMLParser().unescape(user['name']) 569 | 570 | if self.DEBUG and msg_type_id != 0: 571 | print '[MSG] %s:' % user['name'] 572 | content = self.extract_msg_content(msg_type_id, msg) 573 | message = {'msg_type_id': msg_type_id, 574 | 'msg_id': msg['MsgId'], 575 | 'content': content, 576 | 'to_user_id': msg['ToUserName'], 577 | 'user': user} 578 | self.handle_msg_all(message) 579 | 580 | def schedule(self): 581 | """ 582 | 做任务型事情的函数,如果需要,可以在子类中覆盖此函数 583 | 此函数在处理消息的间隙被调用,请不要长时间阻塞此函数 584 | """ 585 | pass 586 | 587 | def proc_msg(self): 588 | self.test_sync_check() 589 | while True: 590 | check_time = time.time() 591 | try: 592 | [retcode, selector] = self.sync_check() 593 | # print '[DEBUG] sync_check:', retcode, selector 594 | if retcode == '1100': # 从微信客户端上登出 595 | break 596 | elif retcode == '1101': # 从其它设备上登了网页微信 597 | break 598 | elif retcode == '0': 599 | if selector == '2': # 有新消息 600 | r = self.sync() 601 | if r is not None: 602 | self.handle_msg(r) 603 | elif selector == '3': # 未知 604 | r = self.sync() 605 | if r is not None: 606 | self.handle_msg(r) 607 | elif selector == '6': # 可能是红包 608 | r = self.sync() 609 | if r is not None: 610 | self.handle_msg(r) 611 | elif selector == '7': # 在手机上操作了微信 612 | r = self.sync() 613 | if r is not None: 614 | self.handle_msg(r) 615 | elif selector == '0': # 无事件 616 | pass 617 | else: 618 | print '[DEBUG] sync_check:', retcode, selector 619 | r = self.sync() 620 | if r is not None: 621 | self.handle_msg(r) 622 | else: 623 | print '[DEBUG] sync_check:', retcode, selector 624 | self.schedule() 625 | except: 626 | print '[ERROR] Except in proc_msg' 627 | print format_exc() 628 | check_time = time.time() - check_time 629 | if check_time < 0.8: 630 | time.sleep(1 - check_time) 631 | 632 | def send_msg_by_uid(self, word, dst='filehelper'): 633 | url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % self.pass_ticket 634 | msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '') 635 | word = self.to_unicode(word) 636 | params = { 637 | 'BaseRequest': self.base_request, 638 | 'Msg': { 639 | "Type": 1, 640 | "Content": word, 641 | "FromUserName": self.my_account['UserName'], 642 | "ToUserName": dst, 643 | "LocalID": msg_id, 644 | "ClientMsgId": msg_id 645 | } 646 | } 647 | headers = {'content-type': 'application/json; charset=UTF-8'} 648 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 649 | try: 650 | r = self.session.post(url, data=data, headers=headers) 651 | except (ConnectionError, ReadTimeout): 652 | return False 653 | dic = r.json() 654 | return dic['BaseResponse']['Ret'] == 0 655 | 656 | def upload_media(self, fpath, is_img=False): 657 | if not os.path.exists(fpath): 658 | print '[ERROR] File not exists.' 659 | return None 660 | url_1 = 'https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' 661 | url_2 = 'https://file2.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' 662 | flen = str(os.path.getsize(fpath)) 663 | ftype = mimetypes.guess_type(fpath)[0] or 'application/octet-stream' 664 | files = { 665 | 'id': (None, 'WU_FILE_%s' % str(self.file_index)), 666 | 'name': (None, os.path.basename(fpath)), 667 | 'type': (None, ftype), 668 | 'lastModifiedDate': (None, time.strftime('%m/%d/%Y, %H:%M:%S GMT+0800 (CST)')), 669 | 'size': (None, flen), 670 | 'mediatype': (None, 'pic' if is_img else 'doc'), 671 | 'uploadmediarequest': (None, json.dumps({ 672 | 'BaseRequest': self.base_request, 673 | 'ClientMediaId': int(time.time()), 674 | 'TotalLen': flen, 675 | 'StartPos': 0, 676 | 'DataLen': flen, 677 | 'MediaType': 4, 678 | })), 679 | 'webwx_data_ticket': (None, self.session.cookies['webwx_data_ticket']), 680 | 'pass_ticket': (None, self.pass_ticket), 681 | 'filename': (os.path.basename(os.path.join(self.temp_pwd,fpath)), open(os.path.join(self.temp_pwd,fpath), 'rb'),ftype.split('/')[1]), 682 | } 683 | self.file_index += 1 684 | try: 685 | r = self.session.post(url_1, files=files) 686 | if json.loads(r.text)['BaseResponse']['Ret'] != 0: 687 | # 当file返回值不为0时则为上传失败,尝试第二服务器上传 688 | r = self.session.post(url_2, files=files) 689 | if json.loads(r.text)['BaseResponse']['Ret'] != 0: 690 | print '[ERROR] Upload media failure.' 691 | return None 692 | mid = json.loads(r.text)['MediaId'] 693 | return mid 694 | except Exception,e: 695 | return None 696 | 697 | def send_file_msg_by_uid(self, fpath, uid): 698 | mid = self.upload_media(fpath) 699 | if mid is None or not mid: 700 | return False 701 | url = self.base_uri + '/webwxsendappmsg?fun=async&f=json&pass_ticket=' + self.pass_ticket 702 | msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '') 703 | data = { 704 | 'BaseRequest': self.base_request, 705 | 'Msg': { 706 | 'Type': 6, 707 | 'Content': ("%s6%s%s%s" % (os.path.basename(fpath).encode('utf-8'), str(os.path.getsize(fpath)), mid, fpath.split('.')[-1])).encode('utf8'), 708 | 'FromUserName': self.my_account['UserName'], 709 | 'ToUserName': uid, 710 | 'LocalID': msg_id, 711 | 'ClientMsgId': msg_id, }, } 712 | try: 713 | r = self.session.post(url, data=json.dumps(data)) 714 | res = json.loads(r.text) 715 | if res['BaseResponse']['Ret'] == 0: 716 | return True 717 | else: 718 | return False 719 | except Exception,e: 720 | return False 721 | 722 | def send_img_msg_by_uid(self, fpath, uid): 723 | mid = self.upload_media(fpath, is_img=True) 724 | if mid is None: 725 | return False 726 | url = self.base_uri + '/webwxsendmsgimg?fun=async&f=json' 727 | data = { 728 | 'BaseRequest': self.base_request, 729 | 'Msg': { 730 | 'Type': 3, 731 | 'MediaId': mid, 732 | 'FromUserName': self.my_account['UserName'], 733 | 'ToUserName': uid, 734 | 'LocalID': str(time.time() * 1e7), 735 | 'ClientMsgId': str(time.time() * 1e7), }, } 736 | if fpath[-4:] == '.gif': 737 | url = self.base_uri + '/webwxsendemoticon?fun=sys' 738 | data['Msg']['Type'] = 47 739 | data['Msg']['EmojiFlag'] = 2 740 | try: 741 | r = self.session.post(url, data=json.dumps(data)) 742 | res = json.loads(r.text) 743 | if res['BaseResponse']['Ret'] == 0: 744 | return True 745 | else: 746 | return False 747 | except Exception,e: 748 | return False 749 | 750 | def get_user_id(self, name): 751 | if name == '': 752 | return None 753 | name = self.to_unicode(name) 754 | for contact in self.contact_list: 755 | if 'RemarkName' in contact and contact['RemarkName'] == name: 756 | return contact['UserName'] 757 | elif 'NickName' in contact and contact['NickName'] == name: 758 | return contact['UserName'] 759 | elif 'DisplayName' in contact and contact['DisplayName'] == name: 760 | return contact['UserName'] 761 | for group in self.group_list: 762 | if 'RemarkName' in group and group['RemarkName'] == name: 763 | return group['UserName'] 764 | if 'NickName' in group and group['NickName'] == name: 765 | return group['UserName'] 766 | if 'DisplayName' in group and group['DisplayName'] == name: 767 | return group['UserName'] 768 | 769 | return '' 770 | 771 | def send_msg(self, name, word, isfile=False): 772 | uid = self.get_user_id(name) 773 | if uid is not None: 774 | if isfile: 775 | with open(os.path.join(self.temp_pwd,word), 'r') as f: 776 | result = True 777 | for line in f.readlines(): 778 | line = line.replace('\n', '') 779 | print '-> ' + name + ': ' + line 780 | if self.send_msg_by_uid(line, uid): 781 | pass 782 | else: 783 | result = False 784 | time.sleep(1) 785 | return result 786 | else: 787 | word = self.to_unicode(word) 788 | if self.send_msg_by_uid(word, uid): 789 | return True 790 | else: 791 | return False 792 | else: 793 | if self.DEBUG: 794 | print '[ERROR] This user does not exist .' 795 | return True 796 | 797 | @staticmethod 798 | def search_content(key, content, fmat='attr'): 799 | if fmat == 'attr': 800 | pm = re.search(key + '\s?=\s?"([^"<]+)"', content) 801 | if pm: 802 | return pm.group(1) 803 | elif fmat == 'xml': 804 | pm = re.search('<{0}>([^<]+)'.format(key), content) 805 | if pm: 806 | return pm.group(1) 807 | return 'unknown' 808 | 809 | def run(self): 810 | self.get_uuid() 811 | self.gen_qr_code(os.path.join(self.temp_pwd,'wxqr.png')) 812 | print '[INFO] Please use WeChat to scan the QR code .' 813 | 814 | result = self.wait4login() 815 | if result != SUCCESS: 816 | print '[ERROR] Web WeChat login failed. failed code=%s' % (result,) 817 | return 818 | 819 | if self.login(): 820 | print '[INFO] Web WeChat login succeed .' 821 | else: 822 | print '[ERROR] Web WeChat login failed .' 823 | return 824 | 825 | if self.init(): 826 | print '[INFO] Web WeChat init succeed .' 827 | else: 828 | print '[INFO] Web WeChat init failed' 829 | return 830 | self.status_notify() 831 | self.get_contact() 832 | print '[INFO] Get %d contacts' % len(self.contact_list) 833 | print '[INFO] Start to process messages .' 834 | self.proc_msg() 835 | 836 | def get_uuid(self): 837 | url = 'https://login.weixin.qq.com/jslogin' 838 | params = { 839 | 'appid': 'wx782c26e4c19acffb', 840 | 'fun': 'new', 841 | 'lang': 'zh_CN', 842 | '_': int(time.time()) * 1000 + random.randint(1, 999), 843 | } 844 | r = self.session.get(url, params=params) 845 | r.encoding = 'utf-8' 846 | data = r.text 847 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' 848 | pm = re.search(regx, data) 849 | if pm: 850 | code = pm.group(1) 851 | self.uuid = pm.group(2) 852 | return code == '200' 853 | return False 854 | 855 | def gen_qr_code(self, qr_file_path): 856 | string = 'https://login.weixin.qq.com/l/' + self.uuid 857 | qr = pyqrcode.create(string) 858 | if self.conf['qr'] == 'png': 859 | qr.png(qr_file_path, scale=8) 860 | show_image(qr_file_path) 861 | # img = Image.open(qr_file_path) 862 | # img.show() 863 | elif self.conf['qr'] == 'tty': 864 | print(qr.terminal(quiet_zone=1)) 865 | 866 | def do_request(self, url): 867 | r = self.session.get(url) 868 | r.encoding = 'utf-8' 869 | data = r.text 870 | param = re.search(r'window.code=(\d+);', data) 871 | code = param.group(1) 872 | return code, data 873 | 874 | def wait4login(self): 875 | """ 876 | http comet: 877 | tip=1, 等待用户扫描二维码, 878 | 201: scaned 879 | 408: timeout 880 | tip=0, 等待用户确认登录, 881 | 200: confirmed 882 | """ 883 | LOGIN_TEMPLATE = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' 884 | tip = 1 885 | 886 | try_later_secs = 1 887 | MAX_RETRY_TIMES = 10 888 | 889 | code = UNKONWN 890 | 891 | retry_time = MAX_RETRY_TIMES 892 | while retry_time > 0: 893 | url = LOGIN_TEMPLATE % (tip, self.uuid, int(time.time())) 894 | code, data = self.do_request(url) 895 | if code == SCANED: 896 | print '[INFO] Please confirm to login .' 897 | tip = 0 898 | elif code == SUCCESS: # 确认登录成功 899 | param = re.search(r'window.redirect_uri="(\S+?)";', data) 900 | redirect_uri = param.group(1) + '&fun=new' 901 | self.redirect_uri = redirect_uri 902 | self.base_uri = redirect_uri[:redirect_uri.rfind('/')] 903 | return code 904 | elif code == TIMEOUT: 905 | print '[ERROR] WeChat login timeout. retry in %s secs later...' % (try_later_secs,) 906 | 907 | tip = 1 # 重置 908 | retry_time -= 1 909 | time.sleep(try_later_secs) 910 | else: 911 | print ('[ERROR] WeChat login exception return_code=%s. retry in %s secs later...' % 912 | (code, try_later_secs)) 913 | tip = 1 914 | retry_time -= 1 915 | time.sleep(try_later_secs) 916 | 917 | return code 918 | 919 | def login(self): 920 | if len(self.redirect_uri) < 4: 921 | print '[ERROR] Login failed due to network problem, please try again.' 922 | return False 923 | r = self.session.get(self.redirect_uri) 924 | r.encoding = 'utf-8' 925 | data = r.text 926 | doc = xml.dom.minidom.parseString(data) 927 | root = doc.documentElement 928 | 929 | for node in root.childNodes: 930 | if node.nodeName == 'skey': 931 | self.skey = node.childNodes[0].data 932 | elif node.nodeName == 'wxsid': 933 | self.sid = node.childNodes[0].data 934 | elif node.nodeName == 'wxuin': 935 | self.uin = node.childNodes[0].data 936 | elif node.nodeName == 'pass_ticket': 937 | self.pass_ticket = node.childNodes[0].data 938 | 939 | if '' in (self.skey, self.sid, self.uin, self.pass_ticket): 940 | return False 941 | 942 | self.base_request = { 943 | 'Uin': self.uin, 944 | 'Sid': self.sid, 945 | 'Skey': self.skey, 946 | 'DeviceID': self.device_id, 947 | } 948 | return True 949 | 950 | def init(self): 951 | url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket) 952 | params = { 953 | 'BaseRequest': self.base_request 954 | } 955 | r = self.session.post(url, data=json.dumps(params)) 956 | r.encoding = 'utf-8' 957 | dic = json.loads(r.text) 958 | self.sync_key = dic['SyncKey'] 959 | self.my_account = dic['User'] 960 | self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val']) 961 | for keyVal in self.sync_key['List']]) 962 | return dic['BaseResponse']['Ret'] == 0 963 | 964 | def status_notify(self): 965 | url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % self.pass_ticket 966 | self.base_request['Uin'] = int(self.base_request['Uin']) 967 | params = { 968 | 'BaseRequest': self.base_request, 969 | "Code": 3, 970 | "FromUserName": self.my_account['UserName'], 971 | "ToUserName": self.my_account['UserName'], 972 | "ClientMsgId": int(time.time()) 973 | } 974 | r = self.session.post(url, data=json.dumps(params)) 975 | r.encoding = 'utf-8' 976 | dic = json.loads(r.text) 977 | return dic['BaseResponse']['Ret'] == 0 978 | 979 | def test_sync_check(self): 980 | for host in ['webpush', 'webpush2']: 981 | self.sync_host = host 982 | retcode = self.sync_check()[0] 983 | if retcode == '0': 984 | return True 985 | return False 986 | 987 | def sync_check(self): 988 | params = { 989 | 'r': int(time.time()), 990 | 'sid': self.sid, 991 | 'uin': self.uin, 992 | 'skey': self.skey, 993 | 'deviceid': self.device_id, 994 | 'synckey': self.sync_key_str, 995 | '_': int(time.time()), 996 | } 997 | url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params) 998 | try: 999 | r = self.session.get(url, timeout=60) 1000 | r.encoding = 'utf-8' 1001 | data = r.text 1002 | pm = re.search(r'window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}', data) 1003 | retcode = pm.group(1) 1004 | selector = pm.group(2) 1005 | return [retcode, selector] 1006 | except: 1007 | return [-1, -1] 1008 | 1009 | def sync(self): 1010 | url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' \ 1011 | % (self.sid, self.skey, self.pass_ticket) 1012 | params = { 1013 | 'BaseRequest': self.base_request, 1014 | 'SyncKey': self.sync_key, 1015 | 'rr': ~int(time.time()) 1016 | } 1017 | try: 1018 | r = self.session.post(url, data=json.dumps(params), timeout=60) 1019 | r.encoding = 'utf-8' 1020 | dic = json.loads(r.text) 1021 | if dic['BaseResponse']['Ret'] == 0: 1022 | self.sync_key = dic['SyncKey'] 1023 | self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val']) 1024 | for keyVal in self.sync_key['List']]) 1025 | return dic 1026 | except: 1027 | return None 1028 | 1029 | def get_icon(self, uid, gid=None): 1030 | """ 1031 | 获取联系人或者群聊成员头像 1032 | :param uid: 联系人id 1033 | :param gid: 群id,如果为非None获取群中成员头像,如果为None则获取联系人头像 1034 | """ 1035 | if gid is None: 1036 | url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (uid, self.skey) 1037 | else: 1038 | url = self.base_uri + '/webwxgeticon?username=%s&skey=%s&chatroomid=%s' % ( 1039 | uid, self.skey, self.encry_chat_room_id_list[gid]) 1040 | r = self.session.get(url) 1041 | data = r.content 1042 | fn = 'icon_' + uid + '.jpg' 1043 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1044 | f.write(data) 1045 | return fn 1046 | 1047 | def get_head_img(self, uid): 1048 | """ 1049 | 获取群头像 1050 | :param uid: 群uid 1051 | """ 1052 | url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (uid, self.skey) 1053 | r = self.session.get(url) 1054 | data = r.content 1055 | fn = 'head_' + uid + '.jpg' 1056 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1057 | f.write(data) 1058 | return fn 1059 | 1060 | def get_msg_img_url(self, msgid): 1061 | return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) 1062 | 1063 | def get_msg_img(self, msgid): 1064 | """ 1065 | 获取图片消息,下载图片到本地 1066 | :param msgid: 消息id 1067 | :return: 保存的本地图片文件路径 1068 | """ 1069 | url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) 1070 | r = self.session.get(url) 1071 | data = r.content 1072 | fn = 'img_' + msgid + '.jpg' 1073 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1074 | f.write(data) 1075 | return fn 1076 | 1077 | def get_voice_url(self, msgid): 1078 | return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) 1079 | 1080 | def get_voice(self, msgid): 1081 | """ 1082 | 获取语音消息,下载语音到本地 1083 | :param msgid: 语音消息id 1084 | :return: 保存的本地语音文件路径 1085 | """ 1086 | url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) 1087 | r = self.session.get(url) 1088 | data = r.content 1089 | fn = 'voice_' + msgid + '.mp3' 1090 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1091 | f.write(data) 1092 | return fn 1093 | --------------------------------------------------------------------------------