Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

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