You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1734 lines
75KB

  1. import aiohttp
  2. import asyncio
  3. import json
  4. import base64
  5. import io
  6. import json
  7. import os
  8. import threading
  9. import time,datetime
  10. import uuid
  11. from fastapi import FastAPI, Depends
  12. from common.singleton import singleton
  13. from typing import Optional
  14. from model.models import WxChatMessage
  15. from aiohttp import ClientError
  16. from json.decoder import JSONDecodeError
  17. from common.log import logger
  18. from common.utils import check_chatroom
  19. from model.models import AddGroupContactsHistory
  20. from services.redis_service import RedisService
  21. #@singleton
  22. class GeWeService:
  23. _instance = None
  24. _lock = asyncio.Lock() # 异步锁,确保单例初始化线程安全
  25. def __init__(self,redis_service:RedisService, base_url: str):
  26. if GeWeService._instance is not None:
  27. raise RuntimeError("请使用 get_instance() 获取单例!")
  28. self.base_url = base_url
  29. self.redis_service=redis_service
  30. @classmethod
  31. async def get_instance(cls, app:FastAPI,base_url: str = "http://api.geweapi.com/gewe"):
  32. """
  33. 获取 GeWeChatCom 单例,确保只初始化一次。
  34. """
  35. async with cls._lock: # 确保多个协程不会并发创建多个实例
  36. if cls._instance is None:
  37. cls._instance = cls(app,base_url)
  38. return cls._instance
  39. ############################### 登录模块 ###############################
  40. async def check_login_async(self, token_id: str, app_id: str, uuid: str, captch_code: str = ""):
  41. """
  42. 执行登录(步骤3)
  43. 获取到登录二维码后需每间隔5s调用本接口来判断是否登录成功。
  44. """
  45. api_url = f"{self.base_url}/v2/api/login/checkLogin"
  46. headers = {
  47. 'X-GEWE-TOKEN': token_id,
  48. 'Content-Type': 'application/json'
  49. }
  50. data = {"appId": app_id, "uuid": uuid}
  51. if captch_code:
  52. data["captchCode"] = captch_code
  53. async with aiohttp.ClientSession() as session:
  54. async with session.post(api_url, headers=headers, json=data) as response:
  55. response_object = await response.json()
  56. logger.info(f'登录验证码-请求参数 {json.dumps(data)}')
  57. logger.info(f'登录验证码-返回响应 {response_object}')
  58. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  59. async def qrCallback(self, uuid: str, base64_string: str):
  60. """
  61. 处理二维码回调,显示二维码图片,并提供多种二维码访问链接
  62. """
  63. try:
  64. from PIL import Image
  65. base64_string = base64_string.split(',')[1]
  66. img_data = base64.b64decode(base64_string)
  67. img = Image.open(io.BytesIO(img_data))
  68. _thread = threading.Thread(target=img.show, args=("QRCode",))
  69. _thread.setDaemon(True)
  70. _thread.start()
  71. except Exception as e:
  72. logger.error(f"处理二维码回调时发生错误: {e}")
  73. url = f"http://weixin.qq.com/x/{uuid}"
  74. qr_api1 = f"https://api.isoyu.com/qr/?m=1&e=L&p=20&url={url}"
  75. qr_api2 = f"https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={url}"
  76. qr_api3 = f"https://api.pwmqr.com/qrcode/create/?url={url}"
  77. qr_api4 = f"https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={url}"
  78. logger.info("您也可以通过以下网站扫描二维码:")
  79. logger.info(f"{qr_api3}\n{qr_api4}\n{qr_api2}\n{qr_api1}")
  80. return [qr_api1, qr_api2, qr_api3, qr_api4]
  81. async def get_login_qr_code_async(self, token_id: str, app_id: str = "", region_id: str = "440000"):
  82. """
  83. 获取登录二维码(步骤2)
  84. 第一次登录时传空,后续登录时需传appId。
  85. """
  86. api_url = f"{self.base_url}/v2/api/login/getLoginQrCode"
  87. headers = {
  88. 'X-GEWE-TOKEN': token_id,
  89. 'Content-Type': 'application/json'
  90. }
  91. data = {
  92. "appId": app_id,
  93. "regionId": region_id
  94. }
  95. async with aiohttp.ClientSession() as session:
  96. async with session.post(api_url, headers=headers, json=data) as response:
  97. response_data = await response.json()
  98. logger.info(f'{token_id} 的登录APP信息:{json.dumps(response_data, separators=(",", ":"), ensure_ascii=False)}')
  99. return response_data.get('data')
  100. ############################### 账号管理 ###############################
  101. async def reconnection(self, token_id, app_id):
  102. '''
  103. 断线重连
  104. 当系统返回账号已离线,但是手机顶部还显示ipad在线,可用此接口尝试重连,若返回错误/失败则必须重新调用步骤一登录
  105. 本接口非常用接口,可忽略
  106. '''
  107. api_url = f"{self.base_url}/v2/api/login/reconnection"
  108. headers = {
  109. 'X-GEWE-TOKEN': token_id,
  110. 'Content-Type': 'application/json'
  111. }
  112. data = {
  113. "appId": app_id
  114. }
  115. async with aiohttp.ClientSession() as session:
  116. async with session.post(api_url, headers=headers, json=data) as response:
  117. response_object = await response.json()
  118. return response_object
  119. async def logout_async(self, token_id, app_id):
  120. '''
  121. 退出
  122. '''
  123. api_url = f"{self.base_url}/v2/api/login/logout"
  124. headers = {
  125. 'X-GEWE-TOKEN': token_id,
  126. 'Content-Type': 'application/json'
  127. }
  128. data = {
  129. "appId": app_id
  130. }
  131. async with aiohttp.ClientSession() as session:
  132. async with session.post(api_url, headers=headers, json=data) as response:
  133. response_data = await response.json()
  134. return response_data.get('data')
  135. async def check_online(self, token_id, app_id):
  136. '''
  137. 检查是否在线
  138. 响应结果的data=true则是在线,反之为离线
  139. '''
  140. api_url = f"{self.base_url}/v2/api/login/checkOnline"
  141. headers = {
  142. 'X-GEWE-TOKEN': token_id,
  143. 'Content-Type': 'application/json'
  144. }
  145. data = {
  146. "appId": app_id
  147. }
  148. async with aiohttp.ClientSession() as session:
  149. async with session.post(api_url, headers=headers, json=data) as response:
  150. response_data = await response.json()
  151. return response_data.get('data')
  152. ############################### 联系人模块 ###############################
  153. async def fetch_contacts_list_async(self, token_id, app_id):
  154. '''
  155. 获取通讯录列表
  156. 本接口为长耗时接口,耗时时间根据好友数量递增,若接口返回超时可通过获取通讯录列表缓存接口获取响应结果
  157. 本接口返回的群聊仅为保存到通讯录中的群聊,若想获取会话列表中的所有群聊,需要通过消息订阅做二次处理。
  158. 原因:当未获取的群有成员在群内发消息的话会有消息回调, 开发者此刻调用获取群详情接口查询群信息入库保存即可,
  159. 比如说手机上三年前不说话的群,侧滑删除了,用户手机上也不会看到被删除的群聊的 ,但是有群成员说了话他会显示,
  160. 原理就是各个终端(Android、IOS、桌面版微信)取得了消息回调,又去获取群详情信息,本地数据库缓存了下来,显示的手机群聊,让用户感知的。
  161. '''
  162. api_url = f"{self.base_url}/v2/api/contacts/fetchContactsList"
  163. headers = {
  164. 'X-GEWE-TOKEN': token_id,
  165. 'Content-Type': 'application/json'
  166. }
  167. data = {
  168. "appId": app_id
  169. }
  170. async with aiohttp.ClientSession() as session:
  171. async with session.post(api_url, headers=headers, json=data) as response:
  172. response_object = await response.json()
  173. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  174. async def fetch_contacts_list_cache_async(self, token_id, app_id):
  175. '''
  176. 获取通讯录列表缓存
  177. 通讯录列表数据缓存10分钟,超时则需要重新调用获取通讯录列表接口
  178. '''
  179. api_url = f"{self.base_url}/v2/api/contacts/fetchContactsListCache"
  180. headers = {
  181. 'X-GEWE-TOKEN': token_id,
  182. 'Content-Type': 'application/json'
  183. }
  184. data = {
  185. "appId": app_id
  186. }
  187. async with aiohttp.ClientSession() as session:
  188. async with session.post(api_url, headers=headers, json=data) as response:
  189. response_data = await response.json()
  190. return response_data.get('data')
  191. async def get_brief_info_async(self, token_id, app_id, wxids: list):
  192. '''
  193. 获取群/好友简要信息
  194. 1<= wxids <=100
  195. '''
  196. api_url = f"{self.base_url}/v2/api/contacts/getBriefInfo"
  197. headers = {
  198. 'X-GEWE-TOKEN': token_id,
  199. 'Content-Type': 'application/json'
  200. }
  201. data = {
  202. "appId": app_id,
  203. "wxids": wxids # list 1<= wxids <=100
  204. }
  205. async with aiohttp.ClientSession() as session:
  206. async with session.post(api_url, headers=headers, json=data) as response:
  207. response_data = await response.json()
  208. return response_data.get('data')
  209. async def get_detail_info_async(self, token_id, app_id, wxids):
  210. '''
  211. 获取群/好友详细信息
  212. 1<= wxids <=20
  213. '''
  214. api_url = f"{self.base_url}/v2/api/contacts/getDetailInfo"
  215. headers = {
  216. 'X-GEWE-TOKEN': token_id,
  217. 'Content-Type': 'application/json'
  218. }
  219. data = {
  220. "appId": app_id,
  221. "wxids": wxids # list 1<= wxids <=20
  222. }
  223. async with aiohttp.ClientSession() as session:
  224. async with session.post(api_url, headers=headers, json=data) as response:
  225. response_data = await response.json()
  226. return response_data.get('data')
  227. async def delete_friend_async(self, token_id, app_id, friend_wxid):
  228. '''
  229. 删除好友
  230. '''
  231. api_url = f"{self.base_url}/v2/api/contacts/deleteFriend"
  232. headers = {
  233. 'X-GEWE-TOKEN': token_id,
  234. 'Content-Type': 'application/json'
  235. }
  236. data = {
  237. "appId": app_id,
  238. "wxid": friend_wxid
  239. }
  240. async with aiohttp.ClientSession() as session:
  241. async with session.post(api_url, headers=headers, json=data) as response:
  242. response_object = await response.json()
  243. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  244. async def set_friend_remark(self, token_id, app_id, friend_wxid, remark):
  245. '''
  246. 设置好友备注
  247. '''
  248. api_url = f"{self.base_url}/v2/api/contacts/setFriendRemark"
  249. headers = {
  250. 'X-GEWE-TOKEN': token_id,
  251. 'Content-Type': 'application/json'
  252. }
  253. data = {
  254. "appId": app_id,
  255. "wxid": friend_wxid,
  256. "remark": remark
  257. }
  258. async with aiohttp.ClientSession() as session:
  259. async with session.post(api_url, headers=headers, json=data) as response:
  260. response_object = await response.json()
  261. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  262. ############################### 消息模块 ###############################
  263. async def post_text_async(self, token_id, app_id, to_wxid, content):
  264. api_url = f"{self.base_url}/v2/api/message/postText"
  265. headers = {
  266. 'X-GEWE-TOKEN': token_id,
  267. 'Content-Type': 'application/json'
  268. }
  269. data = {
  270. "appId": app_id,
  271. "toWxid": to_wxid,
  272. "content": content
  273. }
  274. async with aiohttp.ClientSession() as session:
  275. async with session.post(api_url, headers=headers, json=data) as response:
  276. response_object = await response.json()
  277. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  278. async def post_image_async(self, token_id, app_id, to_wxid, img_url):
  279. api_url = f"{self.base_url}/v2/api/message/postImage"
  280. headers = {
  281. 'X-GEWE-TOKEN': token_id,
  282. 'Content-Type': 'application/json'
  283. }
  284. data = {
  285. "appId": app_id,
  286. "toWxid": to_wxid,
  287. "imgUrl": img_url
  288. }
  289. async with aiohttp.ClientSession() as session:
  290. async with session.post(api_url, headers= headers, json=data) as response:
  291. response_object = await response.json()
  292. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  293. async def post_voice_async(self, token_id, app_id, to_wxid, voice_url, voice_duration):
  294. api_url = f"{self.base_url}/v2/api/message/postVoice"
  295. headers = {
  296. 'X-GEWE-TOKEN': token_id,
  297. 'Content-Type': 'application/json'
  298. }
  299. data = {
  300. "appId": app_id,
  301. "toWxid": to_wxid,
  302. "voiceUrl": voice_url,
  303. "voiceDuration": voice_duration
  304. }
  305. async with aiohttp.ClientSession() as session:
  306. async with session.post(api_url, headers=headers, json=data) as response:
  307. response_object = await response.json()
  308. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  309. async def post_video_async(self, token_id, app_id, to_wxid, video_url, video_thumb_url, video_duration):
  310. api_url = f"{self.base_url}/v2/api/message/postVideo"
  311. headers = {
  312. 'X-GEWE-TOKEN': token_id,
  313. 'Content-Type': 'application/json'
  314. }
  315. data = {
  316. "appId": app_id,
  317. "toWxid": to_wxid,
  318. "videoUrl": video_url,
  319. "videoDuration": video_duration,
  320. "thumbUrl": video_thumb_url
  321. }
  322. async with aiohttp.ClientSession() as session:
  323. async with session.post(api_url, headers=headers, json=data) as response:
  324. response_object = await response.json()
  325. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  326. # 发送文件消息
  327. async def post_file_async(self, token_id, app_id, to_wxid, file_url, file_name):
  328. api_url = f"{self.base_url}/v2/api/message/postFile"
  329. headers = {
  330. 'X-GEWE-TOKEN': token_id,
  331. 'Content-Type': 'application/json'
  332. }
  333. data = {
  334. "appId": app_id,
  335. "toWxid": to_wxid,
  336. "fileUrl": file_url,
  337. "fileName": file_name,
  338. }
  339. async with aiohttp.ClientSession() as session:
  340. async with session.post(api_url, headers=headers, json=data) as response:
  341. response_object = await response.json()
  342. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  343. async def post_mini_app_aysnc(self, token_id, app_id,to_wxid, mini_app_id,display_name,page_path,cover_img_url,title,user_name):
  344. '''
  345. 发送小程序消息
  346. '''
  347. api_url = f"{self.base_url}/v2/api/message/postMiniApp"
  348. headers = {
  349. 'X-GEWE-TOKEN': token_id,
  350. 'Content-Type': 'application/json'
  351. }
  352. data = {
  353. "appId": app_id,
  354. "toWxid": to_wxid,
  355. "miniAppId": mini_app_id,
  356. "displayName": display_name,
  357. "pagePath": page_path,
  358. "coverImgUrl": cover_img_url,
  359. "title": title,
  360. "userName": user_name
  361. }
  362. async with aiohttp.ClientSession() as session:
  363. async with session.post(api_url, headers=headers, json=data) as response:
  364. response_object = await response.json()
  365. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  366. async def post_link_async(self,token_id, app_id,to_wxid, title,desc,link_url,thumb_url):
  367. '''
  368. 发送链接消息
  369. '''
  370. api_url = f"{self.base_url}/v2/api/message/postLink"
  371. headers = {
  372. 'X-GEWE-TOKEN': token_id,
  373. 'Content-Type': 'application/json'
  374. }
  375. data = {
  376. "appId": app_id,
  377. "toWxid": to_wxid,
  378. "title": title,
  379. "desc": desc,
  380. "linkUrl": link_url,
  381. "thumbUrl": thumb_url
  382. }
  383. async with aiohttp.ClientSession() as session:
  384. async with session.post(api_url, headers=headers, json=data) as response:
  385. response_object = await response.json()
  386. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  387. async def post_name_card_async(self,token_id, app_id,to_wxid,nickName,nameCard_wxid):
  388. '''
  389. 发送名片消息
  390. '''
  391. api_url = f"{self.base_url}/v2/api/message/postNameCard"
  392. headers = {
  393. 'X-GEWE-TOKEN': token_id,
  394. 'Content-Type': 'application/json'
  395. }
  396. data = {
  397. "appId": app_id,
  398. "toWxid": to_wxid,
  399. "nickName": nickName,
  400. "nameCardWxid": nameCard_wxid
  401. }
  402. async with aiohttp.ClientSession() as session:
  403. async with session.post(api_url, headers=headers, json=data) as response:
  404. response_object = await response.json()
  405. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  406. # 发送emoji消息
  407. async def post_emoji_async(self,token_id, app_id,to_wxid,emoji_md5,emoji_size):
  408. api_url = f"{self.base_url}/v2/api/message/postEmoji"
  409. headers = {
  410. 'X-GEWE-TOKEN': token_id,
  411. 'Content-Type': 'application/json'
  412. }
  413. data = {
  414. "appId": app_id,
  415. "toWxid": to_wxid,
  416. "emojiMd5": emoji_md5,
  417. "emojiSize": emoji_size
  418. }
  419. async with aiohttp.ClientSession() as session:
  420. async with session.post(api_url, headers=headers, json=data) as response:
  421. response_object = await response.json()
  422. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  423. # 发送appmsg消息
  424. async def post_app_msg_async(self,token_id, app_id,to_wxid,appmsg):
  425. '''
  426. 本接口可用于发送所有包含节点的消息,例如:音乐分享、视频号、引用消息等等
  427. '''
  428. api_url = f"{self.base_url}/v2/api/message/postAppMsg"
  429. headers = {
  430. 'X-GEWE-TOKEN': token_id,
  431. 'Content-Type': 'application/json'
  432. }
  433. data = {
  434. "appId": app_id,
  435. "toWxid": to_wxid,
  436. "appmsg": appmsg
  437. }
  438. async with aiohttp.ClientSession() as session:
  439. async with session.post(api_url, headers=headers, json=data) as response:
  440. response_object = await response.json()
  441. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  442. async def forward_image_async(self, token_id, app_id, to_wxid, aeskey, cdnthumburl, cdnthumblength, cdnthumbheight, cdnthumbwidth, length, md5):
  443. api_url = f"{self.base_url}/v2/api/message/forwardImage"
  444. headers = {
  445. 'X-GEWE-TOKEN': token_id,
  446. 'Content-Type': 'application/json'
  447. }
  448. data = {
  449. "appId": app_id,
  450. "toWxid": to_wxid,
  451. "xml": f"<?xml version=\"1.0\"?>\n<msg>\n\t<img aeskey=\"{aeskey}\" encryver=\"1\" cdnthumbaeskey=\"{aeskey}\" cdnthumburl=\"{cdnthumburl}\" cdnthumblength=\"{cdnthumblength}\" cdnthumbheight=\"{cdnthumbheight}\" cdnthumbwidth=\"{cdnthumbwidth}\" cdnmidheight=\"0\" cdnmidwidth=\"0\" cdnhdheight=\"0\" cdnhdwidth=\"0\" cdnmidimgurl=\"{cdnthumburl}\" length=\"{length}\" md5=\"{md5}\" />\n\t<platform_signature></platform_signature>\n\t<imgdatahash></imgdatahash>\n</msg>"
  452. }
  453. async with aiohttp.ClientSession() as session:
  454. async with session.post(api_url, headers=headers, json=data) as response:
  455. response_object = await response.json()
  456. return response_object.get('data', None), response_object.get('ret', None), response_object.get('msg', None)
  457. async def forward_video_async(self, token_id, app_id, to_wxid, aeskey, cdnvideourl, length,video_duration):
  458. api_url = f"{self.base_url}/v2/api/message/forwardVideo"
  459. headers = {
  460. 'X-GEWE-TOKEN': token_id,
  461. 'Content-Type': 'application/json'
  462. }
  463. data = {
  464. "appId": app_id,
  465. "toWxid": to_wxid,
  466. "xml": f"<?xml version=\"1.0\"?>\n<msg>\n\t<videomsg aeskey=\"{aeskey}\" cdnvideourl=\"{cdnvideourl}\" cdnthumbaeskey=\"{aeskey}\" cdnthumburl=\"{cdnvideourl}\" length=\"{length}\" playlength=\"{video_duration}\" cdnthumblength=\"8192\" cdnthumbwidth=\"135\" cdnthumbheight=\"240\" fromusername=\"zhangchuan2288\" md5=\"8804c121e9db91dd844f7a34035beb88\" newmd5=\"\" isplaceholder=\"0\" rawmd5=\"\" rawlength=\"0\" cdnrawvideourl=\"\" cdnrawvideoaeskey=\"\" overwritenewmsgid=\"0\" originsourcemd5=\"\" isad=\"0\" />\n</msg>"
  467. }
  468. async with aiohttp.ClientSession() as session:
  469. async with session.post(api_url, headers=headers, json=data) as response:
  470. response_object = await response.json()
  471. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  472. # 转发文件
  473. async def forward_file_async(self, token_id, app_id, to_wxid, xml):
  474. api_url = f"{self.base_url}/v2/api/message/forwardFile"
  475. headers = {
  476. 'X-GEWE-TOKEN': token_id,
  477. 'Content-Type': 'application/json'
  478. }
  479. data = {
  480. "appId": app_id,
  481. "toWxid": to_wxid,
  482. "xml": f"<?xml version=\"1.0\"?>\n<msg>\n\t<appmsg appid=\"\" sdkver=\"0\">\n\t\t<title>info.json</title>\n\t\t<des />\n\t\t<action />\n\t\t<type>6</type>\n\t\t<showtype>0</showtype>\n\t\t<soundtype>0</soundtype>\n\t\t<mediatagname />\n\t\t<messageext />\n\t\t<messageaction />\n\t\t<content />\n\t\t<contentattr>0</contentattr>\n\t\t<url />\n\t\t<lowurl />\n\t\t<dataurl />\n\t\t<lowdataurl />\n\t\t<appattach>\n\t\t\t<totallen>63</totallen>\n\t\t\t<attachid>@cdn_3057020100044b304902010002043904752002032f7d6d02046bb5bade02046593760c042433653765306131612d646138622d346662322d383239362d3964343665623766323061370204051400050201000405004c53d900_f46be643aa0dc009ae5fb63bbc73335d_1</attachid>\n\t\t\t<emoticonmd5 />\n\t\t\t<fileext>json</fileext>\n\t\t\t<cdnattachurl>3057020100044b304902010002043904752002032f7d6d02046bb5bade02046593760c042433653765306131612d646138622d346662322d383239362d3964343665623766323061370204051400050201000405004c53d900</cdnattachurl>\n\t\t\t<aeskey>f46be643aa0dc009ae5fb63bbc73335d</aeskey>\n\t\t\t<encryver>0</encryver>\n\t\t\t<overwrite_newmsgid>594239960546299206</overwrite_newmsgid>\n\t\t\t<fileuploadtoken>v1_0bgfyCkUmoZYYyvXys0cCiJdd2R/pKPdD2TNi9IY6FOt+Tvlhp3ijUoupZHzyB2Lp7xYgdVFaUGL4iu3Pm9/YACCt20egPGpT+DKe+VymOzD7tJfsS8YW7JObTbN8eVoFEetU5HSRWTgS/48VVsPZMoDF6Gz1XJDLN/dWRxvzrbOzVGGNvmY4lpXb0kRwXkSxwL+dO4=</fileuploadtoken>\n\t\t</appattach>\n\t\t<extinfo />\n\t\t<sourceusername />\n\t\t<sourcedisplayname />\n\t\t<thumburl />\n\t\t<md5>d16070253eee7173e467dd7237d76f60</md5>\n\t\t<statextstr />\n\t</appmsg>\n\t<fromusername>zhangchuan2288</fromusername>\n\t<scene>0</scene>\n\t<appinfo>\n\t\t<version>1</version>\n\t\t<appname></appname>\n\t</appinfo>\n\t<commenturl></commenturl>\n</msg>"
  483. }
  484. async with aiohttp.ClientSession() as session:
  485. async with session.post(api_url, headers=headers, json=data) as response:
  486. response_object = await response.json()
  487. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  488. # 转发链接
  489. async def forward_link_async(self, token_id, app_id, to_wxid, xml):
  490. api_url = f"{self.base_url}/v2/api/message/forwardLink"
  491. headers = {
  492. 'X-GEWE-TOKEN': token_id,
  493. 'Content-Type': 'application/json'
  494. }
  495. data = {
  496. "appId": app_id,
  497. "toWxid": to_wxid,
  498. "xml": xml
  499. }
  500. async with aiohttp.ClientSession() as session:
  501. async with session.post(api_url, headers, json=data) as response:
  502. response_object = await response.json()
  503. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  504. # 转发小程序
  505. async def forward_mini_app_async(self, token_id, app_id, to_wxid, xml,cover_img_url):
  506. api_url = f"{self.base_url}/v2/api/message/forwardMiniApp"
  507. headers = {
  508. 'X-GEWE-TOKEN': token_id,
  509. 'Content-Type': 'application/json'
  510. }
  511. data = {
  512. "appId": app_id,
  513. "toWxid": to_wxid,
  514. "xml": xml,
  515. "coverImgUrl":cover_img_url
  516. }
  517. async with aiohttp.ClientSession() as session:
  518. async with session.post(api_url, headers=headers, json=data) as response:
  519. response_object = await response.json()
  520. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  521. async def add_contacts_async(self, token_id: str, app_id: str, scene: int, option: int, v3: str, v4: str, content: str):
  522. api_url = f"{self.base_url}/v2/api/contacts/addContacts"
  523. headers = {
  524. 'X-GEWE-TOKEN': token_id,
  525. 'Content-Type': 'application/json'
  526. }
  527. data = {
  528. "appId": app_id,
  529. "scene": scene,
  530. "option": option,
  531. "v3": v3,
  532. "v4": v4,
  533. "content": content
  534. }
  535. async with aiohttp.ClientSession() as session:
  536. async with session.post(api_url, headers=headers, json=data) as response:
  537. response_object = await response.json()
  538. return response_object.get('ret', None), response_object.get('msg', None)
  539. async def check_relation(self, token_id, app_id, wxids: list):
  540. '''
  541. 检查好友关系
  542. '''
  543. api_url = f"{self.base_url}/v2/api/contacts/checkRelation"
  544. headers = {
  545. 'X-GEWE-TOKEN': token_id,
  546. 'Content-Type': 'application/json'
  547. }
  548. data = {
  549. "appId": app_id,
  550. "wxids": wxids # list 1<= wxids <=20
  551. }
  552. async with aiohttp.ClientSession() as session:
  553. async with session.post(api_url, headers=headers, json=data) as response:
  554. response_object = await response.json()
  555. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  556. ############################### 下载模块 ###############################
  557. async def download_audio_msg_async(self, token_id: str, app_id: str, msg_id: int, xml: str):
  558. data = {
  559. "appId": app_id,
  560. "msgId": msg_id,
  561. "xml": xml
  562. }
  563. headers = {
  564. 'X-GEWE-TOKEN': token_id,
  565. 'Content-Type': 'application/json'
  566. }
  567. url = f'{self.base_url}/v2/api/message/downloadVoice'
  568. async with aiohttp.ClientSession() as session:
  569. async with session.post(url, json=data, headers=headers) as response:
  570. if response.ok:
  571. data = await response.json()
  572. if data['ret'] == 200:
  573. return data['data']['fileUrl']
  574. else:
  575. return False
  576. else:
  577. return False
  578. async def download_image_msg_async(self, token_id: str, app_id: str, xml: str):
  579. data = {
  580. "appId": app_id,
  581. "type": 2,
  582. "xml": xml
  583. }
  584. headers = {
  585. 'X-GEWE-TOKEN': token_id,
  586. 'Content-Type': 'application/json'
  587. }
  588. async with aiohttp.ClientSession() as session:
  589. async with session.post(f"{self.base_url}/v2/api/message/downloadImage", json=data, headers=headers) as response:
  590. if response.ok:
  591. data = await response.json()
  592. response_object = await response.json()
  593. if data['ret'] == 200:
  594. return data['data']['fileUrl']
  595. else:
  596. logger.warning(f'下载图片失败 {data.get("ret", None),data.get("msg", None), data.get("data", None)}')
  597. return ""
  598. else:
  599. logger.warning(f'下载图片失败 {response.status} {response.reason}')
  600. return ""
  601. async def download_cdn_msg_async(self, token_id:str,aeskey: str, file_id: str, type: str,total_size:str,suffix:str):
  602. api_url = f"{self.base_url}/v2/api/message/downloadCdn"
  603. data = {
  604. "aeskey": aeskey,
  605. "fileId":file_id,
  606. "type": type, #下载的文件类型 1:高清图片 2:常规图片 3:缩略图 4:视频 5:文件
  607. "totalSize":total_size, #文件大小
  608. "suffix": suffix #下载类型为文件时,传文件的后缀(例:doc)
  609. }
  610. headers = {
  611. 'X-GEWE-TOKEN': token_id,
  612. 'Content-Type': 'application/json'
  613. }
  614. async with aiohttp.ClientSession() as session:
  615. async with session.post(api_url, headers=headers, json=data) as response:
  616. response_object = await response.json()
  617. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  618. async def download_audio_file(self, fileUrl: str, file_name: str):
  619. local_filename = f'./silk/{file_name}.silk'
  620. async with aiohttp.ClientSession() as session:
  621. async with session.get(fileUrl) as response:
  622. if response.status == 200:
  623. with open(local_filename, 'wb') as f:
  624. while True:
  625. chunk = await response.content.read(1024)
  626. if not chunk:
  627. break
  628. f.write(chunk)
  629. return local_filename
  630. else:
  631. return None
  632. ############################### 群模块 ###############################
  633. async def get_chatroom_info_async(self, token_id, app_id, chatroom_id):
  634. '''
  635. 获取群信息
  636. '''
  637. api_url = f"{self.base_url}/v2/api/group/getChatroomInfo"
  638. headers = {
  639. 'X-GEWE-TOKEN': token_id,
  640. 'Content-Type': 'application/json'
  641. }
  642. data = {
  643. "appId": app_id,
  644. "chatroomId": chatroom_id
  645. }
  646. async with aiohttp.ClientSession() as session:
  647. async with session.post(api_url, headers=headers, json=data) as response:
  648. response_object = await response.json()
  649. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  650. async def add_group_member_as_friend_async(self, token_id, app_id, chatroom_id, member_wxid, content):
  651. '''
  652. 添加群成员为好友
  653. '''
  654. api_url = f"{self.base_url}/v2/api/group/addGroupMemberAsFriend"
  655. headers = {
  656. 'X-GEWE-TOKEN': token_id,
  657. 'Content-Type': 'application/json'
  658. }
  659. data = {
  660. "appId": app_id,
  661. "chatroomId": chatroom_id,
  662. "content": content,
  663. "memberWxid": member_wxid,
  664. }
  665. async with aiohttp.ClientSession() as session:
  666. async with session.post(api_url, headers=headers, json=data) as response:
  667. response_object = await response.json()
  668. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  669. async def save_contract_list_async(self, token_id, app_id, chatroom_id, oper_type):
  670. '''
  671. 群保存到通讯录
  672. 操作类型 3保存到通讯录 2从通讯录移除
  673. '''
  674. api_url = f"{self.base_url}/v2/api/group/saveContractList"
  675. headers = {
  676. 'X-GEWE-TOKEN': token_id,
  677. 'Content-Type': 'application/json'
  678. }
  679. data = {
  680. "appId": app_id,
  681. "chatroomId": chatroom_id,
  682. "operType": oper_type
  683. }
  684. async with aiohttp.ClientSession() as session:
  685. async with session.post(api_url, headers=headers, json=data) as response:
  686. response_object = await response.json()
  687. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  688. async def get_group_memberlist_async(self, token_id, app_id, chatroom_id):
  689. '''
  690. 获取群成员列表
  691. '''
  692. api_url = f"{self.base_url}/v2/api/group/getChatroomMemberList"
  693. headers = {
  694. 'X-GEWE-TOKEN': token_id,
  695. 'Content-Type': 'application/json'
  696. }
  697. data = {
  698. "appId": app_id,
  699. "chatroomId": chatroom_id,
  700. }
  701. async with aiohttp.ClientSession() as session:
  702. async with session.post(api_url, headers=headers, json=data) as response:
  703. response_object = await response.json()
  704. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  705. async def agree_join_room_async(self, token_id, app_id, url):
  706. '''
  707. 同意入群
  708. '''
  709. api_url = f"{self.base_url}/v2/api/group/agreeJoinRoom"
  710. headers = {
  711. 'X-GEWE-TOKEN': token_id,
  712. 'Content-Type': 'application/json'
  713. }
  714. data = {
  715. "appId": app_id,
  716. "url": url,
  717. }
  718. async with aiohttp.ClientSession() as session:
  719. async with session.post(api_url, headers=headers, json=data) as response:
  720. response_object = await response.json()
  721. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  722. ############################### 个人模块 ###############################
  723. async def get_profile_async(self, token_id, app_id):
  724. api_url = f"{self.base_url}/v2/api/personal/getProfile"
  725. headers = {
  726. 'X-GEWE-TOKEN': token_id,
  727. 'Content-Type': 'application/json'
  728. }
  729. data = {
  730. "appId": app_id,
  731. }
  732. async with aiohttp.ClientSession() as session:
  733. async with session.post(api_url, headers=headers, json=data) as response:
  734. response_object = await response.json()
  735. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  736. ############################### 朋友圈模块 ###################################
  737. # 在新设备登录后的1-3天内,您将无法使用朋友圈发布、点赞、评论等功能。在此期间,如果尝试进行这些操作,您将收到来自微信团队的提醒。请注意遵守相关规定。
  738. async def sns_visible_scope(self, token_id, app_id, option):
  739. '''
  740. 朋友圈可见范围 option 可选项
  741. 1:全部
  742. 2:最近半年
  743. 3:最近一个月
  744. 4:最近三天
  745. '''
  746. api_url = f"{self.base_url}/v2/api/sns/snsVisibleScope"
  747. headers = {
  748. 'X-GEWE-TOKEN': token_id,
  749. 'Content-Type': 'application/json'
  750. }
  751. data = {
  752. "appId": app_id,
  753. "option": option,
  754. }
  755. async with aiohttp.ClientSession() as session:
  756. async with session.post(api_url, headers=headers, json=data) as response:
  757. response_object = await response.json()
  758. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  759. async def stranger_visibility_enabled(self, token_id, app_id, enabled: bool):
  760. '''
  761. 是否允许陌生人查看朋友圈
  762. '''
  763. api_url = f"{self.base_url}/v2/api/sns/strangerVisibilityEnabled"
  764. headers = {
  765. 'X-GEWE-TOKEN': token_id,
  766. 'Content-Type': 'application/json'
  767. }
  768. data = {
  769. "appId": app_id,
  770. "enabled": enabled
  771. }
  772. async with aiohttp.ClientSession() as session:
  773. async with session.post(api_url, headers=headers, json=data) as response:
  774. response_object = await response.json()
  775. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  776. async def send_text_sns_async(self, token_id, app_id, content):
  777. '''
  778. 发送文字朋友圈
  779. '''
  780. api_url = f"{self.base_url}/v2/api/sns/sendTextSns"
  781. headers = {
  782. 'X-GEWE-TOKEN': token_id,
  783. 'Content-Type': 'application/json'
  784. }
  785. data = {
  786. "appId": app_id,
  787. "content": content
  788. }
  789. async with aiohttp.ClientSession() as session:
  790. async with session.post(api_url, headers=headers, json=data) as response:
  791. response_object = await response.json()
  792. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  793. async def send_image_sns_async(self, token_id, app_id, content, img_infos: list):
  794. '''
  795. 发送图片朋友圈
  796. '''
  797. api_url = f"{self.base_url}/v2/api/sns/sendImgSns"
  798. headers = {
  799. 'X-GEWE-TOKEN': token_id,
  800. 'Content-Type': 'application/json'
  801. }
  802. data = {
  803. "appId": app_id,
  804. "allowWxIds": [],
  805. "atWxIds": [],
  806. "disableWxIds": [],
  807. "content": content,
  808. "imgInfos": img_infos, # 通过上传朋友圈图片接口获取
  809. "privacy": False
  810. }
  811. async with aiohttp.ClientSession() as session:
  812. async with session.post(api_url, headers=headers, json=data) as response:
  813. response_object = await response.json()
  814. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  815. async def upload_sns_image_async(self, token_id, app_id, img_urls: list):
  816. '''
  817. 上传朋友圈图片
  818. '''
  819. api_url = f"{self.base_url}/v2/api/sns/uploadSnsImage"
  820. headers = {
  821. 'X-GEWE-TOKEN': token_id,
  822. 'Content-Type': 'application/json'
  823. }
  824. data = {
  825. "appId": app_id,
  826. "imgUrls": img_urls
  827. }
  828. async with aiohttp.ClientSession() as session:
  829. async with session.post(api_url, headers=headers, json=data) as response:
  830. response_object = await response.json()
  831. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  832. async def upload_send_image_sns_async(self, token_id, app_id, content, img_infos: list):
  833. '''
  834. 上传并发送图片朋友圈
  835. '''
  836. ret, msg, data = await self.upload_sns_image_async(token_id, app_id, img_infos)
  837. if ret != 200:
  838. logger.warning(f'上传图片失败 {ret} {msg} {data}')
  839. return ret, msg, data
  840. ret, msg, data = await self.send_image_sns_async(token_id, app_id, content, data)
  841. if ret != 200:
  842. logger.warning(f'发送图片失败 {ret} {msg} {data}')
  843. return ret, msg, data
  844. async def send_video_sns_async(self, token_id, app_id, content: str, video_info: dict):
  845. '''
  846. 发送视频朋友圈
  847. '''
  848. api_url = f"{self.base_url}/v2/api/sns/sendVideoSns"
  849. headers = {
  850. 'X-GEWE-TOKEN': token_id,
  851. 'Content-Type': 'application/json'
  852. }
  853. data = {
  854. "appId": app_id,
  855. "content": content,
  856. "allowWxIds": [],
  857. "atWxIds": [],
  858. "disableWxIds": [],
  859. "videoInfo": video_info,
  860. "privacy": False
  861. }
  862. async with aiohttp.ClientSession() as session:
  863. async with session.post(api_url, headers=headers, json=data) as response:
  864. response_object = await response.json()
  865. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  866. async def upload_sns_video_async(self, token_id, app_id, video_url: str, video_thumb_url: str):
  867. '''
  868. 上传朋友圈视频
  869. '''
  870. api_url = f"{self.base_url}/v2/api/sns/uploadSnsVideo"
  871. headers = {
  872. 'X-GEWE-TOKEN': token_id,
  873. 'Content-Type': 'application/json'
  874. }
  875. data = {
  876. "appId": app_id,
  877. "thumbUrl": video_thumb_url,
  878. "videoUrl": video_url,
  879. }
  880. async with aiohttp.ClientSession() as session:
  881. async with session.post(api_url, headers=headers, json=data) as response:
  882. response_object = await response.json()
  883. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  884. async def upload_send_video_sns_async(self, token_id, app_id, content: str, video_url: str, video_thumb_url: str):
  885. '''
  886. 上传并发送视频朋友圈
  887. '''
  888. ret, msg, data = self.upload_sns_video_async(token_id, app_id, video_url, video_thumb_url)
  889. if ret != 200:
  890. logger.warning(f'上传视频失败 {ret} {msg} {data}')
  891. return ret, msg, data
  892. ret,msg,data=self.send_video_sns_async(token_id, app_id, content, data)
  893. if ret != 200:
  894. logger.warning(f'发送视频失败 {ret} {msg} {data}')
  895. return ret,msg,data
  896. async def get_sns_list_async(self, token_id, app_id, sns_id: str):
  897. api_url = f"{self.base_url}/v2/api/sns/snsList"
  898. headers = {
  899. 'X-GEWE-TOKEN': token_id,
  900. 'Content-Type': 'application/json'
  901. }
  902. data = {
  903. "appId": app_id,
  904. "snsId": sns_id,
  905. }
  906. async with aiohttp.ClientSession() as session:
  907. async with session.post(api_url, headers=headers, json=data) as response:
  908. response_object = await response.json()
  909. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  910. ############################### 标签模块 ###############################
  911. async def label_add_async(self, token_id, app_id, label_name):
  912. api_url = f"{self.base_url}/v2/api/label/add"
  913. headers = {
  914. 'X-GEWE-TOKEN': token_id,
  915. 'Content-Type': 'application/json'
  916. }
  917. data = {
  918. "appId": app_id,
  919. "labelName": label_name,
  920. }
  921. async with aiohttp.ClientSession() as session:
  922. async with session.post(api_url, headers=headers, json=data) as response:
  923. response_object = await response.json()
  924. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  925. async def label_delete_async(self, token_id, app_id, label_ids: list):
  926. api_url = f"{self.base_url}/v2/api/label/delete"
  927. label_ids_str = ','.join(map(str, label_ids))
  928. headers = {
  929. 'X-GEWE-TOKEN': token_id,
  930. 'Content-Type': 'application/json'
  931. }
  932. data = {
  933. "appId": app_id,
  934. "labelIds": label_ids_str,
  935. }
  936. async with aiohttp.ClientSession() as session:
  937. async with session.post(api_url, headers=headers, json=data) as response:
  938. response_object = await response.json()
  939. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  940. async def label_list_async(self, token_id, app_id):
  941. api_url = f"{self.base_url}/v2/api/label/list"
  942. headers = {
  943. 'X-GEWE-TOKEN': token_id,
  944. 'Content-Type': 'application/json'
  945. }
  946. data = {
  947. "appId": app_id,
  948. }
  949. try:
  950. async with aiohttp.ClientSession() as session:
  951. try:
  952. async with session.post(api_url, headers=headers, json=data) as response:
  953. # 检查响应状态码
  954. if response.status != 200:
  955. return response.status, f"HTTP Error: {response.status}", None
  956. try:
  957. response_object = await response.json()
  958. except JSONDecodeError:
  959. return 501, "Invalid JSON response", None
  960. # 检查返回的数据结构
  961. if not isinstance(response_object, dict):
  962. return 501, "Invalid response format", None
  963. # 返回处理后的数据
  964. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  965. except ClientError as e:
  966. return 501, f"Network error: {str(e)}", None
  967. except Exception as e:
  968. return None, f"Unexpected error: {str(e)}", None
  969. async def label_modify_members_async(self, token_id, app_id,label_ids: list,wx_ids: list):
  970. '''
  971. 注意
  972. 由于好友标签信息存储在用户客户端,因此每次在修改时都需要进行全量修改。举例来说,考虑好友A(wxid_asdfaihp123),该好友已经被标记为标签ID为1和2。
  973. 在添加标签ID为3时,传递的参数如下:labelIds:1,2,3,wxIds:[wxid_asdfaihp123]。这表示要给好友A添加标签ID为3,同时保留已有的标签ID 1和2。
  974. 而在删除标签ID为1时,传递的参数如下:labelIds:2,3 ,wxIds:[wxid_asdfaihp123]。这表示要将好友A的标签ID 1删除,而保留标签ID 2。
  975. '''
  976. api_url = f"{self.base_url}/v2/api/label/modifyMemberList"
  977. label_ids_str = ','.join(map(str, label_ids))
  978. headers = {
  979. 'X-GEWE-TOKEN': token_id,
  980. 'Content-Type': 'application/json'
  981. }
  982. data = {
  983. "appId": app_id,
  984. "wxIds": wx_ids,
  985. "labelIds": label_ids_str
  986. }
  987. async with aiohttp.ClientSession() as session:
  988. async with session.post(api_url, headers=headers, json=data) as response:
  989. response_object = await response.json()
  990. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  991. ############################### 其他 ###############################
  992. async def save_session_messages_to_cache_async(self, hash_key,item:object)->list:
  993. '''
  994. 对话列表
  995. '''
  996. messages = await self.redis_service.get_hash(hash_key)
  997. wxid=hash_key.split(':')[-1]
  998. if not messages:
  999. messages=[{"role": "system", "content": ""}]
  1000. messages.append(item)
  1001. await self.redis_service.set_hash(hash_key, {"data": json.dumps(messages, ensure_ascii=False)}, 600)
  1002. else:
  1003. messages_str = await self.redis_service.get_hash_field(hash_key, "data")
  1004. messages = json.loads(messages_str) if messages_str else []
  1005. #判断是否含有图片
  1006. last_message = messages[-1]
  1007. content = last_message.get("content", [])
  1008. if isinstance(content, list) and content:
  1009. last_content_type = content[-1].get("type")
  1010. if last_content_type == 'image_url':
  1011. content.append(item['content'][0])
  1012. messages[-1]['content']=content
  1013. else:
  1014. messages.append(item)
  1015. else:
  1016. if last_message!= item:
  1017. messages.append(item)
  1018. await self.redis_service.set_hash(hash_key,{"data":json.dumps(messages,ensure_ascii=False)},600)
  1019. return messages
  1020. async def get_contacts_brief_from_cache_async(self, wxid)->list:
  1021. """
  1022. 获取联系人信息保存到 Redis 缓存。
  1023. """
  1024. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  1025. cache_str = await self.redis_service.get_hash_field(hash_key, "data")
  1026. return json.loads(cache_str) if cache_str else []
  1027. async def save_contacts_brief_to_cache_async(self, token_id, app_id, wxid, contacts_wxids: list)->list:
  1028. """
  1029. 将联系人信息保存到 Redis 缓存。
  1030. """
  1031. # Redis 缓存的 key
  1032. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  1033. # 获取缓存中的数据
  1034. cache_str = await self.redis_service.get_hash_field(hash_key, "data")
  1035. cache = json.loads(cache_str) if cache_str else []
  1036. # 回调处理
  1037. if len(contacts_wxids) == 1:
  1038. cache_wxids = [f['userName'] for f in cache]
  1039. friends_brief = await self.get_brief_info_async(token_id, app_id, contacts_wxids)
  1040. if contacts_wxids[0] in cache_wxids:
  1041. # 替换已经存在的数据
  1042. for i in range(len(cache)):
  1043. if cache[i]['userName'] == contacts_wxids[0]:
  1044. cache[i] = friends_brief[0]
  1045. else:
  1046. cache.extend(f for f in friends_brief if f["nickName"])
  1047. friends_no_brief_wxid = [f['userName'] for f in friends_brief if not f["nickName"]]
  1048. if friends_no_brief_wxid:
  1049. detailed_info = await self.get_detail_info_async(token_id, app_id, friends_no_brief_wxid)
  1050. cache.extend(detailed_info)
  1051. # 分批处理
  1052. else:
  1053. cache=[]
  1054. # 缓存为空,分批处理 contacts_wxids
  1055. batch_size = 100
  1056. for i in range(0, len(contacts_wxids), batch_size):
  1057. batch = contacts_wxids[i:i + batch_size]
  1058. friends_brief = await self.get_brief_info_async(token_id, app_id, batch)
  1059. cache.extend(f for f in friends_brief if f["nickName"])
  1060. friends_no_brief_wxid = [f['userName'] for f in friends_brief if not f["nickName"]]
  1061. if friends_no_brief_wxid:
  1062. detailed_info = await self.get_detail_info_async(token_id, app_id, friends_no_brief_wxid)
  1063. cache.extend(detailed_info)
  1064. # 更新缓存
  1065. await self.redis_service.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False))
  1066. return cache
  1067. async def delete_contacts_brief_from_cache_async(self, wxid, contacts_wxids: list):
  1068. """
  1069. 删除联系人信息保存到 Redis 缓存。
  1070. """
  1071. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  1072. cache_str = await self.redis_service.get_hash_field(hash_key, "data")
  1073. cache = json.loads(cache_str) if cache_str else []
  1074. # 将 contacts_wxids 转换为集合,提高查找效率
  1075. wxids_set = set(contacts_wxids)
  1076. # 过滤 cache:保留 userName 不在集合中的对象
  1077. filtered_cache_async = [contact for contact in cache if contact["userName"] not in wxids_set]
  1078. # # 如果需要原地修改原 cache 列表:
  1079. # cache[:] = filtered_cache_async
  1080. await self.redis_service.update_hash_field(hash_key, "data", json.dumps(filtered_cache_async, ensure_ascii=False))
  1081. async def save_groups_info_to_cache_async(self, token_id, app_id, wxid, chatroom_ids: list)->list:
  1082. """
  1083. 将群信息保存到 Redis 缓存。
  1084. """
  1085. # Redis 缓存的 key
  1086. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  1087. # 获取当前缓存中所有的 chatroom_id
  1088. existing_chatrooms = await self.redis_service.get_hash(hash_key)
  1089. # 找出需要删除的 chatroom_ids
  1090. chatrooms_to_delete = set(existing_chatrooms.keys()) - set(chatroom_ids)
  1091. # 删除缓存中不再需要的 chatroom_id 数据
  1092. for chatroom_id in chatrooms_to_delete:
  1093. await self.redis_service.delete_hash_field(hash_key, chatroom_id)
  1094. for chatroom_id in chatroom_ids:
  1095. if not check_chatroom(chatroom_id):
  1096. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_INFO:{wxid}',chatroom_id)
  1097. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}',chatroom_id)
  1098. logger.info(f'{chatroom_id} 不是有效的群,不处理')
  1099. continue
  1100. # 获取群信息
  1101. ret, msg, data =await self.get_chatroom_info_async(token_id, app_id, chatroom_id)
  1102. if ret != 200:
  1103. continue
  1104. # print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
  1105. # print('群信息save_groups_info_to_cache_async')
  1106. # print(data)
  1107. # print(bool(not data.get('memberList', [])))
  1108. # print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
  1109. if not data.get('memberList', []):
  1110. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_INFO:{wxid}',chatroom_id)
  1111. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}',chatroom_id)
  1112. logger.info(f'通过 成员列表为空 清理 {chatroom_id}群成员信息')
  1113. continue
  1114. # 更新缓存
  1115. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  1116. await asyncio.sleep(0.1)
  1117. async def save_groups_members_to_cache_async(self, token_id, app_id, wxid, chatroom_ids: list):
  1118. """
  1119. 将群成员保存到 Redis 缓存。
  1120. """
  1121. # Redis 缓存的 key
  1122. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  1123. # 获取当前缓存中所有的 chatroom_id
  1124. existing_chatrooms = await self.redis_service.get_hash(hash_key)
  1125. # 找出需要删除的 chatroom_ids
  1126. chatrooms_to_delete = set(existing_chatrooms.keys()) - set(chatroom_ids)
  1127. # 删除缓存中不再需要的 chatroom_id 数据
  1128. for chatroom_id in chatrooms_to_delete:
  1129. await self.redis_service.delete_hash_field(hash_key, chatroom_id)
  1130. for chatroom_id in chatroom_ids:
  1131. # 获取群信息
  1132. ret, msg, data = await self.get_group_memberlist_async(token_id, app_id, chatroom_id)
  1133. if ret != 200:
  1134. continue
  1135. # 更新缓存
  1136. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  1137. await asyncio.sleep(0.1)
  1138. async def update_group_members_to_cache_async(self, token_id, app_id, wxid, chatroom_id: str):
  1139. """
  1140. 更新将群信息保存到 Redis 缓存。
  1141. """
  1142. if not check_chatroom(chatroom_id):
  1143. return
  1144. # Redis 缓存的 key
  1145. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  1146. # 获取群信息
  1147. ret, msg, data = await self.get_group_memberlist_async(token_id, app_id, chatroom_id)
  1148. if ret != 200:
  1149. if msg in '获取群成员列表异常:null':
  1150. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_INFO:{wxid}',chatroom_id)
  1151. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}',chatroom_id)
  1152. logger.info(f'通过 [获取群成员列表异常:null] 清理 {chatroom_id}群成员信息')
  1153. return
  1154. else:
  1155. logger.error(f"获取{chatroom_id}群成员信息失败,错误信息:{ret} {msg}")
  1156. return
  1157. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  1158. async def get_group_members_from_cache_async(self, wxid,chatroom_id)->dict:
  1159. """
  1160. 获取缓存中单个群成员。
  1161. """
  1162. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  1163. cache = await self.redis_service.get_hash_field(hash_key,chatroom_id)
  1164. groups=json.loads(cache) if cache else {}
  1165. return groups
  1166. async def get_groups_members_from_cache_async(self, wxid)->list:
  1167. """
  1168. 获取缓存中所有群成员。
  1169. """
  1170. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  1171. cache = await self.redis_service.get_hash(hash_key)
  1172. groups=[json.loads(v) for v in cache.values()]
  1173. return groups
  1174. async def get_groups_info_members_from_cache_async(self, wxid)->list:
  1175. groups_info_hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  1176. groups_info_cache = await self.redis_service.get_hash(groups_info_hash_key)
  1177. groups_menbers_hash_key= f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  1178. groups_menbers_cache = await self.redis_service.get_hash(groups_menbers_hash_key)
  1179. groups_info_cache_list=[{"chatroom_id": field, "value": json.loads(value)} for field, value in groups_info_cache.items()]
  1180. groups_menbers_cache_list=[{"chatroom_id": field, "value": json.loads(value)} for field, value in groups_menbers_cache.items()]
  1181. # 合并逻辑
  1182. merged_data = []
  1183. # 遍历 group_info
  1184. for info in groups_info_cache_list:
  1185. chatroom_id = info['chatroom_id']
  1186. group_data = info['value']
  1187. # 查找对应的 group_members
  1188. members = next((m['value'] for m in groups_menbers_cache_list if m['chatroom_id'] == chatroom_id), None)
  1189. if members:
  1190. # 合并数据
  1191. merged_group = {
  1192. "nickName": group_data['nickName'],
  1193. "chatroomId": group_data['chatroomId'],
  1194. "memberList": members['memberList'],
  1195. "chatroomOwner": members.get('chatroomOwner', None),
  1196. "adminWxid": members.get('adminWxid',None)
  1197. }
  1198. merged_data.append(merged_group)
  1199. return merged_data
  1200. async def get_group_info_members_from_cache_async(self, wxid,chatroom_id)->dict:
  1201. group_info_cache= await self.get_group_info_from_cache_async(wxid,chatroom_id)
  1202. group_menbers_cache= await self.get_group_members_from_cache_async(wxid,chatroom_id)
  1203. group_info_members = {
  1204. "nickName": group_info_cache['nickName'],
  1205. "chatroomId": group_info_cache['chatroomId'],
  1206. "memberList": group_menbers_cache['memberList'],
  1207. "chatroomOwner": group_menbers_cache['chatroomOwner'],
  1208. "adminWxid": group_menbers_cache['adminWxid']
  1209. }
  1210. return group_info_members
  1211. async def update_group_info_to_cache_async(self, token_id, app_id, wxid, chatroom_id: str):
  1212. """
  1213. 更新将群信息保存到 Redis 缓存。
  1214. """
  1215. if not check_chatroom(chatroom_id):
  1216. return
  1217. # Redis 缓存的 key
  1218. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  1219. if not check_chatroom(chatroom_id):
  1220. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_INFO:{wxid}',chatroom_id)
  1221. await self.redis_service.delete_hash_field(f'__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}',chatroom_id)
  1222. return
  1223. # 获取群信息
  1224. ret, msg, data =await self.get_chatroom_info_async(token_id, app_id, chatroom_id)
  1225. if ret != 200:
  1226. logger.error(f"获取{chatroom_id}群成员信息失败,错误信息:{ret} {msg}")
  1227. return
  1228. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  1229. async def get_groups_info_from_cache_async(self, wxid)->list:
  1230. """
  1231. 获取群信息保存到 Redis 缓存。
  1232. """
  1233. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  1234. cache = await self.redis_service.get_hash(hash_key)
  1235. groups=[json.loads(v) for v in cache.values()]
  1236. return groups
  1237. async def get_group_info_from_cache_async(self, wxid,chatroom_id)->dict:
  1238. """
  1239. 获取群信息保存到 Redis 缓存。
  1240. """
  1241. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  1242. cache = await self.redis_service.get_hash_field(hash_key,chatroom_id)
  1243. groups=json.loads(cache) if cache else {}
  1244. return groups
  1245. async def get_wxchat_config_from_cache_async(self, wxid):
  1246. """
  1247. 获取配置信息
  1248. """
  1249. hash_key = f"__AI_OPS_WX__:WXCHAT_CONFIG"
  1250. config = await self.redis_service.get_hash_field(hash_key, wxid)
  1251. return json.loads(config) if config else {}
  1252. async def save_wxchat_config_async(self, wxid, config:dict):
  1253. """
  1254. 保存配置信息
  1255. """
  1256. hash_key = f"__AI_OPS_WX__:WXCHAT_CONFIG"
  1257. await self.redis_service.update_hash_field(hash_key, wxid, json.dumps(config, ensure_ascii=False))
  1258. async def get_global_config_from_cache_async(self):
  1259. """
  1260. 获取配置信息
  1261. """
  1262. hash_key = f"__AI_OPS_WX__:GLOBAL_CONFIG"
  1263. config = await self.redis_service.get_hash_field(hash_key,"data")
  1264. return json.loads(config) if config else {}
  1265. async def save_global_config_async(self, config:dict):
  1266. """
  1267. 保存配置信息
  1268. """
  1269. hash_key = f"__AI_OPS_WX__:GLOBAL_CONFIG"
  1270. await self.redis_service.update_hash_field(hash_key, "data,", json.dumps(config, ensure_ascii=False))
  1271. async def get_login_info_from_cache_async(self,tel):
  1272. hash_key = f"__AI_OPS_WX__:LOGININFO:{tel}"
  1273. cache = await self.redis_service.get_hash(hash_key)
  1274. return cache
  1275. async def get_login_info_by_wxid_async(self,wxid:str)->tuple:
  1276. cursor = 0
  1277. while True:
  1278. cursor, login_keys =await self.redis_service.client.scan(cursor, match='__AI_OPS_WX__:LOGININFO:*')
  1279. # 批量获取所有键的 hash 数据
  1280. for k in login_keys:
  1281. r = await self.redis_service.get_hash(k)
  1282. if r.get("wxid") == wxid:
  1283. return k,r
  1284. # 如果游标为 0,则表示扫描完成
  1285. if cursor == 0:
  1286. break
  1287. return None,None
  1288. async def get_login_info_by_app_id_async(self,app_id:str)->tuple:
  1289. cursor = 0
  1290. while True:
  1291. cursor, login_keys =await self.redis_service.client.scan(cursor, match='__AI_OPS_WX__:LOGININFO:*')
  1292. #print(f'login_keys:{login_keys}')
  1293. # 批量获取所有键的 hash 数据
  1294. for k in login_keys:
  1295. r = await self.redis_service.get_hash(k)
  1296. #print(r)
  1297. if r.get("appId") == app_id:
  1298. return k,r
  1299. # 如果游标为 0,则表示扫描完成
  1300. if cursor == 0:
  1301. break
  1302. return None,None
  1303. async def get_token_id_by_app_id_async(self,app_id: str) -> str:
  1304. # 使用 SCAN 避免一次性返回所有的匹配键,逐步扫描
  1305. cursor = 0
  1306. while True:
  1307. cursor, login_keys =await self.redis_service.client.scan(cursor, match='__AI_OPS_WX__:LOGININFO:*')
  1308. # 批量获取所有键的 hash 数据
  1309. for k in login_keys:
  1310. r = await self.redis_service.get_hash(k)
  1311. if r.get("appId") == app_id:
  1312. return r.get("tokenId", "")
  1313. # 如果游标为 0,则表示扫描完成
  1314. if cursor == 0:
  1315. break
  1316. return ""
  1317. async def save_login_wx_captch_code_to_cache_async(self,tel,captch_code):
  1318. hash_key = f"__AI_OPS_WX__:WXCAPTCHCODE:{tel}"
  1319. await self.redis_service.set_hash(hash_key,{"data":captch_code},30)
  1320. # async def get_login_wx_captch_code_from_cache_async(self,token_id)->str:
  1321. # hash_key = f"__AI_OPS_WX__:WXCAPTCHCODE:{token_id}"
  1322. # r=await self.redis_service.get_hash_field(hash_key,"data")
  1323. # return r
  1324. async def get_login_wx_captch_code_from_cache_async(self,tel)->str:
  1325. hash_key = f"__AI_OPS_WX__:WXCAPTCHCODE:{tel}"
  1326. r=await self.redis_service.get_hash_field(hash_key,"data")
  1327. return r
  1328. async def acquire_login_lock_async(self, token_id, expire_time=10):
  1329. hash_key = f"__AI_OPS_WX__:LOGINLOCK:{token_id}"
  1330. identifier=str(uuid.uuid4())
  1331. if await self.redis_service.client.setnx(hash_key, identifier):
  1332. await self.redis_service.client.expire(hash_key, expire_time)
  1333. return True
  1334. return False
  1335. async def release_login_lock_async(self, token_id):
  1336. hash_key = f"__AI_OPS_WX__:LOGINLOCK:{token_id}"
  1337. await self.redis_service.client.delete(hash_key)
  1338. async def save_group_add_contacts_history_async(self,wxid,chatroom_id,contact_wxid,history:AddGroupContactsHistory):
  1339. '''
  1340. 保存群加好友历史
  1341. '''
  1342. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  1343. data_str=await self.redis_service.get_hash_field(hash_key,contact_wxid)
  1344. data=json.loads(data_str) if data_str else []
  1345. data.append(history.model_dump())
  1346. await self.redis_service.update_hash_field(hash_key, contact_wxid, json.dumps(data, ensure_ascii=False))
  1347. async def get_group_add_contacts_history_async(self,wxid,chatroom_id,contact_wxid)->list[AddGroupContactsHistory]:
  1348. '''
  1349. 获取群加好友历史
  1350. '''
  1351. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  1352. data_str=await self.redis_service.get_hash_field(hash_key,contact_wxid)
  1353. data=json.loads(data_str) if data_str else []
  1354. #TypeAdapter.validate_python(List[Models.AddGroupContactsHistory], data)
  1355. return [AddGroupContactsHistory.model_validate(item) for item in data]
  1356. async def check_wixd_group_add_contacts_history_async(self,wxid,chatroom_id):
  1357. '''
  1358. 返回群发送好友达到2次的wxid
  1359. '''
  1360. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  1361. cache = await self.redis_service.get_hash(hash_key)
  1362. wxids = [key for key, value in cache.items() if len(json.loads(value)) == 2]
  1363. return wxids
  1364. async def is_group_add_contacts_history_one_day_200_async(self, wxid, chatroom_id) -> bool:
  1365. # 生成 hash_key
  1366. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  1367. cache = await self.redis_service.get_hash(hash_key)
  1368. today_list = []
  1369. today = datetime.datetime.now().date()
  1370. for key, value in cache.items():
  1371. value_data_list = json.loads(value)
  1372. for value_data in value_data_list:
  1373. add_time_date = datetime.datetime.fromtimestamp(value_data["addTime"]).date()
  1374. if add_time_date == today:
  1375. today_list.append(value_data)
  1376. if len(today_list) == 200:
  1377. return True
  1378. return False
  1379. async def is_group_add_contacts_history_one_day_90_async(self, wxid) -> bool:
  1380. today_list = []
  1381. today = datetime.datetime.now().date()
  1382. cursor = 0
  1383. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:*"
  1384. while True:
  1385. cursor, history_keys =await self.redis_service.client.scan(cursor, match= hash_key)
  1386. #print(f'login_keys:{login_keys}')
  1387. # 批量获取所有键的 hash 数据
  1388. for k in history_keys:
  1389. cache = await self.redis_service.get_hash(k)
  1390. for key, value in cache.items():
  1391. value_data_list = json.loads(value)
  1392. for value_data in value_data_list:
  1393. add_time_date = datetime.datetime.fromtimestamp(value_data["addTime"]).date()
  1394. if add_time_date == today:
  1395. today_list.append(value_data)
  1396. if len(today_list) == 90:
  1397. return True
  1398. # 如果游标为 0,则表示扫描完成
  1399. if cursor == 0:
  1400. break
  1401. return False
  1402. async def is_group_add_contacts_history_one_day_async(self, wxid,today_total=90) -> bool:
  1403. today_list = []
  1404. today = datetime.datetime.now().date()
  1405. cursor = 0
  1406. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:*"
  1407. while True:
  1408. cursor, history_keys =await self.redis_service.client.scan(cursor, match= hash_key)
  1409. #print(f'login_keys:{login_keys}')
  1410. # 批量获取所有键的 hash 数据
  1411. for k in history_keys:
  1412. cache = await self.redis_service.get_hash(k)
  1413. for key, value in cache.items():
  1414. value_data_list = json.loads(value)
  1415. for value_data in value_data_list:
  1416. add_time_date = datetime.datetime.fromtimestamp(value_data["addTime"]).date()
  1417. if add_time_date == today:
  1418. today_list.append(value_data)
  1419. if len(today_list) == today_total:
  1420. return True
  1421. # 如果游标为 0,则表示扫描完成
  1422. if cursor == 0:
  1423. break
  1424. return False
  1425. async def enqueue_to_add_contacts_async(self,wxid,scene:int,v3,v4):
  1426. """
  1427. 入列待添加好友
  1428. """
  1429. hash_key = f'__AI_OPS_WX__:TO_ADD_CONTACTS:{wxid}'
  1430. data_str=json.dumps({"wxid":wxid,"scene":scene,"v3":v3,"v4":v4},ensure_ascii=False)
  1431. await self.redis_service.enqueue(hash_key,data_str)
  1432. async def dequeue_to_add_contacts_async(self,wxid)->dict:
  1433. """
  1434. 出列待添加好友
  1435. """
  1436. hash_key = f'__AI_OPS_WX__:TO_ADD_CONTACTS:{wxid}'
  1437. data=await self.redis_service.dequeue(hash_key)
  1438. return json.loads(data) if data else {}
  1439. async def enqueue_wxchat_message_async(self,msg:WxChatMessage):
  1440. """
  1441. 入列消息
  1442. """
  1443. hash_key = f'__AI_OPS_WX__:WX_CHAT_MESSAGE:{msg.belongToWxid}'
  1444. data_str=json.dumps(msg.model_dump(),ensure_ascii=False)
  1445. await self.redis_service.enqueue(hash_key,data_str)
  1446. async def dequeue_wxchat_message_async(self,wxid)->Optional[WxChatMessage]:
  1447. """
  1448. 出列消息
  1449. """
  1450. hash_key = f'__AI_OPS_WX__:WX_CHAT_MESSAGE:{wxid}'
  1451. data = await self.redis_service.dequeue(hash_key)
  1452. if not data:
  1453. return None
  1454. try:
  1455. # 将 JSON 数据反序列化为 WxChatMessage 对象
  1456. return WxChatMessage.model_validate(data)
  1457. except Exception as e:
  1458. # 处理反序列化错误
  1459. print(f"Failed to deserialize message: {e}")
  1460. return None
  1461. async def save_task_run_time_async(self,task_name,log:list,expire_time=None):
  1462. '''
  1463. 任务运行锁
  1464. '''
  1465. hash_key = f"__AI_OPS_WX__:TASK_RUN_TIME:{task_name}"
  1466. await self.redis_service.set_hash(hash_key,{"data": json.dumps(log, ensure_ascii=False)}, expire_time)
  1467. async def get_task_run_time_async(self,task_name)->list:
  1468. hash_key = f"__AI_OPS_WX__:TASK_RUN_TIME:{task_name}"
  1469. cache= await self.redis_service.get_hash_field(hash_key,"data")
  1470. return json.loads(cache) if cache else []
  1471. async def save_task_run_time_by_wxid_async(self,wxid,task_name,log:list,expire_time=None):
  1472. '''
  1473. 任务运行锁
  1474. '''
  1475. hash_key = f"__AI_OPS_WX__:TASK_RUN_TIME:{wxid}:{task_name}"
  1476. await self.redis_service.set_hash(hash_key,{"data": json.dumps(log, ensure_ascii=False)}, expire_time)
  1477. async def get_task_run_time_by_wxid_async(self,wxid,task_name)->list:
  1478. hash_key = f"__AI_OPS_WX__:TASK_RUN_TIME:{wxid}:{task_name}"
  1479. cache= await self.redis_service.get_hash_field(hash_key,"data")
  1480. return json.loads(cache) if cache else []
  1481. async def save_wx_expection_async(self,wxid,api_name,exception:str,expire_time=None):
  1482. hash_key = f"__AI_OPS_WX__:WX_EXCEPTION:{wxid}:{api_name}"
  1483. await self.redis_service.set_hash(hash_key,{"data": json.dumps(exception, ensure_ascii=False)}, expire_time)
  1484. async def get_wx_expection_async(self,wxid,api_name:str)->str:
  1485. hash_key = f"__AI_OPS_WX__:WX_EXCEPTION:{wxid}:{api_name}"
  1486. cache= await self.redis_service.get_hash_field(hash_key,"data")
  1487. return json.loads(cache) if cache else ""
  1488. async def wx_add_contacts_from_chatroom_task_status_async(self,wxid,chatroom_id)->int:
  1489. history_hash_key = f'__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}'
  1490. cache=await self.redis_service.get_hash(history_hash_key)
  1491. group=await self. get_group_members_from_cache_async(wxid,chatroom_id)
  1492. chatroom_member_list = group.get('memberList', [])
  1493. chatroom_owner_wxid = group.get('chatroomOwner', None)
  1494. admin_wxids = group.get('adminWxid', [])
  1495. admin_wxids = group.get('adminWxid')
  1496. if admin_wxids is None:
  1497. admin_wxids = []
  1498. if chatroom_owner_wxid:
  1499. admin_wxids.append(chatroom_owner_wxid)
  1500. #unavailable_wixds=set(admin_wxids)
  1501. available_members=[m["wxid"] for m in chatroom_member_list if m["wxid"] not in admin_wxids]
  1502. if len(available_members) > len(cache.keys()):
  1503. return 1
  1504. for key, value in cache.items():
  1505. value_data_list = json.loads(value)
  1506. if len(value_data_list) <2:
  1507. return 1
  1508. return 2
  1509. async def delete_group_add_contacts_history_async(self,wxid,chatroom_id)->dict:
  1510. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  1511. await self.redis_service.delete_hash(hash_key)
  1512. async def is_human_handle_msg_async(self,wxid)->bool:
  1513. hash_key = f"__AI_OPS_WX__:HUMAN_HANDLE_MSG:{wxid}"
  1514. cache= await self.redis_service.get_hash_field(hash_key,"data")
  1515. return True if cache else False
  1516. async def set_human_handle_msg_async(self,wxid,expire_time=60*30):
  1517. hash_key = f"__AI_OPS_WX__:HUMAN_HANDLE_MSG:{wxid}"
  1518. await self.redis_service.set_hash(hash_key,{"data": json.dumps(int(time.time()), ensure_ascii=False)}, expire_time)
  1519. async def is_human_handle_msg_with_contact_wxid_async(self,wxid,contact_wxid)->bool:
  1520. hash_key = f"__AI_OPS_WX__:HUMAN_HANDLE_MSG_WTIH_CONTACT:{wxid}:{contact_wxid}"
  1521. cache= await self.redis_service.get_hash_field(hash_key,"data")
  1522. return True if cache else False
  1523. async def set_human_handle_msg_with_contact_wxid_async(self,wxid,contact_wxid,expire_time=60*30)->bool:
  1524. hash_key = f"__AI_OPS_WX__:HUMAN_HANDLE_MSG_WTIH_CONTACT:{wxid}:{contact_wxid}"
  1525. cache= await self.redis_service.set_hash(hash_key,{"data": json.dumps(int(time.time()), ensure_ascii=False)}, expire_time)
  1526. return True if cache else False
  1527. # 依赖项:获取 GeWeChatCom 单例
  1528. async def get_gewe_service(app: FastAPI = Depends()) -> GeWeService:
  1529. return app.state.gewe_service