├── 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 | 
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}>([^<]+){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 |
--------------------------------------------------------------------------------