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

contact.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import time, re, io
  2. import json, copy
  3. import logging
  4. from .. import config, utils
  5. from ..components.contact import accept_friend
  6. from ..returnvalues import ReturnValue
  7. from ..storage import contact_change
  8. from ..utils import update_info_dict
  9. logger = logging.getLogger('itchat')
  10. def load_contact(core):
  11. core.update_chatroom = update_chatroom
  12. core.update_friend = update_friend
  13. core.get_contact = get_contact
  14. core.get_friends = get_friends
  15. core.get_chatrooms = get_chatrooms
  16. core.get_mps = get_mps
  17. core.set_alias = set_alias
  18. core.set_pinned = set_pinned
  19. core.accept_friend = accept_friend
  20. core.get_head_img = get_head_img
  21. core.create_chatroom = create_chatroom
  22. core.set_chatroom_name = set_chatroom_name
  23. core.delete_member_from_chatroom = delete_member_from_chatroom
  24. core.add_member_into_chatroom = add_member_into_chatroom
  25. def update_chatroom(self, userName, detailedMember=False):
  26. if not isinstance(userName, list):
  27. userName = [userName]
  28. url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
  29. self.loginInfo['url'], int(time.time()))
  30. headers = {
  31. 'ContentType': 'application/json; charset=UTF-8',
  32. 'User-Agent' : config.USER_AGENT }
  33. data = {
  34. 'BaseRequest': self.loginInfo['BaseRequest'],
  35. 'Count': len(userName),
  36. 'List': [{
  37. 'UserName': u,
  38. 'ChatRoomId': '', } for u in userName], }
  39. chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
  40. ).content.decode('utf8', 'replace')).get('ContactList')
  41. if not chatroomList:
  42. return ReturnValue({'BaseResponse': {
  43. 'ErrMsg': 'No chatroom found',
  44. 'Ret': -1001, }})
  45. if detailedMember:
  46. def get_detailed_member_info(encryChatroomId, memberList):
  47. url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
  48. self.loginInfo['url'], int(time.time()))
  49. headers = {
  50. 'ContentType': 'application/json; charset=UTF-8',
  51. 'User-Agent' : config.USER_AGENT, }
  52. data = {
  53. 'BaseRequest': self.loginInfo['BaseRequest'],
  54. 'Count': len(memberList),
  55. 'List': [{
  56. 'UserName': member['UserName'],
  57. 'EncryChatRoomId': encryChatroomId} \
  58. for member in memberList], }
  59. return json.loads(self.s.post(url, data=json.dumps(data), headers=headers
  60. ).content.decode('utf8', 'replace'))['ContactList']
  61. MAX_GET_NUMBER = 50
  62. for chatroom in chatroomList:
  63. totalMemberList = []
  64. for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)):
  65. memberList = chatroom['MemberList'][i*MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER]
  66. totalMemberList += get_detailed_member_info(chatroom['EncryChatRoomId'], memberList)
  67. chatroom['MemberList'] = totalMemberList
  68. update_local_chatrooms(self, chatroomList)
  69. r = [self.storageClass.search_chatrooms(userName=c['UserName'])
  70. for c in chatroomList]
  71. return r if 1 < len(r) else r[0]
  72. def update_friend(self, userName):
  73. if not isinstance(userName, list):
  74. userName = [userName]
  75. url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
  76. self.loginInfo['url'], int(time.time()))
  77. headers = {
  78. 'ContentType': 'application/json; charset=UTF-8',
  79. 'User-Agent' : config.USER_AGENT }
  80. data = {
  81. 'BaseRequest': self.loginInfo['BaseRequest'],
  82. 'Count': len(userName),
  83. 'List': [{
  84. 'UserName': u,
  85. 'EncryChatRoomId': '', } for u in userName], }
  86. friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
  87. ).content.decode('utf8', 'replace')).get('ContactList')
  88. update_local_friends(self, friendList)
  89. r = [self.storageClass.search_friends(userName=f['UserName'])
  90. for f in friendList]
  91. return r if len(r) != 1 else r[0]
  92. @contact_change
  93. def update_local_chatrooms(core, l):
  94. '''
  95. get a list of chatrooms for updating local chatrooms
  96. return a list of given chatrooms with updated info
  97. '''
  98. for chatroom in l:
  99. # format new chatrooms
  100. utils.emoji_formatter(chatroom, 'NickName')
  101. for member in chatroom['MemberList']:
  102. if 'NickName' in member:
  103. utils.emoji_formatter(member, 'NickName')
  104. if 'DisplayName' in member:
  105. utils.emoji_formatter(member, 'DisplayName')
  106. if 'RemarkName' in member:
  107. utils.emoji_formatter(member, 'RemarkName')
  108. # update it to old chatrooms
  109. oldChatroom = utils.search_dict_list(
  110. core.chatroomList, 'UserName', chatroom['UserName'])
  111. if oldChatroom:
  112. update_info_dict(oldChatroom, chatroom)
  113. # - update other values
  114. memberList = chatroom.get('MemberList', [])
  115. oldMemberList = oldChatroom['MemberList']
  116. if memberList:
  117. for member in memberList:
  118. oldMember = utils.search_dict_list(
  119. oldMemberList, 'UserName', member['UserName'])
  120. if oldMember:
  121. update_info_dict(oldMember, member)
  122. else:
  123. oldMemberList.append(member)
  124. else:
  125. core.chatroomList.append(chatroom)
  126. oldChatroom = utils.search_dict_list(
  127. core.chatroomList, 'UserName', chatroom['UserName'])
  128. # delete useless members
  129. if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \
  130. chatroom['MemberList']:
  131. existsUserNames = [member['UserName'] for member in chatroom['MemberList']]
  132. delList = []
  133. for i, member in enumerate(oldChatroom['MemberList']):
  134. if member['UserName'] not in existsUserNames:
  135. delList.append(i)
  136. delList.sort(reverse=True)
  137. for i in delList:
  138. del oldChatroom['MemberList'][i]
  139. # - update OwnerUin
  140. if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'):
  141. owner = utils.search_dict_list(oldChatroom['MemberList'],
  142. 'UserName', oldChatroom['ChatRoomOwner'])
  143. oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0)
  144. # - update IsAdmin
  145. if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0:
  146. oldChatroom['IsAdmin'] = \
  147. oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin'])
  148. else:
  149. oldChatroom['IsAdmin'] = None
  150. # - update Self
  151. newSelf = utils.search_dict_list(oldChatroom['MemberList'],
  152. 'UserName', core.storageClass.userName)
  153. oldChatroom['Self'] = newSelf or copy.deepcopy(core.loginInfo['User'])
  154. return {
  155. 'Type' : 'System',
  156. 'Text' : [chatroom['UserName'] for chatroom in l],
  157. 'SystemInfo' : 'chatrooms',
  158. 'FromUserName' : core.storageClass.userName,
  159. 'ToUserName' : core.storageClass.userName, }
  160. @contact_change
  161. def update_local_friends(core, l):
  162. '''
  163. get a list of friends or mps for updating local contact
  164. '''
  165. fullList = core.memberList + core.mpList
  166. for friend in l:
  167. if 'NickName' in friend:
  168. utils.emoji_formatter(friend, 'NickName')
  169. if 'DisplayName' in friend:
  170. utils.emoji_formatter(friend, 'DisplayName')
  171. if 'RemarkName' in friend:
  172. utils.emoji_formatter(friend, 'RemarkName')
  173. oldInfoDict = utils.search_dict_list(
  174. fullList, 'UserName', friend['UserName'])
  175. if oldInfoDict is None:
  176. oldInfoDict = copy.deepcopy(friend)
  177. if oldInfoDict['VerifyFlag'] & 8 == 0:
  178. core.memberList.append(oldInfoDict)
  179. else:
  180. core.mpList.append(oldInfoDict)
  181. else:
  182. update_info_dict(oldInfoDict, friend)
  183. @contact_change
  184. def update_local_uin(core, msg):
  185. '''
  186. content contains uins and StatusNotifyUserName contains username
  187. they are in same order, so what I do is to pair them together
  188. I caught an exception in this method while not knowing why
  189. but don't worry, it won't cause any problem
  190. '''
  191. uins = re.search('<username>([^<]*?)<', msg['Content'])
  192. usernameChangedList = []
  193. r = {
  194. 'Type': 'System',
  195. 'Text': usernameChangedList,
  196. 'SystemInfo': 'uins', }
  197. if uins:
  198. uins = uins.group(1).split(',')
  199. usernames = msg['StatusNotifyUserName'].split(',')
  200. if 0 < len(uins) == len(usernames):
  201. for uin, username in zip(uins, usernames):
  202. if not '@' in username: continue
  203. fullContact = core.memberList + core.chatroomList + core.mpList
  204. userDicts = utils.search_dict_list(fullContact,
  205. 'UserName', username)
  206. if userDicts:
  207. if userDicts.get('Uin', 0) == 0:
  208. userDicts['Uin'] = uin
  209. usernameChangedList.append(username)
  210. logger.debug('Uin fetched: %s, %s' % (username, uin))
  211. else:
  212. if userDicts['Uin'] != uin:
  213. logger.debug('Uin changed: %s, %s' % (
  214. userDicts['Uin'], uin))
  215. else:
  216. if '@@' in username:
  217. core.storageClass.updateLock.release()
  218. update_chatroom(core, username)
  219. core.storageClass.updateLock.acquire()
  220. newChatroomDict = utils.search_dict_list(
  221. core.chatroomList, 'UserName', username)
  222. if newChatroomDict is None:
  223. newChatroomDict = utils.struct_friend_info({
  224. 'UserName': username,
  225. 'Uin': uin,
  226. 'Self': copy.deepcopy(core.loginInfo['User'])})
  227. core.chatroomList.append(newChatroomDict)
  228. else:
  229. newChatroomDict['Uin'] = uin
  230. elif '@' in username:
  231. core.storageClass.updateLock.release()
  232. update_friend(core, username)
  233. core.storageClass.updateLock.acquire()
  234. newFriendDict = utils.search_dict_list(
  235. core.memberList, 'UserName', username)
  236. if newFriendDict is None:
  237. newFriendDict = utils.struct_friend_info({
  238. 'UserName': username,
  239. 'Uin': uin, })
  240. core.memberList.append(newFriendDict)
  241. else:
  242. newFriendDict['Uin'] = uin
  243. usernameChangedList.append(username)
  244. logger.debug('Uin fetched: %s, %s' % (username, uin))
  245. else:
  246. logger.debug('Wrong length of uins & usernames: %s, %s' % (
  247. len(uins), len(usernames)))
  248. else:
  249. logger.debug('No uins in 51 message')
  250. logger.debug(msg['Content'])
  251. return r
  252. def get_contact(self, update=False):
  253. if not update:
  254. return utils.contact_deep_copy(self, self.chatroomList)
  255. def _get_contact(seq=0):
  256. url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (self.loginInfo['url'],
  257. int(time.time()), seq, self.loginInfo['skey'])
  258. headers = {
  259. 'ContentType': 'application/json; charset=UTF-8',
  260. 'User-Agent' : config.USER_AGENT, }
  261. try:
  262. r = self.s.get(url, headers=headers)
  263. except:
  264. logger.info('Failed to fetch contact, that may because of the amount of your chatrooms')
  265. for chatroom in self.get_chatrooms():
  266. self.update_chatroom(chatroom['UserName'], detailedMember=True)
  267. return 0, []
  268. j = json.loads(r.content.decode('utf-8', 'replace'))
  269. return j.get('Seq', 0), j.get('MemberList')
  270. seq, memberList = 0, []
  271. while 1:
  272. seq, batchMemberList = _get_contact(seq)
  273. memberList.extend(batchMemberList)
  274. if seq == 0:
  275. break
  276. chatroomList, otherList = [], []
  277. for m in memberList:
  278. if m['Sex'] != 0:
  279. otherList.append(m)
  280. elif '@@' in m['UserName']:
  281. chatroomList.append(m)
  282. elif '@' in m['UserName']:
  283. # mp will be dealt in update_local_friends as well
  284. otherList.append(m)
  285. if chatroomList:
  286. update_local_chatrooms(self, chatroomList)
  287. if otherList:
  288. update_local_friends(self, otherList)
  289. return utils.contact_deep_copy(self, chatroomList)
  290. def get_friends(self, update=False):
  291. if update:
  292. self.get_contact(update=True)
  293. return utils.contact_deep_copy(self, self.memberList)
  294. def get_chatrooms(self, update=False, contactOnly=False):
  295. if contactOnly:
  296. return self.get_contact(update=True)
  297. else:
  298. if update:
  299. self.get_contact(True)
  300. return utils.contact_deep_copy(self, self.chatroomList)
  301. def get_mps(self, update=False):
  302. if update: self.get_contact(update=True)
  303. return utils.contact_deep_copy(self, self.mpList)
  304. def set_alias(self, userName, alias):
  305. oldFriendInfo = utils.search_dict_list(
  306. self.memberList, 'UserName', userName)
  307. if oldFriendInfo is None:
  308. return ReturnValue({'BaseResponse': {
  309. 'Ret': -1001, }})
  310. url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % (
  311. self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket'])
  312. data = {
  313. 'UserName' : userName,
  314. 'CmdId' : 2,
  315. 'RemarkName' : alias,
  316. 'BaseRequest' : self.loginInfo['BaseRequest'], }
  317. headers = { 'User-Agent' : config.USER_AGENT}
  318. r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'),
  319. headers=headers)
  320. r = ReturnValue(rawResponse=r)
  321. if r:
  322. oldFriendInfo['RemarkName'] = alias
  323. return r
  324. def set_pinned(self, userName, isPinned=True):
  325. url = '%s/webwxoplog?pass_ticket=%s' % (
  326. self.loginInfo['url'], self.loginInfo['pass_ticket'])
  327. data = {
  328. 'UserName' : userName,
  329. 'CmdId' : 3,
  330. 'OP' : int(isPinned),
  331. 'BaseRequest' : self.loginInfo['BaseRequest'], }
  332. headers = { 'User-Agent' : config.USER_AGENT}
  333. r = self.s.post(url, json=data, headers=headers)
  334. return ReturnValue(rawResponse=r)
  335. def accept_friend(self, userName, v4= '', autoUpdate=True):
  336. url = f"{self.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={self.loginInfo['pass_ticket']}"
  337. data = {
  338. 'BaseRequest': self.loginInfo['BaseRequest'],
  339. 'Opcode': 3, # 3
  340. 'VerifyUserListSize': 1,
  341. 'VerifyUserList': [{
  342. 'Value': userName,
  343. 'VerifyUserTicket': v4, }],
  344. 'VerifyContent': '',
  345. 'SceneListCount': 1,
  346. 'SceneList': [33],
  347. 'skey': self.loginInfo['skey'], }
  348. headers = {
  349. 'ContentType': 'application/json; charset=UTF-8',
  350. 'User-Agent' : config.USER_AGENT }
  351. r = self.s.post(url, headers=headers,
  352. data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace'))
  353. if autoUpdate:
  354. self.update_friend(userName)
  355. return ReturnValue(rawResponse=r)
  356. def get_head_img(self, userName=None, chatroomUserName=None, picDir=None):
  357. ''' get head image
  358. * if you want to get chatroom header: only set chatroomUserName
  359. * if you want to get friend header: only set userName
  360. * if you want to get chatroom member header: set both
  361. '''
  362. params = {
  363. 'userName': userName or chatroomUserName or self.storageClass.userName,
  364. 'skey': self.loginInfo['skey'],
  365. 'type': 'big', }
  366. url = '%s/webwxgeticon' % self.loginInfo['url']
  367. if chatroomUserName is None:
  368. infoDict = self.storageClass.search_friends(userName=userName)
  369. if infoDict is None:
  370. return ReturnValue({'BaseResponse': {
  371. 'ErrMsg': 'No friend found',
  372. 'Ret': -1001, }})
  373. else:
  374. if userName is None:
  375. url = '%s/webwxgetheadimg' % self.loginInfo['url']
  376. else:
  377. chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName)
  378. if chatroomUserName is None:
  379. return ReturnValue({'BaseResponse': {
  380. 'ErrMsg': 'No chatroom found',
  381. 'Ret': -1001, }})
  382. if 'EncryChatRoomId' in chatroom:
  383. params['chatroomid'] = chatroom['EncryChatRoomId']
  384. params['chatroomid'] = params.get('chatroomid') or chatroom['UserName']
  385. headers = { 'User-Agent' : config.USER_AGENT}
  386. r = self.s.get(url, params=params, stream=True, headers=headers)
  387. tempStorage = io.BytesIO()
  388. for block in r.iter_content(1024):
  389. tempStorage.write(block)
  390. if picDir is None:
  391. return tempStorage.getvalue()
  392. with open(picDir, 'wb') as f:
  393. f.write(tempStorage.getvalue())
  394. tempStorage.seek(0)
  395. return ReturnValue({'BaseResponse': {
  396. 'ErrMsg': 'Successfully downloaded',
  397. 'Ret': 0, },
  398. 'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
  399. def create_chatroom(self, memberList, topic=''):
  400. url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % (
  401. self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time()))
  402. data = {
  403. 'BaseRequest': self.loginInfo['BaseRequest'],
  404. 'MemberCount': len(memberList.split(',')),
  405. 'MemberList': [{'UserName': member} for member in memberList.split(',')],
  406. 'Topic': topic, }
  407. headers = {
  408. 'content-type': 'application/json; charset=UTF-8',
  409. 'User-Agent' : config.USER_AGENT }
  410. r = self.s.post(url, headers=headers,
  411. data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
  412. return ReturnValue(rawResponse=r)
  413. def set_chatroom_name(self, chatroomUserName, name):
  414. url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % (
  415. self.loginInfo['url'], self.loginInfo['pass_ticket'])
  416. data = {
  417. 'BaseRequest': self.loginInfo['BaseRequest'],
  418. 'ChatRoomName': chatroomUserName,
  419. 'NewTopic': name, }
  420. headers = {
  421. 'content-type': 'application/json; charset=UTF-8',
  422. 'User-Agent' : config.USER_AGENT }
  423. r = self.s.post(url, headers=headers,
  424. data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
  425. return ReturnValue(rawResponse=r)
  426. def delete_member_from_chatroom(self, chatroomUserName, memberList):
  427. url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % (
  428. self.loginInfo['url'], self.loginInfo['pass_ticket'])
  429. data = {
  430. 'BaseRequest': self.loginInfo['BaseRequest'],
  431. 'ChatRoomName': chatroomUserName,
  432. 'DelMemberList': ','.join([member['UserName'] for member in memberList]), }
  433. headers = {
  434. 'content-type': 'application/json; charset=UTF-8',
  435. 'User-Agent' : config.USER_AGENT}
  436. r = self.s.post(url, data=json.dumps(data),headers=headers)
  437. return ReturnValue(rawResponse=r)
  438. def add_member_into_chatroom(self, chatroomUserName, memberList,
  439. useInvitation=False):
  440. ''' add or invite member into chatroom
  441. * there are two ways to get members into chatroom: invite or directly add
  442. * but for chatrooms with more than 40 users, you can only use invite
  443. * but don't worry we will auto-force userInvitation for you when necessary
  444. '''
  445. if not useInvitation:
  446. chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName)
  447. if not chatroom: chatroom = self.update_chatroom(chatroomUserName)
  448. if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']:
  449. useInvitation = True
  450. if useInvitation:
  451. fun, memberKeyName = 'invitemember', 'InviteMemberList'
  452. else:
  453. fun, memberKeyName = 'addmember', 'AddMemberList'
  454. url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % (
  455. self.loginInfo['url'], fun, self.loginInfo['pass_ticket'])
  456. params = {
  457. 'BaseRequest' : self.loginInfo['BaseRequest'],
  458. 'ChatRoomName' : chatroomUserName,
  459. memberKeyName : memberList, }
  460. headers = {
  461. 'content-type': 'application/json; charset=UTF-8',
  462. 'User-Agent' : config.USER_AGENT}
  463. r = self.s.post(url, data=json.dumps(params),headers=headers)
  464. return ReturnValue(rawResponse=r)