您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

1136 行
48KB

  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
  10. import uuid
  11. from fastapi import FastAPI, Depends
  12. from common.singleton import singleton
  13. from common.log import logger
  14. from model.models import AddGroupContactsHistory
  15. #@singleton
  16. class GeWeService:
  17. _instance = None
  18. _lock = asyncio.Lock() # 异步锁,确保单例初始化线程安全
  19. def __init__(self,app:FastAPI, base_url: str):
  20. if GeWeService._instance is not None:
  21. raise RuntimeError("请使用 get_instance() 获取单例!")
  22. self.base_url = base_url
  23. self.redis_service=app.state.redis_service
  24. @classmethod
  25. async def get_instance(cls, app:FastAPI,base_url: str = "http://api.geweapi.com/gewe"):
  26. """
  27. 获取 GeWeChatCom 单例,确保只初始化一次。
  28. """
  29. async with cls._lock: # 确保多个协程不会并发创建多个实例
  30. if cls._instance is None:
  31. cls._instance = cls(app,base_url)
  32. return cls._instance
  33. # _instance = None
  34. # def __new__(cls,app:FastAPI,base_url="http://api.geweapi.com/gewe"):
  35. # if not cls._instance:
  36. # cls._instance = super(GeWeService, cls).__new__(cls)
  37. # cls._instance.client = None
  38. # cls._instance.lock_renewal_thread = None
  39. # return cls._instance
  40. # def __init__(self,app:FastAPI,base_url="http://api.geweapi.com/gewe"):
  41. # if not hasattr(self, 'initialized'):
  42. # #self.kafka_service =kafka_service # 获取 KafkaService 单例
  43. # # self.kafka_service =app.state.kafka_service
  44. # self.redis_service=app.state.redis_service
  45. # self.base_url = base_url
  46. # self.initialized = True
  47. ############################### 登录模块 ###############################
  48. async def check_login_async(self, token_id: str, app_id: str, uuid: str, captch_code: str = ""):
  49. """
  50. 执行登录(步骤3)
  51. 获取到登录二维码后需每间隔5s调用本接口来判断是否登录成功。
  52. """
  53. api_url = f"{self.base_url}/v2/api/login/checkLogin"
  54. headers = {
  55. 'X-GEWE-TOKEN': token_id,
  56. 'Content-Type': 'application/json'
  57. }
  58. data = {"appId": app_id, "uuid": uuid}
  59. if captch_code:
  60. data["captchCode"] = captch_code
  61. async with aiohttp.ClientSession() as session:
  62. async with session.post(api_url, headers=headers, json=data) as response:
  63. response_object = await response.json()
  64. logger.info(f'登录验证码-请求参数 {json.dumps(data)}')
  65. logger.info(f'登录验证码-返回响应 {response_object}')
  66. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  67. async def qrCallback(self, uuid: str, base64_string: str):
  68. """
  69. 处理二维码回调,显示二维码图片,并提供多种二维码访问链接
  70. """
  71. try:
  72. from PIL import Image
  73. base64_string = base64_string.split(',')[1]
  74. img_data = base64.b64decode(base64_string)
  75. img = Image.open(io.BytesIO(img_data))
  76. _thread = threading.Thread(target=img.show, args=("QRCode",))
  77. _thread.setDaemon(True)
  78. _thread.start()
  79. except Exception as e:
  80. logger.error(f"处理二维码回调时发生错误: {e}")
  81. url = f"http://weixin.qq.com/x/{uuid}"
  82. qr_api1 = f"https://api.isoyu.com/qr/?m=1&e=L&p=20&url={url}"
  83. qr_api2 = f"https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={url}"
  84. qr_api3 = f"https://api.pwmqr.com/qrcode/create/?url={url}"
  85. qr_api4 = f"https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={url}"
  86. logger.info("您也可以通过以下网站扫描二维码:")
  87. logger.info(f"{qr_api3}\n{qr_api4}\n{qr_api2}\n{qr_api1}")
  88. return [qr_api1, qr_api2, qr_api3, qr_api4]
  89. async def get_login_qr_code_async(self, token_id: str, app_id: str = "", region_id: str = "440000"):
  90. """
  91. 获取登录二维码(步骤2)
  92. 第一次登录时传空,后续登录时需传appId。
  93. """
  94. api_url = f"{self.base_url}/v2/api/login/getLoginQrCode"
  95. headers = {
  96. 'X-GEWE-TOKEN': token_id,
  97. 'Content-Type': 'application/json'
  98. }
  99. data = {
  100. "appId": app_id,
  101. "regionId": region_id
  102. }
  103. async with aiohttp.ClientSession() as session:
  104. async with session.post(api_url, headers=headers, json=data) as response:
  105. response_data = await response.json()
  106. logger.info(f'{token_id} 的登录APP信息:{json.dumps(response_data, separators=(",", ":"), ensure_ascii=False)}')
  107. return response_data.get('data')
  108. ############################### 账号管理 ###############################
  109. async def reconnection(self, token_id, app_id):
  110. '''
  111. 断线重连
  112. 当系统返回账号已离线,但是手机顶部还显示ipad在线,可用此接口尝试重连,若返回错误/失败则必须重新调用步骤一登录
  113. 本接口非常用接口,可忽略
  114. '''
  115. api_url = f"{self.base_url}/v2/api/login/reconnection"
  116. headers = {
  117. 'X-GEWE-TOKEN': token_id,
  118. 'Content-Type': 'application/json'
  119. }
  120. data = {
  121. "appId": app_id
  122. }
  123. async with aiohttp.ClientSession() as session:
  124. async with session.post(api_url, headers=headers, json=data) as response:
  125. response_object = await response.json()
  126. return response_object
  127. async def logout_async(self, token_id, app_id):
  128. '''
  129. 退出
  130. '''
  131. api_url = f"{self.base_url}/v2/api/login/logout"
  132. headers = {
  133. 'X-GEWE-TOKEN': token_id,
  134. 'Content-Type': 'application/json'
  135. }
  136. data = {
  137. "appId": app_id
  138. }
  139. async with aiohttp.ClientSession() as session:
  140. async with session.post(api_url, headers=headers, json=data) as response:
  141. response_data = await response.json()
  142. return response_data.get('data')
  143. async def check_online(self, token_id, app_id):
  144. '''
  145. 检查是否在线
  146. 响应结果的data=true则是在线,反之为离线
  147. '''
  148. api_url = f"{self.base_url}/v2/api/login/checkOnline"
  149. headers = {
  150. 'X-GEWE-TOKEN': token_id,
  151. 'Content-Type': 'application/json'
  152. }
  153. data = {
  154. "appId": app_id
  155. }
  156. async with aiohttp.ClientSession() as session:
  157. async with session.post(api_url, headers=headers, json=data) as response:
  158. response_data = await response.json()
  159. return response_data.get('data')
  160. ############################### 联系人模块 ###############################
  161. async def fetch_contacts_list_async(self, token_id, app_id):
  162. '''
  163. 获取通讯录列表
  164. 本接口为长耗时接口,耗时时间根据好友数量递增,若接口返回超时可通过获取通讯录列表缓存接口获取响应结果
  165. 本接口返回的群聊仅为保存到通讯录中的群聊,若想获取会话列表中的所有群聊,需要通过消息订阅做二次处理。
  166. 原因:当未获取的群有成员在群内发消息的话会有消息回调, 开发者此刻调用获取群详情接口查询群信息入库保存即可,
  167. 比如说手机上三年前不说话的群,侧滑删除了,用户手机上也不会看到被删除的群聊的 ,但是有群成员说了话他会显示,
  168. 原理就是各个终端(Android、IOS、桌面版微信)取得了消息回调,又去获取群详情信息,本地数据库缓存了下来,显示的手机群聊,让用户感知的。
  169. '''
  170. api_url = f"{self.base_url}/v2/api/contacts/fetchContactsList"
  171. headers = {
  172. 'X-GEWE-TOKEN': token_id,
  173. 'Content-Type': 'application/json'
  174. }
  175. data = {
  176. "appId": app_id
  177. }
  178. async with aiohttp.ClientSession() as session:
  179. async with session.post(api_url, headers=headers, json=data) as response:
  180. response_object = await response.json()
  181. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  182. async def fetch_contacts_list_cache_async(self, token_id, app_id):
  183. '''
  184. 获取通讯录列表缓存
  185. 通讯录列表数据缓存10分钟,超时则需要重新调用获取通讯录列表接口
  186. '''
  187. api_url = f"{self.base_url}/v2/api/contacts/fetchContactsListCache"
  188. headers = {
  189. 'X-GEWE-TOKEN': token_id,
  190. 'Content-Type': 'application/json'
  191. }
  192. data = {
  193. "appId": app_id
  194. }
  195. async with aiohttp.ClientSession() as session:
  196. async with session.post(api_url, headers=headers, json=data) as response:
  197. response_data = await response.json()
  198. return response_data.get('data')
  199. async def get_brief_info_async(self, token_id, app_id, wxids: list):
  200. '''
  201. 获取群/好友简要信息
  202. 1<= wxids <=100
  203. '''
  204. api_url = f"{self.base_url}/v2/api/contacts/getBriefInfo"
  205. headers = {
  206. 'X-GEWE-TOKEN': token_id,
  207. 'Content-Type': 'application/json'
  208. }
  209. data = {
  210. "appId": app_id,
  211. "wxids": wxids # list 1<= wxids <=100
  212. }
  213. async with aiohttp.ClientSession() as session:
  214. async with session.post(api_url, headers=headers, json=data) as response:
  215. response_data = await response.json()
  216. return response_data.get('data')
  217. async def get_detail_info(self, token_id, app_id, wxids):
  218. '''
  219. 获取群/好友详细信息
  220. 1<= wxids <=20
  221. '''
  222. api_url = f"{self.base_url}/v2/api/contacts/getDetailInfo"
  223. headers = {
  224. 'X-GEWE-TOKEN': token_id,
  225. 'Content-Type': 'application/json'
  226. }
  227. data = {
  228. "appId": app_id,
  229. "wxids": wxids # list 1<= wxids <=20
  230. }
  231. async with aiohttp.ClientSession() as session:
  232. async with session.post(api_url, headers=headers, json=data) as response:
  233. response_data = await response.json()
  234. return response_data.get('data')
  235. async def delete_friend_async(self, token_id, app_id, friend_wxid):
  236. '''
  237. 删除好友
  238. '''
  239. api_url = f"{self.base_url}/v2/api/contacts/deleteFriend"
  240. headers = {
  241. 'X-GEWE-TOKEN': token_id,
  242. 'Content-Type': 'application/json'
  243. }
  244. data = {
  245. "appId": app_id,
  246. "wxid": friend_wxid
  247. }
  248. async with aiohttp.ClientSession() as session:
  249. async with session.post(api_url, headers=headers, json=data) as response:
  250. response_object = await response.json()
  251. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  252. async def set_friend_remark(self, token_id, app_id, friend_wxid, remark):
  253. '''
  254. 设置好友备注
  255. '''
  256. api_url = f"{self.base_url}/v2/api/contacts/setFriendRemark"
  257. headers = {
  258. 'X-GEWE-TOKEN': token_id,
  259. 'Content-Type': 'application/json'
  260. }
  261. data = {
  262. "appId": app_id,
  263. "wxid": friend_wxid,
  264. "remark": remark
  265. }
  266. async with aiohttp.ClientSession() as session:
  267. async with session.post(api_url, headers=headers, json=data) as response:
  268. response_object = await response.json()
  269. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  270. ############################### 消息模块 ###############################
  271. async def post_text_async(self, token_id, app_id, to_wxid, content):
  272. api_url = f"{self.base_url}/v2/api/message/postText"
  273. headers = {
  274. 'X-GEWE-TOKEN': token_id,
  275. 'Content-Type': 'application/json'
  276. }
  277. data = {
  278. "appId": app_id,
  279. "toWxid": to_wxid,
  280. "content": content
  281. }
  282. async with aiohttp.ClientSession() as session:
  283. async with session.post(api_url, headers=headers, json=data) as response:
  284. response_object = await response.json()
  285. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  286. async def post_image_async(self, token_id, app_id, to_wxid, img_url):
  287. api_url = f"{self.base_url}/v2/api/message/postImage"
  288. headers = {
  289. 'X-GEWE-TOKEN': token_id,
  290. 'Content-Type': 'application/json'
  291. }
  292. data = {
  293. "appId": app_id,
  294. "toWxid": to_wxid,
  295. "imgUrl": img_url
  296. }
  297. async with aiohttp.ClientSession() as session:
  298. async with session.post(api_url, headers= headers, json=data) as response:
  299. response_object = await response.json()
  300. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  301. async def post_voice_async(self, token_id, app_id, to_wxid, voice_url, voice_duration):
  302. api_url = f"{self.base_url}/v2/api/message/postVoice"
  303. headers = {
  304. 'X-GEWE-TOKEN': token_id,
  305. 'Content-Type': 'application/json'
  306. }
  307. data = {
  308. "appId": app_id,
  309. "toWxid": to_wxid,
  310. "voiceUrl": voice_url,
  311. "voiceDuration": voice_duration
  312. }
  313. async with aiohttp.ClientSession() as session:
  314. async with session.post(api_url, headers=headers, json=data) as response:
  315. response_object = await response.json()
  316. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  317. async def post_video_async(self, token_id, app_id, to_wxid, video_url, video_thumb_url, video_duration):
  318. api_url = f"{self.base_url}/v2/api/message/postVideo"
  319. headers = {
  320. 'X-GEWE-TOKEN': token_id,
  321. 'Content-Type': 'application/json'
  322. }
  323. data = {
  324. "appId": app_id,
  325. "toWxid": to_wxid,
  326. "videoUrl": video_url,
  327. "videoDuration": video_duration,
  328. "thumbUrl": video_thumb_url
  329. }
  330. async with aiohttp.ClientSession() as session:
  331. async with session.post(api_url, headers=headers, json=data) as response:
  332. response_object = await response.json()
  333. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  334. async def forward_image_async(self, token_id, app_id, to_wxid, aeskey, cdnthumburl, cdnthumblength, cdnthumbheight, cdnthumbwidth, length, md5):
  335. api_url = f"{self.base_url}/v2/api/message/forwardImage"
  336. headers = {
  337. 'X-GEWE-TOKEN': token_id,
  338. 'Content-Type': 'application/json'
  339. }
  340. data = {
  341. "appId": app_id,
  342. "toWxid": to_wxid,
  343. "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>"
  344. }
  345. async with aiohttp.ClientSession() as session:
  346. async with session.post(api_url, headers=headers, json=data) as response:
  347. response_object = await response.json()
  348. return response_object.get('data', None), response_object.get('ret', None), response_object.get('msg', None)
  349. async def forward_video_async(self, token_id, app_id, to_wxid, aeskey, cdnvideourl, length):
  350. api_url = f"{self.base_url}/v2/api/message/forwardVideo"
  351. headers = {
  352. 'X-GEWE-TOKEN': token_id,
  353. 'Content-Type': 'application/json'
  354. }
  355. data = {
  356. "appId": app_id,
  357. "toWxid": to_wxid,
  358. "xml": f"<?xml version=\"1.0\"?>\n<msg>\n\t<videomsg aeskey=\"{aeskey}\" cdnvideourl=\"{cdnvideourl}\" cdnthumbaeskey=\"{aeskey}\" cdnthumburl=\"{cdnvideourl}\" length=\"{length}\" playlength=\"7\" 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>"
  359. }
  360. async with aiohttp.ClientSession() as session:
  361. async with session.post(api_url, headers=headers, json=data) as response:
  362. response_object = await response.json()
  363. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  364. async def add_contacts_async(self, token_id: str, app_id: str, scene: int, option: int, v3: str, v4: str, content: str):
  365. api_url = f"{self.base_url}/v2/api/contacts/addContacts"
  366. headers = {
  367. 'X-GEWE-TOKEN': token_id,
  368. 'Content-Type': 'application/json'
  369. }
  370. data = {
  371. "appId": app_id,
  372. "scene": scene,
  373. "option": option,
  374. "v3": v3,
  375. "v4": v4,
  376. "content": content
  377. }
  378. async with aiohttp.ClientSession() as session:
  379. async with session.post(api_url, headers=headers, json=data) as response:
  380. response_object = await response.json()
  381. return response_object.get('ret', None), response_object.get('msg', None)
  382. async def check_relation(self, token_id, app_id, wxids: list):
  383. '''
  384. 检查好友关系
  385. '''
  386. api_url = f"{self.base_url}/v2/api/contacts/checkRelation"
  387. headers = {
  388. 'X-GEWE-TOKEN': token_id,
  389. 'Content-Type': 'application/json'
  390. }
  391. data = {
  392. "appId": app_id,
  393. "wxids": wxids # list 1<= wxids <=20
  394. }
  395. async with aiohttp.ClientSession() as session:
  396. async with session.post(api_url, headers=headers, json=data) as response:
  397. response_object = await response.json()
  398. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  399. ############################### 下载模块 ###############################
  400. async def download_audio_msg_async(self, token_id: str, app_id: str, msg_id: int, xml: str):
  401. data = {
  402. "appId": app_id,
  403. "msgId": msg_id,
  404. "xml": xml
  405. }
  406. headers = {
  407. 'X-GEWE-TOKEN': token_id,
  408. 'Content-Type': 'application/json'
  409. }
  410. url = 'http://api.geweapi.com/gewe/v2/api/message/downloadVoice'
  411. async with aiohttp.ClientSession() as session:
  412. async with session.post(url, json=data, headers=headers) as response:
  413. if response.ok:
  414. data = await response.json()
  415. if data['ret'] == 200:
  416. return data['data']['fileUrl']
  417. else:
  418. return False
  419. else:
  420. return False
  421. async def download_image_msg_async(self, token_id: str, app_id: str, xml: str):
  422. data = {
  423. "appId": app_id,
  424. "type": 2,
  425. "xml": xml
  426. }
  427. headers = {
  428. 'X-GEWE-TOKEN': token_id,
  429. 'Content-Type': 'application/json'
  430. }
  431. async with aiohttp.ClientSession() as session:
  432. async with session.post(f"{self.base_url}/v2/api/message/downloadImage", json=data, headers=headers) as response:
  433. if response.ok:
  434. data = await response.json()
  435. if data['ret'] == 200:
  436. return data['data']['fileUrl']
  437. else:
  438. return False
  439. else:
  440. return False
  441. async def download_audio_file(self, fileUrl: str, file_name: str):
  442. local_filename = f'./silk/{file_name}.silk'
  443. async with aiohttp.ClientSession() as session:
  444. async with session.get(fileUrl) as response:
  445. if response.status == 200:
  446. with open(local_filename, 'wb') as f:
  447. while True:
  448. chunk = await response.content.read(1024)
  449. if not chunk:
  450. break
  451. f.write(chunk)
  452. return local_filename
  453. else:
  454. return None
  455. ############################### 群模块 ###############################
  456. async def get_chatroom_info_async(self, token_id, app_id, chatroom_id):
  457. '''
  458. 获取群信息
  459. '''
  460. api_url = f"{self.base_url}/v2/api/group/getChatroomInfo"
  461. headers = {
  462. 'X-GEWE-TOKEN': token_id,
  463. 'Content-Type': 'application/json'
  464. }
  465. data = {
  466. "appId": app_id,
  467. "chatroomId": chatroom_id
  468. }
  469. async with aiohttp.ClientSession() as session:
  470. async with session.post(api_url, headers=headers, json=data) as response:
  471. response_object = await response.json()
  472. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  473. async def add_group_member_as_friend(self, token_id, app_id, chatroom_id, member_wxid, content):
  474. '''
  475. 添加群成员为好友
  476. '''
  477. api_url = f"{self.base_url}/v2/api/group/addGroupMemberAsFriend"
  478. headers = {
  479. 'X-GEWE-TOKEN': token_id,
  480. 'Content-Type': 'application/json'
  481. }
  482. data = {
  483. "appId": app_id,
  484. "chatroomId": chatroom_id,
  485. "content": content,
  486. "memberWxid": member_wxid,
  487. }
  488. async with aiohttp.ClientSession() as session:
  489. async with session.post(api_url, headers=headers, json=data) as response:
  490. response_object = await response.json()
  491. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  492. async def save_contract_list_async(self, token_id, app_id, chatroom_id, oper_type):
  493. '''
  494. 群保存到通讯录
  495. 操作类型 3保存到通讯录 2从通讯录移除
  496. '''
  497. api_url = f"{self.base_url}/v2/api/group/saveContractList"
  498. headers = {
  499. 'X-GEWE-TOKEN': token_id,
  500. 'Content-Type': 'application/json'
  501. }
  502. data = {
  503. "appId": app_id,
  504. "chatroomId": chatroom_id,
  505. "operType": oper_type
  506. }
  507. async with aiohttp.ClientSession() as session:
  508. async with session.post(api_url, headers=headers, json=data) as response:
  509. response_object = await response.json()
  510. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  511. async def get_group_memberlist_async(self, token_id, app_id, chatroom_id):
  512. '''
  513. 获取群成员列表
  514. '''
  515. api_url = f"{self.base_url}/v2/api/group/getChatroomMemberList"
  516. headers = {
  517. 'X-GEWE-TOKEN': token_id,
  518. 'Content-Type': 'application/json'
  519. }
  520. data = {
  521. "appId": app_id,
  522. "chatroomId": chatroom_id,
  523. }
  524. async with aiohttp.ClientSession() as session:
  525. async with session.post(api_url, headers=headers, json=data) as response:
  526. response_object = await response.json()
  527. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  528. async def agree_join_room_async(self, token_id, app_id, url):
  529. '''
  530. 同意入群
  531. '''
  532. api_url = f"{self.base_url}/v2/api/group/agreeJoinRoom"
  533. headers = {
  534. 'X-GEWE-TOKEN': token_id,
  535. 'Content-Type': 'application/json'
  536. }
  537. data = {
  538. "appId": app_id,
  539. "url": url,
  540. }
  541. async with aiohttp.ClientSession() as session:
  542. async with session.post(api_url, headers=headers, json=data) as response:
  543. response_object = await response.json()
  544. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  545. ############################### 朋友圈模块 ###################################
  546. # 在新设备登录后的1-3天内,您将无法使用朋友圈发布、点赞、评论等功能。在此期间,如果尝试进行这些操作,您将收到来自微信团队的提醒。请注意遵守相关规定。
  547. async def sns_visible_scope(self, token_id, app_id, option):
  548. '''
  549. 朋友圈可见范围 option 可选项
  550. 1:全部
  551. 2:最近半年
  552. 3:最近一个月
  553. 4:最近三天
  554. '''
  555. api_url = f"{self.base_url}/v2/api/sns/snsVisibleScope"
  556. headers = {
  557. 'X-GEWE-TOKEN': token_id,
  558. 'Content-Type': 'application/json'
  559. }
  560. data = {
  561. "appId": app_id,
  562. "option": option,
  563. }
  564. async with aiohttp.ClientSession() as session:
  565. async with session.post(api_url, headers=headers, json=data) as response:
  566. response_object = await response.json()
  567. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  568. async def stranger_visibility_enabled(self, token_id, app_id, enabled: bool):
  569. '''
  570. 是否允许陌生人查看朋友圈
  571. '''
  572. api_url = f"{self.base_url}/v2/api/sns/strangerVisibilityEnabled"
  573. headers = {
  574. 'X-GEWE-TOKEN': token_id,
  575. 'Content-Type': 'application/json'
  576. }
  577. data = {
  578. "appId": app_id,
  579. "enabled": enabled
  580. }
  581. async with aiohttp.ClientSession() as session:
  582. async with session.post(api_url, headers=headers, json=data) as response:
  583. response_object = await response.json()
  584. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  585. async def send_text_sns_async(self, token_id, app_id, content):
  586. '''
  587. 发送文字朋友圈
  588. '''
  589. api_url = f"{self.base_url}/v2/api/sns/sendTextSns"
  590. headers = {
  591. 'X-GEWE-TOKEN': token_id,
  592. 'Content-Type': 'application/json'
  593. }
  594. data = {
  595. "appId": app_id,
  596. "content": content
  597. }
  598. async with aiohttp.ClientSession() as session:
  599. async with session.post(api_url, headers=headers, json=data) as response:
  600. response_object = await response.json()
  601. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  602. async def send_image_sns_async(self, token_id, app_id, content, img_infos: list):
  603. '''
  604. 发送图片朋友圈
  605. '''
  606. api_url = f"{self.base_url}/v2/api/sns/sendImgSns"
  607. headers = {
  608. 'X-GEWE-TOKEN': token_id,
  609. 'Content-Type': 'application/json'
  610. }
  611. data = {
  612. "appId": app_id,
  613. "allowWxIds": [],
  614. "atWxIds": [],
  615. "disableWxIds": [],
  616. "content": content,
  617. "imgInfos": img_infos, # 通过上传朋友圈图片接口获取
  618. "privacy": False
  619. }
  620. async with aiohttp.ClientSession() as session:
  621. async with session.post(api_url, headers=headers, json=data) as response:
  622. response_object = await response.json()
  623. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  624. async def send_video_sns_async(self, token_id, app_id, content: str, video_info: dict):
  625. '''
  626. 发送视频朋友圈
  627. '''
  628. api_url = f"{self.base_url}/v2/api/sns/sendVideoSns"
  629. headers = {
  630. 'X-GEWE-TOKEN': token_id,
  631. 'Content-Type': 'application/json'
  632. }
  633. data = {
  634. "appId": app_id,
  635. "content": content,
  636. "allowWxIds": [],
  637. "atWxIds": [],
  638. "disableWxIds": [],
  639. "videoInfo": video_info,
  640. "privacy": False
  641. }
  642. async with aiohttp.ClientSession() as session:
  643. async with session.post(api_url, headers=headers, json=data) as response:
  644. response_object = await response.json()
  645. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  646. async def upload_sns_image_async(self, token_id, app_id, img_urls: list):
  647. '''
  648. 上传朋友圈图片
  649. '''
  650. api_url = f"{self.base_url}/v2/api/sns/uploadSnsImage"
  651. headers = {
  652. 'X-GEWE-TOKEN': token_id,
  653. 'Content-Type': 'application/json'
  654. }
  655. data = {
  656. "appId": app_id,
  657. "imgUrls": img_urls
  658. }
  659. async with aiohttp.ClientSession() as session:
  660. async with session.post(api_url, headers=headers, json=data) as response:
  661. response_object = await response.json()
  662. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  663. async def upload_sns_video_async(self, token_id, app_id, video_url: str, video_thumb_url: str):
  664. '''
  665. 上传朋友圈视频
  666. '''
  667. api_url = f"{self.base_url}/v2/api/sns/uploadSnsVideo"
  668. headers = {
  669. 'X-GEWE-TOKEN': token_id,
  670. 'Content-Type': 'application/json'
  671. }
  672. data = {
  673. "appId": app_id,
  674. "thumbUrl": video_thumb_url,
  675. "videoUrl": video_url,
  676. }
  677. async with aiohttp.ClientSession() as session:
  678. async with session.post(api_url, headers=headers, json=data) as response:
  679. response_object = await response.json()
  680. return response_object.get('ret', None), response_object.get('msg', None), response_object.get('data', None)
  681. ############################### 其他 ###############################
  682. async def save_session_messages_to_cache_async(self, hash_key,item:object)->list:
  683. '''
  684. 对话列表
  685. '''
  686. messages = await self.redis_service.get_hash(hash_key)
  687. wxid=hash_key.split(':')[-1]
  688. if not messages:
  689. messages=[{"role": "system", "content": ""}]
  690. messages.append(item)
  691. await self.redis_service.set_hash(hash_key, {"data": json.dumps(messages, ensure_ascii=False)}, 600)
  692. else:
  693. messages_str = await self.redis_service.get_hash_field(hash_key, "data")
  694. messages = json.loads(messages_str) if messages_str else []
  695. #判断是否含有图片
  696. last_message = messages[-1]
  697. content = last_message.get("content", [])
  698. if isinstance(content, list) and content:
  699. last_content_type = content[-1].get("type")
  700. if last_content_type == 'image_url':
  701. content.append(item['content'][0])
  702. messages[-1]['content']=content
  703. else:
  704. messages.append(item)
  705. else:
  706. if last_message!= item:
  707. messages.append(item)
  708. await self.redis_service.set_hash(hash_key,{"data":json.dumps(messages,ensure_ascii=False)},600)
  709. return messages
  710. async def get_contacts_brief_from_cache_async(self, wxid)->list:
  711. """
  712. 获取联系人信息保存到 Redis 缓存。
  713. """
  714. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  715. cache_str = await self.redis_service.get_hash_field(hash_key, "data")
  716. return json.loads(cache_str) if cache_str else []
  717. async def save_contacts_brief_to_cache_async(self, token_id, app_id, wxid, contacts_wxids: list)->list:
  718. """
  719. 将联系人信息保存到 Redis 缓存。
  720. """
  721. # Redis 缓存的 key
  722. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  723. # 获取缓存中的数据
  724. cache_str = await self.redis_service.get_hash_field(hash_key, "data")
  725. cache = json.loads(cache_str) if cache_str else []
  726. # 回调处理
  727. if len(contacts_wxids) == 1:
  728. cache_wxids = [f['userName'] for f in cache]
  729. friends_brief = await self.get_brief_info_async(token_id, app_id, contacts_wxids)
  730. if contacts_wxids[0] in cache_wxids:
  731. # 替换已经存在的数据
  732. for i in range(len(cache)):
  733. if cache[i]['userName'] == contacts_wxids[0]:
  734. cache[i] = friends_brief[0]
  735. else:
  736. cache.extend(f for f in friends_brief if f["nickName"])
  737. friends_no_brief_wxid = [f['userName'] for f in friends_brief if not f["nickName"]]
  738. if friends_no_brief_wxid:
  739. detailed_info = self.get_detail_info(token_id, app_id, friends_no_brief_wxid)
  740. cache.extend(detailed_info)
  741. # 分批处理
  742. else:
  743. cache=[]
  744. # 缓存为空,分批处理 contacts_wxids
  745. batch_size = 100
  746. for i in range(0, len(contacts_wxids), batch_size):
  747. batch = contacts_wxids[i:i + batch_size]
  748. friends_brief = await self.get_brief_info_async(token_id, app_id, batch)
  749. cache.extend(f for f in friends_brief if f["nickName"])
  750. friends_no_brief_wxid = [f['userName'] for f in friends_brief if not f["nickName"]]
  751. if friends_no_brief_wxid:
  752. detailed_info = self.get_detail_info(token_id, app_id, friends_no_brief_wxid)
  753. cache.extend(detailed_info)
  754. # 更新缓存
  755. await self.redis_service.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False))
  756. return cache
  757. async def delete_contacts_brief_from_cache_async(self, wxid, contacts_wxids: list):
  758. """
  759. 删除联系人信息保存到 Redis 缓存。
  760. """
  761. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  762. cache_str = await self.redis_service.get_hash_field(hash_key, "data")
  763. cache = json.loads(cache_str) if cache_str else []
  764. # 将 contacts_wxids 转换为集合,提高查找效率
  765. wxids_set = set(contacts_wxids)
  766. # 过滤 cache:保留 userName 不在集合中的对象
  767. filtered_cache_async = [contact for contact in cache if contact["userName"] not in wxids_set]
  768. # # 如果需要原地修改原 cache 列表:
  769. # cache[:] = filtered_cache_async
  770. await self.redis_service.update_hash_field(hash_key, "data", json.dumps(filtered_cache_async, ensure_ascii=False))
  771. async def save_groups_info_to_cache_async(self, token_id, app_id, wxid, chatroom_ids: list)->list:
  772. """
  773. 将群信息保存到 Redis 缓存。
  774. """
  775. # Redis 缓存的 key
  776. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  777. # 获取当前缓存中所有的 chatroom_id
  778. existing_chatrooms = await self.redis_service.get_hash(hash_key)
  779. # 找出需要删除的 chatroom_ids
  780. chatrooms_to_delete = set(existing_chatrooms.keys()) - set(chatroom_ids)
  781. # 删除缓存中不再需要的 chatroom_id 数据
  782. for chatroom_id in chatrooms_to_delete:
  783. await self.redis_service.delete_hash_field(hash_key, chatroom_id)
  784. for chatroom_id in chatroom_ids:
  785. # 获取群信息
  786. ret, msg, data =await self.get_chatroom_info_async(token_id, app_id, chatroom_id)
  787. if ret != 200:
  788. continue
  789. # 更新缓存
  790. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  791. await asyncio.sleep(0.1)
  792. async def save_groups_members_to_cache_async(self, token_id, app_id, wxid, chatroom_ids: list):
  793. """
  794. 将群成员保存到 Redis 缓存。
  795. """
  796. # Redis 缓存的 key
  797. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  798. # 获取当前缓存中所有的 chatroom_id
  799. existing_chatrooms = await self.redis_service.get_hash(hash_key)
  800. # 找出需要删除的 chatroom_ids
  801. chatrooms_to_delete = set(existing_chatrooms.keys()) - set(chatroom_ids)
  802. # 删除缓存中不再需要的 chatroom_id 数据
  803. for chatroom_id in chatrooms_to_delete:
  804. await self.redis_service.delete_hash_field(hash_key, chatroom_id)
  805. for chatroom_id in chatroom_ids:
  806. # 获取群信息
  807. ret, msg, data = await self.get_group_memberlist_async(token_id, app_id, chatroom_id)
  808. if ret != 200:
  809. continue
  810. # 更新缓存
  811. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  812. await asyncio.sleep(0.1)
  813. async def update_group_members_to_cache_async(self, token_id, app_id, wxid, chatroom_id: str):
  814. """
  815. 更新将群信息保存到 Redis 缓存。
  816. """
  817. # Redis 缓存的 key
  818. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  819. # 获取群信息
  820. ret, msg, data = await self.get_group_memberlist_async(token_id, app_id, chatroom_id)
  821. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  822. async def get_group_members_from_cache_async(self, wxid,chatroom_id)->dict:
  823. """
  824. 获取缓存中单个群成员。
  825. """
  826. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  827. cache = await self.redis_service.get_hash_field(hash_key,chatroom_id)
  828. groups=json.loads(cache) if cache else {}
  829. return groups
  830. async def get_groups_members_from_cache_async(self, wxid)->list:
  831. """
  832. 获取缓存中所有群成员。
  833. """
  834. hash_key = f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  835. cache = await self.redis_service.get_hash(hash_key)
  836. groups=[json.loads(v) for v in cache.values()]
  837. return groups
  838. async def get_groups_info_members_from_cache_async(self, wxid)->list:
  839. groups_info_hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  840. groups_info_cache = await self.redis_service.get_hash(groups_info_hash_key)
  841. groups_menbers_hash_key= f"__AI_OPS_WX__:GROUPS_MEMBERS:{wxid}"
  842. groups_menbers_cache = await self.redis_service.get_hash(groups_menbers_hash_key)
  843. groups_info_cache_list=[{"chatroom_id": field, "value": json.loads(value)} for field, value in groups_info_cache.items()]
  844. groups_menbers_cache_list=[{"chatroom_id": field, "value": json.loads(value)} for field, value in groups_menbers_cache.items()]
  845. # 合并逻辑
  846. merged_data = []
  847. # 遍历 group_info
  848. for info in groups_info_cache_list:
  849. chatroom_id = info['chatroom_id']
  850. group_data = info['value']
  851. # 查找对应的 group_members
  852. members = next((m['value'] for m in groups_menbers_cache_list if m['chatroom_id'] == chatroom_id), None)
  853. if members:
  854. # 合并数据
  855. merged_group = {
  856. "nickName": group_data['nickName'],
  857. "chatroomId": group_data['chatroomId'],
  858. "memberList": members['memberList'],
  859. "chatroomOwner": members.get('chatroomOwner', None),
  860. "adminWxid": members.get('adminWxid',None)
  861. }
  862. merged_data.append(merged_group)
  863. return merged_data
  864. async def get_group_info_members_from_cache_async(self, wxid,chatroom_id)->dict:
  865. group_info_cache= await self.get_group_info_from_cache_async(wxid,chatroom_id)
  866. group_menbers_cache= await self.get_group_members_from_cache_async(wxid,chatroom_id)
  867. group_info_members = {
  868. "nickName": group_info_cache['nickName'],
  869. "chatroomId": group_info_cache['chatroomId'],
  870. "memberList": group_menbers_cache['memberList'],
  871. "chatroomOwner": group_menbers_cache['chatroomOwner'],
  872. "adminWxid": group_menbers_cache['adminWxid']
  873. }
  874. return group_info_members
  875. async def update_group_info_to_cache_async(self, token_id, app_id, wxid, chatroom_id: str):
  876. """
  877. 更新将群信息保存到 Redis 缓存。
  878. """
  879. # Redis 缓存的 key
  880. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  881. # 获取群信息
  882. ret, msg, data =await self.get_chatroom_info_async(token_id, app_id, chatroom_id)
  883. await self.redis_service.update_hash_field(hash_key, chatroom_id, json.dumps(data, ensure_ascii=False))
  884. async def get_groups_info_from_cache_async(self, wxid)->list:
  885. """
  886. 获取群信息保存到 Redis 缓存。
  887. """
  888. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  889. cache = await self.redis_service.get_hash(hash_key)
  890. groups=[json.loads(v) for v in cache.values()]
  891. return groups
  892. async def get_group_info_from_cache_async(self, wxid,chatroom_id)->dict:
  893. """
  894. 获取群信息保存到 Redis 缓存。
  895. """
  896. hash_key = f"__AI_OPS_WX__:GROUPS_INFO:{wxid}"
  897. cache = await self.redis_service.get_hash_field(hash_key,chatroom_id)
  898. groups=json.loads(cache) if cache else {}
  899. return groups
  900. async def get_wxchat_config_from_cache_async(self, wxid):
  901. """
  902. 获取配置信息
  903. """
  904. hash_key = f"__AI_OPS_WX__:WXCHAT_CONFIG"
  905. config = await self.redis_service.get_hash_field(hash_key, wxid)
  906. return json.loads(config) if config else {}
  907. async def save_wxchat_config_async(self, wxid, config:dict):
  908. """
  909. 保存配置信息
  910. """
  911. hash_key = f"__AI_OPS_WX__:WXCHAT_CONFIG"
  912. await self.redis_service.update_hash_field(hash_key, wxid, json.dumps(config, ensure_ascii=False))
  913. async def get_login_info_from_cache_async(self,tel):
  914. hash_key = f"__AI_OPS_WX__:LOGININFO:{tel}"
  915. cache = await self.redis_service.get_hash(hash_key)
  916. return cache
  917. async def get_login_info_by_wxid_async(self,wxid:str)->tuple:
  918. cursor = 0
  919. while True:
  920. cursor, login_keys =await self.redis_service.client.scan(cursor, match='__AI_OPS_WX__:LOGININFO:*')
  921. # 批量获取所有键的 hash 数据
  922. for k in login_keys:
  923. r = await self.redis_service.get_hash(k)
  924. if r.get("wxid") == wxid:
  925. return k,r
  926. # 如果游标为 0,则表示扫描完成
  927. if cursor == 0:
  928. break
  929. return None,None
  930. async def get_login_info_by_app_id_async(self,app_id:str)->tuple:
  931. cursor = 0
  932. while True:
  933. cursor, login_keys =await self.redis_service.client.scan(cursor, match='__AI_OPS_WX__:LOGININFO:*')
  934. print(f'login_keys:{login_keys}')
  935. # 批量获取所有键的 hash 数据
  936. for k in login_keys:
  937. r = await self.redis_service.get_hash(k)
  938. print(r)
  939. if r.get("appId") == app_id:
  940. return k,r
  941. # 如果游标为 0,则表示扫描完成
  942. if cursor == 0:
  943. break
  944. return None,None
  945. async def get_token_id_by_app_id_async(self,app_id: str) -> str:
  946. # 使用 SCAN 避免一次性返回所有的匹配键,逐步扫描
  947. cursor = 0
  948. while True:
  949. cursor, login_keys =await self.redis_service.client.scan(cursor, match='__AI_OPS_WX__:LOGININFO:*')
  950. # 批量获取所有键的 hash 数据
  951. for k in login_keys:
  952. r = await self.redis_service.get_hash(k)
  953. if r.get("appId") == app_id:
  954. return r.get("tokenId", "")
  955. # 如果游标为 0,则表示扫描完成
  956. if cursor == 0:
  957. break
  958. return ""
  959. async def save_login_wx_captch_code_to_cache_async(self,token_id,captch_code):
  960. hash_key = f"__AI_OPS_WX__:WXCAPTCHCODE:{token_id}"
  961. await self.redis_service.set_hash(hash_key,{"data":captch_code},30)
  962. async def get_login_wx_captch_code_from_cache_async(self,token_id)->str:
  963. hash_key = f"__AI_OPS_WX__:WXCAPTCHCODE:{token_id}"
  964. r=await self.redis_service.get_hash_field(hash_key,"data")
  965. return r
  966. async def acquire_login_lock_async(self, token_id, expire_time=10):
  967. hash_key = f"__AI_OPS_WX__:LOGINLOCK:{token_id}"
  968. identifier=str(uuid.uuid4())
  969. if await self.redis_service.client.setnx(hash_key, identifier):
  970. await self.redis_service.client.expire(hash_key, expire_time)
  971. return True
  972. return False
  973. async def release_login_lock_async(self, token_id):
  974. hash_key = f"__AI_OPS_WX__:LOGINLOCK:{token_id}"
  975. await self.redis_service.client.delete(hash_key)
  976. async def save_group_add_contacts_history_async(self,wxid,chatroom_id,contact_wxid,history:AddGroupContactsHistory):
  977. '''
  978. 保存群加好友历史
  979. '''
  980. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  981. data_str=await self.redis_service.get_hash_field(hash_key,contact_wxid)
  982. data=json.loads(data_str) if data_str else []
  983. data.append(history.model_dump())
  984. await self.redis_service.update_hash_field(hash_key, contact_wxid, json.dumps(data, ensure_ascii=False))
  985. async def get_group_add_contacts_history_async(self,wxid,chatroom_id,contact_wxid)->list[AddGroupContactsHistory]:
  986. '''
  987. 获取群加好友历史
  988. '''
  989. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  990. data_str=await self.redis_service.get_hash_field(hash_key,contact_wxid)
  991. data=json.loads(data_str) if data_str else []
  992. #TypeAdapter.validate_python(List[Models.AddGroupContactsHistory], data)
  993. return [AddGroupContactsHistory.model_validate(item) for item in data]
  994. async def check_wixd_group_add_contacts_history_async(self,wxid,chatroom_id):
  995. '''
  996. 返回群发送好友达到2次的wxid
  997. '''
  998. hash_key = f"__AI_OPS_WX__:GROUPS_ADD_CONTACT_HISTORY:{wxid}:{chatroom_id}"
  999. cache = await self.redis_service.get_hash(hash_key)
  1000. wxids = [key for key, value in cache.items() if len(json.loads(value)) == 2]
  1001. return wxids
  1002. async def enqueue_to_add_contacts_async(self,wxid,scene:int,v3,v4):
  1003. """
  1004. 入列待添加好友
  1005. """
  1006. hash_key = f'__AI_OPS_WX__:TO_ADD_CONTACTS:{wxid}'
  1007. data_str=json.dumps({"wxid":wxid,"scene":scene,"v3":v3,"v4":v4},ensure_ascii=False)
  1008. await self.redis_service.enqueue(hash_key,data_str)
  1009. async def dequeue_to_add_contacts_async(self,wxid)->dict:
  1010. """
  1011. 出列待添加好友
  1012. """
  1013. hash_key = f'__AI_OPS_WX__:TO_ADD_CONTACTS:{wxid}'
  1014. data=await self.redis_service.dequeue(hash_key)
  1015. return json.loads(data) if data else {}
  1016. # 依赖项:获取 GeWeChatCom 单例
  1017. async def get_gewe_service(app: FastAPI = Depends()) -> GeWeService:
  1018. return app.state.gewe_service