import requests import json import base64 import io import json import os import threading import time import requests from io import BytesIO from PIL import Image from common import redis_helper wxchat=None class GeWeChat: def __init__(self, base_url): self.base_url = base_url ############################### 登录模块 ############################### def get_token_id(self): ''' 获取header 的 token ''' api_url = f"{self.base_url}/v2/api/tools/getTokenId" response = requests.post(url=api_url) response_data = response.json() print(response_data) return response_data.get('data') def get_login_qr_code(self, token_id): ''' 获取登录二维码(步骤2) appId参数为设备ID,首次登录传空,会自动触发创建设备,掉线后重新登录则必须传接口返回的appId,注意同一个号避免重复创建设备,以免触发官方风控 取码时传的appId需要与上次登录扫码的微信一致,否则会导致登录失败 响应结果中的qrImgBase64为微信二维码图片的base64,前端需要将二维码图片展示给用户并进行手机扫码操作(PS: 扫码后调用步骤2,手机上才显示登录)。 (或使用响应结果中的qrData生成二维码) ''' api_url = f"{self.base_url}/v2/api/login/getLoginQrCode" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": "" } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() return response_data.get('data') def check_login(self, token_id, app_id, uuid): ''' 执行登录(步骤3) 获取到登录二维码后需每间隔5s调用本接口来判断是否登录成功 新设备登录平台,次日凌晨会掉线一次,重新登录时需调用获取二维码且传appId取码,登录成功后则可以长期在线 登录成功后请保存appId与wxid的对应关系,后续接口中会用到 ''' api_url = f"{self.base_url}/v2/api/login/checkLogin" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "uuid": uuid } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def qrCallback(self,uuid, base64_string): try: from PIL import Image base64_string = base64_string.split(',')[1] img_data = base64.b64decode(base64_string) img = Image.open(io.BytesIO(img_data)) _thread = threading.Thread(target=img.show, args=("QRCode",)) _thread.setDaemon(True) _thread.start() except Exception as e: pass import qrcode # url = f"https://login.weixin.qq.com/l/{uuid}" # http://weixin.qq.com/x/4b7fY2d93zNCXhHFkNk8 url = f"http://weixin.qq.com/x/{uuid}" qr_api1 = "https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url) qr_api2 = "https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(url) qr_api3 = "https://api.pwmqr.com/qrcode/create/?url={}".format(url) qr_api4 = "https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(url) print("You can also scan QRCode in any website below:") print(qr_api3) print(qr_api4) print(qr_api2) print(qr_api1) # _send_qr_code([qr_api3, qr_api4, qr_api2, qr_api1]) qr = qrcode.QRCode(border=1) qr.add_data(url) qr.make(fit=True) qr.print_ascii(invert=True) def callback_collect(self,token_id,url): ''' 设置消息回调地址 ''' api_url = f"{self.base_url}/v2/api/tools/setCallback" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "token": token_id, "callbackUrl": url } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('msg') ############################### 账号管理 ############################### def reconnection(self,token_id,app_id): ''' 断线重连 当系统返回账号已离线,但是手机顶部还显示ipad在线,可用此接口尝试重连,若返回错误/失败则必须重新调用步骤一登录 本接口非常用接口,可忽略 ''' api_url = f"{self.base_url}/v2/api/login/reconnection" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def logout(self,token_id,app_id): ''' 退出 ''' api_url = f"{self.base_url}/v2/api/login/logout" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def check_online(self,token_id,app_id): ''' 检查是否在线 响应结果的data=true则是在线,反之为离线 ''' api_url = f"{self.base_url}/v2/api/login/checkOnline" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') ############################### 联系人模块 ############################### def fetch_contacts_list(self, token_id, app_id): ''' 获取通讯录列表 本接口为长耗时接口,耗时时间根据好友数量递增,若接口返回超时可通过获取通讯录列表缓存接口获取响应结果 本接口返回的群聊仅为保存到通讯录中的群聊,若想获取会话列表中的所有群聊,需要通过消息订阅做二次处理。 原因:当未获取的群有成员在群内发消息的话会有消息回调, 开发者此刻调用获取群详情接口查询群信息入库保存即可, 比如说手机上三年前不说话的群,侧滑删除了,用户手机上也不会看到被删除的群聊的 ,但是有群成员说了话他会显示, 原理就是各个终端(Android、IOS、桌面版微信)取得了消息回调,又去获取群详情信息,本地数据库缓存了下来,显示的手机群聊,让用户感知的。 ''' api_url = f"{self.base_url}/v2/api/contacts/fetchContactsList" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def fetch_contacts_list_cache(self, token_id, app_id): ''' 获取通讯录列表缓存 通讯录列表数据缓存10分钟,超时则需要重新调用获取通讯录列表接口 ''' api_url = f"{self.base_url}/v2/api/contacts/fetchContactsListCache" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def get_brief_info(self,token_id, app_id,wxids): ''' 获取群/好友简要信息 1<= wxids <=100 ''' api_url = f"{self.base_url}/v2/api/contacts/getBriefInfo" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "wxids":wxids # list 1<= wxids <=100 } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def get_detail_info(self,token_id, app_id,wxids): ''' 获取群/好友详细信息 1<= wxids <=20 ''' api_url = f"{self.base_url}/v2/api/contacts/getDetailInfo" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "wxids":wxids # list 1<= wxids <=20 } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') ############################### 消息模块 ############################### def post_text(self,token_id,app_id,to_wxid,content): api_url = f"{self.base_url}/v2/api/message/postText" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "toWxid": to_wxid, "content": content } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def post_image(self,token_id,app_id,to_wxid,img_url): api_url = f"{self.base_url}/v2/api/message/postImage" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "toWxid": to_wxid, "imgUrl": img_url } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def post_voice(self,token_id,app_id,to_wxid,voice_url,voice_duration): api_url = f"{self.base_url}/v2/api/message/postVoice" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "toWxid": to_wxid, "voiceUrl": voice_url, "voiceDuration":voice_duration } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') class GeWeChatCom: def __init__(self, base_url): self.base_url = base_url ############################### 登录模块 ############################### def check_login(self, token_id, app_id, uuid): ''' 执行登录(步骤3) 获取到登录二维码后需每间隔5s调用本接口来判断是否登录成功 新设备登录平台,次日凌晨会掉线一次,重新登录时需调用获取二维码且传appId取码,登录成功后则可以长期在线 登录成功后请保存appId与wxid的对应关系,后续接口中会用到 ''' api_url = f"{self.base_url}/v2/api/login/checkLogin" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "uuid": uuid } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def get_login_qr_code(self, token_id,app_id=""): ''' 获取登录二维码(步骤2) appId参数为设备ID,首次登录传空,会自动触发创建设备,掉线后重新登录则必须传接口返回的appId,注意同一个号避免重复创建设备,以免触发官方风控 取码时传的appId需要与上次登录扫码的微信一致,否则会导致登录失败 响应结果中的qrImgBase64为微信二维码图片的base64,前端需要将二维码图片展示给用户并进行手机扫码操作(PS: 扫码后调用步骤2,手机上才显示登录)。 (或使用响应结果中的qrData生成二维码) ''' api_url = f"{self.base_url}/v2/api/login/getLoginQrCode" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } if app_id=="": data = { "appId": app_id } else: data = { "appId": app_id, "regionId":"440000" } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() return response_data.get('data') def qrCallback(self,uuid, base64_string): try: from PIL import Image base64_string = base64_string.split(',')[1] img_data = base64.b64decode(base64_string) img = Image.open(io.BytesIO(img_data)) _thread = threading.Thread(target=img.show, args=("QRCode",)) _thread.setDaemon(True) _thread.start() except Exception as e: pass import qrcode # url = f"https://login.weixin.qq.com/l/{uuid}" # http://weixin.qq.com/x/4b7fY2d93zNCXhHFkNk8 url = f"http://weixin.qq.com/x/{uuid}" qr_api1 = "https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url) qr_api2 = "https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(url) qr_api3 = "https://api.pwmqr.com/qrcode/create/?url={}".format(url) qr_api4 = "https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(url) print("You can also scan QRCode in any website below:") print(qr_api3) print(qr_api4) print(qr_api2) print(qr_api1) # _send_qr_code([qr_api3, qr_api4, qr_api2, qr_api1]) qr = qrcode.QRCode(border=1) qr.add_data(url) qr.make(fit=True) qr.print_ascii(invert=True) ############################### 账号管理 ############################### def reconnection(self,token_id,app_id): ''' 断线重连 当系统返回账号已离线,但是手机顶部还显示ipad在线,可用此接口尝试重连,若返回错误/失败则必须重新调用步骤一登录 本接口非常用接口,可忽略 ''' api_url = f"{self.base_url}/v2/api/login/reconnection" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response = response.json() print(response) return response def logout(self,token_id,app_id): ''' 退出 ''' api_url = f"{self.base_url}/v2/api/login/logout" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def check_online(self,token_id,app_id): ''' 检查是否在线 响应结果的data=true则是在线,反之为离线 ''' api_url = f"{self.base_url}/v2/api/login/checkOnline" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') ############################### 联系人模块 ############################### def fetch_contacts_list(self, token_id, app_id): ''' 获取通讯录列表 本接口为长耗时接口,耗时时间根据好友数量递增,若接口返回超时可通过获取通讯录列表缓存接口获取响应结果 本接口返回的群聊仅为保存到通讯录中的群聊,若想获取会话列表中的所有群聊,需要通过消息订阅做二次处理。 原因:当未获取的群有成员在群内发消息的话会有消息回调, 开发者此刻调用获取群详情接口查询群信息入库保存即可, 比如说手机上三年前不说话的群,侧滑删除了,用户手机上也不会看到被删除的群聊的 ,但是有群成员说了话他会显示, 原理就是各个终端(Android、IOS、桌面版微信)取得了消息回调,又去获取群详情信息,本地数据库缓存了下来,显示的手机群聊,让用户感知的。 ''' api_url = f"{self.base_url}/v2/api/contacts/fetchContactsList" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def fetch_contacts_list_cache(self, token_id, app_id): ''' 获取通讯录列表缓存 通讯录列表数据缓存10分钟,超时则需要重新调用获取通讯录列表接口 ''' api_url = f"{self.base_url}/v2/api/contacts/fetchContactsListCache" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def get_brief_info(self,token_id, app_id,wxids): ''' 获取群/好友简要信息 1<= wxids <=100 ''' api_url = f"{self.base_url}/v2/api/contacts/getBriefInfo" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "wxids":wxids # list 1<= wxids <=100 } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def get_detail_info(self,token_id, app_id,wxids): ''' 获取群/好友详细信息 1<= wxids <=20 ''' api_url = f"{self.base_url}/v2/api/contacts/getDetailInfo" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "wxids":wxids # list 1<= wxids <=20 } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') ############################### 消息模块 ############################### def post_text(self,token_id,app_id,to_wxid,content): api_url = f"{self.base_url}/v2/api/message/postText" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "toWxid": to_wxid, "content": content } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def post_image(self,token_id,app_id,to_wxid,img_url): api_url = f"{self.base_url}/v2/api/message/postImage" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "toWxid": to_wxid, "imgUrl": img_url } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def post_voice(self,token_id,app_id,to_wxid,voice_url,voice_duration): api_url = f"{self.base_url}/v2/api/message/postVoice" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "toWxid": to_wxid, "voiceUrl": voice_url, "voiceDuration":voice_duration } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') def forward_image(self,token_id,app_id,to_wxid,aeskey,cdnthumburl,cdnthumblength,cdnthumbheight,cdnthumbwidth,length,md5): api_url = f"{self.base_url}/v2/api/message/forwardImage" headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } data = { "appId": app_id, "toWxid": to_wxid, "xml": f"\n\n\t\n\t\n\t\n" } response = requests.post(url=api_url, headers=headers, data=json.dumps(data)) response_data = response.json() print(response_data) return response_data.get('data') ############################### 下载模块 ############################### def download_audio_msg(self,token_id:str,app_id:str,msg_id: int, xml: str): data = { "appId": app_id, "msgId": msg_id, "xml": xml } print(json.dumps(data)) headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } # http://api.geweapi.com/gewe/v2/api/gewe/v2/api/message/downloadVoice # response = requests.post(f"{self.base_url}/v2/api/gewe/v2/api/message/downloadVoice", json=data, headers=headers) url='http://api.geweapi.com/gewe/v2/api/message/downloadVoice' # url='http://api.geweapi.com/gewe/v2/api/gewe/v2/api/message/downloadVoice' response = requests.post(f"{url}", json=data, headers=headers) if response.ok: data = response.json() print(data) if data['ret'] == 200: print("Gewe download audio msg successfully.") print(data['data']['fileUrl']) return data['data']['fileUrl'] else: print("Gewe download audio msg in error.") return False else: return False def download_image_msg(self,token_id:str,app_id:str,xml: str): data = { "appId": app_id, "type": 2, "xml": xml } print(json.dumps(data)) headers = { 'X-GEWE-TOKEN': token_id, 'Content-Type': 'application/json' } response = requests.post(f"{self.base_url}/v2/api/message/downloadImage", json=data, headers=headers) if response.ok: data = response.json() # print(data) if data['ret'] == 200: print("Gewe download image msg successfully.") print(data['data']['fileUrl']) return data['data']['fileUrl'] else: print("Gewe download image msg in error.") return False else: return False def download_audio_file(fileUrl: str, file_name: str): # 定义保存文件的本地路径和文件名 local_filename = f'./silk/{file_name}.silk' # 使用requests库的get方法获取文件内容 response = requests.get(fileUrl, stream=True) # 检查请求是否成功 if response.status_code == 200: # 打开文件以二进制写入模式 with open(local_filename, 'wb') as f: # 逐块写入文件,通常使用1024字节的块大小 for chunk in response.iter_content(1024): f.write(chunk) print(f"文件已成功下载到 {local_filename}") else: print(f"请求失败,状态码: {response.status_code}") ############################### 其他 ############################### # def save_contacts_brief_to_cache(self, token_id, app_id, wxid, contacts_wxids: list): # # 将联系人信息保存到 Redis,使用一个合适的 key # hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}" # # 获取缓存中的数据,如果缓存不存在则初始化为空列表 # cache_str = redis_helper.redis_helper.get_hash_field(hash_key, "data") # cache = json.loads(cache_str) if cache_str else [] # # 如果缓存为空,获取并保存联系人详细信息 # if not cache: # friends_brief = self.get_brief_info(token_id, app_id, contacts_wxids) # contacts_no_info=[] # for f in friends_brief: # if f["nickName"]: # cache.append(f) # else: # contacts_no_info.append(f) # if len(contacts_no_info)!=0: # f_detail = self.get_detail_info(token_id, app_id, contacts_no_info) # cache.extend(f_detail) # # 更新缓存 # redis_helper.redis_helper.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False)) # else: # # 获取现有缓存中的 userName 列表,避免重复 # existing_usernames = {contact['userName'] for contact in cache} # new_contacts_wxids = [contact_wxid for contact_wxid in contacts_wxids if contact_wxid not in existing_usernames] # if new_contacts_wxids: # f_detail = self.get_detail_info(token_id, app_id, new_contacts_wxids) # print(f_detail) # cache.extend(f_detail) # # 更新缓存 # redis_helper.redis_helper.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False)) def save_contacts_brief_to_cache(self, token_id, app_id, wxid, contacts_wxids: list): """ 将联系人信息保存到 Redis 缓存。 """ # Redis 缓存的 key hash_key = f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}" # 获取缓存中的数据 cache_str = redis_helper.redis_helper.get_hash_field(hash_key, "data") cache = json.loads(cache_str) if cache_str else [] if not cache: # 缓存为空,分批处理 contacts_wxids batch_size = 100 for i in range(0, len(contacts_wxids), batch_size): batch = contacts_wxids[i:i + batch_size] friends_brief = self.get_brief_info(token_id, app_id, batch) friends_no_brief_wxid = [f['userName'] for f in friends_brief if not f["nickName"]] cache.extend(f for f in friends_brief if f["nickName"]) if friends_no_brief_wxid: detailed_info = self.get_detail_info(token_id, app_id, friends_no_brief_wxid) cache.extend(detailed_info) # 更新缓存 redis_helper.redis_helper.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False)) return # 缓存已存在,检查新联系人 existing_usernames = {contact['userName'] for contact in cache} new_contacts_wxids = [wxid for wxid in contacts_wxids if wxid not in existing_usernames] # 如果有新联系人,分批获取详细信息并更新缓存 if new_contacts_wxids: batch_size = 20 for i in range(0, len(new_contacts_wxids), batch_size): batch = new_contacts_wxids[i:i + batch_size] detailed_info = self.get_detail_info(token_id, app_id, batch) cache.extend(detailed_info) redis_helper.redis_helper.update_hash_field(hash_key, "data", json.dumps(cache, ensure_ascii=False)) def start(): global wxchat # base_url = "http://192.168.88.11:2531" # wxchat = GeWeChat(base_url) wxchat = GeWeChatCom('http://api.geweapi.com/gewe')