wxbot.py 44 KB

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