def get_group_contact(): """ 获取群组数据 :return: """ # 获取所有联系人的列表 all_contact_res = proxy.get_contact() if all_contact_res['code'] == 200: contact_list = all_contact_res['data']['contact_list'] # contact_list [{'Uin': 0, 'UserName': '******', 'NickName': '该帐号已冻结' ...... # if contact_list[0]['Uin'] == 0: # return jsonify(ret_val.gen(ret_val.CODE_PROXY_ERR, extra_msg=contact_list[0]['NickName'])) # 从联系人列表中提取出群列表 group_name_list = groups_username_from_contacts(contact_list) # 存储群组的联系人信息 group_contact_res = proxy.batch_get_group_contact(group_name_list) if group_contact_res['code'] == ret_val.CODE_SUCCESS: group_contact_list = group_contact_res['data'][ 'group_contact_list'] # 对群做筛选 utils.filter_group_contact_list(group_contact_list) save_group_contact_list(group_contact_list) return jsonify( ret_val.gen(ret_val.CODE_SUCCESS, data=group_contact_list, extra_msg='提取群组信息成功')) else: return jsonify(group_contact_res) else: return jsonify(all_contact_res)
def get_msg(): # 从代理拉取新消息 ret = proxy.get_msg() if ret['code'] == ret_val.CODE_SUCCESS: # 消息列表 AddMsgList = ret['data']['AddMsgList'] group_msg_list = { 'msg_list': [], # 所有的消息列表 'msg_list_detected': [], # 检测出有问题的消息列表 } if AddMsgList: group_msg_list = utils.produce_group_msg(AddMsgList) WxGroupMessage.batch_insert(group_msg_list['msg_list']) if group_msg_list['msg_list_detected']: MsgDetectResult.batch_insert( group_msg_list['msg_list_detected']) # 联系人变动列表 ModContactList = ret['data']['ModContactList'] print('mod contact before : ', ModContactList) mod_group_list = groups_from_contacts(ModContactList) print('mod contact after : ', mod_group_list) # 因为处于已登录状态,所以更新联系人以username(@@)作为唯一ID update_group_contact_list(mod_group_list) return jsonify( ret_val.gen(ret_val.CODE_SUCCESS, data={ 'group_msg_list': group_msg_list, 'mod_group_list': mod_group_list })) else: return jsonify(ret)
def web_init(): """ 初始化微信首页栏的联系人、公众号等 ** 最重要的是** 初始化登录者的信息(昵称、性别等),初始化同步消息所用的SycnKey :return: 用于信息列表 """ # 组装请求参数及url url = '%s/webwxinit' % session.get(SESSION_KEY.WxLoginInfo).get("url") params = { 'r': int(-time.time() / 1579), 'pass_ticket': session.get(SESSION_KEY.WxLoginInfo).get("passticket") } data = {'BaseRequest': getBaseRequest()} headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent': config.USER_AGENT, } # 发起请求获取返回结果 r = s.post(url, params=params, data=json.dumps(data), headers=headers) dic = json.loads(r.content.decode('utf-8', 'replace')) if not dic or not dic.get('User').get('Uin'): return ret_val.gen( ret_val.CODE_PROXY_ERR, extra_msg='Cannot get wx_init response ! 微信web_init接口未得到正确的返回结果') print(session) ''' 处理用户数据 ''' user_dict = {} emoji_formatter(dic['User'], 'NickName') # 格式化昵称中的表情 # 将返回的user信息与user_dict合并,userdict里面的键全小写 for key in dic['User'].keys(): user_dict[key.lower()] = dic['User'][key] user_dict['invitestartcount'] = int(dic['InviteStartCount']) user_dict['skey'] = dic['SKey'] user_dict['synckeydict'] = dic['SyncKey'] # 用于同步消息的 synckey 结果示例:1_690237487|2_690237829|3_690237697|1000_1545727682 user_dict['synckey'] = '|'.join([ '%s_%s' % (item['Key'], item['Val']) for item in dic['SyncKey']['List'] ]) return ret_val.gen(ret_val.CODE_SUCCESS, data={"user_dict": user_dict})
def get_member_head_img(): # 群组信息 group_id = request.args.get('group_id') encry_chatroom_id = request.args.get('encry_chatroom_id') # 成员信息 username = request.args.get('username') user_nickname = request.args.get('user_nickname') # TODO: 强制要求头像更新 force = request.args.get('user_nickname') if not all((group_id, encry_chatroom_id, username, user_nickname)): return jsonify( ret_val.gen( ret_val.CODE_PARAMS_ERR, extra_msg= '需要传入 group_id, encry_chatroom_id, username, user_nickname')) filedir = 'IMMonitor/static/wxheadimg/' filename = '%s%s' % (group_id, user_nickname) filename = hashlib.md5(filename.encode('utf8')).hexdigest() file_format = '.png' file = '{filedir}{filename}{file_format}'.format(filedir=filedir, filename=filename, file_format=file_format) # 不存在头像才请求新的 if not os.path.exists(file): bit_img = proxy.get_member_head_img(EncryChatRoomId=encry_chatroom_id, username=username) if len(bit_img) > 10: with open(file, 'wb') as f: f.write(bit_img) return jsonify( ret_val.gen(ret_val.CODE_SUCCESS, data={'FilePath': file.replace('IMMonitor', '')})) else: return jsonify( ret_val.gen(ret_val.CODE_SUCCESS, data={'FilePath': '/static/wxheadimg/default.png'})) # 存在头像了返回原来的头像 else: return jsonify( ret_val.gen(ret_val.CODE_SUCCESS, data={'FilePath': file.replace('IMMonitor', '')}))
def get_msg(): """ 拉取新消息代理 :param self: :return: tuple (dic['AddMsgList'], dic['ModContactList']) AddMsgList: list 所有的新消息列表 ModContactList:list 所有联系人有变动的列表 """ loginInfo = session.get(SESSION_KEY.WxLoginInfo) # 组装获取新消息的url及参数 url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( loginInfo['url'], loginInfo['sid'], loginInfo['skey'], loginInfo['passticket']) data = { 'BaseRequest': getBaseRequest(), 'SyncKey': loginInfo['synckeydict'], 'rr': ~int(time.time()), } headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent': config.USER_AGENT} # 发起拉取新消息的请求 r = s.post(url, data=json.dumps(data), headers=headers, timeout=config.TIMEOUT) response = json.loads(r.content.decode('utf-8', 'replace')) if response['BaseResponse']['Ret'] != 0: return ret_val.gen(ret_val.CODE_PROXY_ERR, extra_msg='Error get msg ! 拉取新消息出错 !') # 更新session登录信息中的synckeyresponset及synckey session[SESSION_KEY.WxLoginInfo]['synckeydict'] = response['SyncKey'] session[SESSION_KEY.WxLoginInfo]['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) for item in response['SyncCheckKey']['List']]) return ret_val.gen(ret_val.CODE_SUCCESS, data={ 'AddMsgList': response['AddMsgList'], 'ModContactList': response['ModContactList'] })
def on_join(data): ''' 加入一个用户自己的房间,实现多用户通信 :param data: socket传过来的值 :return: socket emit 加入房间状态 ''' params_username = data.get('username') session_username = session.get('user_id') if not params_username or session_username is None or params_username != session_username: retmsg = ret_val.gen(ret_val.CODE_SOCKET_ERR, extra_msg='Can not join Room %s ! 无法加入房间:%s' % (params_username, params_username)) socketio.emit('check_join', data=retmsg) return join_room(session_username) retmsg = ret_val.gen(ret_val.CODE_SUCCESS, extra_msg='Join room named %s successfully ! 成功加入房间' % (params_username, params_username)) socketio.emit('check_join', data=retmsg, room=session_username) return
def get_QR(): """ 根据扫码登录uuid生成登录二维码 :param uuid: 扫码登录uuid :return: BytesIO 二维码二进制io流 """ uuid = _get_QRuuid() # 如果没有请求到扫码登录的uuid,返回错误 if not uuid: return ret_val.gen(ret_val.CODE_PROXY_ERR, extra_msg='Cannot get QRuuid ! 无法获取二维码扫码登录uuid !') else: session[SESSION_KEY.WxLoginInfo]['uuid'] = uuid qrStorage = io.BytesIO() qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid) qrCode.png(qrStorage, scale=10) return ret_val.gen(ret_val.CODE_SUCCESS, data={ "qrcode": bytes.decode( base64.b64encode(qrStorage.getvalue())) })
def send_raw_msg(msgType, content, toUserName): loginInfo = session.get(SESSION_KEY.WxLoginInfo) url = '%s/webwxsendmsg' % loginInfo['url'] data = { 'BaseRequest': getBaseRequest(), 'Msg': { 'Type': msgType, 'Content': content, 'FromUserName': loginInfo['username'], 'ToUserName': toUserName, 'LocalID': int(time.time() * 1e4), 'ClientMsgId': int(time.time() * 1e4), }, 'Scene': 0, } headers = {'ContentType': 'application/json; charset=UTF-8', 'User-Agent': config.USER_AGENT} r = s.post(url, headers=headers, data=json.dumps(data, ensure_ascii=False).encode('utf8')) response = json.loads(r.content.decode('utf-8', 'replace')) if response['BaseResponse']['Ret'] != 0: return ret_val.gen(ret_val.CODE_PROXY_ERR, extra_msg='Error get msg ! 消息发送失败 !') else: return ret_val.gen(ret_val.CODE_SUCCESS)
def send_raw_msg(): """ 发送消息, 目前只支持文字 TODO: 支持发送图片、语音、视频等 :return: """ content = request.args.get('content') to_username = request.args.get('to_username') if not content or not to_username: return jsonify( ret_val.gen(ret_val.CODE_PARAMS_ERR, extra_msg='需要传入 content, to_username 参数')) ret = proxy.send_raw_msg(1, content=content, toUserName=to_username) return jsonify(ret)
def check_login(): """ 检测用户是否已登录 :param self: :param uuid: 扫描登录的uuid :return: 200 确认登录 201 扫描成功(还未按登录按钮) 204 现在正有一个检查登录请求在发送,不需要重复请求 408 一直未扫描(或者登陆超时) 400/424 代理出错 """ ''' ----------------------------------------------------------------------------------------------------- | 接口地址 | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login ----------------------------------------------------------------------------------------------------- | 请求方法 | GET ----------------------------------------------------------------------------------------------------- | | tip: 1 未扫描 0 已扫描 | 传递参数 | uuid: xxx | | _: 时间戳 ----------------------------------------------------------------------------------------------------- | 返回值 | window.code=xxx; | | xxx: | | 200 确认登录 | | 201 扫描成功(还未按登录按钮) | | 408 一直未扫描(或者登陆超时) | | 400 424 代理出错 | | | | 当返回200时,在返回的body中,还会返回以下数据让前端重定向到登录后的页面, | | window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx"; | | 这里我们使用 proccess_login_info 函数再起发起该重定向地址的请求 ----------------------------------------------------------------------------------------------------- ''' uuid = session.get(SESSION_KEY.WxLoginInfo).get('uuid') # return ret_val.gen(ret_val.CODE_SUCCESS) # 构造checklogin请求url及参数 url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL localTime = int(time.time()) params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( uuid, int(-localTime / 1579), localTime) headers = {'User-Agent': config.USER_AGENT} # 发起请求 r = s.get(url, params=params, headers=headers) regx = r'window.code=(\d+)' data = re.search(regx, r.text) # TODO: 扫码的时候返回用户头像用于显示 # r.text = window.code=201;window.userAvatar = 'data:img/jpg;base64,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'; # 如果是确认登录 if not data: return ret_val.gen( ret_val.CODE_PROXY_ERR, extra_msg= 'Regex extract nothing in check login response data ! 正则匹配没有在检测登录接口的返回值中匹配到任何信息 !' ) retcode = data.group(1) # 确认登录 if retcode == '200': # 处理确认登录的返回结果 if process_login_info(r.text): # 登录之后设置checklogin为True return ret_val.gen(ret_val.CODE_SUCCESS) else: return ret_val.gen( ret_val.CODE_PROXY_ERR, extra_msg='Error when process confirm login ! 处理确认登录时发送错误 !') else: return {"code": int(retcode), "status": "ok"}
def get_head_img(): username = request.args.get('username') group_id = request.args.get('group_id') uin = request.args.get('uin') if not username: return jsonify( ret_val.gen(ret_val.CODE_PARAMS_ERR, extra_msg='需要传入username')) filedir = 'IMMonitor/static/wxheadimg/' file_format = '.png' if uin: file = '{filedir}user_{filename}{file_format}'.format( filedir=filedir, filename=uin, file_format=file_format) # 不存在头像才请求新的 if not os.path.exists(file): bit_img = proxy.get_head_img(username=username, type=0) if len(bit_img) > 10: with open(file, 'wb') as f: f.write(bit_img) return jsonify( ret_val.gen( ret_val.CODE_SUCCESS, data={'FilePath': file.replace('IMMonitor', '')})) else: return jsonify( ret_val.gen( ret_val.CODE_SUCCESS, data={'FilePath': '/static/wxheadimg/default.png'})) # 存在头像了返回原来的头像 else: return jsonify( ret_val.gen(ret_val.CODE_SUCCESS, data={'FilePath': file.replace('IMMonitor', '')})) elif group_id: file = '{filedir}group_{filename}{file_format}'.format( filedir=filedir, filename=group_id, file_format=file_format) # 不存在头像才请求新的 if not os.path.exists(file): bit_img = proxy.get_head_img(username=username, type=1) if len(bit_img) > 10: with open(file, 'wb') as f: f.write(bit_img) return jsonify( ret_val.gen( ret_val.CODE_SUCCESS, data={'FilePath': file.replace('IMMonitor', '')})) else: return jsonify( ret_val.gen( ret_val.CODE_SUCCESS, data={'FilePath': '/static/wxheadimg/default.png'})) # 存在头像了返回原来的头像 else: return jsonify( ret_val.gen(ret_val.CODE_SUCCESS, data={'FilePath': file.replace('IMMonitor', '')}))
def sync_check(): """ 检查是否有新消息代理 :return: """ ''' ----------------------------------------------------------------------------------------------------- | 接口地址 | https://webpush2.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck ----------------------------------------------------------------------------------------------------- | 请求方法 | GET ----------------------------------------------------------------------------------------------------- | | r=时间戳(ms) | | skey=xxx | | sid=xxx | 传递参数 | uin=xxx | | deviceid=xxx | | synckey=1_654585659%7C2_654585745%7C3_654585673%7C1000_1467162721_=1467184052133 ----------------------------------------------------------------------------------------------------- | 返回值 | {window.synccheck={retcode:"xxx",selector:"xxx"} ----------------------------------------------------------------------------------------------------- | | retcode: | | 0 正常 | | 1100 失败/退出微信 | 返回参数 | selector: | | 0 正常,无新消息 | | 2 新的消息 | | 4 朋友圈有动态 | | 6 有消息返回结果 | | 7 进入/离开聊天界面 ----------------------------------------------------------------------------------------------------- web微信主要的过程就是轮询+获取消息的循环。轮询的接口为synccheck,获取消息接口为webwxsync。 首先向synccheck接口发起GET请求,如果暂时没有新消息的话,保持住连接不返回直至超时。 超时后会返回一个类似这样的数据包: {window.synccheck={retcode:"xxx",selector:"xxx"} 其中RETCODE为返回状态,非0代表有错误; SELECTOR代表消息,0为无消息,非0值则有消息。 因此,对于超时的情况,selector是为0的。 如果在GET请求后发现有新消息,那么服务器会立刻返回一个同样格式的数据包,RETCODE为0,SELECTOR不为0。 此时,就需要调用webwxsync接口,用POST方式去获取新消息。 POST请求的返回除了有消息以外,header里还有Set cookie指令(不过好像每次的cookie都一样而且过期时间有一天之多), 另外response body里还有一个重要的东西,就是syncKey和syncCheckKey。 这里就是我前文中提到的过时的情况之一,网上绝大多数资料都是只有一个syncKey,实际返回的却有一个syncKey和一个syncCheckKey。 从名字就能看出来,前者用于synccheck接口,后者用于webwxsync接口。 由于syncKey每次都更新,所以如果某一次webwxsync接口的响应出了意外,后面的程序是没法进行下去的(本地key已经过期了)。 参考文档: 1、 https://blog.csdn.net/wonxxx/article/details/51787041 2、https://www.cnblogs.com/shawnye/p/6376400.html ''' loginInfo = session.get('WxLoginInfo') # 组装请求url及参数 url = '%s/synccheck' % loginInfo.get('syncUrl', loginInfo['url']) params = { 'r': int(time.time() * 1000), 'skey': loginInfo['skey'], 'sid': loginInfo['sid'], 'uin': loginInfo['uin'], 'deviceid': loginInfo['deviceid'], 'synckey': loginInfo['synckey'], '_': loginInfo['logintime'], } headers = {'User-Agent': config.USER_AGENT} loginInfo['logintime'] += 1 # 同步更新session中的logintime session[SESSION_KEY.WxLoginInfo]['logintime'] = loginInfo['logintime'] try: r = s.get(url, params=params, headers=headers, timeout=config.TIMEOUT) except requests.exceptions.ConnectionError as e: try: if not isinstance(e.args[0].args[1], BadStatusLine): raise # will return a package with status '0 -' # and value like: # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93 # seems like status of typing, but before I make further achievement code will remain like this return '2' except: raise # 如果有连接为404等异常,使用Request.raise_for_status 抛出异常 r.raise_for_status() # 提取返回参数 regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' pm = re.search(regx, r.text) # 筛选消息 # 如果返回中的 retcode参数 == 0,返回 select的值 # 其他即判断为出错返回None if pm is None or pm.group(1) != '0': return ret_val.gen(ret_val.CODE_PROXY_ERR, extra_msg='Weixin proxy sync_check get wrong response ' '微信sync_check检查消息接口返回值不正确,失败或退出微信') return ret_val.gen(ret_val.CODE_SUCCESS, data={ "message_status": pm.group(2) })