wxbot.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. import os
  4. import sys
  5. import webbrowser
  6. import pyqrcode
  7. import requests
  8. import json
  9. import xml.dom.minidom
  10. import urllib
  11. import time
  12. import re
  13. import random
  14. from requests.exceptions import ConnectionError, ReadTimeout
  15. import HTMLParser
  16. UNKONWN = 'unkonwn'
  17. SUCCESS = '200'
  18. SCANED = '201'
  19. TIMEOUT = '408'
  20. def show_image(file):
  21. """
  22. 跨平台显示图片文件
  23. :param file: 图片文件路径
  24. """
  25. if sys.version_info >= (3, 3):
  26. from shlex import quote
  27. else:
  28. from pipes import quote
  29. if sys.platform == "darwin":
  30. command = "open -a /Applications/Preview.app %s&" % quote(file)
  31. os.system(command)
  32. else:
  33. webbrowser.open(file)
  34. class WXBot:
  35. """WXBot功能类"""
  36. def __init__(self):
  37. self.DEBUG = False
  38. self.uuid = ''
  39. self.base_uri = ''
  40. self.redirect_uri = ''
  41. self.uin = ''
  42. self.sid = ''
  43. self.skey = ''
  44. self.pass_ticket = ''
  45. self.device_id = 'e' + repr(random.random())[2:17]
  46. self.base_request = {}
  47. self.sync_key_str = ''
  48. self.sync_key = []
  49. self.sync_host = ''
  50. self.session = requests.Session()
  51. self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'})
  52. self.conf = {'qr': 'png'}
  53. self.my_account = {} # 当前账户
  54. # 所有相关账号: 联系人, 公众号, 群组, 特殊账号
  55. self.member_list = []
  56. # 所有群组的成员, {'group_id1': [member1, member2, ...], ...}
  57. self.group_members = {}
  58. # 所有账户, {'group_member':{'id':{'type':'group_member', 'info':{}}, ...}, 'normal_member':{'id':{}, ...}}
  59. self.account_info = {'group_member': {}, 'normal_member': {}}
  60. self.contact_list = [] # 联系人列表
  61. self.public_list = [] # 公众账号列表
  62. self.group_list = [] # 群聊列表
  63. self.special_list = [] # 特殊账号列表
  64. @staticmethod
  65. def to_unicode(string, encoding='utf-8'):
  66. """
  67. 将字符串转换为Unicode
  68. :param string: 待转换字符串
  69. :param encoding: 字符串解码方式
  70. :return: 转换后的Unicode字符串
  71. """
  72. if isinstance(string, str):
  73. return string.decode(encoding)
  74. elif isinstance(string, unicode):
  75. return string
  76. else:
  77. raise Exception('Unknown Type')
  78. def get_contact(self):
  79. """获取当前账户的所有相关账号(包括联系人、公众号、群聊、特殊账号)"""
  80. url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' \
  81. % (self.pass_ticket, self.skey, int(time.time()))
  82. r = self.session.post(url, data='{}')
  83. r.encoding = 'utf-8'
  84. if self.DEBUG:
  85. with open('contacts.json', 'w') as f:
  86. f.write(r.text.encode('utf-8'))
  87. dic = json.loads(r.text)
  88. self.member_list = dic['MemberList']
  89. special_users = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail',
  90. 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle',
  91. 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp',
  92. 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp',
  93. 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder',
  94. 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c',
  95. 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11',
  96. 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages']
  97. self.contact_list = []
  98. self.public_list = []
  99. self.special_list = []
  100. self.group_list = []
  101. for contact in self.member_list:
  102. if contact['VerifyFlag'] & 8 != 0: # 公众号
  103. self.public_list.append(contact)
  104. self.account_info['normal_member'][contact['UserName']] = {'type': 'public', 'info': contact}
  105. elif contact['UserName'] in special_users: # 特殊账户
  106. self.special_list.append(contact)
  107. self.account_info['normal_member'][contact['UserName']] = {'type': 'special', 'info': contact}
  108. elif contact['UserName'].find('@@') != -1: # 群聊
  109. self.group_list.append(contact)
  110. self.account_info['normal_member'][contact['UserName']] = {'type': 'group', 'info': contact}
  111. elif contact['UserName'] == self.my_account['UserName']: # 自己
  112. self.account_info['normal_member'][contact['UserName']] = {'type': 'self', 'info': contact}
  113. pass
  114. else:
  115. self.contact_list.append(contact)
  116. self.account_info['normal_member'][contact['UserName']] = {'type': 'contact', 'info': contact}
  117. self.group_members = self.batch_get_group_members()
  118. for group in self.group_members:
  119. for member in self.group_members[group]:
  120. if member['UserName'] not in self.account_info:
  121. self.account_info['group_member'][member['UserName']] = {'type': 'group_member',
  122. 'info': member,
  123. 'group': group}
  124. if self.DEBUG:
  125. with open('contact_list.json', 'w') as f:
  126. f.write(json.dumps(self.contact_list))
  127. with open('special_list.json', 'w') as f:
  128. f.write(json.dumps(self.special_list))
  129. with open('group_list.json', 'w') as f:
  130. f.write(json.dumps(self.group_list))
  131. with open('public_list.json', 'w') as f:
  132. f.write(json.dumps(self.public_list))
  133. with open('member_list.json', 'w') as f:
  134. f.write(json.dumps(self.member_list))
  135. with open('group_users.json', 'w') as f:
  136. f.write(json.dumps(self.group_members))
  137. with open('account_info.json', 'w') as f:
  138. f.write(json.dumps(self.account_info))
  139. return True
  140. def batch_get_group_members(self):
  141. """批量获取所有群聊成员信息"""
  142. url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  143. params = {
  144. 'BaseRequest': self.base_request,
  145. "Count": len(self.group_list),
  146. "List": [{"UserName": group['UserName'], "EncryChatRoomId": ""} for group in self.group_list]
  147. }
  148. r = self.session.post(url, data=json.dumps(params))
  149. r.encoding = 'utf-8'
  150. dic = json.loads(r.text)
  151. group_members = {}
  152. for group in dic['ContactList']:
  153. gid = group['UserName']
  154. members = group['MemberList']
  155. group_members[gid] = members
  156. return group_members
  157. def get_group_member_name(self, gid, uid):
  158. """
  159. 获取群聊中指定成员的名称信息
  160. :param gid: 群id
  161. :param uid: 群聊成员id
  162. :return: 名称信息,类似 {"display_name": "test_user", "nickname": "test", "remark_name": "for_test" }
  163. """
  164. if gid not in self.group_members:
  165. return None
  166. group = self.group_members[gid]
  167. for member in group:
  168. if member['UserName'] == uid:
  169. names = {}
  170. if 'RemarkName' in member and member['RemarkName']:
  171. names['remark_name'] = member['RemarkName']
  172. if 'NickName' in member and member['NickName']:
  173. names['nickname'] = member['NickName']
  174. if 'DisplayName' in member and member['DisplayName']:
  175. names['display_name'] = member['DisplayName']
  176. return names
  177. return None
  178. def get_contact_info(self, uid):
  179. if uid in self.account_info['normal_member']:
  180. return self.account_info['normal_member'][uid]
  181. else:
  182. return None
  183. def get_group_member_info(self, uid):
  184. if uid in self.account_info['group_member']:
  185. return self.account_info['group_member'][uid]
  186. else:
  187. return None
  188. def get_group_member_info(self, uid, gid):
  189. if gid not in self.group_members:
  190. return None
  191. for member in self.group_members[gid]:
  192. if member['UserName'] == uid:
  193. return {'type': 'group_member', 'info': member}
  194. return None
  195. def get_contact_name(self, uid):
  196. info = self.get_contact_info(uid)
  197. if info is None:
  198. return None
  199. info = info['info']
  200. name = {}
  201. if 'RemarkName' in info and info['RemarkName']:
  202. name['remark_name'] = info['RemarkName']
  203. if 'NickName' in info and info['NickName']:
  204. name['nickname'] = info['NickName']
  205. if 'DisplayName' in info and info['DisplayName']:
  206. name['display_name'] = info['DisplayName']
  207. if len(name) == 0:
  208. return None
  209. else:
  210. return name
  211. def get_group_member_name(self, uid):
  212. info = self.get_group_member_info(uid)
  213. if info is None:
  214. return None
  215. info = info['info']
  216. name = {}
  217. if 'RemarkName' in info and info['RemarkName']:
  218. name['remark_name'] = info['RemarkName']
  219. if 'NickName' in info and info['NickName']:
  220. name['nickname'] = info['NickName']
  221. if 'DisplayName' in info and info['DisplayName']:
  222. name['display_name'] = info['DisplayName']
  223. if len(name) == 0:
  224. return None
  225. else:
  226. return name
  227. def get_group_member_name(self, uid, gid):
  228. info = self.get_group_member_info(uid, gid)
  229. if info is None:
  230. return None
  231. info = info['info']
  232. name = {}
  233. if 'RemarkName' in info and info['RemarkName']:
  234. name['remark_name'] = info['RemarkName']
  235. if 'NickName' in info and info['NickName']:
  236. name['nickname'] = info['NickName']
  237. if 'DisplayName' in info and info['DisplayName']:
  238. name['display_name'] = info['DisplayName']
  239. if len(name) == 0:
  240. return None
  241. else:
  242. return name
  243. @staticmethod
  244. def get_contact_prefer_name(name):
  245. if name is None:
  246. return None
  247. if 'remark_name' in name:
  248. return name['remark_name']
  249. if 'nickname' in name:
  250. return name['nickname']
  251. if 'display_name' in name:
  252. return name['display_name']
  253. return None
  254. @staticmethod
  255. def get_group_member_prefer_name(name):
  256. if name is None:
  257. return None
  258. if 'remark_name' in name:
  259. return name['remark_name']
  260. if 'display_name' in name:
  261. return name['display_name']
  262. if 'nickname' in name:
  263. return name['nickname']
  264. return None
  265. def get_user_type(self, wx_user_id):
  266. """
  267. 获取特定账号与自己的关系
  268. :param wx_user_id: 账号id:
  269. :return: 与当前账号的关系
  270. """
  271. for account in self.contact_list:
  272. if wx_user_id == account['UserName']:
  273. return 'contact'
  274. for account in self.public_list:
  275. if wx_user_id == account['UserName']:
  276. return 'public'
  277. for account in self.special_list:
  278. if wx_user_id == account['UserName']:
  279. return 'special'
  280. for account in self.group_list:
  281. if wx_user_id == account['UserName']:
  282. return 'group'
  283. for group in self.group_members:
  284. for member in self.group_members[group]:
  285. if member['UserName'] == wx_user_id:
  286. return 'group_member'
  287. return 'unknown'
  288. def is_contact(self, uid):
  289. for account in self.contact_list:
  290. if uid == account['UserName']:
  291. return True
  292. return False
  293. def is_public(self, uid):
  294. for account in self.public_list:
  295. if uid == account['UserName']:
  296. return True
  297. return False
  298. def is_special(self, uid):
  299. for account in self.special_list:
  300. if uid == account['UserName']:
  301. return True
  302. return False
  303. def handle_msg_all(self, msg):
  304. """
  305. 处理所有消息,请子类化后覆盖此函数
  306. msg:
  307. msg_id -> 消息id
  308. msg_type_id -> 消息类型id
  309. user -> 发送消息的账号id
  310. content -> 消息内容
  311. :param msg: 收到的消息
  312. """
  313. pass
  314. @staticmethod
  315. def proc_at_info(msg):
  316. if not msg:
  317. return '', []
  318. segs = msg.split(u'\u2005')
  319. str_msg_all = ''
  320. str_msg = ''
  321. infos = []
  322. if len(segs) > 1:
  323. for i in range(0, len(segs)-1):
  324. segs[i] += u'\u2005'
  325. pm = re.search(u'@.*\u2005', segs[i]).group()
  326. if pm:
  327. name = pm[1:-1]
  328. string = segs[i].replace(pm, '')
  329. str_msg_all += string + '@' + name + ' '
  330. str_msg += string
  331. if string:
  332. infos.append({'type': 'str', 'value': string})
  333. infos.append({'type': 'at', 'value': name})
  334. else:
  335. infos.append({'type': 'str', 'value': segs[i]})
  336. str_msg_all += segs[i]
  337. str_msg += segs[i]
  338. str_msg_all += segs[-1]
  339. str_msg += segs[-1]
  340. infos.append({'type': 'str', 'value': segs[-1]})
  341. else:
  342. infos.append({'type': 'str', 'value': segs[-1]})
  343. str_msg_all = msg
  344. str_msg = msg
  345. return str_msg_all.replace(u'\u2005', ''), str_msg.replace(u'\u2005', ''), infos
  346. def extract_msg_content(self, msg_type_id, msg):
  347. """
  348. content_type_id:
  349. 0 -> Text
  350. 1 -> Location
  351. 3 -> Image
  352. 4 -> Voice
  353. 5 -> Recommend
  354. 6 -> Animation
  355. 7 -> Share
  356. 8 -> Video
  357. 9 -> VideoCall
  358. 10 -> Redraw
  359. 11 -> Empty
  360. 99 -> Unknown
  361. :param msg_type_id: 消息类型id
  362. :param msg: 消息结构体
  363. :return: 解析的消息
  364. """
  365. mtype = msg['MsgType']
  366. content = HTMLParser.HTMLParser().unescape(msg['Content'])
  367. msg_id = msg['MsgId']
  368. msg_content = {}
  369. if msg_type_id == 0:
  370. return {'type': 11, 'data': ''}
  371. elif msg_type_id == 2: # File Helper
  372. return {'type': 0, 'data': content.replace('<br/>', '\n')}
  373. elif msg_type_id == 3: # 群聊
  374. sp = content.find('<br/>')
  375. uid = content[:sp]
  376. content = content[sp:]
  377. content = content.replace('<br/>', '')
  378. uid = uid[:-1]
  379. name = self.get_contact_prefer_name(self.get_contact_name(uid))
  380. if not name:
  381. name = self.get_group_member_prefer_name(self.get_group_member_name(uid, msg['FromUserName']))
  382. if not name:
  383. name = 'unknown'
  384. msg_content['user'] = {'id': uid, 'name': name}
  385. else: # Self, Contact, Special, Public, Unknown
  386. pass
  387. msg_prefix = (msg_content['user']['name'] + ':') if 'user' in msg_content else ''
  388. if mtype == 1:
  389. if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1:
  390. r = self.session.get(content)
  391. r.encoding = 'gbk'
  392. data = r.text
  393. pos = self.search_content('title', data, 'xml')
  394. msg_content['type'] = 1
  395. msg_content['data'] = pos
  396. msg_content['detail'] = data
  397. if self.DEBUG:
  398. print ' %s[Location] %s ' % (msg_prefix, pos)
  399. else:
  400. msg_content['type'] = 0
  401. if msg_type_id == 3 or (msg_type_id == 1 and msg['ToUserName'][:2] == '@@'): # Group text message
  402. msg_infos = self.proc_at_info(content)
  403. str_msg_all = msg_infos[0]
  404. str_msg = msg_infos[1]
  405. detail = msg_infos[2]
  406. msg_content['data'] = str_msg_all
  407. msg_content['detail'] = detail
  408. msg_content['desc'] = str_msg
  409. else:
  410. msg_content['data'] = content
  411. if self.DEBUG:
  412. try:
  413. print ' %s[Text] %s' % (msg_prefix, msg_content['data'])
  414. except UnicodeEncodeError:
  415. print ' %s[Text] (illegal text).' % msg_prefix
  416. elif mtype == 3:
  417. msg_content['type'] = 3
  418. msg_content['data'] = self.get_msg_img_url(msg_id)
  419. if self.DEBUG:
  420. image = self.get_msg_img(msg_id)
  421. print ' %s[Image] %s' % (msg_prefix, image)
  422. elif mtype == 34:
  423. msg_content['type'] = 4
  424. msg_content['data'] = self.get_voice_url(msg_id)
  425. if self.DEBUG:
  426. voice = self.get_voice(msg_id)
  427. print ' %s[Voice] %s' % (msg_prefix, voice)
  428. elif mtype == 42:
  429. msg_content['type'] = 5
  430. info = msg['RecommendInfo']
  431. msg_content['data'] = {'nickname': info['NickName'],
  432. 'alias': info['Alias'],
  433. 'province': info['Province'],
  434. 'city': info['City'],
  435. 'gender': ['unknown', 'male', 'female'][info['Sex']]}
  436. if self.DEBUG:
  437. print ' %s[Recommend]' % msg_prefix
  438. print ' -----------------------------'
  439. print ' | NickName: %s' % info['NickName']
  440. print ' | Alias: %s' % info['Alias']
  441. print ' | Local: %s %s' % (info['Province'], info['City'])
  442. print ' | Gender: %s' % ['unknown', 'male', 'female'][info['Sex']]
  443. print ' -----------------------------'
  444. elif mtype == 47:
  445. msg_content['type'] = 6
  446. msg_content['data'] = self.search_content('cdnurl', content)
  447. if self.DEBUG:
  448. print ' %s[Animation] %s' % (msg_prefix, msg_content['data'])
  449. elif mtype == 49:
  450. msg_content['type'] = 7
  451. app_msg_type = ''
  452. if msg['AppMsgType'] == 3:
  453. app_msg_type = 'music'
  454. elif msg['AppMsgType'] == 5:
  455. app_msg_type = 'link'
  456. elif msg['AppMsgType'] == 7:
  457. app_msg_type = 'weibo'
  458. else:
  459. app_msg_type = 'unknown'
  460. msg_content['data'] = {'type': app_msg_type,
  461. 'title': msg['FileName'],
  462. 'desc': self.search_content('des', content, 'xml'),
  463. 'url': msg['Url'],
  464. 'from': self.search_content('appname', content, 'xml')}
  465. if self.DEBUG:
  466. print ' %s[Share] %s' % (msg_prefix, app_msg_type)
  467. print ' --------------------------'
  468. print ' | title: %s' % msg['FileName']
  469. print ' | desc: %s' % self.search_content('des', content, 'xml')
  470. print ' | link: %s' % msg['Url']
  471. print ' | from: %s' % self.search_content('appname', content, 'xml')
  472. print ' --------------------------'
  473. elif mtype == 62:
  474. msg_content['type'] = 8
  475. msg_content['data'] = content
  476. if self.DEBUG:
  477. print ' %s[Video] Please check on mobiles' % msg_prefix
  478. elif mtype == 53:
  479. msg_content['type'] = 9
  480. msg_content['data'] = content
  481. if self.DEBUG:
  482. print ' %s[Video Call]' % msg_prefix
  483. elif mtype == 10002:
  484. msg_content['type'] = 10
  485. msg_content['data'] = content
  486. if self.DEBUG:
  487. print ' %s[Redraw]' % msg_prefix
  488. elif mtype == 10000: # unknown, maybe red packet, or group invite
  489. msg_content['type'] = 12
  490. msg_content['data'] = msg['Content']
  491. if self.DEBUG:
  492. print ' [Unknown]'
  493. else:
  494. msg_content['type'] = 99
  495. msg_content['data'] = content
  496. if self.DEBUG:
  497. print ' %s[Unknown]' % msg_prefix
  498. return msg_content
  499. def handle_msg(self, r):
  500. """
  501. 处理原始微信消息的内部函数
  502. msg_type_id:
  503. 0 -> Init
  504. 1 -> Self
  505. 2 -> FileHelper
  506. 3 -> Group
  507. 4 -> Contact
  508. 5 -> Public
  509. 6 -> Special
  510. 99 -> Unknown
  511. :param r: 原始微信消息
  512. """
  513. for msg in r['AddMsgList']:
  514. msg_type_id = 99
  515. user = {'id': msg['FromUserName'], 'name': 'unknown'}
  516. if msg['MsgType'] == 51: # init message
  517. msg_type_id = 0
  518. user['name'] = 'system'
  519. elif msg['FromUserName'] == self.my_account['UserName']: # Self
  520. msg_type_id = 1
  521. user['name'] = 'self'
  522. elif msg['ToUserName'] == 'filehelper': # File Helper
  523. msg_type_id = 2
  524. user['name'] = 'file_helper'
  525. elif msg['FromUserName'][:2] == '@@': # Group
  526. msg_type_id = 3
  527. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  528. elif self.is_contact(msg['FromUserName']): # Contact
  529. msg_type_id = 4
  530. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  531. elif self.is_public(msg['FromUserName']): # Public
  532. msg_type_id = 5
  533. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  534. elif self.is_special(msg['FromUserName']): # Special
  535. msg_type_id = 6
  536. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  537. else:
  538. msg_type_id = 99
  539. user['name'] = 'unknown'
  540. if not user['name']:
  541. user['name'] = 'unknown'
  542. user['name'] = HTMLParser.HTMLParser().unescape(user['name'])
  543. if self.DEBUG and msg_type_id != 0:
  544. print '[MSG] %s:' % user['name']
  545. content = self.extract_msg_content(msg_type_id, msg)
  546. message = {'msg_type_id': msg_type_id,
  547. 'msg_id': msg['MsgId'],
  548. 'content': content,
  549. 'to_user_id': msg['ToUserName'],
  550. 'user': user}
  551. self.handle_msg_all(message)
  552. def schedule(self):
  553. """
  554. 做任务型事情的函数,如果需要,可以在子类中覆盖此函数
  555. 此函数在处理消息的间隙被调用,请不要长时间阻塞此函数
  556. """
  557. pass
  558. def proc_msg(self):
  559. self.test_sync_check()
  560. while True:
  561. check_time = time.time()
  562. [retcode, selector] = self.sync_check()
  563. if retcode == '1100': # 从微信客户端上登出
  564. break
  565. elif retcode == '1101': # 从其它设备上登了网页微信
  566. break
  567. elif retcode == '0':
  568. if selector == '2': # 有新消息
  569. r = self.sync()
  570. if r is not None:
  571. self.handle_msg(r)
  572. elif selector == '7': # 在手机上操作了微信
  573. r = self.sync()
  574. if r is not None:
  575. self.handle_msg(r)
  576. elif selector == '0': # 无事件
  577. pass
  578. else:
  579. pass
  580. self.schedule()
  581. check_time = time.time() - check_time
  582. if check_time < 0.8:
  583. time.sleep(1 - check_time)
  584. def send_msg_by_uid(self, word, dst='filehelper'):
  585. url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % self.pass_ticket
  586. msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '')
  587. if type(word) == 'str':
  588. word = word.decode('utf-8')
  589. params = {
  590. 'BaseRequest': self.base_request,
  591. 'Msg': {
  592. "Type": 1,
  593. "Content": word,
  594. "FromUserName": self.my_account['UserName'],
  595. "ToUserName": dst,
  596. "LocalID": msg_id,
  597. "ClientMsgId": msg_id
  598. }
  599. }
  600. headers = {'content-type': 'application/json; charset=UTF-8'}
  601. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  602. try:
  603. r = self.session.post(url, data=data, headers=headers)
  604. except (ConnectionError, ReadTimeout):
  605. return False
  606. dic = r.json()
  607. return dic['BaseResponse']['Ret'] == 0
  608. def get_user_id(self, name):
  609. if name == '':
  610. return ''
  611. for contact in self.contact_list:
  612. if 'RemarkName' in contact and contact['RemarkName'] == name:
  613. return contact['UserName']
  614. elif 'NickName' in contact and contact['NickName'] == name:
  615. return contact['UserName']
  616. elif 'DisplayName' in contact and contact['DisplayName'] == name:
  617. return contact['UserName']
  618. return ''
  619. def send_msg(self, name, word, isfile=False):
  620. uid = self.get_user_id(name)
  621. if uid:
  622. if isfile:
  623. with open(word, 'r') as f:
  624. result = True
  625. for line in f.readlines():
  626. line = line.replace('\n', '')
  627. print '-> ' + name + ': ' + line
  628. if self.send_msg_by_uid(line, uid):
  629. pass
  630. else:
  631. result = False
  632. time.sleep(1)
  633. return result
  634. else:
  635. if self.send_msg_by_uid(word, uid):
  636. return True
  637. else:
  638. return False
  639. else:
  640. if self.DEBUG:
  641. print '[ERROR] This user does not exist .'
  642. return True
  643. @staticmethod
  644. def search_content(key, content, fmat='attr'):
  645. if fmat == 'attr':
  646. pm = re.search(key + '\s?=\s?"([^"<]+)"', content)
  647. if pm:
  648. return pm.group(1)
  649. elif fmat == 'xml':
  650. pm = re.search('<{0}>([^<]+)</{0}>'.format(key), content)
  651. if pm:
  652. return pm.group(1)
  653. return 'unknown'
  654. def run(self):
  655. self.get_uuid()
  656. self.gen_qr_code('qr.png')
  657. print '[INFO] Please use WeChat to scan the QR code .'
  658. result = self.wait4login()
  659. if result != SUCCESS:
  660. print '[ERROR] Web WeChat login failed. failed code=%s'%(result, )
  661. return
  662. if self.login():
  663. print '[INFO] Web WeChat login succeed .'
  664. else:
  665. print '[ERROR] Web WeChat login failed .'
  666. return
  667. if self.init():
  668. print '[INFO] Web WeChat init succeed .'
  669. else:
  670. print '[INFO] Web WeChat init failed'
  671. return
  672. self.status_notify()
  673. self.get_contact()
  674. print '[INFO] Get %d contacts' % len(self.contact_list)
  675. print '[INFO] Start to process messages .'
  676. self.proc_msg()
  677. def get_uuid(self):
  678. url = 'https://login.weixin.qq.com/jslogin'
  679. params = {
  680. 'appid': 'wx782c26e4c19acffb',
  681. 'fun': 'new',
  682. 'lang': 'zh_CN',
  683. '_': int(time.time()) * 1000 + random.randint(1, 999),
  684. }
  685. r = self.session.get(url, params=params)
  686. r.encoding = 'utf-8'
  687. data = r.text
  688. regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
  689. pm = re.search(regx, data)
  690. if pm:
  691. code = pm.group(1)
  692. self.uuid = pm.group(2)
  693. return code == '200'
  694. return False
  695. def gen_qr_code(self, qr_file_path):
  696. string = 'https://login.weixin.qq.com/l/' + self.uuid
  697. qr = pyqrcode.create(string)
  698. if self.conf['qr'] == 'png':
  699. qr.png(qr_file_path, scale=8)
  700. show_image(qr_file_path)
  701. # img = Image.open(qr_file_path)
  702. # img.show()
  703. elif self.conf['qr'] == 'tty':
  704. print(qr.terminal(quiet_zone=1))
  705. def do_request(self, url):
  706. r = self.session.get(url)
  707. r.encoding = 'utf-8'
  708. data = r.text
  709. param = re.search(r'window.code=(\d+);', data)
  710. code = param.group(1)
  711. return code, data
  712. def wait4login(self):
  713. """
  714. http comet:
  715. tip=1, 等待用户扫描二维码,
  716. 201: scaned
  717. 408: timeout
  718. tip=0, 等待用户确认登录,
  719. 200: confirmed
  720. """
  721. LOGIN_TEMPLATE = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s'
  722. tip = 1
  723. try_later_secs = 1
  724. MAX_RETRY_TIMES = 10
  725. code = UNKONWN
  726. retry_time = MAX_RETRY_TIMES
  727. while retry_time > 0:
  728. url = LOGIN_TEMPLATE % (tip, self.uuid, int(time.time()))
  729. code, data = self.do_request(url)
  730. if code == SCANED:
  731. print '[INFO] Please confirm to login .'
  732. tip = 0
  733. elif code == SUCCESS: # 确认登录成功
  734. param = re.search(r'window.redirect_uri="(\S+?)";', data)
  735. redirect_uri = param.group(1) + '&fun=new'
  736. self.redirect_uri = redirect_uri
  737. self.base_uri = redirect_uri[:redirect_uri.rfind('/')]
  738. return code
  739. elif code == TIMEOUT:
  740. print '[ERROR] WeChat login timeout. retry in %s secs later...'%(try_later_secs, )
  741. tip = 1 # 重置
  742. retry_time -= 1
  743. time.sleep(try_later_secs)
  744. else:
  745. print ('[ERROR] WeChat login exception return_code=%s. retry in %s secs later...' %
  746. (code, try_later_secs))
  747. tip = 1
  748. retry_time -= 1
  749. time.sleep(try_later_secs)
  750. return code
  751. def login(self):
  752. if len(self.redirect_uri) < 4:
  753. print '[ERROR] Login failed due to network problem, please try again.'
  754. return False
  755. r = self.session.get(self.redirect_uri)
  756. r.encoding = 'utf-8'
  757. data = r.text
  758. doc = xml.dom.minidom.parseString(data)
  759. root = doc.documentElement
  760. for node in root.childNodes:
  761. if node.nodeName == 'skey':
  762. self.skey = node.childNodes[0].data
  763. elif node.nodeName == 'wxsid':
  764. self.sid = node.childNodes[0].data
  765. elif node.nodeName == 'wxuin':
  766. self.uin = node.childNodes[0].data
  767. elif node.nodeName == 'pass_ticket':
  768. self.pass_ticket = node.childNodes[0].data
  769. if '' in (self.skey, self.sid, self.uin, self.pass_ticket):
  770. return False
  771. self.base_request = {
  772. 'Uin': self.uin,
  773. 'Sid': self.sid,
  774. 'Skey': self.skey,
  775. 'DeviceID': self.device_id,
  776. }
  777. return True
  778. def init(self):
  779. url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  780. params = {
  781. 'BaseRequest': self.base_request
  782. }
  783. r = self.session.post(url, data=json.dumps(params))
  784. r.encoding = 'utf-8'
  785. dic = json.loads(r.text)
  786. self.sync_key = dic['SyncKey']
  787. self.my_account = dic['User']
  788. self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
  789. for keyVal in self.sync_key['List']])
  790. return dic['BaseResponse']['Ret'] == 0
  791. def status_notify(self):
  792. url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % self.pass_ticket
  793. self.base_request['Uin'] = int(self.base_request['Uin'])
  794. params = {
  795. 'BaseRequest': self.base_request,
  796. "Code": 3,
  797. "FromUserName": self.my_account['UserName'],
  798. "ToUserName": self.my_account['UserName'],
  799. "ClientMsgId": int(time.time())
  800. }
  801. r = self.session.post(url, data=json.dumps(params))
  802. r.encoding = 'utf-8'
  803. dic = json.loads(r.text)
  804. return dic['BaseResponse']['Ret'] == 0
  805. def test_sync_check(self):
  806. for host in ['webpush', 'webpush2']:
  807. self.sync_host = host
  808. retcode = self.sync_check()[0]
  809. if retcode == '0':
  810. return True
  811. return False
  812. def sync_check(self):
  813. params = {
  814. 'r': int(time.time()),
  815. 'sid': self.sid,
  816. 'uin': self.uin,
  817. 'skey': self.skey,
  818. 'deviceid': self.device_id,
  819. 'synckey': self.sync_key_str,
  820. '_': int(time.time()),
  821. }
  822. url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params)
  823. try:
  824. r = self.session.get(url, timeout=60)
  825. except (ConnectionError, ReadTimeout):
  826. return [-1, -1]
  827. r.encoding = 'utf-8'
  828. data = r.text
  829. pm = re.search(r'window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}', data)
  830. retcode = pm.group(1)
  831. selector = pm.group(2)
  832. return [retcode, selector]
  833. def sync(self):
  834. url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' \
  835. % (self.sid, self.skey, self.pass_ticket)
  836. params = {
  837. 'BaseRequest': self.base_request,
  838. 'SyncKey': self.sync_key,
  839. 'rr': ~int(time.time())
  840. }
  841. try:
  842. r = self.session.post(url, data=json.dumps(params), timeout=60)
  843. except (ConnectionError, ReadTimeout):
  844. return None
  845. r.encoding = 'utf-8'
  846. dic = json.loads(r.text)
  847. if dic['BaseResponse']['Ret'] == 0:
  848. self.sync_key = dic['SyncKey']
  849. self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
  850. for keyVal in self.sync_key['List']])
  851. return dic
  852. def get_icon(self, uid):
  853. url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (uid, self.skey)
  854. r = self.session.get(url)
  855. data = r.content
  856. fn = 'img_' + uid + '.jpg'
  857. with open(fn, 'wb') as f:
  858. f.write(data)
  859. return fn
  860. def get_head_img(self, uid):
  861. url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (uid, self.skey)
  862. r = self.session.get(url)
  863. data = r.content
  864. fn = 'img_' + uid + '.jpg'
  865. with open(fn, 'wb') as f:
  866. f.write(data)
  867. return fn
  868. def get_msg_img_url(self, msgid):
  869. return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  870. def get_msg_img(self, msgid):
  871. url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  872. r = self.session.get(url)
  873. data = r.content
  874. fn = 'img_' + msgid + '.jpg'
  875. with open(fn, 'wb') as f:
  876. f.write(data)
  877. return fn
  878. def get_voice_url(self, msgid):
  879. return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  880. def get_voice(self, msgid):
  881. url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  882. r = self.session.get(url)
  883. data = r.content
  884. fn = 'voice_' + msgid + '.mp3'
  885. with open(fn, 'wb') as f:
  886. f.write(data)
  887. return fn