選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

login.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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, }})