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.

412 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. skey = re.findall('<skey>(.*?)</skey>', r.text, re.S)[0]
  182. pass_ticket = re.findall(
  183. '<pass_ticket>(.*?)</pass_ticket>', r.text, re.S)[0]
  184. core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey
  185. core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"]
  186. core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"]
  187. core.loginInfo['pass_ticket'] = pass_ticket
  188. # A question : why pass_ticket == DeviceID ?
  189. # deviceID is only a randomly generated number
  190. # UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM
  191. # for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes:
  192. # if node.nodeName == 'skey':
  193. # core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data
  194. # elif node.nodeName == 'wxsid':
  195. # core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data
  196. # elif node.nodeName == 'wxuin':
  197. # core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data
  198. # elif node.nodeName == 'pass_ticket':
  199. # core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data
  200. if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]):
  201. logger.error(
  202. 'Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text)
  203. core.isLogging = False
  204. return False
  205. return True
  206. def web_init(self):
  207. url = '%s/webwxinit' % self.loginInfo['url']
  208. params = {
  209. 'r': int(-time.time() / 1579),
  210. 'pass_ticket': self.loginInfo['pass_ticket'], }
  211. data = {'BaseRequest': self.loginInfo['BaseRequest'], }
  212. headers = {
  213. 'ContentType': 'application/json; charset=UTF-8',
  214. 'User-Agent': config.USER_AGENT, }
  215. r = self.s.post(url, params=params, data=json.dumps(data), headers=headers)
  216. dic = json.loads(r.content.decode('utf-8', 'replace'))
  217. # deal with login info
  218. utils.emoji_formatter(dic['User'], 'NickName')
  219. self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount'])
  220. self.loginInfo['User'] = wrap_user_dict(
  221. utils.struct_friend_info(dic['User']))
  222. self.memberList.append(self.loginInfo['User'])
  223. self.loginInfo['SyncKey'] = dic['SyncKey']
  224. self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
  225. for item in dic['SyncKey']['List']])
  226. self.storageClass.userName = dic['User']['UserName']
  227. self.storageClass.nickName = dic['User']['NickName']
  228. # deal with contact list returned when init
  229. contactList = dic.get('ContactList', [])
  230. chatroomList, otherList = [], []
  231. for m in contactList:
  232. if m['Sex'] != 0:
  233. otherList.append(m)
  234. elif '@@' in m['UserName']:
  235. m['MemberList'] = [] # don't let dirty info pollute the list
  236. chatroomList.append(m)
  237. elif '@' in m['UserName']:
  238. # mp will be dealt in update_local_friends as well
  239. otherList.append(m)
  240. if chatroomList:
  241. update_local_chatrooms(self, chatroomList)
  242. if otherList:
  243. update_local_friends(self, otherList)
  244. return dic
  245. def show_mobile_login(self):
  246. url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (
  247. self.loginInfo['url'], self.loginInfo['pass_ticket'])
  248. data = {
  249. 'BaseRequest': self.loginInfo['BaseRequest'],
  250. 'Code': 3,
  251. 'FromUserName': self.storageClass.userName,
  252. 'ToUserName': self.storageClass.userName,
  253. 'ClientMsgId': int(time.time()), }
  254. headers = {
  255. 'ContentType': 'application/json; charset=UTF-8',
  256. 'User-Agent': config.USER_AGENT, }
  257. r = self.s.post(url, data=json.dumps(data), headers=headers)
  258. return ReturnValue(rawResponse=r)
  259. def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
  260. self.alive = True
  261. def maintain_loop():
  262. retryCount = 0
  263. while self.alive:
  264. try:
  265. i = sync_check(self)
  266. if i is None:
  267. self.alive = False
  268. elif i == '0':
  269. pass
  270. else:
  271. msgList, contactList = self.get_msg()
  272. if msgList:
  273. msgList = produce_msg(self, msgList)
  274. for msg in msgList:
  275. self.msgList.put(msg)
  276. if contactList:
  277. chatroomList, otherList = [], []
  278. for contact in contactList:
  279. if '@@' in contact['UserName']:
  280. chatroomList.append(contact)
  281. else:
  282. otherList.append(contact)
  283. chatroomMsg = update_local_chatrooms(
  284. self, chatroomList)
  285. chatroomMsg['User'] = self.loginInfo['User']
  286. self.msgList.put(chatroomMsg)
  287. update_local_friends(self, otherList)
  288. retryCount = 0
  289. except requests.exceptions.ReadTimeout:
  290. pass
  291. except:
  292. retryCount += 1
  293. logger.error(traceback.format_exc())
  294. if self.receivingRetryCount < retryCount:
  295. self.alive = False
  296. else:
  297. time.sleep(1)
  298. self.logout()
  299. if hasattr(exitCallback, '__call__'):
  300. exitCallback()
  301. else:
  302. logger.info('LOG OUT!')
  303. if getReceivingFnOnly:
  304. return maintain_loop
  305. else:
  306. maintainThread = threading.Thread(target=maintain_loop)
  307. maintainThread.setDaemon(True)
  308. maintainThread.start()
  309. def sync_check(self):
  310. url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url'])
  311. params = {
  312. 'r': int(time.time() * 1000),
  313. 'skey': self.loginInfo['skey'],
  314. 'sid': self.loginInfo['wxsid'],
  315. 'uin': self.loginInfo['wxuin'],
  316. 'deviceid': self.loginInfo['deviceid'],
  317. 'synckey': self.loginInfo['synckey'],
  318. '_': self.loginInfo['logintime'], }
  319. headers = {'User-Agent': config.USER_AGENT}
  320. self.loginInfo['logintime'] += 1
  321. try:
  322. r = self.s.get(url, params=params, headers=headers,
  323. timeout=config.TIMEOUT)
  324. except requests.exceptions.ConnectionError as e:
  325. try:
  326. if not isinstance(e.args[0].args[1], BadStatusLine):
  327. raise
  328. # will return a package with status '0 -'
  329. # and value like:
  330. # 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
  331. # seems like status of typing, but before I make further achievement code will remain like this
  332. return '2'
  333. except:
  334. raise
  335. r.raise_for_status()
  336. regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
  337. pm = re.search(regx, r.text)
  338. if pm is None or pm.group(1) != '0':
  339. logger.debug('Unexpected sync check result: %s' % r.text)
  340. return None
  341. return pm.group(2)
  342. def get_msg(self):
  343. self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
  344. url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (
  345. self.loginInfo['url'], self.loginInfo['wxsid'],
  346. self.loginInfo['skey'], self.loginInfo['pass_ticket'])
  347. data = {
  348. 'BaseRequest': self.loginInfo['BaseRequest'],
  349. 'SyncKey': self.loginInfo['SyncKey'],
  350. 'rr': ~int(time.time()), }
  351. headers = {
  352. 'ContentType': 'application/json; charset=UTF-8',
  353. 'User-Agent': config.USER_AGENT}
  354. r = self.s.post(url, data=json.dumps(data),
  355. headers=headers, timeout=config.TIMEOUT)
  356. dic = json.loads(r.content.decode('utf-8', 'replace'))
  357. if dic['BaseResponse']['Ret'] != 0:
  358. return None, None
  359. self.loginInfo['SyncKey'] = dic['SyncKey']
  360. self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
  361. for item in dic['SyncCheckKey']['List']])
  362. return dic['AddMsgList'], dic['ModContactList']
  363. def logout(self):
  364. if self.alive:
  365. url = '%s/webwxlogout' % self.loginInfo['url']
  366. params = {
  367. 'redirect': 1,
  368. 'type': 1,
  369. 'skey': self.loginInfo['skey'], }
  370. headers = {'User-Agent': config.USER_AGENT}
  371. self.s.get(url, params=params, headers=headers)
  372. self.alive = False
  373. self.isLogging = False
  374. self.s.cookies.clear()
  375. del self.chatroomList[:]
  376. del self.memberList[:]
  377. del self.mpList[:]
  378. return ReturnValue({'BaseResponse': {
  379. 'ErrMsg': 'logout successfully.',
  380. 'Ret': 0, }})