Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

423 rindas
17KB

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