529 lines
22KB

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