wxbot.py 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529
  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. from __future__ import print_function
  4. import os
  5. import sys
  6. import tempfile
  7. import traceback
  8. import webbrowser
  9. import binascii
  10. import jsonpickle
  11. import pyqrcode
  12. import requests
  13. import mimetypes
  14. import json
  15. import xml.dom.minidom
  16. import urllib
  17. import time
  18. import re
  19. import random
  20. from traceback import format_exc
  21. from requests.exceptions import ConnectionError, ReadTimeout
  22. import sys
  23. if sys.version_info[0] == 3:
  24. from html import parser as HTMLParser
  25. from urllib.parse import urlencode
  26. unicode = str
  27. else:
  28. import HTMLParser
  29. from urllib import urlencode
  30. import logging
  31. log = logging.getLogger('wxbot')
  32. UNKONWN = 'unkonwn'
  33. SUCCESS = '200'
  34. SCANED = '201'
  35. TIMEOUT = '408'
  36. def map_username_batch(user_name):
  37. return {"UserName": user_name, "EncryChatRoomId": ""}
  38. def show_image(file_path):
  39. """
  40. 跨平台显示图片文件
  41. :param file_path: 图片文件路径
  42. """
  43. if sys.version_info >= (3, 3):
  44. from shlex import quote
  45. else:
  46. from pipes import quote
  47. if sys.platform == "darwin":
  48. command = "open -a /Applications/Preview.app %s&" % quote(file_path)
  49. os.system(command)
  50. else:
  51. webbrowser.open(os.path.join(tempfile.gettempdir(), 'wxbot', file_path))
  52. class SafeSession(requests.Session):
  53. def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None,
  54. timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None,
  55. json=None):
  56. for i in range(3):
  57. try:
  58. return super(SafeSession, self).request(method, url, params, data, headers, cookies, files, auth,
  59. timeout,
  60. allow_redirects, proxies, hooks, stream, verify, cert, json)
  61. except Exception as e:
  62. log.exception('request %s failed' % url)
  63. continue
  64. # 重试3次以后再加一次,抛出异常
  65. try:
  66. return super(SafeSession, self).request(method, url, params, data, headers, cookies, files, auth,
  67. timeout,
  68. allow_redirects, proxies, hooks, stream, verify, cert, json)
  69. except Exception as e:
  70. raise e
  71. class WXBot:
  72. """WXBot功能类"""
  73. def __init__(self):
  74. self.DEBUG = False
  75. self.uuid = ''
  76. self.base_uri = ''
  77. self.base_host = ''
  78. self.redirect_uri = ''
  79. self.uin = ''
  80. self.sid = ''
  81. self.skey = ''
  82. self.pass_ticket = ''
  83. self.device_id = 'e' + repr(random.random())[2:17]
  84. self.base_request = {}
  85. self.sync_key_str = ''
  86. self.sync_key = []
  87. self.sync_host = ''
  88. self.batch_count = 50 # 一次拉取50个联系人的信息
  89. self.full_user_name_list = [] # 直接获取不到通讯录时,获取的username列表
  90. self.wxid_list = [] # 获取到的wxid的列表
  91. self.cursor = 0 # 拉取联系人信息的游标
  92. self.is_big_contact = False # 通讯录人数过多,无法直接获取
  93. # 文件缓存目录
  94. self.temp_pwd = os.path.join(os.getcwd(), 'temp')
  95. if not os.path.exists(self.temp_pwd):
  96. log.info("Temp path not exists, create it: " + self.temp_pwd)
  97. os.makedirs(self.temp_pwd)
  98. else:
  99. log.info("Temp path exists: " + self.temp_pwd)
  100. self.session = SafeSession()
  101. self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'})
  102. self.conf = {'qr': 'png'}
  103. self.my_account = {} # 当前账户
  104. # 所有相关账号: 联系人, 公众号, 群组, 特殊账号
  105. self.member_list = []
  106. # 所有群组的成员, {'group_id1': [member1, member2, ...], ...}
  107. self.group_members = {}
  108. # 所有账户, {'group_member':{'id':{'type':'group_member', 'info':{}}, ...}, 'normal_member':{'id':{}, ...}}
  109. self.account_info = {'group_member': {}, 'normal_member': {}}
  110. self.contact_list = [] # 联系人列表
  111. self.public_list = [] # 公众账号列表
  112. self.group_list = [] # 群聊列表
  113. self.special_list = [] # 特殊账号列表
  114. self.encry_chat_room_id_list = [] # 存储群聊的EncryChatRoomId,获取群内成员头像时需要用到
  115. self.file_index = 0
  116. self.state_file = os.path.join(self.temp_pwd, 'session_state.json')
  117. @staticmethod
  118. def to_unicode(string, encoding='utf-8'):
  119. """
  120. 将字符串转换为Unicode
  121. :param string: 待转换字符串
  122. :param encoding: 字符串解码方式
  123. :return: 转换后的Unicode字符串
  124. """
  125. if isinstance(string, unicode):
  126. return string
  127. elif isinstance(string, str):
  128. return string.decode(encoding)
  129. else:
  130. raise Exception('Unknown Type')
  131. def get_contact(self):
  132. """获取当前账户的所有相关账号(包括联系人、公众号、群聊、特殊账号)"""
  133. if self.is_big_contact:
  134. return False
  135. url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' \
  136. % (self.pass_ticket, self.skey, int(time.time()))
  137. # 如果通讯录联系人过多,这里会直接获取失败
  138. try:
  139. r = self.session.post(url, data='{}')
  140. except Exception as e:
  141. self.is_big_contact = True
  142. return False
  143. r.encoding = 'utf-8'
  144. if self.DEBUG:
  145. with open(os.path.join(self.temp_pwd,'contacts.json'), 'wb') as f:
  146. f.write(r.text.encode('utf-8'))
  147. dic = json.loads(r.text)
  148. self.member_list = dic['MemberList']
  149. special_users = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail',
  150. 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle',
  151. 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp',
  152. 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp',
  153. 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder',
  154. 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c',
  155. 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11',
  156. 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages']
  157. self.contact_list = []
  158. self.public_list = []
  159. self.special_list = []
  160. self.group_list = []
  161. for contact in self.member_list:
  162. if contact['VerifyFlag'] & 8 != 0: # 公众号
  163. self.public_list.append(contact)
  164. self.account_info['normal_member'][contact['UserName']] = {'type': 'public', 'info': contact}
  165. elif contact['UserName'] in special_users: # 特殊账户
  166. self.special_list.append(contact)
  167. self.account_info['normal_member'][contact['UserName']] = {'type': 'special', 'info': contact}
  168. elif contact['UserName'].find('@@') != -1: # 群聊
  169. self.group_list.append(contact)
  170. self.account_info['normal_member'][contact['UserName']] = {'type': 'group', 'info': contact}
  171. elif contact['UserName'] == self.my_account['UserName']: # 自己
  172. self.account_info['normal_member'][contact['UserName']] = {'type': 'self', 'info': contact}
  173. else:
  174. self.contact_list.append(contact)
  175. self.account_info['normal_member'][contact['UserName']] = {'type': 'contact', 'info': contact}
  176. self.batch_get_group_members()
  177. for group in self.group_members:
  178. for member in self.group_members[group]:
  179. if member['UserName'] not in self.account_info:
  180. self.account_info['group_member'][member['UserName']] = \
  181. {'type': 'group_member', 'info': member, 'group': group}
  182. if self.DEBUG:
  183. with open(os.path.join(self.temp_pwd,'contact_list.json'), 'w') as f:
  184. f.write(json.dumps(self.contact_list))
  185. with open(os.path.join(self.temp_pwd,'special_list.json'), 'w') as f:
  186. f.write(json.dumps(self.special_list))
  187. with open(os.path.join(self.temp_pwd,'group_list.json'), 'w') as f:
  188. f.write(json.dumps(self.group_list))
  189. with open(os.path.join(self.temp_pwd,'public_list.json'), 'w') as f:
  190. f.write(json.dumps(self.public_list))
  191. with open(os.path.join(self.temp_pwd,'member_list.json'), 'w') as f:
  192. f.write(json.dumps(self.member_list))
  193. with open(os.path.join(self.temp_pwd,'group_users.json'), 'w') as f:
  194. f.write(json.dumps(self.group_members))
  195. with open(os.path.join(self.temp_pwd,'account_info.json'), 'w') as f:
  196. f.write(json.dumps(self.account_info))
  197. return True
  198. def get_big_contact(self):
  199. total_len = len(self.full_user_name_list)
  200. user_info_list = []
  201. # 一次拉取50个联系人的信息,包括所有的群聊,公众号,好友
  202. while self.cursor < total_len:
  203. cur_batch = self.full_user_name_list[self.cursor:(self.cursor+self.batch_count)]
  204. self.cursor += self.batch_count
  205. cur_batch = map(map_username_batch, cur_batch)
  206. user_info_list += self.batch_get_contact(cur_batch)
  207. log.info("Get batch contacts")
  208. self.member_list = user_info_list
  209. special_users = ['newsapp', 'filehelper', 'weibo', 'qqmail',
  210. 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle',
  211. 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp',
  212. 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp',
  213. 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder',
  214. 'weixinreminder', 'wxid_novlwrv3lqwv11',
  215. 'officialaccounts',
  216. 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages', 'notifymessage']
  217. self.contact_list = []
  218. self.public_list = []
  219. self.special_list = []
  220. self.group_list = []
  221. for i, contact in enumerate(self.member_list):
  222. if contact['VerifyFlag'] & 8 != 0: # 公众号
  223. self.public_list.append(contact)
  224. self.account_info['normal_member'][contact['UserName']] = {'type': 'public', 'info': contact}
  225. elif contact['UserName'] in special_users or self.wxid_list[i] in special_users: # 特殊账户
  226. self.special_list.append(contact)
  227. self.account_info['normal_member'][contact['UserName']] = {'type': 'special', 'info': contact}
  228. elif contact['UserName'].find('@@') != -1: # 群聊
  229. self.group_list.append(contact)
  230. self.account_info['normal_member'][contact['UserName']] = {'type': 'group', 'info': contact}
  231. elif contact['UserName'] == self.my_account['UserName']: # 自己
  232. self.account_info['normal_member'][contact['UserName']] = {'type': 'self', 'info': contact}
  233. else:
  234. self.contact_list.append(contact)
  235. self.account_info['normal_member'][contact['UserName']] = {'type': 'contact', 'info': contact}
  236. group_members = {}
  237. encry_chat_room_id = {}
  238. for group in self.group_list:
  239. gid = group['UserName']
  240. members = group['MemberList']
  241. group_members[gid] = members
  242. encry_chat_room_id[gid] = group['EncryChatRoomId']
  243. self.group_members = group_members
  244. self.encry_chat_room_id_list = encry_chat_room_id
  245. for group in self.group_members:
  246. for member in self.group_members[group]:
  247. if member['UserName'] not in self.account_info:
  248. self.account_info['group_member'][member['UserName']] = \
  249. {'type': 'group_member', 'info': member, 'group': group}
  250. if self.DEBUG:
  251. with open(os.path.join(self.temp_pwd,'contact_list.json'), 'w') as f:
  252. f.write(json.dumps(self.contact_list))
  253. with open(os.path.join(self.temp_pwd,'special_list.json'), 'w') as f:
  254. f.write(json.dumps(self.special_list))
  255. with open(os.path.join(self.temp_pwd,'group_list.json'), 'w') as f:
  256. f.write(json.dumps(self.group_list))
  257. with open(os.path.join(self.temp_pwd,'public_list.json'), 'w') as f:
  258. f.write(json.dumps(self.public_list))
  259. with open(os.path.join(self.temp_pwd,'member_list.json'), 'w') as f:
  260. f.write(json.dumps(self.member_list))
  261. with open(os.path.join(self.temp_pwd,'group_users.json'), 'w') as f:
  262. f.write(json.dumps(self.group_members))
  263. with open(os.path.join(self.temp_pwd,'account_info.json'), 'w') as f:
  264. f.write(json.dumps(self.account_info))
  265. log.info('Get %d contacts' % len(self.contact_list))
  266. log.info('Start to process messages .')
  267. return True
  268. def batch_get_contact(self, cur_batch):
  269. """批量获取成员信息"""
  270. url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  271. params = {
  272. 'BaseRequest': self.base_request,
  273. "Count": len(cur_batch),
  274. "List": cur_batch
  275. }
  276. r = self.session.post(url, data=json.dumps(params))
  277. r.encoding = 'utf-8'
  278. dic = json.loads(r.text)
  279. return dic['ContactList']
  280. def batch_get_group_members(self):
  281. """批量获取所有群聊成员信息"""
  282. url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  283. params = {
  284. 'BaseRequest': self.base_request,
  285. "Count": len(self.group_list),
  286. "List": [{"UserName": group['UserName'], "EncryChatRoomId": ""} for group in self.group_list]
  287. }
  288. r = self.session.post(url, data=json.dumps(params))
  289. r.encoding = 'utf-8'
  290. dic = json.loads(r.text)
  291. group_members = {}
  292. encry_chat_room_id = {}
  293. for group in dic['ContactList']:
  294. gid = group['UserName']
  295. members = group['MemberList']
  296. group_members[gid] = members
  297. encry_chat_room_id[gid] = group['EncryChatRoomId']
  298. self.group_members = group_members
  299. self.encry_chat_room_id_list = encry_chat_room_id
  300. def get_group_member_name(self, gid, uid):
  301. """
  302. 获取群聊中指定成员的名称信息
  303. :param gid: 群id
  304. :param uid: 群聊成员id
  305. :return: 名称信息,类似 {"display_name": "test_user", "nickname": "test", "remark_name": "for_test" }
  306. """
  307. if gid not in self.group_members:
  308. return None
  309. group = self.group_members[gid]
  310. for member in group:
  311. if member['UserName'] == uid:
  312. names = {}
  313. if 'RemarkName' in member and member['RemarkName']:
  314. names['remark_name'] = member['RemarkName']
  315. if 'NickName' in member and member['NickName']:
  316. names['nickname'] = member['NickName']
  317. if 'DisplayName' in member and member['DisplayName']:
  318. names['display_name'] = member['DisplayName']
  319. return names
  320. return None
  321. def get_contact_info(self, uid):
  322. return self.account_info['normal_member'].get(uid)
  323. def get_group_member_info(self, uid):
  324. return self.account_info['group_member'].get(uid)
  325. def get_contact_name(self, uid):
  326. info = self.get_contact_info(uid)
  327. if info is None:
  328. return None
  329. info = info['info']
  330. name = {}
  331. if 'RemarkName' in info and info['RemarkName']:
  332. name['remark_name'] = info['RemarkName']
  333. if 'NickName' in info and info['NickName']:
  334. name['nickname'] = info['NickName']
  335. if 'DisplayName' in info and info['DisplayName']:
  336. name['display_name'] = info['DisplayName']
  337. if len(name) == 0:
  338. return None
  339. else:
  340. return name
  341. @staticmethod
  342. def get_contact_prefer_name(name):
  343. if name is None:
  344. return None
  345. if 'remark_name' in name:
  346. return name['remark_name']
  347. if 'nickname' in name:
  348. return name['nickname']
  349. if 'display_name' in name:
  350. return name['display_name']
  351. return None
  352. @staticmethod
  353. def get_group_member_prefer_name(name):
  354. if name is None:
  355. return None
  356. if 'remark_name' in name:
  357. return name['remark_name']
  358. if 'display_name' in name:
  359. return name['display_name']
  360. if 'nickname' in name:
  361. return name['nickname']
  362. return None
  363. def get_user_type(self, wx_user_id):
  364. """
  365. 获取特定账号与自己的关系
  366. :param wx_user_id: 账号id:
  367. :return: 与当前账号的关系
  368. """
  369. for account in self.contact_list:
  370. if wx_user_id == account['UserName']:
  371. return 'contact'
  372. for account in self.public_list:
  373. if wx_user_id == account['UserName']:
  374. return 'public'
  375. for account in self.special_list:
  376. if wx_user_id == account['UserName']:
  377. return 'special'
  378. for account in self.group_list:
  379. if wx_user_id == account['UserName']:
  380. return 'group'
  381. for group in self.group_members:
  382. for member in self.group_members[group]:
  383. if member['UserName'] == wx_user_id:
  384. return 'group_member'
  385. return 'unknown'
  386. def is_contact(self, uid):
  387. for account in self.contact_list:
  388. if uid == account['UserName']:
  389. return True
  390. return False
  391. def is_public(self, uid):
  392. for account in self.public_list:
  393. if uid == account['UserName']:
  394. return True
  395. return False
  396. def is_special(self, uid):
  397. for account in self.special_list:
  398. if uid == account['UserName']:
  399. return True
  400. return False
  401. def handle_msg_all(self, msg):
  402. """
  403. 处理所有消息,请子类化后覆盖此函数
  404. msg:
  405. msg_id -> 消息id
  406. msg_type_id -> 消息类型id
  407. user -> 发送消息的账号id
  408. content -> 消息内容
  409. :param msg: 收到的消息
  410. """
  411. pass
  412. @staticmethod
  413. def proc_at_info(msg):
  414. if not msg:
  415. return '', []
  416. segs = msg.split(u'\u2005')
  417. str_msg_all = ''
  418. str_msg = ''
  419. infos = []
  420. if len(segs) > 1:
  421. for i in range(0, len(segs) - 1):
  422. segs[i] += u'\u2005'
  423. pm = re.search(u'@.*\u2005', segs[i]).group()
  424. if pm:
  425. name = pm[1:-1]
  426. string = segs[i].replace(pm, '')
  427. str_msg_all += string + '@' + name + ' '
  428. str_msg += string
  429. if string:
  430. infos.append({'type': 'str', 'value': string})
  431. infos.append({'type': 'at', 'value': name})
  432. else:
  433. infos.append({'type': 'str', 'value': segs[i]})
  434. str_msg_all += segs[i]
  435. str_msg += segs[i]
  436. str_msg_all += segs[-1]
  437. str_msg += segs[-1]
  438. infos.append({'type': 'str', 'value': segs[-1]})
  439. else:
  440. infos.append({'type': 'str', 'value': segs[-1]})
  441. str_msg_all = msg
  442. str_msg = msg
  443. return str_msg_all.replace(u'\u2005', ''), str_msg.replace(u'\u2005', ''), infos
  444. def extract_msg_content(self, msg_type_id, msg):
  445. """
  446. content_type_id:
  447. 0 -> Text
  448. 1 -> Location
  449. 3 -> Image
  450. 4 -> Voice
  451. 5 -> Recommend
  452. 6 -> Animation
  453. 7 -> Share
  454. 8 -> Video
  455. 9 -> VideoCall
  456. 10 -> Redraw
  457. 11 -> Empty
  458. 99 -> Unknown
  459. :param msg_type_id: 消息类型id
  460. :param msg: 消息结构体
  461. :return: 解析的消息
  462. """
  463. mtype = msg['MsgType']
  464. content = HTMLParser.HTMLParser().unescape(msg['Content'])
  465. msg_id = msg['MsgId']
  466. msg_content = {}
  467. if msg_type_id == 0:
  468. return {'type': 11, 'data': ''}
  469. elif msg_type_id == 2: # File Helper
  470. return {'type': 0, 'data': content.replace('<br/>', '\n')}
  471. elif msg_type_id == 3: # 群聊
  472. sp = content.find('<br/>')
  473. uid = content[:sp]
  474. content = content[sp:]
  475. content = content.replace('<br/>', '')
  476. uid = uid[:-1]
  477. name = self.get_contact_prefer_name(self.get_contact_name(uid))
  478. if not name:
  479. name = self.get_group_member_prefer_name(self.get_group_member_name(msg['FromUserName'], uid))
  480. if not name:
  481. name = 'unknown'
  482. msg_content['user'] = {'id': uid, 'name': name}
  483. else: # Self, Contact, Special, Public, Unknown
  484. pass
  485. msg_prefix = (msg_content['user']['name'] + ':') if 'user' in msg_content else ''
  486. if mtype == 1:
  487. if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1:
  488. r = self.session.get(content)
  489. r.encoding = 'gbk'
  490. data = r.text
  491. pos = self.search_content('title', data, 'xml')
  492. msg_content['type'] = 1
  493. msg_content['data'] = pos
  494. msg_content['detail'] = data
  495. if self.DEBUG:
  496. log.info(' %s[Location] %s ' % (msg_prefix, pos))
  497. else:
  498. msg_content['type'] = 0
  499. if msg_type_id == 3 or (msg_type_id == 1 and msg['ToUserName'][:2] == '@@'): # Group text message
  500. msg_infos = self.proc_at_info(content)
  501. str_msg_all = msg_infos[0]
  502. str_msg = msg_infos[1]
  503. detail = msg_infos[2]
  504. msg_content['data'] = str_msg_all
  505. msg_content['detail'] = detail
  506. msg_content['desc'] = str_msg
  507. else:
  508. msg_content['data'] = content
  509. if self.DEBUG:
  510. try:
  511. log.info(' %s[Text] %s' % (msg_prefix, msg_content['data']))
  512. except UnicodeEncodeError:
  513. log.info(' %s[Text] (illegal text).' % msg_prefix)
  514. elif mtype == 3:
  515. msg_content['type'] = 3
  516. msg_content['data'] = self.get_msg_img_url(msg_id)
  517. msg_content['img'] = binascii.hexlify(self.session.get(msg_content['data']).content)
  518. if self.DEBUG:
  519. image = self.get_msg_img(msg_id)
  520. log.info(' %s[Image] %s' % (msg_prefix, image))
  521. elif mtype == 34:
  522. msg_content['type'] = 4
  523. msg_content['data'] = self.get_voice_url(msg_id)
  524. msg_content['voice'] = binascii.hexlify(self.session.get(msg_content['data']).content)
  525. if self.DEBUG:
  526. voice = self.get_voice(msg_id)
  527. log.info(' %s[Voice] %s' % (msg_prefix, voice))
  528. elif mtype == 37:
  529. msg_content['type'] = 37
  530. msg_content['data'] = msg['RecommendInfo']
  531. if self.DEBUG:
  532. log.info(' %s[useradd] %s' % (msg_prefix,msg['RecommendInfo']['NickName']))
  533. elif mtype == 42:
  534. msg_content['type'] = 5
  535. info = msg['RecommendInfo']
  536. msg_content['data'] = {'nickname': info['NickName'],
  537. 'alias': info['Alias'],
  538. 'province': info['Province'],
  539. 'city': info['City'],
  540. 'gender': ['unknown', 'male', 'female'][info['Sex']]}
  541. if self.DEBUG:
  542. log.info(' %s[Recommend]' % msg_prefix)
  543. log.info(' -----------------------------')
  544. log.info(' | NickName: %s' % info['NickName'])
  545. log.info(' | Alias: %s' % info['Alias'])
  546. log.info(' | Local: %s %s' % (info['Province'], info['City']))
  547. log.info(' | Gender: %s' % ['unknown', 'male', 'female'][info['Sex']])
  548. log.info(' -----------------------------')
  549. elif mtype == 47:
  550. msg_content['type'] = 6
  551. msg_content['data'] = self.search_content('cdnurl', content)
  552. if self.DEBUG:
  553. log.info(' %s[Animation] %s' % (msg_prefix, msg_content['data']))
  554. elif mtype == 49:
  555. msg_content['type'] = 7
  556. if msg['AppMsgType'] == 3:
  557. app_msg_type = 'music'
  558. elif msg['AppMsgType'] == 5:
  559. app_msg_type = 'link'
  560. elif msg['AppMsgType'] == 7:
  561. app_msg_type = 'weibo'
  562. else:
  563. app_msg_type = 'unknown'
  564. msg_content['data'] = {'type': app_msg_type,
  565. 'title': msg['FileName'],
  566. 'desc': self.search_content('des', content, 'xml'),
  567. 'url': msg['Url'],
  568. 'from': self.search_content('appname', content, 'xml'),
  569. 'content': msg.get('Content') # 有的公众号会发一次性3 4条链接一个大图,如果只url那只能获取第一条,content里面有所有的链接
  570. }
  571. if self.DEBUG:
  572. log.info(' %s[Share] %s' % (msg_prefix, app_msg_type))
  573. log.info(' --------------------------')
  574. log.info(' | title: %s' % msg['FileName'])
  575. log.info(' | desc: %s' % self.search_content('des', content, 'xml'))
  576. log.info(' | link: %s' % msg['Url'])
  577. log.info(' | from: %s' % self.search_content('appname', content, 'xml'))
  578. log.info(' | content: %s' % (msg.get('content')[:20] if msg.get('content') else "unknown"))
  579. log.info(' --------------------------')
  580. elif mtype == 62:
  581. msg_content['type'] = 8
  582. msg_content['data'] = content
  583. if self.DEBUG:
  584. log.info(' %s[Video] Please check on mobiles' % msg_prefix)
  585. elif mtype == 53:
  586. msg_content['type'] = 9
  587. msg_content['data'] = content
  588. if self.DEBUG:
  589. log.info(' %s[Video Call]' % msg_prefix)
  590. elif mtype == 10002:
  591. msg_content['type'] = 10
  592. msg_content['data'] = content
  593. if self.DEBUG:
  594. log.info(' %s[Redraw]' % msg_prefix)
  595. elif mtype == 10000: # unknown, maybe red packet, or group invite
  596. msg_content['type'] = 12
  597. msg_content['data'] = msg['Content']
  598. if self.DEBUG:
  599. log.info(' [Unknown]')
  600. elif mtype == 43:
  601. msg_content['type'] = 13
  602. msg_content['data'] = self.get_video_url(msg_id)
  603. if self.DEBUG:
  604. log.info(' %s[video] %s' % (msg_prefix, msg_content['data']))
  605. else:
  606. msg_content['type'] = 99
  607. msg_content['data'] = content
  608. if self.DEBUG:
  609. log.info(' %s[Unknown]' % msg_prefix)
  610. return msg_content
  611. def handle_msg(self, r):
  612. """
  613. 处理原始微信消息的内部函数
  614. msg_type_id:
  615. 0 -> Init
  616. 1 -> Self
  617. 2 -> FileHelper
  618. 3 -> Group
  619. 4 -> Contact
  620. 5 -> Public
  621. 6 -> Special
  622. 99 -> Unknown
  623. :param r: 原始微信消息
  624. """
  625. for msg in r['AddMsgList']:
  626. user = {'id': msg['FromUserName'], 'name': 'unknown'}
  627. if msg['MsgType'] == 51 and msg['StatusNotifyCode'] == 4: # init message
  628. msg_type_id = 0
  629. user['name'] = 'system'
  630. # 会获取所有联系人的username 和 wxid,但是会收到3次这个消息,只取第一次
  631. if self.is_big_contact and len(self.full_user_name_list) == 0:
  632. self.full_user_name_list = msg['StatusNotifyUserName'].split(",")
  633. self.wxid_list = re.search(r"username&gt;(.*?)&lt;/username", msg["Content"]).group(1).split(",")
  634. with open(os.path.join(self.temp_pwd,'UserName.txt'), 'w') as f:
  635. f.write(msg['StatusNotifyUserName'])
  636. with open(os.path.join(self.temp_pwd,'wxid.txt'), 'w') as f:
  637. f.write(json.dumps(self.wxid_list))
  638. log.info('Contact list is too big. Now start to fetch member list .')
  639. self.get_big_contact()
  640. elif msg['MsgType'] == 37: # friend request
  641. msg_type_id = 37
  642. pass
  643. # content = msg['Content']
  644. # username = content[content.index('fromusername='): content.index('encryptusername')]
  645. # username = username[username.index('"') + 1: username.rindex('"')]
  646. # print u'[Friend Request]'
  647. # print u' Nickname:' + msg['RecommendInfo']['NickName']
  648. # print u' 附加消息:'+msg['RecommendInfo']['Content']
  649. # # print u'Ticket:'+msg['RecommendInfo']['Ticket'] # Ticket添加好友时要用
  650. # print u' 微信号:'+username #未设置微信号的 腾讯会自动生成一段微信ID 但是无法通过搜索 搜索到此人
  651. elif msg['FromUserName'] == self.my_account['UserName']: # Self
  652. msg_type_id = 1
  653. user['name'] = 'self'
  654. elif msg['ToUserName'] == 'filehelper': # File Helper
  655. msg_type_id = 2
  656. user['name'] = 'file_helper'
  657. elif msg['FromUserName'][:2] == '@@': # Group
  658. msg_type_id = 3
  659. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  660. elif self.is_contact(msg['FromUserName']): # Contact
  661. msg_type_id = 4
  662. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  663. elif self.is_public(msg['FromUserName']): # Public
  664. msg_type_id = 5
  665. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  666. elif self.is_special(msg['FromUserName']): # Special
  667. msg_type_id = 6
  668. user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id']))
  669. else:
  670. msg_type_id = 99
  671. user['name'] = 'unknown'
  672. if not user['name']:
  673. user['name'] = 'unknown'
  674. user['name'] = HTMLParser.HTMLParser().unescape(user['name'])
  675. if self.DEBUG and msg_type_id != 0:
  676. log.info(u'[MSG] %s:' % user['name'])
  677. content = self.extract_msg_content(msg_type_id, msg)
  678. message = {'msg_type_id': msg_type_id,
  679. 'msg_id': msg['MsgId'],
  680. 'content': content,
  681. 'to_user_id': msg['ToUserName'],
  682. 'user': user}
  683. self.handle_msg_all(message)
  684. def schedule(self):
  685. """
  686. 做任务型事情的函数,如果需要,可以在子类中覆盖此函数
  687. 此函数在处理消息的间隙被调用,请不要长时间阻塞此函数
  688. """
  689. pass
  690. def proc_msg(self):
  691. self.test_sync_check()
  692. while True:
  693. check_time = time.time()
  694. try:
  695. [retcode, selector] = self.sync_check()
  696. # log.debug('sync_check:', retcode, selector)
  697. if retcode == '1100': # 从微信客户端上登出
  698. break
  699. elif retcode == '1101': # 从其它设备上登了网页微信
  700. break
  701. elif retcode == '0':
  702. if selector == '2': # 有新消息
  703. r = self.sync()
  704. if r is not None:
  705. self.handle_msg(r)
  706. elif selector == '3': # 未知
  707. r = self.sync()
  708. if r is not None:
  709. self.handle_msg(r)
  710. elif selector == '4': # 通讯录更新
  711. r = self.sync()
  712. if r is not None:
  713. self.get_contact()
  714. elif selector == '6': # 可能是红包
  715. r = self.sync()
  716. if r is not None:
  717. self.handle_msg(r)
  718. elif selector == '7': # 在手机上操作了微信
  719. r = self.sync()
  720. if r is not None:
  721. self.handle_msg(r)
  722. elif selector == '0': # 无事件
  723. pass
  724. else:
  725. log.debug('sync_check:', retcode, selector)
  726. r = self.sync()
  727. if r is not None:
  728. self.handle_msg(r)
  729. else:
  730. log.debug('sync_check:', retcode, selector)
  731. time.sleep(10)
  732. self.schedule()
  733. except:
  734. log.exception('Except in proc_msg')
  735. check_time = time.time() - check_time
  736. if check_time < 0.8:
  737. time.sleep(1 - check_time)
  738. def apply_useradd_requests(self,RecommendInfo):
  739. url = self.base_uri + '/webwxverifyuser?r='+str(int(time.time()))+'&lang=zh_CN'
  740. params = {
  741. "BaseRequest": self.base_request,
  742. "Opcode": 3,
  743. "VerifyUserListSize": 1,
  744. "VerifyUserList": [
  745. {
  746. "Value": RecommendInfo['UserName'],
  747. "VerifyUserTicket": RecommendInfo['Ticket'] }
  748. ],
  749. "VerifyContent": "",
  750. "SceneListCount": 1,
  751. "SceneList": [
  752. 33
  753. ],
  754. "skey": self.skey
  755. }
  756. headers = {'content-type': 'application/json; charset=UTF-8'}
  757. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  758. try:
  759. r = self.session.post(url, data=data, headers=headers)
  760. except (ConnectionError, ReadTimeout):
  761. return False
  762. dic = r.json()
  763. return dic['BaseResponse']['Ret'] == 0
  764. def add_groupuser_to_friend_by_uid(self,uid,VerifyContent):
  765. """
  766. 主动向群内人员打招呼,提交添加好友请求
  767. uid-群内人员得uid VerifyContent-好友招呼内容
  768. 慎用此接口!封号后果自负!慎用此接口!封号后果自负!慎用此接口!封号后果自负!
  769. """
  770. if self.is_contact(uid):
  771. return True
  772. url = self.base_uri + '/webwxverifyuser?r='+str(int(time.time()))+'&lang=zh_CN'
  773. params ={
  774. "BaseRequest": self.base_request,
  775. "Opcode": 2,
  776. "VerifyUserListSize": 1,
  777. "VerifyUserList": [
  778. {
  779. "Value": uid,
  780. "VerifyUserTicket": ""
  781. }
  782. ],
  783. "VerifyContent": VerifyContent,
  784. "SceneListCount": 1,
  785. "SceneList": [
  786. 33
  787. ],
  788. "skey": self.skey
  789. }
  790. headers = {'content-type': 'application/json; charset=UTF-8'}
  791. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  792. try:
  793. r = self.session.post(url, data=data, headers=headers)
  794. except (ConnectionError, ReadTimeout):
  795. return False
  796. dic = r.json()
  797. return dic['BaseResponse']['Ret'] == 0
  798. def add_friend_to_group(self,uid,group_name):
  799. """
  800. 将好友加入到群聊中
  801. """
  802. gid = ''
  803. # 通过群名获取群id,群没保存到通讯录中的话无法添加哦
  804. for group in self.group_list:
  805. if group['NickName'] == group_name:
  806. gid = group['UserName']
  807. if gid == '':
  808. return False
  809. # 通过群id判断uid是否在群中
  810. for user in self.group_members[gid]:
  811. if user['UserName'] == uid:
  812. # 已经在群里面了,不用加了
  813. return True
  814. url = self.base_uri + '/webwxupdatechatroom?fun=addmember&pass_ticket=%s' % self.pass_ticket
  815. params ={
  816. "AddMemberList": uid,
  817. "ChatRoomName": gid,
  818. "BaseRequest": self.base_request
  819. }
  820. headers = {'content-type': 'application/json; charset=UTF-8'}
  821. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  822. try:
  823. r = self.session.post(url, data=data, headers=headers)
  824. except (ConnectionError, ReadTimeout):
  825. return False
  826. dic = r.json()
  827. return dic['BaseResponse']['Ret'] == 0
  828. def invite_friend_to_group(self,uid,group_name):
  829. """
  830. 将好友加入到群中。对人数多的群,需要调用此方法。
  831. 拉人时,可以先尝试使用add_friend_to_group方法,当调用失败(Ret=1)时,再尝试调用此方法。
  832. """
  833. gid = ''
  834. # 通过群名获取群id,群没保存到通讯录中的话无法添加哦
  835. for group in self.group_list:
  836. if group['NickName'] == group_name:
  837. gid = group['UserName']
  838. if gid == '':
  839. return False
  840. # 通过群id判断uid是否在群中
  841. for user in self.group_members[gid]:
  842. if user['UserName'] == uid:
  843. # 已经在群里面了,不用加了
  844. return True
  845. url = self.base_uri + '/webwxupdatechatroom?fun=invitemember&pass_ticket=%s' % self.pass_ticket
  846. params = {
  847. "InviteMemberList": uid,
  848. "ChatRoomName": gid,
  849. "BaseRequest": self.base_request
  850. }
  851. headers = {'content-type': 'application/json; charset=UTF-8'}
  852. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  853. try:
  854. r = self.session.post(url, data=data, headers=headers)
  855. except (ConnectionError, ReadTimeout):
  856. return False
  857. dic = r.json()
  858. return dic['BaseResponse']['Ret'] == 0
  859. def delete_user_from_group(self,uname,gid):
  860. """
  861. 将群用户从群中剔除,只有群管理员有权限
  862. """
  863. uid = ""
  864. for user in self.group_members[gid]:
  865. if user['NickName'] == uname:
  866. uid = user['UserName']
  867. if uid == "":
  868. return False
  869. url = self.base_uri + '/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % self.pass_ticket
  870. params ={
  871. "DelMemberList": uid,
  872. "ChatRoomName": gid,
  873. "BaseRequest": self.base_request
  874. }
  875. headers = {'content-type': 'application/json; charset=UTF-8'}
  876. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  877. try:
  878. r = self.session.post(url, data=data, headers=headers)
  879. except (ConnectionError, ReadTimeout):
  880. return False
  881. dic = r.json()
  882. return dic['BaseResponse']['Ret'] == 0
  883. def set_group_name(self,gid,gname):
  884. """
  885. 设置群聊名称
  886. """
  887. url = self.base_uri + '/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % self.pass_ticket
  888. params ={
  889. "NewTopic": gname,
  890. "ChatRoomName": gid,
  891. "BaseRequest": self.base_request
  892. }
  893. headers = {'content-type': 'application/json; charset=UTF-8'}
  894. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  895. try:
  896. r = self.session.post(url, data=data, headers=headers)
  897. except (ConnectionError, ReadTimeout):
  898. return False
  899. dic = r.json()
  900. return dic['BaseResponse']['Ret'] == 0
  901. def send_msg_by_uid(self, word, dst='filehelper'):
  902. url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % self.pass_ticket
  903. msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '')
  904. word = self.to_unicode(word)
  905. params = {
  906. 'BaseRequest': self.base_request,
  907. 'Msg': {
  908. "Type": 1,
  909. "Content": word,
  910. "FromUserName": self.my_account['UserName'],
  911. "ToUserName": dst,
  912. "LocalID": msg_id,
  913. "ClientMsgId": msg_id
  914. }
  915. }
  916. headers = {'content-type': 'application/json; charset=UTF-8'}
  917. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  918. try:
  919. r = self.session.post(url, data=data, headers=headers)
  920. except (ConnectionError, ReadTimeout):
  921. return False
  922. dic = r.json()
  923. return dic['BaseResponse']['Ret'] == 0
  924. def upload_media(self, fpath, is_img=False):
  925. if not os.path.exists(fpath):
  926. log.error('File not exists.')
  927. return None
  928. url_1 = 'https://file.'+self.base_host+'/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json'
  929. url_2 = 'https://file2.'+self.base_host+'/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json'
  930. flen = str(os.path.getsize(fpath))
  931. ftype = mimetypes.guess_type(fpath)[0] or 'application/octet-stream'
  932. files = {
  933. 'id': (None, 'WU_FILE_%s' % str(self.file_index)),
  934. 'name': (None, os.path.basename(fpath)),
  935. 'type': (None, ftype),
  936. 'lastModifiedDate': (None, time.strftime('%m/%d/%Y, %H:%M:%S GMT+0800 (CST)')),
  937. 'size': (None, flen),
  938. 'mediatype': (None, 'pic' if is_img else 'doc'),
  939. 'uploadmediarequest': (None, json.dumps({
  940. 'BaseRequest': self.base_request,
  941. 'ClientMediaId': int(time.time()),
  942. 'TotalLen': flen,
  943. 'StartPos': 0,
  944. 'DataLen': flen,
  945. 'MediaType': 4,
  946. })),
  947. 'webwx_data_ticket': (None, self.session.cookies['webwx_data_ticket']),
  948. 'pass_ticket': (None, self.pass_ticket),
  949. 'filename': (os.path.basename(fpath), open(fpath, 'rb'),ftype.split('/')[1]),
  950. }
  951. self.file_index += 1
  952. try:
  953. r = self.session.post(url_1, files=files)
  954. if json.loads(r.text)['BaseResponse']['Ret'] != 0:
  955. # 当file返回值不为0时则为上传失败,尝试第二服务器上传
  956. r = self.session.post(url_2, files=files)
  957. if json.loads(r.text)['BaseResponse']['Ret'] != 0:
  958. log.error('Upload media failure.')
  959. return None
  960. mid = json.loads(r.text)['MediaId']
  961. return mid
  962. except:
  963. return None
  964. def send_file_msg_by_uid(self, fpath, uid):
  965. mid = self.upload_media(fpath)
  966. if mid is None or not mid:
  967. return False
  968. url = self.base_uri + '/webwxsendappmsg?fun=async&f=json&pass_ticket=' + self.pass_ticket
  969. msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '')
  970. data = {
  971. 'BaseRequest': self.base_request,
  972. 'Msg': {
  973. 'Type': 6,
  974. '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'),
  975. 'FromUserName': self.my_account['UserName'],
  976. 'ToUserName': uid,
  977. 'LocalID': msg_id,
  978. 'ClientMsgId': msg_id, }, }
  979. try:
  980. r = self.session.post(url, data=json.dumps(data))
  981. res = json.loads(r.text)
  982. if res['BaseResponse']['Ret'] == 0:
  983. return True
  984. else:
  985. return False
  986. except:
  987. return False
  988. def send_img_msg_by_uid(self, fpath, uid):
  989. mid = self.upload_media(fpath, is_img=True)
  990. if mid is None:
  991. log.error("upload media failed")
  992. return False
  993. url = self.base_uri + '/webwxsendmsgimg?fun=async&f=json'
  994. data = {
  995. 'BaseRequest': self.base_request,
  996. 'Msg': {
  997. 'Type': 3,
  998. 'MediaId': mid,
  999. 'FromUserName': self.my_account['UserName'],
  1000. 'ToUserName': uid,
  1001. 'LocalID': str(time.time() * 1e7),
  1002. 'ClientMsgId': str(time.time() * 1e7), }, }
  1003. if fpath[-4:] == '.gif':
  1004. url = self.base_uri + '/webwxsendemoticon?fun=sys'
  1005. data['Msg']['Type'] = 47
  1006. data['Msg']['EmojiFlag'] = 2
  1007. try:
  1008. r = self.session.post(url, data=json.dumps(data))
  1009. res = json.loads(r.text)
  1010. if res['BaseResponse']['Ret'] == 0:
  1011. return True
  1012. else:
  1013. return False
  1014. except:
  1015. return False
  1016. def get_user_id(self, name):
  1017. if name == '':
  1018. return None
  1019. name = self.to_unicode(name)
  1020. for contact in self.contact_list:
  1021. if 'RemarkName' in contact and contact['RemarkName'] == name:
  1022. return contact['UserName']
  1023. elif 'NickName' in contact and contact['NickName'] == name:
  1024. return contact['UserName']
  1025. elif 'DisplayName' in contact and contact['DisplayName'] == name:
  1026. return contact['UserName']
  1027. for group in self.group_list:
  1028. if 'RemarkName' in group and group['RemarkName'] == name:
  1029. return group['UserName']
  1030. if 'NickName' in group and group['NickName'] == name:
  1031. return group['UserName']
  1032. if 'DisplayName' in group and group['DisplayName'] == name:
  1033. return group['UserName']
  1034. return ''
  1035. def send_msg(self, name, word, isfile=False):
  1036. uid = self.get_user_id(name)
  1037. if uid is not None:
  1038. if isfile:
  1039. with open(word, 'r') as f:
  1040. result = True
  1041. for line in f.readlines():
  1042. line = line.replace('\n', '')
  1043. log.info('-> ' + name + ': ' + line)
  1044. if self.send_msg_by_uid(line, uid):
  1045. pass
  1046. else:
  1047. result = False
  1048. time.sleep(1)
  1049. return result
  1050. else:
  1051. word = self.to_unicode(word)
  1052. if self.send_msg_by_uid(word, uid):
  1053. return True
  1054. else:
  1055. return False
  1056. else:
  1057. if self.DEBUG:
  1058. log.error('This user does not exist .')
  1059. return True
  1060. @staticmethod
  1061. def search_content(key, content, fmat='attr'):
  1062. if fmat == 'attr':
  1063. pm = re.search(key + '\s?=\s?"([^"<]+)"', content)
  1064. if pm:
  1065. return pm.group(1)
  1066. elif fmat == 'xml':
  1067. pm = re.search('<{0}>([^<]+)</{0}>'.format(key), content)
  1068. if pm:
  1069. return pm.group(1)
  1070. return 'unknown'
  1071. def save_to_file(self):
  1072. with open(os.path.join(self.temp_pwd, 'session_state.json'), 'w') as fp:
  1073. json.dump({
  1074. 'uuid': self.uuid,
  1075. 'base_uri': self.base_uri,
  1076. 'base_host': self.base_host,
  1077. 'redirect_uri': self.redirect_uri,
  1078. 'uin': self.uin,
  1079. 'sid': self.sid,
  1080. 'skey': self.skey,
  1081. 'pass_ticket': self.pass_ticket,
  1082. 'device_id': self.device_id,
  1083. 'base_request': self.base_request,
  1084. 'session': jsonpickle.encode(self.session),
  1085. }, fp)
  1086. def load_from_file(self):
  1087. if not os.path.exists(self.state_file):
  1088. return False
  1089. try:
  1090. with open(self.state_file) as fp:
  1091. state = json.load(fp)
  1092. self.uuid = state['uuid']
  1093. self.base_uri = state['base_uri']
  1094. self.base_host = state['base_host']
  1095. self.redirect_uri = state['redirect_uri']
  1096. self.uin = state['uin']
  1097. self.sid = state['sid']
  1098. self.skey = state['skey']
  1099. self.pass_ticket = state['pass_ticket']
  1100. self.device_id = state['device_id']
  1101. self.base_request = state['base_request']
  1102. self.session = jsonpickle.decode(state['session'])
  1103. except:
  1104. log.info("Failed to load from local file")
  1105. return False
  1106. if self.init():
  1107. log.info('Web WeChat init succeed .')
  1108. return True
  1109. else:
  1110. log.info('Web WeChat init failed')
  1111. return False
  1112. def login(self):
  1113. self.get_uuid()
  1114. self.gen_qr_code(os.path.join(self.temp_pwd,'wxqr.png'))
  1115. log.info('Please use WeChat to scan the QR code .')
  1116. result = self.wait4login()
  1117. if result != SUCCESS:
  1118. log.error('Web WeChat login failed. failed code=%s' % (result,))
  1119. return False
  1120. if self._login():
  1121. log.info('Web WeChat login succeed .')
  1122. else:
  1123. log.error('Web WeChat login failed .')
  1124. return False
  1125. if self.init():
  1126. log.info('Web WeChat init succeed .')
  1127. self.save_to_file()
  1128. return True
  1129. else:
  1130. log.error('Web WeChat init failed')
  1131. return False
  1132. def run(self):
  1133. if not self.load_from_file() and not self.login():
  1134. log.error('Both recovered login status and fresh login are failed.')
  1135. return False
  1136. self.status_notify()
  1137. if self.get_contact():
  1138. log.info('Get %d contacts' % len(self.contact_list))
  1139. log.info('Start to process messages .')
  1140. self.proc_msg()
  1141. def get_uuid(self):
  1142. url = 'https://login.weixin.qq.com/jslogin'
  1143. params = {
  1144. 'appid': 'wx782c26e4c19acffb',
  1145. 'fun': 'new',
  1146. 'lang': 'zh_CN',
  1147. '_': int(time.time()) * 1000 + random.randint(1, 999),
  1148. }
  1149. r = self.session.get(url, params=params)
  1150. r.encoding = 'utf-8'
  1151. data = r.text
  1152. regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
  1153. pm = re.search(regx, data)
  1154. if pm:
  1155. code = pm.group(1)
  1156. self.uuid = pm.group(2)
  1157. return code == '200'
  1158. return False
  1159. def gen_qr_code(self, qr_file_path):
  1160. string = 'https://login.weixin.qq.com/l/' + self.uuid
  1161. qr = pyqrcode.create(string)
  1162. if self.conf['qr'] == 'png':
  1163. qr.png(qr_file_path, scale=8)
  1164. show_image(qr_file_path)
  1165. # img = Image.open(qr_file_path)
  1166. # img.show()
  1167. elif self.conf['qr'] == 'tty':
  1168. print(qr.terminal(quiet_zone=1))
  1169. def do_request(self, url):
  1170. r = self.session.get(url)
  1171. r.encoding = 'utf-8'
  1172. data = r.text
  1173. param = re.search(r'window.code=(\d+);', data)
  1174. code = param.group(1)
  1175. return code, data
  1176. def wait4login(self):
  1177. """
  1178. http comet:
  1179. tip=1, 等待用户扫描二维码,
  1180. 201: scaned
  1181. 408: timeout
  1182. tip=0, 等待用户确认登录,
  1183. 200: confirmed
  1184. """
  1185. LOGIN_TEMPLATE = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s'
  1186. tip = 1
  1187. try_later_secs = 1
  1188. MAX_RETRY_TIMES = 10
  1189. code = UNKONWN
  1190. retry_time = MAX_RETRY_TIMES
  1191. while retry_time > 0:
  1192. url = LOGIN_TEMPLATE % (tip, self.uuid, int(time.time()))
  1193. code, data = self.do_request(url)
  1194. if code == SCANED:
  1195. log.info('Please confirm to login .')
  1196. tip = 0
  1197. elif code == SUCCESS: # 确认登录成功
  1198. param = re.search(r'window.redirect_uri="(\S+?)";', data)
  1199. redirect_uri = param.group(1) + '&fun=new'
  1200. self.redirect_uri = redirect_uri
  1201. self.base_uri = redirect_uri[:redirect_uri.rfind('/')]
  1202. temp_host = self.base_uri[8:]
  1203. self.base_host = temp_host[:temp_host.find("/")]
  1204. return code
  1205. elif code == TIMEOUT:
  1206. log.error('WeChat login timeout. retry in %s secs later...' % (try_later_secs,))
  1207. tip = 1 # 重置
  1208. retry_time -= 1
  1209. time.sleep(try_later_secs)
  1210. else:
  1211. log.error('WeChat login exception return_code=%s. retry in %s secs later...' %
  1212. (code, try_later_secs))
  1213. tip = 1
  1214. retry_time -= 1
  1215. time.sleep(try_later_secs)
  1216. return code
  1217. def _login(self):
  1218. if len(self.redirect_uri) < 4:
  1219. log.error('Login failed due to network problem, please try again.')
  1220. return False
  1221. r = self.session.get(self.redirect_uri)
  1222. r.encoding = 'utf-8'
  1223. data = r.text
  1224. doc = xml.dom.minidom.parseString(data)
  1225. root = doc.documentElement
  1226. for node in root.childNodes:
  1227. if node.nodeName == 'skey':
  1228. self.skey = node.childNodes[0].data
  1229. elif node.nodeName == 'wxsid':
  1230. self.sid = node.childNodes[0].data
  1231. elif node.nodeName == 'wxuin':
  1232. self.uin = node.childNodes[0].data
  1233. elif node.nodeName == 'pass_ticket':
  1234. self.pass_ticket = node.childNodes[0].data
  1235. if '' in (self.skey, self.sid, self.uin, self.pass_ticket):
  1236. return False
  1237. self.base_request = {
  1238. 'Uin': self.uin,
  1239. 'Sid': self.sid,
  1240. 'Skey': self.skey,
  1241. 'DeviceID': self.device_id,
  1242. }
  1243. return True
  1244. def init(self):
  1245. url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  1246. params = {
  1247. 'BaseRequest': self.base_request
  1248. }
  1249. r = self.session.post(url, data=json.dumps(params))
  1250. r.encoding = 'utf-8'
  1251. dic = json.loads(r.text)
  1252. self.sync_key = dic['SyncKey']
  1253. self.my_account = dic['User']
  1254. self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
  1255. for keyVal in self.sync_key['List']])
  1256. return dic['BaseResponse']['Ret'] == 0
  1257. def status_notify(self):
  1258. url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % self.pass_ticket
  1259. self.base_request['Uin'] = int(self.base_request['Uin'])
  1260. params = {
  1261. 'BaseRequest': self.base_request,
  1262. "Code": 3,
  1263. "FromUserName": self.my_account['UserName'],
  1264. "ToUserName": self.my_account['UserName'],
  1265. "ClientMsgId": int(time.time())
  1266. }
  1267. r = self.session.post(url, data=json.dumps(params))
  1268. r.encoding = 'utf-8'
  1269. dic = json.loads(r.text)
  1270. return dic['BaseResponse']['Ret'] == 0
  1271. def test_sync_check(self):
  1272. for host1 in ['webpush.', 'webpush2.']:
  1273. self.sync_host = host1+self.base_host
  1274. try:
  1275. retcode = self.sync_check()[0]
  1276. except:
  1277. retcode = -1
  1278. if retcode == '0':
  1279. return True
  1280. return False
  1281. def sync_check(self):
  1282. params = {
  1283. 'r': int(time.time()),
  1284. 'sid': self.sid,
  1285. 'uin': self.uin,
  1286. 'skey': self.skey,
  1287. 'deviceid': self.device_id,
  1288. 'synckey': self.sync_key_str,
  1289. '_': int(time.time()),
  1290. }
  1291. url = 'https://' + self.sync_host + '/cgi-bin/mmwebwx-bin/synccheck?' + urlencode(params)
  1292. try:
  1293. r = self.session.get(url, timeout=60)
  1294. r.encoding = 'utf-8'
  1295. data = r.text
  1296. pm = re.search(r'window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}', data)
  1297. retcode = pm.group(1)
  1298. selector = pm.group(2)
  1299. return [retcode, selector]
  1300. except:
  1301. return [-1, -1]
  1302. def sync(self):
  1303. url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' \
  1304. % (self.sid, self.skey, self.pass_ticket)
  1305. params = {
  1306. 'BaseRequest': self.base_request,
  1307. 'SyncKey': self.sync_key,
  1308. 'rr': ~int(time.time())
  1309. }
  1310. try:
  1311. r = self.session.post(url, data=json.dumps(params), timeout=60)
  1312. r.encoding = 'utf-8'
  1313. dic = json.loads(r.text)
  1314. if dic['BaseResponse']['Ret'] == 0:
  1315. self.sync_key = dic['SyncKey']
  1316. self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
  1317. for keyVal in self.sync_key['List']])
  1318. return dic
  1319. except:
  1320. return None
  1321. def get_icon(self, uid, gid=None):
  1322. """
  1323. 获取联系人或者群聊成员头像
  1324. :param uid: 联系人id
  1325. :param gid: 群id,如果为非None获取群中成员头像,如果为None则获取联系人头像
  1326. """
  1327. if gid is None:
  1328. url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (uid, self.skey)
  1329. else:
  1330. url = self.base_uri + '/webwxgeticon?username=%s&skey=%s&chatroomid=%s' % (
  1331. uid, self.skey, self.encry_chat_room_id_list[gid])
  1332. r = self.session.get(url)
  1333. data = r.content
  1334. fn = 'icon_' + uid + '.jpg'
  1335. with open(os.path.join(self.temp_pwd,fn), 'wb') as f:
  1336. f.write(data)
  1337. return fn
  1338. def get_head_img(self, uid):
  1339. """
  1340. 获取群头像
  1341. :param uid: 群uid
  1342. """
  1343. url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (uid, self.skey)
  1344. r = self.session.get(url)
  1345. data = r.content
  1346. fn = 'head_' + uid + '.jpg'
  1347. with open(os.path.join(self.temp_pwd,fn), 'wb') as f:
  1348. f.write(data)
  1349. return fn
  1350. def get_msg_img_url(self, msgid):
  1351. return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  1352. def get_msg_img(self, msgid):
  1353. """
  1354. 获取图片消息,下载图片到本地
  1355. :param msgid: 消息id
  1356. :return: 保存的本地图片文件路径
  1357. """
  1358. url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  1359. r = self.session.get(url)
  1360. data = r.content
  1361. fn = 'img_' + msgid + '.jpg'
  1362. with open(os.path.join(self.temp_pwd,fn), 'wb') as f:
  1363. f.write(data)
  1364. return fn
  1365. def get_voice_url(self, msgid):
  1366. return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  1367. def get_voice(self, msgid):
  1368. """
  1369. 获取语音消息,下载语音到本地
  1370. :param msgid: 语音消息id
  1371. :return: 保存的本地语音文件路径
  1372. """
  1373. url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  1374. r = self.session.get(url)
  1375. data = r.content
  1376. fn = 'voice_' + msgid + '.mp3'
  1377. with open(os.path.join(self.temp_pwd,fn), 'wb') as f:
  1378. f.write(data)
  1379. return fn
  1380. def get_video_url(self, msgid):
  1381. return self.base_uri + '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey)
  1382. def get_video(self, msgid):
  1383. """
  1384. 获取视频消息,下载视频到本地
  1385. :param msgid: 视频消息id
  1386. :return: 保存的本地视频文件路径
  1387. """
  1388. url = self.base_uri + '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey)
  1389. headers = {'Range': 'bytes=0-'}
  1390. r = self.session.get(url, headers=headers)
  1391. data = r.content
  1392. fn = 'video_' + msgid + '.mp4'
  1393. with open(os.path.join(self.temp_pwd,fn), 'wb') as f:
  1394. f.write(data)
  1395. return fn
  1396. def set_remarkname(self,uid,remarkname):#设置联系人的备注名
  1397. url = self.base_uri + '/webwxoplog?lang=zh_CN&pass_ticket=%s' \
  1398. % (self.pass_ticket)
  1399. remarkname = self.to_unicode(remarkname)
  1400. params = {
  1401. 'BaseRequest': self.base_request,
  1402. 'CmdId': 2,
  1403. 'RemarkName': remarkname,
  1404. 'UserName': uid
  1405. }
  1406. try:
  1407. r = self.session.post(url, data=json.dumps(params), timeout=60)
  1408. r.encoding = 'utf-8'
  1409. dic = json.loads(r.text)
  1410. return dic['BaseResponse']['ErrMsg']
  1411. except:
  1412. return None