wxbot.py 35 KB

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