You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

419 lines
16KB

  1. import os
  2. import time
  3. import re
  4. import io
  5. import threading
  6. import json
  7. import xml.dom.minidom
  8. import random
  9. import traceback
  10. import logging
  11. try:
  12. from httplib import BadStatusLine
  13. except ImportError:
  14. from http.client import BadStatusLine
  15. import requests
  16. from pyqrcode import QRCode
  17. from .. import config, utils
  18. from ..returnvalues import ReturnValue
  19. from ..storage.templates import wrap_user_dict
  20. from .contact import update_local_chatrooms, update_local_friends
  21. from .messages import produce_msg
  22. logger = logging.getLogger('itchat')
  23. def load_login(core):
  24. core.login = login
  25. core.get_QRuuid = get_QRuuid
  26. core.get_QR = get_QR
  27. core.check_login = check_login
  28. core.web_init = web_init
  29. core.show_mobile_login = show_mobile_login
  30. core.start_receiving = start_receiving
  31. core.get_msg = get_msg
  32. core.logout = logout
  33. def login(self, enableCmdQR=False, picDir=None, qrCallback=None,
  34. loginCallback=None, exitCallback=None):
  35. if self.alive or self.isLogging:
  36. logger.warning('itchat has already logged in.')
  37. return
  38. self.isLogging = True
  39. logger.info('Ready to login.')
  40. while self.isLogging:
  41. uuid = push_login(self)
  42. if uuid:
  43. qrStorage = io.BytesIO()
  44. else:
  45. logger.info('Getting uuid of QR code.')
  46. while not self.get_QRuuid():
  47. time.sleep(1)
  48. logger.info('Downloading QR code.')
  49. qrStorage = self.get_QR(enableCmdQR=enableCmdQR,
  50. picDir=picDir, qrCallback=qrCallback)
  51. # logger.info('Please scan the QR code to log in.')
  52. isLoggedIn = False
  53. while not isLoggedIn:
  54. status = self.check_login()
  55. if hasattr(qrCallback, '__call__'):
  56. qrCallback(uuid=self.uuid, status=status,
  57. qrcode=qrStorage.getvalue())
  58. if status == '200':
  59. isLoggedIn = True
  60. elif status == '201':
  61. if isLoggedIn is not None:
  62. logger.info('Please press confirm on your phone.')
  63. isLoggedIn = None
  64. time.sleep(7)
  65. time.sleep(0.5)
  66. elif status != '408':
  67. break
  68. if isLoggedIn:
  69. break
  70. elif self.isLogging:
  71. logger.info('Log in time out, reloading QR code.')
  72. else:
  73. return # log in process is stopped by user
  74. logger.info('Loading the contact, this may take a little while.')
  75. self.web_init()
  76. self.show_mobile_login()
  77. self.get_contact(True)
  78. if hasattr(loginCallback, '__call__'):
  79. r = loginCallback()
  80. else:
  81. # utils.clear_screen()
  82. if os.path.exists(picDir or config.DEFAULT_QR):
  83. os.remove(picDir or config.DEFAULT_QR)
  84. logger.info('Login successfully as %s' % self.storageClass.nickName)
  85. self.start_receiving(exitCallback)
  86. self.isLogging = False
  87. def push_login(core):
  88. cookiesDict = core.s.cookies.get_dict()
  89. if 'wxuin' in cookiesDict:
  90. url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % (
  91. config.BASE_URL, cookiesDict['wxuin'])
  92. headers = {'User-Agent': config.USER_AGENT}
  93. r = core.s.get(url, headers=headers).json()
  94. if 'uuid' in r and r.get('ret') in (0, '0'):
  95. core.uuid = r['uuid']
  96. return r['uuid']
  97. return False
  98. def get_QRuuid(self):
  99. url = '%s/jslogin' % config.BASE_URL
  100. params = {
  101. 'appid': 'wx782c26e4c19acffb',
  102. 'fun': 'new',
  103. 'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop',
  104. 'lang': 'zh_CN'}
  105. headers = {'User-Agent': config.USER_AGENT}
  106. r = self.s.get(url, params=params, headers=headers)
  107. regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";'
  108. data = re.search(regx, r.text)
  109. if data and data.group(1) == '200':
  110. self.uuid = data.group(2)
  111. return self.uuid
  112. def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None):
  113. uuid = uuid or self.uuid
  114. picDir = picDir or config.DEFAULT_QR
  115. qrStorage = io.BytesIO()
  116. qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid)
  117. qrCode.png(qrStorage, scale=10)
  118. if hasattr(qrCallback, '__call__'):
  119. qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue())
  120. else:
  121. with open(picDir, 'wb') as f:
  122. f.write(qrStorage.getvalue())
  123. if enableCmdQR:
  124. utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR)
  125. else:
  126. utils.print_qr(picDir)
  127. return qrStorage
  128. def check_login(self, uuid=None):
  129. uuid = uuid or self.uuid
  130. url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL
  131. localTime = int(time.time())
  132. params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % (
  133. uuid, int(-localTime / 1579), localTime)
  134. headers = {'User-Agent': config.USER_AGENT}
  135. r = self.s.get(url, params=params, headers=headers)
  136. regx = r'window.code=(\d+)'
  137. data = re.search(regx, r.text)
  138. if data and data.group(1) == '200':
  139. if process_login_info(self, r.text):
  140. return '200'
  141. else:
  142. return '400'
  143. elif data:
  144. return data.group(1)
  145. else:
  146. return '400'
  147. def process_login_info(core, loginContent):
  148. ''' when finish login (scanning qrcode)
  149. * syncUrl and fileUploadingUrl will be fetched
  150. * deviceid and msgid will be generated
  151. * skey, wxsid, wxuin, pass_ticket will be fetched
  152. '''
  153. regx = r'window.redirect_uri="(\S+)";'
  154. core.loginInfo['url'] = re.search(regx, loginContent).group(1)
  155. headers = {'User-Agent': config.USER_AGENT,
  156. 'client-version': config.UOS_PATCH_CLIENT_VERSION,
  157. 'extspam': config.UOS_PATCH_EXTSPAM,
  158. 'referer': 'https://wx.qq.com/?&lang=zh_CN&target=t'
  159. }
  160. r = core.s.get(core.loginInfo['url'],
  161. headers=headers, allow_redirects=False)
  162. core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind(
  163. '/')]
  164. for indexUrl, detailedUrl in (
  165. ("wx2.qq.com", ("file.wx2.qq.com", "webpush.wx2.qq.com")),
  166. ("wx8.qq.com", ("file.wx8.qq.com", "webpush.wx8.qq.com")),
  167. ("qq.com", ("file.wx.qq.com", "webpush.wx.qq.com")),
  168. ("web2.wechat.com", ("file.web2.wechat.com", "webpush.web2.wechat.com")),
  169. ("wechat.com", ("file.web.wechat.com", "webpush.web.wechat.com"))):
  170. fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' %
  171. url for url in detailedUrl]
  172. if indexUrl in core.loginInfo['url']:
  173. core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \
  174. fileUrl, syncUrl
  175. break
  176. else:
  177. core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url']
  178. core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
  179. core.loginInfo['logintime'] = int(time.time() * 1e3)
  180. core.loginInfo['BaseRequest'] = {}
  181. cookies = core.s.cookies.get_dict()
  182. res = re.findall('<skey>(.*?)</skey>', r.text, re.S)
  183. skey = res[0] if res else None
  184. res = re.findall(
  185. '<pass_ticket>(.*?)</pass_ticket>', r.text, re.S)
  186. pass_ticket = res[0] if res else None
  187. if skey is not None:
  188. core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey
  189. core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"]
  190. core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"]
  191. if pass_ticket is not None:
  192. core.loginInfo['pass_ticket'] = pass_ticket
  193. # A question : why pass_ticket == DeviceID ?
  194. # deviceID is only a randomly generated number
  195. # UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM
  196. # for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes:
  197. # if node.nodeName == 'skey':
  198. # core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data
  199. # elif node.nodeName == 'wxsid':
  200. # core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data
  201. # elif node.nodeName == 'wxuin':
  202. # core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data
  203. # elif node.nodeName == 'pass_ticket':
  204. # core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data
  205. if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]):
  206. logger.error(
  207. 'Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text)
  208. core.isLogging = False
  209. return False
  210. return True
  211. def web_init(self):
  212. url = '%s/webwxinit' % self.loginInfo['url']
  213. params = {
  214. 'r': int(-time.time() / 1579),
  215. 'pass_ticket': self.loginInfo['pass_ticket'], }
  216. data = {'BaseRequest': self.loginInfo['BaseRequest'], }
  217. headers = {
  218. 'ContentType': 'application/json; charset=UTF-8',
  219. 'User-Agent': config.USER_AGENT, }
  220. r = self.s.post(url, params=params, data=json.dumps(data), headers=headers)
  221. dic = json.loads(r.content.decode('utf-8', 'replace'))
  222. # deal with login info
  223. utils.emoji_formatter(dic['User'], 'NickName')
  224. self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount'])
  225. self.loginInfo['User'] = wrap_user_dict(
  226. utils.struct_friend_info(dic['User']))
  227. self.memberList.append(self.loginInfo['User'])
  228. self.loginInfo['SyncKey'] = dic['SyncKey']
  229. self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
  230. for item in dic['SyncKey']['List']])
  231. self.storageClass.userName = dic['User']['UserName']
  232. self.storageClass.nickName = dic['User']['NickName']
  233. # deal with contact list returned when init
  234. contactList = dic.get('ContactList', [])
  235. chatroomList, otherList = [], []
  236. for m in contactList:
  237. if m['Sex'] != 0:
  238. otherList.append(m)
  239. elif '@@' in m['UserName']:
  240. m['MemberList'] = [] # don't let dirty info pollute the list
  241. chatroomList.append(m)
  242. elif '@' in m['UserName']:
  243. # mp will be dealt in update_local_friends as well
  244. otherList.append(m)
  245. if chatroomList:
  246. update_local_chatrooms(self, chatroomList)
  247. if otherList:
  248. update_local_friends(self, otherList)
  249. return dic
  250. def show_mobile_login(self):
  251. url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (
  252. self.loginInfo['url'], self.loginInfo['pass_ticket'])
  253. data = {
  254. 'BaseRequest': self.loginInfo['BaseRequest'],
  255. 'Code': 3,
  256. 'FromUserName': self.storageClass.userName,
  257. 'ToUserName': self.storageClass.userName,
  258. 'ClientMsgId': int(time.time()), }
  259. headers = {
  260. 'ContentType': 'application/json; charset=UTF-8',
  261. 'User-Agent': config.USER_AGENT, }
  262. r = self.s.post(url, data=json.dumps(data), headers=headers)
  263. return ReturnValue(rawResponse=r)
  264. def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
  265. self.alive = True
  266. def maintain_loop():
  267. retryCount = 0
  268. while self.alive:
  269. try:
  270. i = sync_check(self)
  271. if i is None:
  272. self.alive = False
  273. elif i == '0':
  274. pass
  275. else:
  276. msgList, contactList = self.get_msg()
  277. if msgList:
  278. msgList = produce_msg(self, msgList)
  279. for msg in msgList:
  280. self.msgList.put(msg)
  281. if contactList:
  282. chatroomList, otherList = [], []
  283. for contact in contactList:
  284. if '@@' in contact['UserName']:
  285. chatroomList.append(contact)
  286. else:
  287. otherList.append(contact)
  288. chatroomMsg = update_local_chatrooms(
  289. self, chatroomList)
  290. chatroomMsg['User'] = self.loginInfo['User']
  291. self.msgList.put(chatroomMsg)
  292. update_local_friends(self, otherList)
  293. retryCount = 0
  294. except requests.exceptions.ReadTimeout:
  295. pass
  296. except:
  297. retryCount += 1
  298. logger.error(traceback.format_exc())
  299. if self.receivingRetryCount < retryCount:
  300. logger.error("Having tried %s times, but still failed. " % (
  301. retryCount) + "Stop trying...")
  302. self.alive = False
  303. else:
  304. time.sleep(1)
  305. self.logout()
  306. if hasattr(exitCallback, '__call__'):
  307. exitCallback()
  308. else:
  309. logger.info('LOG OUT!')
  310. if getReceivingFnOnly:
  311. return maintain_loop
  312. else:
  313. maintainThread = threading.Thread(target=maintain_loop)
  314. maintainThread.setDaemon(True)
  315. maintainThread.start()
  316. def sync_check(self):
  317. url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url'])
  318. params = {
  319. 'r': int(time.time() * 1000),
  320. 'skey': self.loginInfo['skey'],
  321. 'sid': self.loginInfo['wxsid'],
  322. 'uin': self.loginInfo['wxuin'],
  323. 'deviceid': self.loginInfo['deviceid'],
  324. 'synckey': self.loginInfo['synckey'],
  325. '_': self.loginInfo['logintime'], }
  326. headers = {'User-Agent': config.USER_AGENT}
  327. self.loginInfo['logintime'] += 1
  328. try:
  329. r = self.s.get(url, params=params, headers=headers,
  330. timeout=config.TIMEOUT)
  331. except requests.exceptions.ConnectionError as e:
  332. try:
  333. if not isinstance(e.args[0].args[1], BadStatusLine):
  334. raise
  335. # will return a package with status '0 -'
  336. # and value like:
  337. # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93
  338. # seems like status of typing, but before I make further achievement code will remain like this
  339. return '2'
  340. except:
  341. raise
  342. r.raise_for_status()
  343. regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
  344. pm = re.search(regx, r.text)
  345. if pm is None or pm.group(1) != '0':
  346. logger.error('Unexpected sync check result: %s' % r.text)
  347. return None
  348. return pm.group(2)
  349. def get_msg(self):
  350. self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
  351. url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (
  352. self.loginInfo['url'], self.loginInfo['wxsid'],
  353. self.loginInfo['skey'], self.loginInfo['pass_ticket'])
  354. data = {
  355. 'BaseRequest': self.loginInfo['BaseRequest'],
  356. 'SyncKey': self.loginInfo['SyncKey'],
  357. 'rr': ~int(time.time()), }
  358. headers = {
  359. 'ContentType': 'application/json; charset=UTF-8',
  360. 'User-Agent': config.USER_AGENT}
  361. r = self.s.post(url, data=json.dumps(data),
  362. headers=headers, timeout=config.TIMEOUT)
  363. dic = json.loads(r.content.decode('utf-8', 'replace'))
  364. if dic['BaseResponse']['Ret'] != 0:
  365. return None, None
  366. self.loginInfo['SyncKey'] = dic['SyncKey']
  367. self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
  368. for item in dic['SyncCheckKey']['List']])
  369. return dic['AddMsgList'], dic['ModContactList']
  370. def logout(self):
  371. if self.alive:
  372. url = '%s/webwxlogout' % self.loginInfo['url']
  373. params = {
  374. 'redirect': 1,
  375. 'type': 1,
  376. 'skey': self.loginInfo['skey'], }
  377. headers = {'User-Agent': config.USER_AGENT}
  378. self.s.get(url, params=params, headers=headers)
  379. self.alive = False
  380. self.isLogging = False
  381. self.s.cookies.clear()
  382. del self.chatroomList[:]
  383. del self.memberList[:]
  384. del self.mpList[:]
  385. return ReturnValue({'BaseResponse': {
  386. 'ErrMsg': 'logout successfully.',
  387. 'Ret': 0, }})