528 lines
22KB

  1. import os, time, re, io
  2. import json
  3. import mimetypes, hashlib
  4. import logging
  5. from collections import OrderedDict
  6. from .. import config, utils
  7. from ..returnvalues import ReturnValue
  8. from ..storage import templates
  9. from .contact import update_local_uin
  10. logger = logging.getLogger('itchat')
  11. def load_messages(core):
  12. core.send_raw_msg = send_raw_msg
  13. core.send_msg = send_msg
  14. core.upload_file = upload_file
  15. core.send_file = send_file
  16. core.send_image = send_image
  17. core.send_video = send_video
  18. core.send = send
  19. core.revoke = revoke
  20. async def get_download_fn(core, url, msgId):
  21. async def download_fn(downloadDir=None):
  22. params = {
  23. 'msgid': msgId,
  24. 'skey': core.loginInfo['skey'],}
  25. headers = { 'User-Agent' : config.USER_AGENT}
  26. r = core.s.get(url, params=params, stream=True, headers = headers)
  27. tempStorage = io.BytesIO()
  28. for block in r.iter_content(1024):
  29. tempStorage.write(block)
  30. if downloadDir is None:
  31. return tempStorage.getvalue()
  32. with open(downloadDir, 'wb') as f:
  33. f.write(tempStorage.getvalue())
  34. tempStorage.seek(0)
  35. return ReturnValue({'BaseResponse': {
  36. 'ErrMsg': 'Successfully downloaded',
  37. 'Ret': 0, },
  38. 'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
  39. return download_fn
  40. def produce_msg(core, msgList):
  41. ''' for messages types
  42. * 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg
  43. * 53 webwxvoipnotifymsg, 9999 sysnotice
  44. '''
  45. rl = []
  46. srl = [40, 43, 50, 52, 53, 9999]
  47. for m in msgList:
  48. # get actual opposite
  49. if m['FromUserName'] == core.storageClass.userName:
  50. actualOpposite = m['ToUserName']
  51. else:
  52. actualOpposite = m['FromUserName']
  53. # produce basic message
  54. if '@@' in m['FromUserName'] or '@@' in m['ToUserName']:
  55. produce_group_chat(core, m)
  56. else:
  57. utils.msg_formatter(m, 'Content')
  58. # set user of msg
  59. if '@@' in actualOpposite:
  60. m['User'] = core.search_chatrooms(userName=actualOpposite) or \
  61. templates.Chatroom({'UserName': actualOpposite})
  62. # we don't need to update chatroom here because we have
  63. # updated once when producing basic message
  64. elif actualOpposite in ('filehelper', 'fmessage'):
  65. m['User'] = templates.User({'UserName': actualOpposite})
  66. else:
  67. m['User'] = core.search_mps(userName=actualOpposite) or \
  68. core.search_friends(userName=actualOpposite) or \
  69. templates.User(userName=actualOpposite)
  70. # by default we think there may be a user missing not a mp
  71. m['User'].core = core
  72. if m['MsgType'] == 1: # words
  73. if m['Url']:
  74. regx = r'(.+?\(.+?\))'
  75. data = re.search(regx, m['Content'])
  76. data = 'Map' if data is None else data.group(1)
  77. msg = {
  78. 'Type': 'Map',
  79. 'Text': data,}
  80. else:
  81. msg = {
  82. 'Type': 'Text',
  83. 'Text': m['Content'],}
  84. elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture
  85. download_fn = get_download_fn(core,
  86. '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
  87. msg = {
  88. 'Type' : 'Picture',
  89. 'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()),
  90. 'png' if m['MsgType'] == 3 else 'gif'),
  91. 'Text' : download_fn, }
  92. elif m['MsgType'] == 34: # voice
  93. download_fn = get_download_fn(core,
  94. '%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId'])
  95. msg = {
  96. 'Type': 'Recording',
  97. 'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
  98. 'Text': download_fn,}
  99. elif m['MsgType'] == 37: # friends
  100. m['User']['UserName'] = m['RecommendInfo']['UserName']
  101. msg = {
  102. 'Type': 'Friends',
  103. 'Text': {
  104. 'status' : m['Status'],
  105. 'userName' : m['RecommendInfo']['UserName'],
  106. 'verifyContent' : m['Ticket'],
  107. 'autoUpdate' : m['RecommendInfo'], }, }
  108. m['User'].verifyDict = msg['Text']
  109. elif m['MsgType'] == 42: # name card
  110. msg = {
  111. 'Type': 'Card',
  112. 'Text': m['RecommendInfo'], }
  113. elif m['MsgType'] in (43, 62): # tiny video
  114. msgId = m['MsgId']
  115. async def download_video(videoDir=None):
  116. url = '%s/webwxgetvideo' % core.loginInfo['url']
  117. params = {
  118. 'msgid': msgId,
  119. 'skey': core.loginInfo['skey'],}
  120. headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT}
  121. r = core.s.get(url, params=params, headers=headers, stream=True)
  122. tempStorage = io.BytesIO()
  123. for block in r.iter_content(1024):
  124. tempStorage.write(block)
  125. if videoDir is None:
  126. return tempStorage.getvalue()
  127. with open(videoDir, 'wb') as f:
  128. f.write(tempStorage.getvalue())
  129. return ReturnValue({'BaseResponse': {
  130. 'ErrMsg': 'Successfully downloaded',
  131. 'Ret': 0, }})
  132. msg = {
  133. 'Type': 'Video',
  134. 'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
  135. 'Text': download_video, }
  136. elif m['MsgType'] == 49: # sharing
  137. if m['AppMsgType'] == 0: # chat history
  138. msg = {
  139. 'Type': 'Note',
  140. 'Text': m['Content'], }
  141. elif m['AppMsgType'] == 6:
  142. rawMsg = m
  143. cookiesList = {name:data for name,data in core.s.cookies.items()}
  144. async def download_atta(attaDir=None):
  145. url = core.loginInfo['fileUrl'] + '/webwxgetmedia'
  146. params = {
  147. 'sender': rawMsg['FromUserName'],
  148. 'mediaid': rawMsg['MediaId'],
  149. 'filename': rawMsg['FileName'],
  150. 'fromuser': core.loginInfo['wxuin'],
  151. 'pass_ticket': 'undefined',
  152. 'webwx_data_ticket': cookiesList['webwx_data_ticket'],}
  153. headers = { 'User-Agent' : config.USER_AGENT}
  154. r = core.s.get(url, params=params, stream=True, headers=headers)
  155. tempStorage = io.BytesIO()
  156. for block in r.iter_content(1024):
  157. tempStorage.write(block)
  158. if attaDir is None:
  159. return tempStorage.getvalue()
  160. with open(attaDir, 'wb') as f:
  161. f.write(tempStorage.getvalue())
  162. return ReturnValue({'BaseResponse': {
  163. 'ErrMsg': 'Successfully downloaded',
  164. 'Ret': 0, }})
  165. msg = {
  166. 'Type': 'Attachment',
  167. 'Text': download_atta, }
  168. elif m['AppMsgType'] == 8:
  169. download_fn = get_download_fn(core,
  170. '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
  171. msg = {
  172. 'Type' : 'Picture',
  173. 'FileName' : '%s.gif' % (
  174. time.strftime('%y%m%d-%H%M%S', time.localtime())),
  175. 'Text' : download_fn, }
  176. elif m['AppMsgType'] == 17:
  177. msg = {
  178. 'Type': 'Note',
  179. 'Text': m['FileName'], }
  180. elif m['AppMsgType'] == 2000:
  181. regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]'
  182. data = re.search(regx, m['Content'])
  183. if data:
  184. data = data.group(2).split(u'\u3002')[0]
  185. else:
  186. data = 'You may found detailed info in Content key.'
  187. msg = {
  188. 'Type': 'Note',
  189. 'Text': data, }
  190. else:
  191. msg = {
  192. 'Type': 'Sharing',
  193. 'Text': m['FileName'], }
  194. elif m['MsgType'] == 51: # phone init
  195. msg = update_local_uin(core, m)
  196. elif m['MsgType'] == 10000:
  197. msg = {
  198. 'Type': 'Note',
  199. 'Text': m['Content'],}
  200. elif m['MsgType'] == 10002:
  201. regx = r'\[CDATA\[(.+?)\]\]'
  202. data = re.search(regx, m['Content'])
  203. data = 'System message' if data is None else data.group(1).replace('\\', '')
  204. msg = {
  205. 'Type': 'Note',
  206. 'Text': data, }
  207. elif m['MsgType'] in srl:
  208. msg = {
  209. 'Type': 'Useless',
  210. 'Text': 'UselessMsg', }
  211. else:
  212. logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m)))
  213. msg = {
  214. 'Type': 'Useless',
  215. 'Text': 'UselessMsg', }
  216. m = dict(m, **msg)
  217. rl.append(m)
  218. return rl
  219. def produce_group_chat(core, msg):
  220. r = re.match('(@[0-9a-z]*?):<br/>(.*)$', msg['Content'])
  221. if r:
  222. actualUserName, content = r.groups()
  223. chatroomUserName = msg['FromUserName']
  224. elif msg['FromUserName'] == core.storageClass.userName:
  225. actualUserName = core.storageClass.userName
  226. content = msg['Content']
  227. chatroomUserName = msg['ToUserName']
  228. else:
  229. msg['ActualUserName'] = core.storageClass.userName
  230. msg['ActualNickName'] = core.storageClass.nickName
  231. msg['IsAt'] = False
  232. utils.msg_formatter(msg, 'Content')
  233. return
  234. chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName)
  235. member = utils.search_dict_list((chatroom or {}).get(
  236. 'MemberList') or [], 'UserName', actualUserName)
  237. if member is None:
  238. chatroom = core.update_chatroom(chatroomUserName)
  239. member = utils.search_dict_list((chatroom or {}).get(
  240. 'MemberList') or [], 'UserName', actualUserName)
  241. if member is None:
  242. logger.debug('chatroom member fetch failed with %s' % actualUserName)
  243. msg['ActualNickName'] = ''
  244. msg['IsAt'] = False
  245. else:
  246. msg['ActualNickName'] = member.get('DisplayName', '') or member['NickName']
  247. atFlag = '@' + (chatroom['Self'].get('DisplayName', '') or core.storageClass.nickName)
  248. msg['IsAt'] = (
  249. (atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' '))
  250. in msg['Content'] or msg['Content'].endswith(atFlag))
  251. msg['ActualUserName'] = actualUserName
  252. msg['Content'] = content
  253. utils.msg_formatter(msg, 'Content')
  254. async def send_raw_msg(self, msgType, content, toUserName):
  255. url = '%s/webwxsendmsg' % self.loginInfo['url']
  256. data = {
  257. 'BaseRequest': self.loginInfo['BaseRequest'],
  258. 'Msg': {
  259. 'Type': msgType,
  260. 'Content': content,
  261. 'FromUserName': self.storageClass.userName,
  262. 'ToUserName': (toUserName if toUserName else self.storageClass.userName),
  263. 'LocalID': int(time.time() * 1e4),
  264. 'ClientMsgId': int(time.time() * 1e4),
  265. },
  266. 'Scene': 0, }
  267. headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT}
  268. r = self.s.post(url, headers=headers,
  269. data=json.dumps(data, ensure_ascii=False).encode('utf8'))
  270. return ReturnValue(rawResponse=r)
  271. async def send_msg(self, msg='Test Message', toUserName=None):
  272. logger.debug('Request to send a text message to %s: %s' % (toUserName, msg))
  273. r = await self.send_raw_msg(1, msg, toUserName)
  274. return r
  275. def _prepare_file(fileDir, file_=None):
  276. fileDict = {}
  277. if file_:
  278. if hasattr(file_, 'read'):
  279. file_ = file_.read()
  280. else:
  281. return ReturnValue({'BaseResponse': {
  282. 'ErrMsg': 'file_ param should be opened file',
  283. 'Ret': -1005, }})
  284. else:
  285. if not utils.check_file(fileDir):
  286. return ReturnValue({'BaseResponse': {
  287. 'ErrMsg': 'No file found in specific dir',
  288. 'Ret': -1002, }})
  289. with open(fileDir, 'rb') as f:
  290. file_ = f.read()
  291. fileDict['fileSize'] = len(file_)
  292. fileDict['fileMd5'] = hashlib.md5(file_).hexdigest()
  293. fileDict['file_'] = io.BytesIO(file_)
  294. return fileDict
  295. def upload_file(self, fileDir, isPicture=False, isVideo=False,
  296. toUserName='filehelper', file_=None, preparedFile=None):
  297. logger.debug('Request to upload a %s: %s' % (
  298. 'picture' if isPicture else 'video' if isVideo else 'file', fileDir))
  299. if not preparedFile:
  300. preparedFile = _prepare_file(fileDir, file_)
  301. if not preparedFile:
  302. return preparedFile
  303. fileSize, fileMd5, file_ = \
  304. preparedFile['fileSize'], preparedFile['fileMd5'], preparedFile['file_']
  305. fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc'
  306. chunks = int((fileSize - 1) / 524288) + 1
  307. clientMediaId = int(time.time() * 1e4)
  308. uploadMediaRequest = json.dumps(OrderedDict([
  309. ('UploadType', 2),
  310. ('BaseRequest', self.loginInfo['BaseRequest']),
  311. ('ClientMediaId', clientMediaId),
  312. ('TotalLen', fileSize),
  313. ('StartPos', 0),
  314. ('DataLen', fileSize),
  315. ('MediaType', 4),
  316. ('FromUserName', self.storageClass.userName),
  317. ('ToUserName', toUserName),
  318. ('FileMd5', fileMd5)]
  319. ), separators = (',', ':'))
  320. r = {'BaseResponse': {'Ret': -1005, 'ErrMsg': 'Empty file detected'}}
  321. for chunk in range(chunks):
  322. r = upload_chunk_file(self, fileDir, fileSymbol, fileSize,
  323. file_, chunk, chunks, uploadMediaRequest)
  324. file_.close()
  325. if isinstance(r, dict):
  326. return ReturnValue(r)
  327. return ReturnValue(rawResponse=r)
  328. def upload_chunk_file(core, fileDir, fileSymbol, fileSize,
  329. file_, chunk, chunks, uploadMediaRequest):
  330. url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \
  331. '/webwxuploadmedia?f=json'
  332. # save it on server
  333. cookiesList = {name:data for name,data in core.s.cookies.items()}
  334. fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream'
  335. fileName = utils.quote(os.path.basename(fileDir))
  336. files = OrderedDict([
  337. ('id', (None, 'WU_FILE_0')),
  338. ('name', (None, fileName)),
  339. ('type', (None, fileType)),
  340. ('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))),
  341. ('size', (None, str(fileSize))),
  342. ('chunks', (None, None)),
  343. ('chunk', (None, None)),
  344. ('mediatype', (None, fileSymbol)),
  345. ('uploadmediarequest', (None, uploadMediaRequest)),
  346. ('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])),
  347. ('pass_ticket', (None, core.loginInfo['pass_ticket'])),
  348. ('filename' , (fileName, file_.read(524288), 'application/octet-stream'))])
  349. if chunks == 1:
  350. del files['chunk']; del files['chunks']
  351. else:
  352. files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks))
  353. headers = { 'User-Agent' : config.USER_AGENT}
  354. return core.s.post(url, files=files, headers=headers, timeout=config.TIMEOUT)
  355. async def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None):
  356. logger.debug('Request to send a file(mediaId: %s) to %s: %s' % (
  357. mediaId, toUserName, fileDir))
  358. if hasattr(fileDir, 'read'):
  359. return ReturnValue({'BaseResponse': {
  360. 'ErrMsg': 'fileDir param should not be an opened file in send_file',
  361. 'Ret': -1005, }})
  362. if toUserName is None:
  363. toUserName = self.storageClass.userName
  364. preparedFile = _prepare_file(fileDir, file_)
  365. if not preparedFile:
  366. return preparedFile
  367. fileSize = preparedFile['fileSize']
  368. if mediaId is None:
  369. r = self.upload_file(fileDir, preparedFile=preparedFile)
  370. if r:
  371. mediaId = r['MediaId']
  372. else:
  373. return r
  374. url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url']
  375. data = {
  376. 'BaseRequest': self.loginInfo['BaseRequest'],
  377. 'Msg': {
  378. 'Type': 6,
  379. 'Content': ("<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''><title>%s</title>" % os.path.basename(fileDir) +
  380. "<des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>" +
  381. "<appattach><totallen>%s</totallen><attachid>%s</attachid>" % (str(fileSize), mediaId) +
  382. "<fileext>%s</fileext></appattach><extinfo></extinfo></appmsg>" % os.path.splitext(fileDir)[1].replace('.','')),
  383. 'FromUserName': self.storageClass.userName,
  384. 'ToUserName': toUserName,
  385. 'LocalID': int(time.time() * 1e4),
  386. 'ClientMsgId': int(time.time() * 1e4), },
  387. 'Scene': 0, }
  388. headers = {
  389. 'User-Agent': config.USER_AGENT,
  390. 'Content-Type': 'application/json;charset=UTF-8', }
  391. r = self.s.post(url, headers=headers,
  392. data=json.dumps(data, ensure_ascii=False).encode('utf8'))
  393. return ReturnValue(rawResponse=r)
  394. async def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
  395. logger.debug('Request to send a image(mediaId: %s) to %s: %s' % (
  396. mediaId, toUserName, fileDir))
  397. if fileDir or file_:
  398. if hasattr(fileDir, 'read'):
  399. file_, fileDir = fileDir, None
  400. if fileDir is None:
  401. fileDir = 'tmp.jpg' # specific fileDir to send gifs
  402. else:
  403. return ReturnValue({'BaseResponse': {
  404. 'ErrMsg': 'Either fileDir or file_ should be specific',
  405. 'Ret': -1005, }})
  406. if toUserName is None:
  407. toUserName = self.storageClass.userName
  408. if mediaId is None:
  409. r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif', file_=file_)
  410. if r:
  411. mediaId = r['MediaId']
  412. else:
  413. return r
  414. url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url']
  415. data = {
  416. 'BaseRequest': self.loginInfo['BaseRequest'],
  417. 'Msg': {
  418. 'Type': 3,
  419. 'MediaId': mediaId,
  420. 'FromUserName': self.storageClass.userName,
  421. 'ToUserName': toUserName,
  422. 'LocalID': int(time.time() * 1e4),
  423. 'ClientMsgId': int(time.time() * 1e4), },
  424. 'Scene': 0, }
  425. if fileDir[-4:] == '.gif':
  426. url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url']
  427. data['Msg']['Type'] = 47
  428. data['Msg']['EmojiFlag'] = 2
  429. headers = {
  430. 'User-Agent': config.USER_AGENT,
  431. 'Content-Type': 'application/json;charset=UTF-8', }
  432. r = self.s.post(url, headers=headers,
  433. data=json.dumps(data, ensure_ascii=False).encode('utf8'))
  434. return ReturnValue(rawResponse=r)
  435. async def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
  436. logger.debug('Request to send a video(mediaId: %s) to %s: %s' % (
  437. mediaId, toUserName, fileDir))
  438. if fileDir or file_:
  439. if hasattr(fileDir, 'read'):
  440. file_, fileDir = fileDir, None
  441. if fileDir is None:
  442. fileDir = 'tmp.mp4' # specific fileDir to send other formats
  443. else:
  444. return ReturnValue({'BaseResponse': {
  445. 'ErrMsg': 'Either fileDir or file_ should be specific',
  446. 'Ret': -1005, }})
  447. if toUserName is None:
  448. toUserName = self.storageClass.userName
  449. if mediaId is None:
  450. r = self.upload_file(fileDir, isVideo=True, file_=file_)
  451. if r:
  452. mediaId = r['MediaId']
  453. else:
  454. return r
  455. url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % (
  456. self.loginInfo['url'], self.loginInfo['pass_ticket'])
  457. data = {
  458. 'BaseRequest': self.loginInfo['BaseRequest'],
  459. 'Msg': {
  460. 'Type' : 43,
  461. 'MediaId' : mediaId,
  462. 'FromUserName' : self.storageClass.userName,
  463. 'ToUserName' : toUserName,
  464. 'LocalID' : int(time.time() * 1e4),
  465. 'ClientMsgId' : int(time.time() * 1e4), },
  466. 'Scene': 0, }
  467. headers = {
  468. 'User-Agent' : config.USER_AGENT,
  469. 'Content-Type': 'application/json;charset=UTF-8', }
  470. r = self.s.post(url, headers=headers,
  471. data=json.dumps(data, ensure_ascii=False).encode('utf8'))
  472. return ReturnValue(rawResponse=r)
  473. async def send(self, msg, toUserName=None, mediaId=None):
  474. if not msg:
  475. r = ReturnValue({'BaseResponse': {
  476. 'ErrMsg': 'No message.',
  477. 'Ret': -1005, }})
  478. elif msg[:5] == '@fil@':
  479. if mediaId is None:
  480. r = await self.send_file(msg[5:], toUserName)
  481. else:
  482. r = await self.send_file(msg[5:], toUserName, mediaId)
  483. elif msg[:5] == '@img@':
  484. if mediaId is None:
  485. r = await self.send_image(msg[5:], toUserName)
  486. else:
  487. r = await self.send_image(msg[5:], toUserName, mediaId)
  488. elif msg[:5] == '@msg@':
  489. r = await self.send_msg(msg[5:], toUserName)
  490. elif msg[:5] == '@vid@':
  491. if mediaId is None:
  492. r = await self.send_video(msg[5:], toUserName)
  493. else:
  494. r = await self.send_video(msg[5:], toUserName, mediaId)
  495. else:
  496. r = await self.send_msg(msg, toUserName)
  497. return r
  498. async def revoke(self, msgId, toUserName, localId=None):
  499. url = '%s/webwxrevokemsg' % self.loginInfo['url']
  500. data = {
  501. 'BaseRequest': self.loginInfo['BaseRequest'],
  502. "ClientMsgId": localId or str(time.time() * 1e3),
  503. "SvrMsgId": msgId,
  504. "ToUserName": toUserName}
  505. headers = {
  506. 'ContentType': 'application/json; charset=UTF-8',
  507. 'User-Agent' : config.USER_AGENT }
  508. r = self.s.post(url, headers=headers,
  509. data=json.dumps(data, ensure_ascii=False).encode('utf8'))
  510. return ReturnValue(rawResponse=r)