Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

471 lines
18KB

  1. import requests
  2. import json
  3. import base64
  4. import io
  5. import json
  6. import os
  7. import threading
  8. import time
  9. import requests
  10. from io import BytesIO
  11. from PIL import Image
  12. from common import redis_helper
  13. wxchat=None
  14. class GeWeChatCom:
  15. def __init__(self, base_url):
  16. self.base_url = base_url
  17. ############################### 登录模块 ###############################
  18. def check_login(self, token_id, app_id, uuid):
  19. '''
  20. 执行登录(步骤3)
  21. 获取到登录二维码后需每间隔5s调用本接口来判断是否登录成功
  22. 新设备登录平台,次日凌晨会掉线一次,重新登录时需调用获取二维码且传appId取码,登录成功后则可以长期在线
  23. 登录成功后请保存appId与wxid的对应关系,后续接口中会用到
  24. '''
  25. api_url = f"{self.base_url}/v2/api/login/checkLogin"
  26. headers = {
  27. 'X-GEWE-TOKEN': token_id,
  28. 'Content-Type': 'application/json'
  29. }
  30. data = {
  31. "appId": app_id,
  32. "uuid": uuid
  33. }
  34. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  35. response_data = response.json()
  36. print(response_data)
  37. return response_data.get('data')
  38. def get_login_qr_code(self, token_id,app_id=""):
  39. '''
  40. 获取登录二维码(步骤2)
  41. appId参数为设备ID,首次登录传空,会自动触发创建设备,掉线后重新登录则必须传接口返回的appId,注意同一个号避免重复创建设备,以免触发官方风控
  42. 取码时传的appId需要与上次登录扫码的微信一致,否则会导致登录失败
  43. 响应结果中的qrImgBase64为微信二维码图片的base64,前端需要将二维码图片展示给用户并进行手机扫码操作(PS: 扫码后调用步骤2,手机上才显示登录)。
  44. (或使用响应结果中的qrData生成二维码)
  45. '''
  46. api_url = f"{self.base_url}/v2/api/login/getLoginQrCode"
  47. headers = {
  48. 'X-GEWE-TOKEN': token_id,
  49. 'Content-Type': 'application/json'
  50. }
  51. if app_id=="":
  52. data = {
  53. "appId": app_id
  54. }
  55. else:
  56. data = {
  57. "appId": app_id,
  58. "regionId":"440000"
  59. }
  60. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  61. response_data = response.json()
  62. return response_data.get('data')
  63. def qrCallback(self,uuid, base64_string):
  64. try:
  65. from PIL import Image
  66. base64_string = base64_string.split(',')[1]
  67. img_data = base64.b64decode(base64_string)
  68. img = Image.open(io.BytesIO(img_data))
  69. _thread = threading.Thread(target=img.show, args=("QRCode",))
  70. _thread.setDaemon(True)
  71. _thread.start()
  72. except Exception as e:
  73. pass
  74. import qrcode
  75. # url = f"https://login.weixin.qq.com/l/{uuid}"
  76. # http://weixin.qq.com/x/4b7fY2d93zNCXhHFkNk8
  77. url = f"http://weixin.qq.com/x/{uuid}"
  78. qr_api1 = "https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url)
  79. qr_api2 = "https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(url)
  80. qr_api3 = "https://api.pwmqr.com/qrcode/create/?url={}".format(url)
  81. qr_api4 = "https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(url)
  82. print("You can also scan QRCode in any website below:")
  83. print(qr_api3)
  84. print(qr_api4)
  85. print(qr_api2)
  86. print(qr_api1)
  87. # _send_qr_code([qr_api3, qr_api4, qr_api2, qr_api1])
  88. qr = qrcode.QRCode(border=1)
  89. qr.add_data(url)
  90. qr.make(fit=True)
  91. qr.print_ascii(invert=True)
  92. ############################### 账号管理 ###############################
  93. def reconnection(self,token_id,app_id):
  94. '''
  95. 断线重连
  96. 当系统返回账号已离线,但是手机顶部还显示ipad在线,可用此接口尝试重连,若返回错误/失败则必须重新调用步骤一登录
  97. 本接口非常用接口,可忽略
  98. '''
  99. api_url = f"{self.base_url}/v2/api/login/reconnection"
  100. headers = {
  101. 'X-GEWE-TOKEN': token_id,
  102. 'Content-Type': 'application/json'
  103. }
  104. data = {
  105. "appId": app_id
  106. }
  107. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  108. response = response.json()
  109. print(response)
  110. return response
  111. def logout(self,token_id,app_id):
  112. '''
  113. 退出
  114. '''
  115. api_url = f"{self.base_url}/v2/api/login/logout"
  116. headers = {
  117. 'X-GEWE-TOKEN': token_id,
  118. 'Content-Type': 'application/json'
  119. }
  120. data = {
  121. "appId": app_id
  122. }
  123. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  124. response_data = response.json()
  125. print(response_data)
  126. return response_data.get('data')
  127. def check_online(self,token_id,app_id):
  128. '''
  129. 检查是否在线
  130. 响应结果的data=true则是在线,反之为离线
  131. '''
  132. api_url = f"{self.base_url}/v2/api/login/checkOnline"
  133. headers = {
  134. 'X-GEWE-TOKEN': token_id,
  135. 'Content-Type': 'application/json'
  136. }
  137. data = {
  138. "appId": app_id
  139. }
  140. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  141. response_data = response.json()
  142. print(response_data)
  143. return response_data.get('data')
  144. ############################### 联系人模块 ###############################
  145. def fetch_contacts_list(self, token_id, app_id):
  146. '''
  147. 获取通讯录列表
  148. 本接口为长耗时接口,耗时时间根据好友数量递增,若接口返回超时可通过获取通讯录列表缓存接口获取响应结果
  149. 本接口返回的群聊仅为保存到通讯录中的群聊,若想获取会话列表中的所有群聊,需要通过消息订阅做二次处理。
  150. 原因:当未获取的群有成员在群内发消息的话会有消息回调, 开发者此刻调用获取群详情接口查询群信息入库保存即可,
  151. 比如说手机上三年前不说话的群,侧滑删除了,用户手机上也不会看到被删除的群聊的 ,但是有群成员说了话他会显示,
  152. 原理就是各个终端(Android、IOS、桌面版微信)取得了消息回调,又去获取群详情信息,本地数据库缓存了下来,显示的手机群聊,让用户感知的。
  153. '''
  154. api_url = f"{self.base_url}/v2/api/contacts/fetchContactsList"
  155. headers = {
  156. 'X-GEWE-TOKEN': token_id,
  157. 'Content-Type': 'application/json'
  158. }
  159. data = {
  160. "appId": app_id
  161. }
  162. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  163. response_data = response.json()
  164. print(response_data)
  165. return response_data.get('data')
  166. def fetch_contacts_list_cache(self, token_id, app_id):
  167. '''
  168. 获取通讯录列表缓存
  169. 通讯录列表数据缓存10分钟,超时则需要重新调用获取通讯录列表接口
  170. '''
  171. api_url = f"{self.base_url}/v2/api/contacts/fetchContactsListCache"
  172. headers = {
  173. 'X-GEWE-TOKEN': token_id,
  174. 'Content-Type': 'application/json'
  175. }
  176. data = {
  177. "appId": app_id
  178. }
  179. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  180. response_data = response.json()
  181. print(response_data)
  182. return response_data.get('data')
  183. def get_brief_info(self,token_id, app_id,wxids):
  184. '''
  185. 获取群/好友简要信息
  186. 1<= wxids <=100
  187. '''
  188. api_url = f"{self.base_url}/v2/api/contacts/getBriefInfo"
  189. headers = {
  190. 'X-GEWE-TOKEN': token_id,
  191. 'Content-Type': 'application/json'
  192. }
  193. data = {
  194. "appId": app_id,
  195. "wxids":wxids # list 1<= wxids <=100
  196. }
  197. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  198. response_data = response.json()
  199. print(response_data)
  200. return response_data.get('data')
  201. def get_detail_info(self,token_id, app_id,wxids):
  202. '''
  203. 获取群/好友详细信息
  204. 1<= wxids <=20
  205. '''
  206. api_url = f"{self.base_url}/v2/api/contacts/getDetailInfo"
  207. headers = {
  208. 'X-GEWE-TOKEN': token_id,
  209. 'Content-Type': 'application/json'
  210. }
  211. data = {
  212. "appId": app_id,
  213. "wxids":wxids # list 1<= wxids <=20
  214. }
  215. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  216. response_data = response.json()
  217. print(response_data)
  218. return response_data.get('data')
  219. ############################### 消息模块 ###############################
  220. def post_text(self,token_id,app_id,to_wxid,content):
  221. api_url = f"{self.base_url}/v2/api/message/postText"
  222. headers = {
  223. 'X-GEWE-TOKEN': token_id,
  224. 'Content-Type': 'application/json'
  225. }
  226. data = {
  227. "appId": app_id,
  228. "toWxid": to_wxid,
  229. "content": content
  230. }
  231. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  232. response_object = response.json()
  233. return response_object.get('ret',None),response_object.get('msg',None),response_object.get('data',None)
  234. def post_image(self,token_id,app_id,to_wxid,img_url):
  235. api_url = f"{self.base_url}/v2/api/message/postImage"
  236. headers = {
  237. 'X-GEWE-TOKEN': token_id,
  238. 'Content-Type': 'application/json'
  239. }
  240. data = {
  241. "appId": app_id,
  242. "toWxid": to_wxid,
  243. "imgUrl": img_url
  244. }
  245. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  246. response_object = response.json()
  247. return response_object.get('ret',None),response_object.get('msg',None),response_object.get('data',None)
  248. def post_voice(self,token_id,app_id,to_wxid,voice_url,voice_duration):
  249. api_url = f"{self.base_url}/v2/api/message/postVoice"
  250. headers = {
  251. 'X-GEWE-TOKEN': token_id,
  252. 'Content-Type': 'application/json'
  253. }
  254. data = {
  255. "appId": app_id,
  256. "toWxid": to_wxid,
  257. "voiceUrl": voice_url,
  258. "voiceDuration":voice_duration
  259. }
  260. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  261. response_object = response.json()
  262. return response_object.get('ret',None),response_object.get('msg',None),response_object.get('data',None)
  263. def forward_image(self,token_id,app_id,to_wxid,aeskey,cdnthumburl,cdnthumblength,cdnthumbheight,cdnthumbwidth,length,md5):
  264. api_url = f"{self.base_url}/v2/api/message/forwardImage"
  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. "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>"
  273. }
  274. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  275. response_object = response.json()
  276. return response_object.get('data',None),response_object.get('ret',None),response_object.get('msg',None)
  277. def add_contacts(self,token_id:str,app_id:str,scene:int,option:int,v3:str,v4:str,content:str):
  278. api_url = f"{self.base_url}/v2/api/contacts/addContacts"
  279. headers = {
  280. 'X-GEWE-TOKEN': token_id,
  281. 'Content-Type': 'application/json'
  282. }
  283. data = {
  284. "appId": app_id,
  285. "scene": scene,
  286. "option": option,
  287. "v3":v3,
  288. "v4":v4,
  289. "content":content
  290. }
  291. response = requests.post(url=api_url, headers=headers, data=json.dumps(data))
  292. response_object = response.json()
  293. print(response_object)
  294. return response_object.get('ret',None),response_object.get('msg',None)
  295. ############################### 下载模块 ###############################
  296. def download_audio_msg(self,token_id:str,app_id:str,msg_id: int, xml: str):
  297. data = {
  298. "appId": app_id,
  299. "msgId": msg_id,
  300. "xml": xml
  301. }
  302. print(json.dumps(data))
  303. headers = {
  304. 'X-GEWE-TOKEN': token_id,
  305. 'Content-Type': 'application/json'
  306. }
  307. # http://api.geweapi.com/gewe/v2/api/gewe/v2/api/message/downloadVoice
  308. # response = requests.post(f"{self.base_url}/v2/api/gewe/v2/api/message/downloadVoice", json=data, headers=headers)
  309. url='http://api.geweapi.com/gewe/v2/api/message/downloadVoice'
  310. # url='http://api.geweapi.com/gewe/v2/api/gewe/v2/api/message/downloadVoice'
  311. response = requests.post(f"{url}", json=data, headers=headers)
  312. if response.ok:
  313. data = response.json()
  314. print(data)
  315. if data['ret'] == 200:
  316. print("Gewe download audio msg successfully.")
  317. print(data['data']['fileUrl'])
  318. return data['data']['fileUrl']
  319. else:
  320. print("Gewe download audio msg in error.")
  321. return False
  322. else:
  323. return False
  324. def download_image_msg(self,token_id:str,app_id:str,xml: str):
  325. data = {
  326. "appId": app_id,
  327. "type": 2,
  328. "xml": xml
  329. }
  330. print(json.dumps(data))
  331. headers = {
  332. 'X-GEWE-TOKEN': token_id,
  333. 'Content-Type': 'application/json'
  334. }
  335. response = requests.post(f"{self.base_url}/v2/api/message/downloadImage", json=data, headers=headers)
  336. if response.ok:
  337. data = response.json()
  338. # print(data)
  339. if data['ret'] == 200:
  340. print("Gewe download image msg successfully.")
  341. print(data['data']['fileUrl'])
  342. return data['data']['fileUrl']
  343. else:
  344. print("Gewe download image msg in error.")
  345. return False
  346. else:
  347. return False
  348. def download_audio_file(fileUrl: str, file_name: str):
  349. # 定义保存文件的本地路径和文件名
  350. local_filename = f'./silk/{file_name}.silk'
  351. # 使用requests库的get方法获取文件内容
  352. response = requests.get(fileUrl, stream=True)
  353. # 检查请求是否成功
  354. if response.status_code == 200:
  355. # 打开文件以二进制写入模式
  356. with open(local_filename, 'wb') as f:
  357. # 逐块写入文件,通常使用1024字节的块大小
  358. for chunk in response.iter_content(1024):
  359. f.write(chunk)
  360. print(f"文件已成功下载到 {local_filename}")
  361. else:
  362. print(f"请求失败,状态码: {response.status_code}")
  363. ############################### 其他 ###############################
  364. def save_contacts_brief_to_cache(self, token_id, app_id, wxid, contacts_wxids: list):
  365. """
  366. 将联系人信息保存到 Redis 缓存。
  367. """
  368. # Redis 缓存的 key
  369. hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}"
  370. # 获取缓存中的数据
  371. cache_str = redis_helper.redis_helper.get_hash_field(hash_key, "data")
  372. cache = json.loads(cache_str) if cache_str else []
  373. if not cache:
  374. # 缓存为空,分批处理 contacts_wxids
  375. batch_size = 100
  376. for i in range(0, len(contacts_wxids), batch_size):
  377. batch = contacts_wxids[i:i + batch_size]
  378. friends_brief = self.get_brief_info(token_id, app_id, batch)
  379. friends_no_brief_wxid = [f['userName'] for f in friends_brief if not f["nickName"]]
  380. cache.extend(f for f in friends_brief if f["nickName"])
  381. if friends_no_brief_wxid:
  382. detailed_info = self.get_detail_info(token_id, app_id, friends_no_brief_wxid)
  383. cache.extend(detailed_info)
  384. # 更新缓存
  385. redis_helper.redis_helper.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False))
  386. return
  387. # 缓存已存在,检查新联系人
  388. existing_usernames = {contact['userName'] for contact in cache}
  389. new_contacts_wxids = [wxid for wxid in contacts_wxids if wxid not in existing_usernames]
  390. # 如果有新联系人,分批获取详细信息并更新缓存
  391. if new_contacts_wxids:
  392. batch_size = 20
  393. for i in range(0, len(new_contacts_wxids), batch_size):
  394. batch = new_contacts_wxids[i:i + batch_size]
  395. detailed_info = self.get_detail_info(token_id, app_id, batch)
  396. cache.extend(detailed_info)
  397. redis_helper.redis_helper.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False))
  398. def start():
  399. global wxchat
  400. # base_url = "http://192.168.88.11:2531"
  401. # wxchat = GeWeChat(base_url)
  402. wxchat = GeWeChatCom('http://api.geweapi.com/gewe')