wxbot.py 42 KB

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