From 2ef3a731af664fba26d14a2d9d2b3be66e3473fd Mon Sep 17 00:00:00 2001 From: lykops Date: Mon, 17 Apr 2017 23:51:37 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- ...\350\266\205\350\277\2071\345\244\251.jpg" | Bin 24803 -> 25652 bytes install/requirements.txt | 3 +- library/config/wechat.py | 7 +- library/file/__init__.py | 0 library/file/get_md5.py | 16 ++ library/file/upload.py | 49 ++++ library/visit_url/request/cookie.py | 9 +- library/wechat/send.py | 232 ++++++++++++++++-- library/wechat/send2.py | 200 +++++++++++++++ library/wechat/send3.py | 222 +++++++++++++++++ lykchat/settings.py | 3 +- lykchat/views.py | 162 +++++++----- templates/wechat.html | 3 + test_sendfile.py | 62 +++++ 15 files changed, 883 insertions(+), 93 deletions(-) create mode 100644 library/file/__init__.py create mode 100644 library/file/get_md5.py create mode 100644 library/file/upload.py create mode 100644 library/wechat/send2.py create mode 100644 library/wechat/send3.py create mode 100644 test_sendfile.py diff --git a/README.md b/README.md index 09d6a2e..cd1b788 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 web管理页面实现可视化管理微信登陆 接口采用URL,简化调用复杂度,返回结果均为json格式 2、信息共享 - 通过共享用户session和微信登陆信息,保证系统长期稳定运行 + 通过共享用户session和微信登陆信息,保证系统长期稳定运行 3、7*24不间断服务 计划任务定时检查微信登陆状态,微信保持登陆超过20天 4、用户管理 @@ -23,10 +23,6 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 ## 截图 ## -管理页面--等待扫码 -![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/web管理--登陆.jpg) - - 管理页面--功能展示 ![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/web页面--功能说明.jpg) @@ -82,7 +78,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 'friendfield':接受信息的好友字段代号,0昵称,1微信号,2备注名,可以为空,默认为0 'friend': 接受信息的好友的昵称、微信号、备注名的其中之一,不能为空 'content': 发送内容,不能为空 - 注意: + 注意: friend一定是该用户下的登陆微信好友列表中的 friendfield最好是微信号(Alias),也可以使用昵称(NickName)或者备注名(RemarkName)(但不能重复出现) 由于好友列表使用缓存机制,新增好友可能发送信息不成功 diff --git "a/doc/\345\276\256\344\277\241\347\231\273\351\231\206\346\227\266\351\227\264\350\266\205\350\277\2071\345\244\251.jpg" "b/doc/\345\276\256\344\277\241\347\231\273\351\231\206\346\227\266\351\227\264\350\266\205\350\277\2071\345\244\251.jpg" index a27bb97c16aa3b886f5a84bb362fde151ebe2dde..4b7e52f72aa38824ff400a2b2f87cea3cc74670e 100644 GIT binary patch literal 25652 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/dev/shm/lykchat.txt 2>&1'), ) - +# 检测登陆状态的计划任务 url_frond = 'http://127.0.0.1/' diff --git a/library/file/__init__.py b/library/file/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/file/get_md5.py b/library/file/get_md5.py new file mode 100644 index 0000000..ed164a3 --- /dev/null +++ b/library/file/get_md5.py @@ -0,0 +1,16 @@ +import hashlib, os + +def get_file_md5(filename): + if not os.path.isfile(filename): + return False + + myhash = hashlib.md5() + f = open(filename, 'rb') + while True: + b = f.read(8096) + if not b : + break + myhash.update(b) + f.close() + + return myhash.hexdigest() diff --git a/library/file/upload.py b/library/file/upload.py new file mode 100644 index 0000000..8358353 --- /dev/null +++ b/library/file/upload.py @@ -0,0 +1,49 @@ +import os, time +from lykchat.settings import BASE_DIR + +def upload_file(file, filename='', username=''): + timestr = time.strftime('%Y%m%d' , time.localtime()) + timestr = str(timestr) + logfile = os.path.join(BASE_DIR, 'file/upload/index.txt') + + upload_dir = os.path.join(BASE_DIR, 'file/upload/' + timestr + '/') + if not os.path.exists(upload_dir): + try : + os.mkdir(upload_dir) + except : + os.makedirs(upload_dir) + + ''' + if filename == '' or not filename : + secstr = time.strftime('%H%M%S' , time.localtime()) + secstr = str(secstr) + randomstr = random.randint(1000, 9999) + randomstr = str(randomstr) + filename = timestr + '-' + secstr + randomstr + else : + filename = upload_dir + str(filename) + ''' + + datetimestr = time.strftime('%Y-%m-%d %H:%M:%S' , time.localtime()) + datetimestr = str(datetimestr) + log = str(username) + ' ' + datetimestr + ' ' + filename + '\n' + + filename = upload_dir + str(filename) + secstr = time.strftime('%H%M%S' , time.localtime()) + secstr = str(secstr) + if os.path.exists(filename): + os.rename(filename , filename + '-' + timestr + '-' + secstr) + + + with open(filename, 'wb+') as dest: + for chunk in file.chunks(): + dest.write(chunk) + + # os.system('chmod 444 ' + filename) + open(logfile, 'a').write(log) + return filename + + return False + + + diff --git a/library/visit_url/request/cookie.py b/library/visit_url/request/cookie.py index 8ad1723..950d069 100644 --- a/library/visit_url/request/cookie.py +++ b/library/visit_url/request/cookie.py @@ -4,20 +4,20 @@ class Request_Url(): ''' 使用requests模块访问web页面,必须提供url ''' - def __init__(self, url , headers={} , cookies={} , data={} , params={} , allow_redirects=True): + def __init__(self, url, headers={}, cookies={}, data={}, files={}, params={}, allow_redirects=True): default_headers = { 'Accept' : 'application/json,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Encoding' : 'gzip, deflate, br', 'charset': 'UTF-8', 'Accept-Language': 'en-US,en;q=0.8,zh-CN;q=0.5,zh;q=0.3', 'Connection': 'keep-alive', - 'Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0', + 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:52.0) Gecko/20100101 Firefox/52.0', } if headers == {} : headers = default_headers else : - for header in ['Accept' ,'Accept-Encoding' ,'Accept-Language','Connection', 'Agent']: + for header in ['Accept' , 'Accept-Encoding' , 'Accept-Language', 'Connection', 'User-Agent']: if header not in headers or headers[header] == '': headers[header] = default_headers[header] # 新增headers @@ -33,6 +33,8 @@ def __init__(self, url , headers={} , cookies={} , data={} , params={} , allow_r if data != {} : url_req = requests.post(self.url, headers=headers , data=data, cookies=cookies , allow_redirects=allow_redirects) # post方法 + elif files != {} : + url_req = requests.post(self.url, data=files, headers=headers, timeout=300) else : url_req = requests.get(self.url, headers=headers, cookies=cookies , allow_redirects=allow_redirects, params=params) # get方法 @@ -63,6 +65,7 @@ def __init__(self, url , headers={} , cookies={} , data={} , params={} , allow_r self.cookies = cookies # 指定cookie,用于后续访问,微信中需要保持session即可 + def return_web_request_base_dict(self): return {'headers':self.headers , 'cookies':self.cookies} diff --git a/library/wechat/send.py b/library/wechat/send.py index d0c33a1..660add2 100644 --- a/library/wechat/send.py +++ b/library/wechat/send.py @@ -1,10 +1,13 @@ -import json, re -import time +import json, re, time, mimetypes, os +import random + +from requests_toolbelt.multipart.encoder import MultipartEncoder from library.config import wechat +from library.file.get_md5 import get_file_md5 from library.visit_url.request.cookie import Request_Url -from .friend import Get_Friend +from .friend import Get_Friend class Send_Msg(): @@ -15,13 +18,13 @@ def __init__(self, session_info_dict): self.session_info_dict = session_info_dict self.login_info = self.session_info_dict['login_info'] self.web_request_base_dict = self.session_info_dict['web_request_base_dict'] - self.base_url = self.login_info['url'] self.base_request = self.login_info['BaseRequest'] self.myself = self.session_info_dict['myself'] self.msgid = int(time.time() * 1000 * 1000 * 10) + self.pass_ticket = self.login_info['pass_ticket'] - def send(self, content, msgType='Test Message', tousername='filehelper' , post_field='UserName'): + def send(self, content, msgType='txt', filename='', tousername='filehelper' , post_field='UserName'): ''' 发送信息,返回类型为字典 ''' @@ -35,12 +38,216 @@ def send(self, content, msgType='Test Message', tousername='filehelper' , post_f if not re.search('@', tousername) and tousername != 'filehelper': return {'Msg': '发送失败,账号设置错误', 'Code':-1102, 'ErrMsg':'无法找到好友'} + + if msgType != 'txt' and (filename != '' and filename) : + result_dict = self._upload_media(filename, tousername, msgType, content) + return result_dict + + result_dict = self._send_text(tousername, content) + return result_dict + + + def _upload_media(self, filename, tousername, msgType, content): + base_url = self.session_info_dict['login_info']['file_url'] + url = base_url + '/webwxuploadmedia?f=json' + + name = os.path.basename(filename) # 文件名 + mime_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' # MIME格式,注意是根据文件后缀名来确认的 + media_type = re.split(r'/' , mime_type)[0] or 'application' + + # 微信识别的文档格式 + if media_type == 'image' : + media_type = 'pic' + elif media_type == 'audio' or media_type == 'video' : + media_type = 'video' + else : + media_type = 'doc' + + # 当用户上传类型设置为file时,强制media_type设置为file + # if msgType == 'file' : + # media_type = 'doc' + + modts = os.path.getmtime(filename) # 文件修改日期 + modstr = time.localtime(modts) + lastModifieDate = time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)', modstr) + file_content = open(filename, 'rb') + file_size = os.path.getsize(filename) # 文件大小 + chunksize = 524288 # 每个分开大小 + chunks = int((file_size - 1) / chunksize) + 1 + webwx_data_ticket = self.web_request_base_dict['cookies']['webwx_data_ticket'] + + uploadmediarequest = json.dumps({ + 'UploadType':2, + "BaseRequest": self.base_request, + "ClientMediaId": int(time.time() * 1000), + "TotalLen": file_size, + "StartPos": 0, + "DataLen": file_size, + "MediaType": 4, + 'FromUserName':self.myself['UserName'], + 'ToUserName':tousername, + 'FileMd5':get_file_md5(filename), + }, ensure_ascii=False).encode('utf8') + + for chunk in range(chunks): + ff = file_content.read(chunksize) + if chunks == 1: + multipart_encoder = MultipartEncoder( + fields={ + 'id': 'WU_FILE_0', + 'name': name, + 'type': mime_type, + 'lastModifiedDate': lastModifieDate, + 'size':str(file_size), + 'mediatype': media_type, + 'uploadmediarequest': uploadmediarequest, + 'webwx_data_ticket': webwx_data_ticket, + 'pass_ticket':self.pass_ticket, + 'filename': (name , ff, mime_type) + }, + boundary='---------------------------' + str(random.randint(1e28, 1e29 - 1)) + ) + else: + multipart_encoder = MultipartEncoder( + fields={ + 'id': 'WU_FILE_0', + 'name': name, + 'type': mime_type, + 'lastModifiedDate': lastModifieDate, + 'size':str(file_size), + 'chunks': str(chunks), + 'chunk': str(chunk), + 'mediatype': media_type, + 'uploadmediarequest': uploadmediarequest, + 'webwx_data_ticket': webwx_data_ticket, + 'pass_ticket':self.pass_ticket, + 'filename': (name , ff, mime_type) + }, + boundary='---------------------------' + str(random.randint(1e28, 1e29 - 1)) + ) + + self.web_request_base_dict['headers']['Origin-Type'] = 'https://wx2.qq.com' + self.web_request_base_dict['headers']['Content-Type'] = multipart_encoder.content_type + + open_url = Request_Url(url, files=multipart_encoder, **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + web_reselt_dict = json.loads(url_req.text) + + if web_reselt_dict['BaseResponse']['Ret'] != 0 : + result_dict = {'Msg': '发送失败,上传文件失败', 'Code':-1008, 'ErrMsg': web_reselt_dict['BaseResponse']} + else : + mediaid = web_reselt_dict['MediaId'] + if mediaid : + try : + if media_type == 'pic' : + result_dict = self._send_img(mediaid, tousername, content) + elif media_type == 'video': + result_dict = self._send_video(mediaid, tousername, content) + else : + result_dict = self._send_file(mediaid, name, file_size, tousername, content) + except : + result_dict = {'Msg': '发送失败,发送时出错', 'Code':-1009, 'ErrMsg': {}} + + if result_dict['Code'] != 0 : + content = content + '\n文件发送失败,原因:\n' + str(result_dict) + + result_dict = self._send_text(tousername, content) + return result_dict - url = '%s/webwxsendmsg' % self.base_url - payloads = { + + def _send_img(self, mediaid, tousername, content): + ''' + 发送图片 + ''' + base_url = self.login_info['url'] + url = base_url + '/webwxsendmsgimg?fun=async&f=json&lang=zh_CN' + data = { + 'BaseRequest':self.base_request, + 'Msg':{ + 'Type':3, + 'MediaId' :mediaid, + 'Content':content, + "FromUserName":self.myself['UserName'], + "ToUserName":tousername, + "LocalID":self.msgid, + "ClientMsgId":self.msgid + }, + "Scene":0 + } + data = json.dumps(data, ensure_ascii=False).encode('utf8') + open_url = Request_Url(url, data=data , **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + result_dict = self._handle_result(url_req) + return result_dict + + + def _send_file(self, mediaid, name, file_size, tousername, content): + ''' + 发送普通文件 + ''' + base_url = self.login_info['url'] + url = base_url + '/webwxsendappmsg?fun=async&f=json&pass_ticket=' + self.pass_ticket + fileend = re.split(r'.', name)[-1] # 后缀名 + data = { 'BaseRequest': self.base_request, 'Msg': { - 'Type': msgType, + 'Type': 6, + 'Content': "" + name + "6" + content + "" + str(file_size) + "" + mediaid + "" + fileend + "", + 'FromUserName': self.myself['UserName'], + 'ToUserName': tousername, + 'LocalID': self.msgid, + 'ClientMsgId': self.msgid, + }, + 'Scene': 0, + } + data = json.dumps(data, ensure_ascii=False).encode('utf8') + open_url = Request_Url(url, data=data , **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + result_dict = self._handle_result(url_req) + return result_dict + + + def _send_video(self, mediaid, tousername, content): + ''' + 发送视频文件 + ''' + base_url = self.login_info['url'] + url = base_url + '/webwxsendvideomsg?fun=async&f=json&lang=zh_CN' + data = { + 'BaseRequest':self.base_request, + 'Msg':{ + 'Type':43, + 'MediaId' :mediaid, + 'Content':content, + "FromUserName":self.myself['UserName'], + "ToUserName":tousername, + "LocalID":self.msgid, + "ClientMsgId":self.msgid + }, + "Scene":0 + } + self.login_info['msgid'] += 1 + data = json.dumps(data, ensure_ascii=False).encode('utf8') + open_url = Request_Url(url, data=data , **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + result_dict = self._handle_result(url_req) + return result_dict + + + def _send_text(self, tousername, content): + ''' + 发送文字 + ''' + base_url = self.login_info['url'] + url = '%s/webwxsendmsg' % base_url + data = { + 'BaseRequest': self.base_request, + 'Msg': { + 'Type': 'Test Message', 'Content': content, 'FromUserName': self.myself['UserName'], 'ToUserName': tousername, @@ -49,18 +256,16 @@ def send(self, content, msgType='Test Message', tousername='filehelper' , post_f }, 'Scene' : 0 } - self.login_info['msgid'] += 1 - data = json.dumps(payloads, ensure_ascii=False).encode('utf8') + data = json.dumps(data, ensure_ascii=False).encode('utf8') open_url = Request_Url(url, data=data , **self.web_request_base_dict) self.web_request_base_dict = open_url.return_web_request_base_dict() url_req = open_url.return_context() - - result_dict = self._send_result(url_req) + result_dict = self._handle_result(url_req) return result_dict - def _send_result(self, send_result): + def _handle_result(self, send_result): ''' 返回发送信息结果,返回类型为字典 ''' @@ -79,7 +284,6 @@ def _send_result(self, send_result): 'Data': '', } base_response = value_dict['BaseResponse'] - # raw_msg = base_response.get('ErrMsg', '') result_code = base_response.get('Ret' , -1006) try : diff --git a/library/wechat/send2.py b/library/wechat/send2.py new file mode 100644 index 0000000..e68abbf --- /dev/null +++ b/library/wechat/send2.py @@ -0,0 +1,200 @@ +from collections import OrderedDict +import json, re, time, mimetypes, os + +import requests + +from library.config import wechat +from library.file.get_md5 import get_file_md5 +from library.visit_url.request.cookie import Request_Url + +from .friend import Get_Friend + + +# import requests +# from requests_toolbelt.multipart.encoder import MultipartEncoder +class Send_Msg(): + ''' + 接受和发送信息 + ''' + def __init__(self, session_info_dict): + self.session_info_dict = session_info_dict + self.login_info = self.session_info_dict['login_info'] + self.web_request_base_dict = self.session_info_dict['web_request_base_dict'] + self.base_request = self.login_info['BaseRequest'] + self.myself = self.session_info_dict['myself'] + self.msgid = int(time.time() * 1000 * 1000 * 10) + self.pass_ticket = self.login_info['pass_ticket'] + + + def send(self, content, msgType='txt', filename='', tousername='filehelper' , post_field='UserName'): + ''' + 发送信息,返回类型为字典 + ''' + get_friend = Get_Friend(self.session_info_dict) + friend_dict = get_friend.get_singlefriend_dict(tousername, post_field=post_field) + + try : + tousername = friend_dict['UserName'] + except : + tousername = '' + + if not re.search('@', tousername) and tousername != 'filehelper': + return {'Msg': '发送失败,账号设置错误', 'Code':-1102, 'ErrMsg':'无法找到好友'} + + if msgType == 'txt' : + result_dict = self._txtmsg(tousername, content) + return result_dict + if msgType != 'txt' and (filename != '' and filename) : + result_dict = self._txtmsg(tousername, content) + + self._uploadmedia(filename, tousername, msgType) + return result_dict + + + result_dict = self._txtmsg(tousername, content) + return result_dict + + + def _uploadmedia(self, filename, tousername, msgType): + base_url = self.session_info_dict['login_info']['file_url'] + url = base_url + '/webwxuploadmedia?f=json' + + # 文件名 + name = os.path.basename(filename) + + # MIME格式,注意是根据文件后缀名来确认的 + mime_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc + # pic格式,直接显示。doc格式则显示为文件。 + if msgType == 'img' : + media_type = 'pic' + else : + media_type = 'doc' + + # 上一次修改日期 + modts = os.path.getmtime(filename) + modstr = time.localtime(modts) + lastModifieDate = time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)', modstr) + # 文件大小 + file_content = open(filename, 'rb') + # .read() + file_size = os.path.getsize(filename) + chunksize = 524288 + chunks = int((file_size - 1) / chunksize) + 1 + webwx_data_ticket = self.web_request_base_dict['cookies']['webwx_data_ticket'] + + uploadmediarequest = OrderedDict([ + ('UploadType', 2), + ('BaseRequest', self.base_request), + ('ClientMediaId', int(time.time() * 1000)), + ('TotalLen', file_size), + ('StartPos', 0), + ('DataLen', file_size), + ('MediaType', 4), + ('FromUserName', self.myself['UserName']), + ('ToUserName', tousername), + ('FileMd5', get_file_md5(filename))] + ) + uploadmediarequest = json.dumps(uploadmediarequest, separators=(',', ':')) + + for chunk in range(chunks): + ff = file_content.read(chunksize) + files = OrderedDict([ + ('id', 'WU_FILE_0'), + ('name', name), + ('type', mime_type), + ('lastModifiedDate', lastModifieDate), + ('size', str(file_size)), + ('chunks', None), + ('chunk', None), + ('mediatype', media_type), + ('uploadmediarequest', uploadmediarequest), + ('webwx_data_ticket', webwx_data_ticket), + ('pass_ticket', self.pass_ticket), + # ('filename', (name, ff , 'application/octet-stream')) + # ('filename', (name, ff , mime_type)) + ]) + + if chunks == 1: + del files['chunk']; del files['chunks'] + else: + files['chunk'], files['chunks'] = str(chunk), str(chunks) + + self.web_request_base_dict['headers']['Origin-Type'] = 'https://wx2.qq.com' + self.web_request_base_dict['headers']['Accept'] = '*/*' + self.web_request_base_dict['headers']['Content-Type'] = 'multipart/form-data; boundary=---------------------------160092666810849' + + open_url = Request_Url(url, data=files , files={'filename':(name, ff , mime_type)} , **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + web_reselt_dict = json.loads(url_req.text) + print(web_reselt_dict) + + url_req = requests.options(url, headers=self.web_request_base_dict['headers'], cookies=self.web_request_base_dict['cookies']) + + if web_reselt_dict['BaseResponse']['Ret'] != 0 : + return False + + return web_reselt_dict['MediaId'] + + + def _txtmsg(self, tousername, content): + base_url = self.login_info['url'] + url = '%s/webwxsendmsg' % base_url + payloads = { + 'BaseRequest': self.base_request, + 'Msg': { + 'Type': 'Test Message', + 'Content': content, + 'FromUserName': self.myself['UserName'], + 'ToUserName': tousername, + 'LocalID': self.msgid, + 'ClientMsgId': self.msgid, + }, + 'Scene' : 0 + } + + self.login_info['msgid'] += 1 + data = json.dumps(payloads, ensure_ascii=False).encode('utf8') + open_url = Request_Url(url, data=data , **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + + result_dict = self._send_result(url_req) + return result_dict + + + def _send_result(self, send_result): + ''' + 返回发送信息结果,返回类型为字典 + ''' + value_dict = {} + language = wechat.language + translation_dict = wechat.sendresult_translation_dict + + if send_result: + try: + value_dict = send_result.json() + except ValueError: + value_dict = { + 'BaseResponse': { + 'Ret': -1004, + 'ErrMsg': 'Unexpected return value', }, + 'Data': '', } + + base_response = value_dict['BaseResponse'] + # raw_msg = base_response.get('ErrMsg', '') + result_code = base_response.get('Ret' , -1006) + + try : + err_msg = translation_dict[language][result_code] + except : + err_msg = '未知错误
' + + translation_value_dict = {'Msg' : err_msg , 'Code' : result_code, 'ErrMsg' :value_dict} + + if result_code == 1101 : + translation_value_dict['ResCode'] = -1 + + return translation_value_dict diff --git a/library/wechat/send3.py b/library/wechat/send3.py new file mode 100644 index 0000000..f3a636a --- /dev/null +++ b/library/wechat/send3.py @@ -0,0 +1,222 @@ +from collections import OrderedDict +import json, re, time, mimetypes, os +import random + +import requests +from requests_toolbelt.multipart.encoder import MultipartEncoder + +from library.config import wechat +from library.file.get_md5 import get_file_md5 +from library.visit_url.request.cookie import Request_Url + +from .friend import Get_Friend + + +# import requests +# from requests_toolbelt.multipart.encoder import MultipartEncoder +class Send_Msg(): + ''' + 接受和发送信息 + ''' + def __init__(self, session_info_dict): + self.session_info_dict = session_info_dict + self.login_info = self.session_info_dict['login_info'] + self.web_request_base_dict = self.session_info_dict['web_request_base_dict'] + self.base_request = self.login_info['BaseRequest'] + self.myself = self.session_info_dict['myself'] + self.msgid = int(time.time() * 1000 * 1000 * 10) + self.pass_ticket = self.login_info['pass_ticket'] + + + def send(self, content, msgType='txt', filename='', tousername='filehelper' , post_field='UserName'): + ''' + 发送信息,返回类型为字典 + ''' + get_friend = Get_Friend(self.session_info_dict) + friend_dict = get_friend.get_singlefriend_dict(tousername, post_field=post_field) + + try : + tousername = friend_dict['UserName'] + except : + tousername = '' + + if not re.search('@', tousername) and tousername != 'filehelper': + return {'Msg': '发送失败,账号设置错误', 'Code':-1102, 'ErrMsg':'无法找到好友'} + + if msgType == 'txt' : + result_dict = self._txtmsg(tousername, content) + return result_dict + if msgType != 'txt' and (filename != '' and filename) : + result_dict = self._txtmsg(tousername, content) + + self._uploadmedia(filename, tousername, msgType) + return result_dict + + + result_dict = self._txtmsg(tousername, content) + return result_dict + + + def _uploadmedia(self, filename, tousername, msgType): + base_url = self.session_info_dict['login_info']['file_url'] + url = base_url + '/webwxuploadmedia?f=json' + + # 文件名 + name = os.path.basename(filename) + + # MIME格式,注意是根据文件后缀名来确认的 + mime_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc + # pic格式,直接显示。doc格式则显示为文件。 + if msgType == 'img' : + media_type = 'pic' + else : + media_type = 'doc' + + # 上一次修改日期 + modts = os.path.getmtime(filename) + modstr = time.localtime(modts) + lastModifieDate = time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)', modstr) + # 文件大小 + file_content = open(filename, 'rb') + # .read() + file_size = os.path.getsize(filename) + chunksize = 524288 + chunks = int((file_size - 1) / chunksize) + 1 + webwx_data_ticket = self.web_request_base_dict['cookies']['webwx_data_ticket'] + + uploadmediarequest = OrderedDict([ + ('UploadType', 2), + ('BaseRequest', self.base_request), + ('ClientMediaId', int(time.time() * 1000)), + ('TotalLen', file_size), + ('StartPos', 0), + ('DataLen', file_size), + ('MediaType', 4), + ('FromUserName', self.myself['UserName']), + ('ToUserName', tousername), + ('FileMd5', get_file_md5(filename))] + ) + uploadmediarequest = json.dumps(uploadmediarequest, separators=(',', ':')) + + for chunk in range(chunks): + ff = file_content.read(chunksize) + files = OrderedDict([ + ('id', 'WU_FILE_0'), + ('name', name), + ('type', mime_type), + ('lastModifiedDate', lastModifieDate), + ('size', str(file_size)), + ('chunks', None), + ('chunk', None), + ('mediatype', media_type), + ('uploadmediarequest', uploadmediarequest), + ('webwx_data_ticket', webwx_data_ticket), + ('pass_ticket', self.pass_ticket), + # ('filename', (c, ff , 'application/octet-stream')) + ('filename', (name, ff , mime_type)) + ]) + + multipart_encoder = MultipartEncoder( + fields={ + 'id': 'WU_FILE_4', + 'name': name, + 'type': mime_type, + 'lastModifiedDate': lastModifieDate, + 'size':str(file_size), + 'chunks': None, + 'chunk': None, + 'mediatype': media_type, + 'uploadmediarequest': uploadmediarequest, + 'webwx_data_ticket': webwx_data_ticket, + 'pass_ticket':self.pass_ticket, + 'filename': (name , ff, mime_type) + }, + boundary='---------------------------' + str(random.randint(1e28, 1e29 - 1)) + ) + + if chunks == 1: + del files['chunk']; del files['chunks'] + else: + files['chunk'], files['chunks'] = str(chunk), str(chunks) + + self.web_request_base_dict['headers']['Origin-Type'] = 'https://wx2.qq.com' + self.web_request_base_dict['headers']['Content-Type'] = multipart_encoder.content_type + + open_url = Request_Url(url, data=multipart_encoder, **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + web_reselt_dict = json.loads(url_req.text) + print(web_reselt_dict) + + url_req = requests.options(url, headers=self.web_request_base_dict['headers'], cookies=self.web_request_base_dict['cookies']) + # print(url_req.text) + + if web_reselt_dict['BaseResponse']['Ret'] != 0 : + return False + + return web_reselt_dict['MediaId'] + + # 发送 https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg?fun=async&f=json&pass_ticket=L7l0C%252BnlqeNp4B8%252B31gIkf3%252Fa3wXpkhwI2xz5MhLZFlvGq5r7K1iIwD8WvJC%252B7wB,POST参数{"BaseRequest":{"Uin":678772441,"Sid":"6OFd17VRWbrEOeJm","Skey":"@crypt_366cc18f_f08133ad368abf5f4801a90880d37329","DeviceID":"e386663393871620"},"Msg":{"Type":6,"Content":"InstallConfig.ini648@crypt_4c5e6a74_92c28fcbabf4674b7be228c4c65a0a67c00d34b8d2f38d64d46505f8463da7b76671ce1163f9f1a028c42447884e0fd4c3396f5fdb72f035e487b309afac194a3623cf8b65016b36375c0663cc19561dini","FromUserName":"@bc7e138f68724d8cabb32bc1b7362c35843e115e90a4831d61f04f092c1e7576","ToUserName":"@aa7fa4afa55dc116a2d8a1426266c03bd0c76b3997bb9461b71f1860df03d1ce","LocalID":"14923603744900350","ClientMsgId":"14923603744900350"},"Scene":0} + + + def _txtmsg(self, tousername, content): + base_url = self.login_info['url'] + url = '%s/webwxsendmsg' % base_url + payloads = { + 'BaseRequest': self.base_request, + 'Msg': { + 'Type': 'Test Message', + 'Content': content, + 'FromUserName': self.myself['UserName'], + 'ToUserName': tousername, + 'LocalID': self.msgid, + 'ClientMsgId': self.msgid, + }, + 'Scene' : 0 + } + + self.login_info['msgid'] += 1 + data = json.dumps(payloads, ensure_ascii=False).encode('utf8') + open_url = Request_Url(url, data=data , **self.web_request_base_dict) + self.web_request_base_dict = open_url.return_web_request_base_dict() + url_req = open_url.return_context() + + result_dict = self._send_result(url_req) + return result_dict + + + def _send_result(self, send_result): + ''' + 返回发送信息结果,返回类型为字典 + ''' + value_dict = {} + language = wechat.language + translation_dict = wechat.sendresult_translation_dict + + if send_result: + try: + value_dict = send_result.json() + except ValueError: + value_dict = { + 'BaseResponse': { + 'Ret': -1004, + 'ErrMsg': 'Unexpected return value', }, + 'Data': '', } + + base_response = value_dict['BaseResponse'] + # raw_msg = base_response.get('ErrMsg', '') + result_code = base_response.get('Ret' , -1006) + + try : + err_msg = translation_dict[language][result_code] + except : + err_msg = '未知错误
' + + translation_value_dict = {'Msg' : err_msg , 'Code' : result_code, 'ErrMsg' :value_dict} + + if result_code == 1101 : + translation_value_dict['ResCode'] = -1 + + return translation_value_dict diff --git a/lykchat/settings.py b/lykchat/settings.py index 4393939..49a2e5d 100644 --- a/lykchat/settings.py +++ b/lykchat/settings.py @@ -49,7 +49,7 @@ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -123,5 +123,4 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ - STATIC_URL = '/static/' diff --git a/lykchat/views.py b/lykchat/views.py index d0d5807..f737162 100644 --- a/lykchat/views.py +++ b/lykchat/views.py @@ -1,3 +1,4 @@ +from fileinput import filename import time from django.http.response import HttpResponseRedirect @@ -42,14 +43,14 @@ def _get_friend_info(self): self.session_info_dict = get_friend.update_friend_list() self.session_info_dict = get_friend.get_friend_dict() + - - def _send_msg(self , tousername, content, call_type='' , post_field='UserName'): + def _send_msg(self , tousername, content, filename='', msgType='txt', call_type='' , post_field='UserName'): ''' 发送信息,处理返回值 ''' send_msg = Send_Msg(self.session_info_dict) - send_result_dict = send_msg.send(content, tousername=tousername, post_field=post_field) + send_result_dict = send_msg.send(content, msgType=msgType, filename=filename, tousername=tousername, post_field=post_field) if send_result_dict['Code'] == -1 : self.status = 402 @@ -58,11 +59,9 @@ def _send_msg(self , tousername, content, call_type='' , post_field='UserName'): return send_result_dict if send_result_dict['Code'] == 0 : - # send_result = '信息:' + content + '
发送给' + str(send_result_dict['friend_dict']) + '
成功发送' send_result = '成功发送' return '

' else : - # send_result = '信息:' + content + '
发送给' + str(send_result_dict['friend_dict']) + '
结果为' + send_result_dict['ErrMsg'] + '
返回原文为' + str(send_result_dict) send_result = '成功失败' return '
' + send_result + '
' @@ -82,74 +81,94 @@ def sendmsg(self, request): parameter_dict = { 'username' : '用户' , 'pwd' : '接口密码,注意不等于登陆密码' , + 'type' : '发送信息类型,{"txt":"纯文字" ,"img":"图片","file":"发送文件","video":"视频"},可以为空,默认:没有文件为txt,有文件为file', 'fromalias':'发送者的微信号,目前没有使用该参数', - 'friendfield':'接受者的字段代号,{0:"NickName" , 1:"Alias" , 2:"RemarkName"},可以为空,默认为0 ', + 'friendfield':'接受者的字段代号,{0:"NickName" , 1:"Alias" , 2:"RemarkName"},可以为空,默认为0', 'friend':'接受者的昵称、微信号、备注名的其中一个,不能为空', 'content':'发送内容,不能为空', - 'url' : 'sendmsg?username=zabbix&pwd=123456&friendfield=1&friend=lyk-ops&content=test' + 'get方法测试url' : 'sendmsg?username=zabbix&pwd=123456&type=txt&friendfield=1&friend=lyk-ops&content=test' } - + # curl -F "file=@/root/a" 'http://127.0.0.1/sendmsg?username=zabbix&pwd=123456&type=img&friendfield=1&friend=lyk-ops&content=test' + friendfield_dict = {0:'NickName' , 1:'Alias' , 2:'RemarkName'} + resultpage = 'result.html' request_dict = {} send_result = {} - key_list = ['username', 'pwd', 'fromalias', 'friendfield', 'friend', 'content'] - for key in key_list : - # post或者get字段是否正确 + + for key in ['username', 'pwd', 'friend', 'content'] : try : - if request.method == 'GET' : + try: request_dict[key] = request.GET[key] - else : + except : request_dict[key] = request.POST[key] except : - if key == 'friend': - send_result = {'Code':-1101 , 'Msg' : '接受者不能为空', 'ErrMsg' : parameter_dict} - elif key == 'content' : - send_result = {'Code':-1101 , 'Msg' : '内容不能为空', 'ErrMsg' : parameter_dict} - else : - request_dict[key] = False + send_result = {'Code':-1101 , 'Msg' : '缺少必要的参数', 'ErrMsg' : parameter_dict} + return render(request, resultpage, {'result':send_result}) + + # 验证用户的接口密码是否正确 + username = request_dict['username'] + pwd = request_dict['pwd'] + password = wechat.user_mess_dict[username]['interface_pwd'] + if pwd != password : + send_result = {'Code':-1101 , 'Msg' : str(username) + '接口密码错误,请注意不等于登陆密码', 'ErrMsg' : parameter_dict} + return render(request, resultpage, {'result':send_result}) + + op_info = Manage_Logininfo() + self.session_info_dict = op_info.get_info(username) + status = self.session_info_dict['status'] + if status != 222 : + # 验证登陆情况 + send_result = {'Code': 1101 , 'Msg' : '微信还未登录或者退出登录', 'ErrMsg' : '' } + return render(request, resultpage, {'result':send_result}) + for key in ['type', 'friendfield'] : + try : + try: + request_dict[key] = request.GET[key] + except : + request_dict[key] = request.POST[key] + except : + if key == 'type' : + request_dict[key] = 'txt' + if key == 'friendfield' : + request_dict[key] = 0 + + if request_dict['type'] not in ['txt', 'img', 'file', 'video'] : + request_dict['type'] = 'txt' + + if request_dict['friend'] == 'filehelper' : + tousername = 'filehelper' + friendfield = 'NickName' + else : + tousername = request_dict['friend'] + fieldindex = int(request_dict['friendfield']) + if fieldindex not in friendfield_dict: + friendfield = 'NickName' + else : + friendfield = friendfield_dict[fieldindex] + try : - # 验证用户的接口密码是否正确 - username = request_dict['username'] - pwd = request_dict['pwd'] - password = wechat.user_mess_dict[username]['interface_pwd'] - - if pwd != password : - send_result = {'Code':-1101 , 'Msg' : str(username) + '接口密码错误,请注意不等于登陆密码', 'ErrMsg' : parameter_dict} + file = request.FILES['file'] + if file.size > wechat.max_upload_size: + send_result = { + 'status' : '上传文件超过最大值', + 'file_uuid':'', + } + return render(request, 'result.html', {'result':send_result}) + + request_dict['type'] = 'file' + from library.file.upload import upload_file + filename = str(file) + filename = upload_file(file, filename=filename , username=username) except : - send_result = {'Code':-1101 , 'Msg' : '用户或者接口密码不能为空', 'ErrMsg' : parameter_dict} - - if send_result == {} or not send_result : - op_info = Manage_Logininfo() - self.session_info_dict = op_info.get_info(username) - status = self.session_info_dict['status'] - if status != 222 : - # 验证登陆情况 - send_result = {'Code': 1101 , 'Msg' : '微信还未登录或者退出登录', 'ErrMsg' : '' } - - friendfield_dict = {0:'NickName' , 1:'Alias' , 2:'RemarkName'} - try : - if request_dict['friend'] == 'filehelper' : - tousername = 'filehelper' - friendfield = 'NickName' - else : - tousername = request_dict['friend'] - fieldindex = int(request_dict['friendfield']) - if fieldindex not in friendfield_dict: - friendfield = 'NickName' - else : - friendfield = friendfield_dict[fieldindex] - - nowtime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) - # content = 'lykchat 发送接口推送:' + request_dict['content'] - content = nowtime + '\n' + request_dict['content'] - except: - send_result = {'Code':-1101 , 'Msg' : '参数错误', 'ErrMsg' : parameter_dict} - - if send_result == {} or not send_result : - self._get_friend_info() - send_result = self._send_msg(tousername=tousername , content=content , call_type='interface', post_field=friendfield) - - return render(request, 'result.html', {'result':send_result}) + filename = '' + request_dict['type'] = 'txt' + + # nowtime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + # content = 'lykchat 发送接口推送:' + request_dict['content'] + # content = nowtime + '\n' + request_dict['content'] + content = request_dict['content'] + send_result = self._send_msg(tousername=tousername , content=content , msgType=request_dict['type'] , filename=filename, call_type='interface', post_field=friendfield) + return render(request, resultpage, {'result':send_result}) def check_login(self, request): @@ -230,8 +249,8 @@ def check_login(self, request): 'check_time' : int(time.time()), 'login_time' : session_info_dict['login_stamptime'], 'login_min' : login_min, - 'alias' : session_info_dict['alias'], - 'nickname' : session_info_dict['nickname'], + 'alias' : session_info_dict['myself']['Alias'], + 'nickname' : session_info_dict['myself']['NickName'], 'status' : login_status_code_dict[status]['descript'] } else : @@ -398,11 +417,27 @@ def _checklogin(self, request): if request.method == 'POST' : try : tousername = request.POST['username'] + + try : + file = request.FILES['file'] + if file.size > wechat.max_upload_size: + filename = False + else : + from library.file.upload import upload_file + filename = str(file) + filename = upload_file(file, filename=filename , username='zabbix') + except : + filename = False + try : # nowtime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) content = request.POST['content'] content = 'lykchat web页面推送:' + content - self.send_result = self._send_msg(tousername, content) + + if not filename : + self.send_result = self._send_msg(tousername, content) + else : + self.send_result = self._send_msg(tousername, content, filename=filename, msgType='file') except : self.send_result = '
发送内容为空
' except : @@ -457,7 +492,6 @@ def _displayhtml(self, request): except : display_html_dict['send_result'] = '' - op_info = Manage_Logininfo() op_info.update(self.session_info_dict) request.session['username'] = self.username diff --git a/templates/wechat.html b/templates/wechat.html index 445bf1e..08bcdff 100644 --- a/templates/wechat.html +++ b/templates/wechat.html @@ -47,6 +47,9 @@

-----------------------------------------发送信息-
+ +
+
diff --git a/test_sendfile.py b/test_sendfile.py new file mode 100644 index 0000000..94ee6eb --- /dev/null +++ b/test_sendfile.py @@ -0,0 +1,62 @@ +import os, random, sys, requests +from requests_toolbelt.multipart.encoder import MultipartEncoder + +url = 'http://127.0.0.1/sendmsg' +argvstr = sys.argv[1:] +argv_dict = {} +for argv in argvstr : + argv = str(argv).replace("\r\n" , "") + DICT = eval(argv) + argv_dict.update(DICT) + +# 例子/usr/local/python36/bin/python3 /opt/lykchat/test_upload.py "{'username':'zabbix','pwd':'123456','type':'img','friendfield':'1','friend':'lyk-ops','content':'恭喜发财','file':'/root/b.jpg'}" +# # curl -F "file=@/root/a" 'http://127.0.0.1/sendmsg?username=zabbix&pwd=123456&type=img&friendfield=1&friend=lyk-ops&content=test' + +parameter_dict = { + 'username' : '用户' , + 'pwd' : '接口密码,注意不等于登陆密码' , + 'type' : '发送信息类型,{"txt":"纯文字" ,"img":"图片","file":"发送文件","video":"视频"},可以为空,默认:没有文件为txt,有文件为file', + 'friendfield':'接受者的字段代号,{0:"NickName" , 1:"Alias" , 2:"RemarkName"},可以为空,默认为0', + 'friend':'接受者的昵称、微信号、备注名的其中一个,不能为空', + 'content':'发送内容,不能为空', + 'file':'文件绝对路径,可以为空', + '例子': + ''' + /usr/local/python36/bin/python3 /opt/lykchat/test_sendfile.py "{'username':'zabbix','pwd':'123456','type':'img','friendfield':'1','friend':'lyk-ops','content':'恭喜发财','file':'/root/b.jpg'}" + ''' + } + +headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0', + 'Referer': url + } + +for key in ['username', 'pwd', 'friend', 'content'] : + if key not in argv_dict : + print(parameter_dict) + exit(0) + +if 'file' not in argv_dict : + data = argv_dict + r = requests.post(url, data=data, headers=headers) + print(r.text) + exit(0) + +multipart_encoder = MultipartEncoder( + fields={ + 'username': argv_dict['username'], + 'pwd': argv_dict['pwd'], + 'type': 'txt', + 'friendfield': argv_dict['friendfield'], + 'friend': argv_dict['friend'], + 'content': argv_dict['content'], + 'file': (os.path.basename(argv_dict['file']) , open(argv_dict['file'], 'rb'), 'application/octet-stream') + }, + boundary='-----------------------------' + str(random.randint(1e28, 1e29 - 1)) + ) + +headers['Content-Type'] = multipart_encoder.content_type +# multipart/form-data + +r = requests.post(url, data=multipart_encoder, headers=headers, timeout=300) +print(r.text) From 4e1ac445272d0f9a3bc5795035a3fb2e16f08f12 Mon Sep 17 00:00:00 2001 From: lykops Date: Tue, 18 Apr 2017 00:02:25 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cd1b788..4d602a4 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 ![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/web页面--功能说明.jpg) 管理页面--微信登陆时长 -![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/微信登陆时间超过1天.jpg) - +![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/v2.1.0/doc/微信登陆时间超过1天.jpg) + 接口-发送信息成功 From a89071801718a1daed85664391d1d17f24b81f64 Mon Sep 17 00:00:00 2001 From: lykops Date: Tue, 18 Apr 2017 01:19:13 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 80 +++++-------------- doc/ChangeLog.md | 32 ++++++++ ...77\347\224\250\346\211\213\345\206\214.md" | 30 +++---- ...45\345\217\243\350\257\264\346\230\216.md" | 52 ++++++++++++ 4 files changed, 111 insertions(+), 83 deletions(-) create mode 100644 doc/ChangeLog.md create mode 100644 "doc/\345\217\221\351\200\201\344\277\241\346\201\257\346\216\245\345\217\243\350\257\264\346\230\216.md" diff --git a/README.md b/README.md index 4d602a4..91e8dc9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 实现的功能有用户登录管理、微信登陆管理和微信信息发送功能。 -## 特点 ## +## 特点 1、简单高效 基于个人微信号,模拟微信web端,部署和维护简单 @@ -13,82 +13,41 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 通过共享用户session和微信登陆信息,保证系统长期稳定运行 3、7*24不间断服务 计划任务定时检查微信登陆状态,微信保持登陆超过20天 - 4、用户管理 + 4、支持发送多媒体信息 + 支持发送图片、视频、文件等信息 + 5、用户管理 通过用户隔离微信个人号,不同用户管理不同微信号 用户密码分为管理密码和接口密码,保证用户信息安全性 - 5、微信信息安全 + 6、微信信息安全 不会监控和存储微信聊天信息 不会增加和删除好友 -## 截图 ## +## 截图 管理页面--功能展示 ![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/web页面--功能说明.jpg) 管理页面--微信登陆时长 -![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/v2.1.0/doc/微信登陆时间超过1天.jpg) +![微信登陆时长 截图](https://raw.githubusercontent.com/lykops/lykchat/V2.1.0/doc/微信登陆时间超过1天.jpg) - 接口-发送信息成功 - -![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/接口-发送信息成功.jpg) - - -## 模块 ## - - 1、管理web页面:可视化管理微信个人号 - 用户登录和认证 - 微信号登陆管理 - 发送信息给好友 - 2、发送信息接口: - 通过接口方式为其他业务系统发送信息给指定好友 - 3、计划任务检测微信登陆状态: - 获取所有登录微信成功的用户,通过调用检测微信登陆接口 - 4、会话保持模块: - 存储微信登陆信息和会话信息,同用户在任何地方登陆,保证微信登陆状态一致 - 5、模拟微信web端模块: - 通过微信登陆信息,访问微信web端接口,实现管理登陆、发送信息等功能。 - - -## V2.0.0版本说明 ## +![发送信息成功 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/接口-发送信息成功.jpg) + - 1、修复bug: - 微信登陆时间超过12小时自动退出,测试过程中测得最大登陆时长20天 - 2、完善功能: - 1)、微信会话保持机制: - 保存位置:之前保存在数据库中,修改为数据库只记录用户名,所有信息保持到文件中,减少数据库的查询、写入、加解密压力 - 动态更新微信登陆信息 - 调整会话信息内容 - 2)、优化微信检测登陆流程,大大缩短各个页面执行时间 - 3)、完善获取好友流程 - 3、新增功能: - 1)、增加用户管理机制 - 2)、好友信息缓存机制 - 4、取消功能: - 接受和处理新信息 +## 发送信息接口使用说明 +[https://github.com/lykops/lykchat/wiki/%E5%8F%91%E9%80%81%E4%BF%A1%E6%81%AF%E6%8E%A5%E5%8F%A3%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E](https://github.com/lykops/lykchat/wiki/%E5%8F%91%E9%80%81%E4%BF%A1%E6%81%AF%E6%8E%A5%E5%8F%A3%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E "发送信息接口") +## 模块和工作流程 +[https://github.com/lykops/lykchat/wiki/%E6%A8%A1%E5%9D%97%E5%92%8C%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B](https://github.com/lykops/lykchat/wiki/%E6%A8%A1%E5%9D%97%E5%92%8C%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B "模块和工作流程") -## 发送信息接口 ## - URL地址:http://IP(或者域名)/sendmsg - 支持post和get方法 - 请求参数说明: - 'username' : 管理用户,同管理web页面,通过用户确认微信发送者 - 'pwd' : 接口密码,注意不等于登陆密码, - 'friendfield':接受信息的好友字段代号,0昵称,1微信号,2备注名,可以为空,默认为0 - 'friend': 接受信息的好友的昵称、微信号、备注名的其中之一,不能为空 - 'content': 发送内容,不能为空 - 注意: - friend一定是该用户下的登陆微信好友列表中的 - friendfield最好是微信号(Alias),也可以使用昵称(NickName)或者备注名(RemarkName)(但不能重复出现) - 由于好友列表使用缓存机制,新增好友可能发送信息不成功 - 返回信息: - json格式,{'Msg': 执行结果, 'Code':返回代码, 'ErrMsg':如果-1005返回参数列表,其他发送微信返回信息} - 常见code:0成功;-1101参数错误;-1102无法找到好友;1101微信号退出登录,其他为微信返回错误 - 例子:http://192.168.100.104/sendmsg?username=zabbix&pwd=123456&friendfield=1&friend=lyk-ops&content=test +## 安装手册 +[https://github.com/lykops/lykchat/wiki/%E5%AE%89%E8%A3%85%E6%89%8B%E5%86%8C](https://github.com/lykops/lykchat/wiki/%E5%AE%89%E8%A3%85%E6%89%8B%E5%86%8C "安装手册") +## ChangeLog +[https://github.com/lykops/lykchat/wiki/ChangeLog](https://github.com/lykops/lykchat/wiki/ChangeLog "ChangeLog") -## 说明 ## +## 说明 1、作者尽可能通过严谨测试来验证系统功能,但由于专业水平有限,无法避免出现bug。 2、该项目是基于微信web端进行开发的 @@ -105,6 +64,3 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 邮箱:liyingke112@126.com -![WIKI](https://github.com/lykops/lykchat/wiki/) - - diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md new file mode 100644 index 0000000..a267461 --- /dev/null +++ b/doc/ChangeLog.md @@ -0,0 +1,32 @@ +# V2.1.0 +## 升级内容 + 新增发送图片、视频、文件等多媒体信息 +## 从v2.0.0更新步骤 + 1、下载最新版本 + 2、安装依赖包 + /usr/local/python36/bin/pip3 install -r /opt/lykchat/install/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + 3、修改配置文件 + 配置文件library/config/wechat.py + 新增上传文件最大数max_upload_size(默认为5M,建议不要上传文件太大,导致访问接口超时) + 4、修改nginx的上传文件最大值 + client_max_body_size 10m; +## 说明事项 + django默认启用防CSRF(Cross-site request forgery跨站请求伪造),导致无法使用post方法调用该接口,所以作者强制关闭了防csrf功能。 + 如果你觉得有安全隐患,又不需要发送多媒体文件,请下载2.0版本:https://codeload.github.com/lykops/lykchat/zip/master + +# V2.0.0 + + 1、修复bug: + 微信登陆时间超过12小时自动退出,测试过程中测得最大登陆时长20天 + 2、完善功能: + 1)、微信会话保持机制: + 保存位置:之前保存在数据库中,修改为数据库只记录用户名,所有信息保持到文件中,减少数据库的查询、写入、加解密压力 + 动态更新微信登陆信息 + 调整会话信息内容 + 2)、优化微信检测登陆流程,大大缩短各个页面执行时间 + 3)、完善获取好友流程 + 3、新增功能: + 1)、增加用户管理机制 + 2)、好友信息缓存机制 + 4、取消功能: + 接受和处理新信息 \ No newline at end of file diff --git "a/doc/\344\275\277\347\224\250\346\211\213\345\206\214.md" "b/doc/\344\275\277\347\224\250\346\211\213\345\206\214.md" index 6bc734c..daa0090 100644 --- "a/doc/\344\275\277\347\224\250\346\211\213\345\206\214.md" +++ "b/doc/\344\275\277\347\224\250\346\211\213\345\206\214.md" @@ -1,7 +1,3 @@ -# lykchat工作流程 -![lykchat工作流程](https://raw.githubusercontent.com/lykops/lykchat/master/doc/lykchat工作流程.jpg) - - # 模块说明 ## 管理web页面 @@ -12,26 +8,15 @@ 发送信息给好友:用于测试发送功能是否可用 通过选择好友列表显示获取需要发送信息的好友 好友信息列表只展示文件传输助手、除了自己外的好友(疑似好友表示没有设置该好友没有设置性别)、部分群(是根据第一页好友信息获取的),自动屏蔽掉公众号、微信系统用户、好友为自己。 -## 发送信息接口 - 通过接口方式为其他业务系统发送信息给指定好友 - URL地址:http://IP(或者域名)/sendmsg - 支持post和get方法 - 请求参数说明: - 'username' : 管理用户,同管理web页面,通过用户确认微信发送者 - 'pwd' : 接口密码,注意不等于登陆密码 - 'friendfield':接受信息的好友字段代号,{0:"NickName" , 1:"Alias" , 2:"RemarkName"},可以为空,默认为0 - 'friend': 接受信息的好友的昵称、微信号、备注名的其中之一,不能为空 - 'content': 发送内容,不能为空 - 注意: - friend一定是该用户下的登陆微信好友列表中的 - friendfield最好是微信号(Alias),也可以使用昵称(NickName)或者备注名(RemarkName),但不能重复 - 返回信息: - json格式,{'Msg': 执行结果, 'Code':返回代码, 'ErrMsg':如果-1005返回参数列表,其他发送微信返回信息} - 常见code:0成功,-1101参数错误,-1102无法找到好友,1101微信号退出登录,其他为微信返回错误 - 例子:http://192.168.100.104/sendmsg?username=zabbix&pwd=123456&friendfield=1&friend=lyk-ops&content=test + 上传需要发送的文件 + +## 发送信息接口 ## +[https://github.com/lykops/lykchat/wiki/%E5%8F%91%E9%80%81%E4%BF%A1%E6%81%AF%E6%8E%A5%E5%8F%A3%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E](https://github.com/lykops/lykchat/wiki/%E5%8F%91%E9%80%81%E4%BF%A1%E6%81%AF%E6%8E%A5%E5%8F%A3%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E "发送信息接口") + ## 计划任务 检测微信登陆状态: 获取所有登录微信成功的用户,通过调用检测微信登陆接口 + ## 会话保持模块 存储微信登陆信息和会话信息,同用户在任何地方登陆,保证微信登陆状态一致 访问管理页面和微信登陆检测接口,根据session或者参数获取用户名,然后读取会话文件,页面操作后,再一次更新数据库和会话文件 @@ -47,7 +32,10 @@ json格式 每次访问更新 默认存放在/dev/shm/lykchat下,根据用户名命名 + ## 模拟微信web端模块 它是该系统的核心和底层模块。 通过微信登陆信息,访问微信web端接口,实现管理登陆、发送信息等功能。 +# lykchat工作流程 +![lykchat工作流程](https://raw.githubusercontent.com/lykops/lykchat/master/doc/lykchat工作流程.jpg) \ No newline at end of file diff --git "a/doc/\345\217\221\351\200\201\344\277\241\346\201\257\346\216\245\345\217\243\350\257\264\346\230\216.md" "b/doc/\345\217\221\351\200\201\344\277\241\346\201\257\346\216\245\345\217\243\350\257\264\346\230\216.md" new file mode 100644 index 0000000..b4935c4 --- /dev/null +++ "b/doc/\345\217\221\351\200\201\344\277\241\346\201\257\346\216\245\345\217\243\350\257\264\346\230\216.md" @@ -0,0 +1,52 @@ +发送信息接口为其他业务系统发送信息给指定好友 + +# 特点 + 支持post和get方法 + 除了支持纯文字外,还支持图片、文字等诸多多媒体文件。 + +# 注意 + django默认启用防CSRF(Cross-site request forgery跨站请求伪造),导致无法使用post方法调用该接口,所以作者强制关闭了防csrf功能。 + 如果你觉得有安全隐患,又不需要发送多媒体文件,请下载2.0版本:https://codeload.github.com/lykops/lykchat/zip/master + +# 参数说明 + 下面参数支持post和get方法 + 'username' : + 管理用户,同管理web页面,通过用户确认微信发送者 + 'pwd' : + 接口密码,注意不等于登陆密码 + 'type' : + '发送信息类型 + 可选{"txt":"纯文字" ,"img":"图片","file":"发送文件","video":"视频"},可以为空 + 默认:不发送文件为txt,发送文件为file + 'fromalias': + 保留字段 + 发送者的微信号,目前没有使用该参数 + 'friendfield': + 接受者的字段代号,可选{0:"NickName" , 1:"Alias" , 2:"RemarkName"},可以为空 + 默认为0' + 'friend': + 接受者的昵称、微信号、备注名的其中一个,不能为空 + 'content': + 发送内容,不能为空 + 必须是post方法: + 'file': + 需要发送的文件, + 注意: + friend一定是该用户下的登陆微信好友列表中的 + friendfield最好是微信号(Alias),也可以使用昵称(NickName)或者备注名(RemarkName),但不能重复 + +# 返回信息 + json格式,{'Msg': 执行结果, 'Code':返回代码, 'ErrMsg':如果-1005返回参数列表,其他发送微信返回信息} + 常见code:0成功,-1101参数错误,-1102无法找到好友,1101微信号退出登录,其他为微信返回错误 + +# 使用实例 + 接口URL地址:http://IP(或者域名)/sendmsg + 发送纯文字: + http://192.168.100.104/sendmsg?username=zabbix&pwd=123456&friendfield=1&friend=lyk-ops&content=test + 发送多媒体: + 方法1:Linux的curl命令 + curl -F "file=@/root/a" 'http://127.0.0.1/sendmsg?username=zabbix&pwd=123456&type=img&friendfield=1&friend=lyk-ops&content=test' + 方法2:python脚本test_sendfile.py + 请参照该项目的根目录python脚本test_sendfile.py,执行 + /usr/local/python36/bin/python3 /opt/lykchat/test_upload.py "{'username':'zabbix','pwd':'123456','type':'img','friendfield':'1','friend':'lyk-ops','content':'恭喜发财','file':'/root/b.jpg'}" + \ No newline at end of file From 84cf7bf50858253159fb9a8b8f920f52ad11e5cc Mon Sep 17 00:00:00 2001 From: lykops Date: Tue, 18 Apr 2017 01:21:59 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 91e8dc9..0e5d1fc 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,16 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 ## 截图 管理页面--功能展示 + ![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/web页面--功能说明.jpg) -管理页面--微信登陆时长 + +管理页面--微信登陆时长 + ![微信登陆时长 截图](https://raw.githubusercontent.com/lykops/lykchat/V2.1.0/doc/微信登陆时间超过1天.jpg) 接口-发送信息成功 + ![发送信息成功 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/接口-发送信息成功.jpg) From fe34f879d54e5f6cb90572caaa979055b7d8bd0c Mon Sep 17 00:00:00 2001 From: lykops Date: Tue, 18 Apr 2017 01:26:21 +0800 Subject: [PATCH 05/11] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e5d1fc..11ca9f9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # lykchat信息发送系统 lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基于个人微信号,为系统管理人员提供信息发送工具。 + 实现的功能有用户登录管理、微信登陆管理和微信信息发送功能。 @@ -14,7 +15,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 3、7*24不间断服务 计划任务定时检查微信登陆状态,微信保持登陆超过20天 4、支持发送多媒体信息 - 支持发送图片、视频、文件等信息 + 除了支持发送纯文字信息外,还支持发送图片、视频、文件等信息 5、用户管理 通过用户隔离微信个人号,不同用户管理不同微信号 用户密码分为管理密码和接口密码,保证用户信息安全性 @@ -30,7 +31,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 ![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/web页面--功能说明.jpg) -管理页面--微信登陆时长 +管理页面--微信登陆时长 ![微信登陆时长 截图](https://raw.githubusercontent.com/lykops/lykchat/V2.1.0/doc/微信登陆时间超过1天.jpg) From 1f6ecaf73d00b8c1090ba98606ad674d01327c38 Mon Sep 17 00:00:00 2001 From: lykops Date: Tue, 18 Apr 2017 01:51:21 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- library/wechat/send2.py | 200 ------------------------------------ library/wechat/send3.py | 222 ---------------------------------------- 3 files changed, 1 insertion(+), 423 deletions(-) delete mode 100644 library/wechat/send2.py delete mode 100644 library/wechat/send3.py diff --git a/README.md b/README.md index 11ca9f9..3cba93a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 基于个人微信号,模拟微信web端,部署和维护简单 web管理页面实现可视化管理微信登陆 接口采用URL,简化调用复杂度,返回结果均为json格式 - 2、信息共享 + 2、信息共享 通过共享用户session和微信登陆信息,保证系统长期稳定运行 3、7*24不间断服务 计划任务定时检查微信登陆状态,微信保持登陆超过20天 diff --git a/library/wechat/send2.py b/library/wechat/send2.py deleted file mode 100644 index e68abbf..0000000 --- a/library/wechat/send2.py +++ /dev/null @@ -1,200 +0,0 @@ -from collections import OrderedDict -import json, re, time, mimetypes, os - -import requests - -from library.config import wechat -from library.file.get_md5 import get_file_md5 -from library.visit_url.request.cookie import Request_Url - -from .friend import Get_Friend - - -# import requests -# from requests_toolbelt.multipart.encoder import MultipartEncoder -class Send_Msg(): - ''' - 接受和发送信息 - ''' - def __init__(self, session_info_dict): - self.session_info_dict = session_info_dict - self.login_info = self.session_info_dict['login_info'] - self.web_request_base_dict = self.session_info_dict['web_request_base_dict'] - self.base_request = self.login_info['BaseRequest'] - self.myself = self.session_info_dict['myself'] - self.msgid = int(time.time() * 1000 * 1000 * 10) - self.pass_ticket = self.login_info['pass_ticket'] - - - def send(self, content, msgType='txt', filename='', tousername='filehelper' , post_field='UserName'): - ''' - 发送信息,返回类型为字典 - ''' - get_friend = Get_Friend(self.session_info_dict) - friend_dict = get_friend.get_singlefriend_dict(tousername, post_field=post_field) - - try : - tousername = friend_dict['UserName'] - except : - tousername = '' - - if not re.search('@', tousername) and tousername != 'filehelper': - return {'Msg': '发送失败,账号设置错误', 'Code':-1102, 'ErrMsg':'无法找到好友'} - - if msgType == 'txt' : - result_dict = self._txtmsg(tousername, content) - return result_dict - if msgType != 'txt' and (filename != '' and filename) : - result_dict = self._txtmsg(tousername, content) - - self._uploadmedia(filename, tousername, msgType) - return result_dict - - - result_dict = self._txtmsg(tousername, content) - return result_dict - - - def _uploadmedia(self, filename, tousername, msgType): - base_url = self.session_info_dict['login_info']['file_url'] - url = base_url + '/webwxuploadmedia?f=json' - - # 文件名 - name = os.path.basename(filename) - - # MIME格式,注意是根据文件后缀名来确认的 - mime_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc - # pic格式,直接显示。doc格式则显示为文件。 - if msgType == 'img' : - media_type = 'pic' - else : - media_type = 'doc' - - # 上一次修改日期 - modts = os.path.getmtime(filename) - modstr = time.localtime(modts) - lastModifieDate = time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)', modstr) - # 文件大小 - file_content = open(filename, 'rb') - # .read() - file_size = os.path.getsize(filename) - chunksize = 524288 - chunks = int((file_size - 1) / chunksize) + 1 - webwx_data_ticket = self.web_request_base_dict['cookies']['webwx_data_ticket'] - - uploadmediarequest = OrderedDict([ - ('UploadType', 2), - ('BaseRequest', self.base_request), - ('ClientMediaId', int(time.time() * 1000)), - ('TotalLen', file_size), - ('StartPos', 0), - ('DataLen', file_size), - ('MediaType', 4), - ('FromUserName', self.myself['UserName']), - ('ToUserName', tousername), - ('FileMd5', get_file_md5(filename))] - ) - uploadmediarequest = json.dumps(uploadmediarequest, separators=(',', ':')) - - for chunk in range(chunks): - ff = file_content.read(chunksize) - files = OrderedDict([ - ('id', 'WU_FILE_0'), - ('name', name), - ('type', mime_type), - ('lastModifiedDate', lastModifieDate), - ('size', str(file_size)), - ('chunks', None), - ('chunk', None), - ('mediatype', media_type), - ('uploadmediarequest', uploadmediarequest), - ('webwx_data_ticket', webwx_data_ticket), - ('pass_ticket', self.pass_ticket), - # ('filename', (name, ff , 'application/octet-stream')) - # ('filename', (name, ff , mime_type)) - ]) - - if chunks == 1: - del files['chunk']; del files['chunks'] - else: - files['chunk'], files['chunks'] = str(chunk), str(chunks) - - self.web_request_base_dict['headers']['Origin-Type'] = 'https://wx2.qq.com' - self.web_request_base_dict['headers']['Accept'] = '*/*' - self.web_request_base_dict['headers']['Content-Type'] = 'multipart/form-data; boundary=---------------------------160092666810849' - - open_url = Request_Url(url, data=files , files={'filename':(name, ff , mime_type)} , **self.web_request_base_dict) - self.web_request_base_dict = open_url.return_web_request_base_dict() - url_req = open_url.return_context() - web_reselt_dict = json.loads(url_req.text) - print(web_reselt_dict) - - url_req = requests.options(url, headers=self.web_request_base_dict['headers'], cookies=self.web_request_base_dict['cookies']) - - if web_reselt_dict['BaseResponse']['Ret'] != 0 : - return False - - return web_reselt_dict['MediaId'] - - - def _txtmsg(self, tousername, content): - base_url = self.login_info['url'] - url = '%s/webwxsendmsg' % base_url - payloads = { - 'BaseRequest': self.base_request, - 'Msg': { - 'Type': 'Test Message', - 'Content': content, - 'FromUserName': self.myself['UserName'], - 'ToUserName': tousername, - 'LocalID': self.msgid, - 'ClientMsgId': self.msgid, - }, - 'Scene' : 0 - } - - self.login_info['msgid'] += 1 - data = json.dumps(payloads, ensure_ascii=False).encode('utf8') - open_url = Request_Url(url, data=data , **self.web_request_base_dict) - self.web_request_base_dict = open_url.return_web_request_base_dict() - url_req = open_url.return_context() - - result_dict = self._send_result(url_req) - return result_dict - - - def _send_result(self, send_result): - ''' - 返回发送信息结果,返回类型为字典 - ''' - value_dict = {} - language = wechat.language - translation_dict = wechat.sendresult_translation_dict - - if send_result: - try: - value_dict = send_result.json() - except ValueError: - value_dict = { - 'BaseResponse': { - 'Ret': -1004, - 'ErrMsg': 'Unexpected return value', }, - 'Data': '', } - - base_response = value_dict['BaseResponse'] - # raw_msg = base_response.get('ErrMsg', '') - result_code = base_response.get('Ret' , -1006) - - try : - err_msg = translation_dict[language][result_code] - except : - err_msg = '未知错误
' - - translation_value_dict = {'Msg' : err_msg , 'Code' : result_code, 'ErrMsg' :value_dict} - - if result_code == 1101 : - translation_value_dict['ResCode'] = -1 - - return translation_value_dict diff --git a/library/wechat/send3.py b/library/wechat/send3.py deleted file mode 100644 index f3a636a..0000000 --- a/library/wechat/send3.py +++ /dev/null @@ -1,222 +0,0 @@ -from collections import OrderedDict -import json, re, time, mimetypes, os -import random - -import requests -from requests_toolbelt.multipart.encoder import MultipartEncoder - -from library.config import wechat -from library.file.get_md5 import get_file_md5 -from library.visit_url.request.cookie import Request_Url - -from .friend import Get_Friend - - -# import requests -# from requests_toolbelt.multipart.encoder import MultipartEncoder -class Send_Msg(): - ''' - 接受和发送信息 - ''' - def __init__(self, session_info_dict): - self.session_info_dict = session_info_dict - self.login_info = self.session_info_dict['login_info'] - self.web_request_base_dict = self.session_info_dict['web_request_base_dict'] - self.base_request = self.login_info['BaseRequest'] - self.myself = self.session_info_dict['myself'] - self.msgid = int(time.time() * 1000 * 1000 * 10) - self.pass_ticket = self.login_info['pass_ticket'] - - - def send(self, content, msgType='txt', filename='', tousername='filehelper' , post_field='UserName'): - ''' - 发送信息,返回类型为字典 - ''' - get_friend = Get_Friend(self.session_info_dict) - friend_dict = get_friend.get_singlefriend_dict(tousername, post_field=post_field) - - try : - tousername = friend_dict['UserName'] - except : - tousername = '' - - if not re.search('@', tousername) and tousername != 'filehelper': - return {'Msg': '发送失败,账号设置错误', 'Code':-1102, 'ErrMsg':'无法找到好友'} - - if msgType == 'txt' : - result_dict = self._txtmsg(tousername, content) - return result_dict - if msgType != 'txt' and (filename != '' and filename) : - result_dict = self._txtmsg(tousername, content) - - self._uploadmedia(filename, tousername, msgType) - return result_dict - - - result_dict = self._txtmsg(tousername, content) - return result_dict - - - def _uploadmedia(self, filename, tousername, msgType): - base_url = self.session_info_dict['login_info']['file_url'] - url = base_url + '/webwxuploadmedia?f=json' - - # 文件名 - name = os.path.basename(filename) - - # MIME格式,注意是根据文件后缀名来确认的 - mime_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc - # pic格式,直接显示。doc格式则显示为文件。 - if msgType == 'img' : - media_type = 'pic' - else : - media_type = 'doc' - - # 上一次修改日期 - modts = os.path.getmtime(filename) - modstr = time.localtime(modts) - lastModifieDate = time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)', modstr) - # 文件大小 - file_content = open(filename, 'rb') - # .read() - file_size = os.path.getsize(filename) - chunksize = 524288 - chunks = int((file_size - 1) / chunksize) + 1 - webwx_data_ticket = self.web_request_base_dict['cookies']['webwx_data_ticket'] - - uploadmediarequest = OrderedDict([ - ('UploadType', 2), - ('BaseRequest', self.base_request), - ('ClientMediaId', int(time.time() * 1000)), - ('TotalLen', file_size), - ('StartPos', 0), - ('DataLen', file_size), - ('MediaType', 4), - ('FromUserName', self.myself['UserName']), - ('ToUserName', tousername), - ('FileMd5', get_file_md5(filename))] - ) - uploadmediarequest = json.dumps(uploadmediarequest, separators=(',', ':')) - - for chunk in range(chunks): - ff = file_content.read(chunksize) - files = OrderedDict([ - ('id', 'WU_FILE_0'), - ('name', name), - ('type', mime_type), - ('lastModifiedDate', lastModifieDate), - ('size', str(file_size)), - ('chunks', None), - ('chunk', None), - ('mediatype', media_type), - ('uploadmediarequest', uploadmediarequest), - ('webwx_data_ticket', webwx_data_ticket), - ('pass_ticket', self.pass_ticket), - # ('filename', (c, ff , 'application/octet-stream')) - ('filename', (name, ff , mime_type)) - ]) - - multipart_encoder = MultipartEncoder( - fields={ - 'id': 'WU_FILE_4', - 'name': name, - 'type': mime_type, - 'lastModifiedDate': lastModifieDate, - 'size':str(file_size), - 'chunks': None, - 'chunk': None, - 'mediatype': media_type, - 'uploadmediarequest': uploadmediarequest, - 'webwx_data_ticket': webwx_data_ticket, - 'pass_ticket':self.pass_ticket, - 'filename': (name , ff, mime_type) - }, - boundary='---------------------------' + str(random.randint(1e28, 1e29 - 1)) - ) - - if chunks == 1: - del files['chunk']; del files['chunks'] - else: - files['chunk'], files['chunks'] = str(chunk), str(chunks) - - self.web_request_base_dict['headers']['Origin-Type'] = 'https://wx2.qq.com' - self.web_request_base_dict['headers']['Content-Type'] = multipart_encoder.content_type - - open_url = Request_Url(url, data=multipart_encoder, **self.web_request_base_dict) - self.web_request_base_dict = open_url.return_web_request_base_dict() - url_req = open_url.return_context() - web_reselt_dict = json.loads(url_req.text) - print(web_reselt_dict) - - url_req = requests.options(url, headers=self.web_request_base_dict['headers'], cookies=self.web_request_base_dict['cookies']) - # print(url_req.text) - - if web_reselt_dict['BaseResponse']['Ret'] != 0 : - return False - - return web_reselt_dict['MediaId'] - - # 发送 https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg?fun=async&f=json&pass_ticket=L7l0C%252BnlqeNp4B8%252B31gIkf3%252Fa3wXpkhwI2xz5MhLZFlvGq5r7K1iIwD8WvJC%252B7wB,POST参数{"BaseRequest":{"Uin":678772441,"Sid":"6OFd17VRWbrEOeJm","Skey":"@crypt_366cc18f_f08133ad368abf5f4801a90880d37329","DeviceID":"e386663393871620"},"Msg":{"Type":6,"Content":"InstallConfig.ini648@crypt_4c5e6a74_92c28fcbabf4674b7be228c4c65a0a67c00d34b8d2f38d64d46505f8463da7b76671ce1163f9f1a028c42447884e0fd4c3396f5fdb72f035e487b309afac194a3623cf8b65016b36375c0663cc19561dini","FromUserName":"@bc7e138f68724d8cabb32bc1b7362c35843e115e90a4831d61f04f092c1e7576","ToUserName":"@aa7fa4afa55dc116a2d8a1426266c03bd0c76b3997bb9461b71f1860df03d1ce","LocalID":"14923603744900350","ClientMsgId":"14923603744900350"},"Scene":0} - - - def _txtmsg(self, tousername, content): - base_url = self.login_info['url'] - url = '%s/webwxsendmsg' % base_url - payloads = { - 'BaseRequest': self.base_request, - 'Msg': { - 'Type': 'Test Message', - 'Content': content, - 'FromUserName': self.myself['UserName'], - 'ToUserName': tousername, - 'LocalID': self.msgid, - 'ClientMsgId': self.msgid, - }, - 'Scene' : 0 - } - - self.login_info['msgid'] += 1 - data = json.dumps(payloads, ensure_ascii=False).encode('utf8') - open_url = Request_Url(url, data=data , **self.web_request_base_dict) - self.web_request_base_dict = open_url.return_web_request_base_dict() - url_req = open_url.return_context() - - result_dict = self._send_result(url_req) - return result_dict - - - def _send_result(self, send_result): - ''' - 返回发送信息结果,返回类型为字典 - ''' - value_dict = {} - language = wechat.language - translation_dict = wechat.sendresult_translation_dict - - if send_result: - try: - value_dict = send_result.json() - except ValueError: - value_dict = { - 'BaseResponse': { - 'Ret': -1004, - 'ErrMsg': 'Unexpected return value', }, - 'Data': '', } - - base_response = value_dict['BaseResponse'] - # raw_msg = base_response.get('ErrMsg', '') - result_code = base_response.get('Ret' , -1006) - - try : - err_msg = translation_dict[language][result_code] - except : - err_msg = '未知错误
' - - translation_value_dict = {'Msg' : err_msg , 'Code' : result_code, 'ErrMsg' :value_dict} - - if result_code == 1101 : - translation_value_dict['ResCode'] = -1 - - return translation_value_dict From 88590bc6254581d43afb38bc980b62c2546d8035 Mon Sep 17 00:00:00 2001 From: lykops Date: Sun, 23 Apr 2017 21:16:39 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86?= =?UTF-8?q?=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...11\350\243\205\346\211\213\345\206\214.md" | 8 ++++---- ...\350\266\205\350\277\2071\345\244\251.jpg" | Bin 25652 -> 27121 bytes lykchat/settings.py | 5 +++++ test_sendfile.py | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git "a/doc/\345\256\211\350\243\205\346\211\213\345\206\214.md" "b/doc/\345\256\211\350\243\205\346\211\213\345\206\214.md" index ed00a43..073be93 100644 --- "a/doc/\345\256\211\350\243\205\346\211\213\345\206\214.md" +++ "b/doc/\345\256\211\350\243\205\346\211\213\345\206\214.md" @@ -6,7 +6,7 @@ Python3+django1.10 测试环境使用Python3.5.2、3.6.0两个版本测试 ## web服务器 - Nginx + Nginx 主要解决静态文件展示。 测试环境为nginx 1.10.2 ## 数据库 @@ -21,7 +21,7 @@ ## 安装依赖包 yum install -y epel-release - yum install telnet ntpdate lrzsz bash glibc openssl vim automake autoconf gcc xz ncurses-devel patch python-devel git python-pip gcc-c++ redhat-rpm-config -y + yum install telnet ntpdate lrzsz bash glibc openssl vim automake autoconf gcc xz ncurses-devel patch python-devel git python-pip gcc-c++ redhat-rpm-config openssl-devel openssl-static openssl098e openssl-libs -y yum upgrade -y ## 配置nginx @@ -43,7 +43,7 @@ 在本地安装mysql rpm -ivh http://dev.mysql.com/get/mysql57-community-release-el7-9.noarch.rpm - yum install mysql-community-* mysql-connector-python* --skip-broke + yum install mysql-community-client mysql-community-common mysql-community-devel mysql-community-libs mysql-community-libs-compat mysql-community-server --skip-broke 新增一个数据库lykchat 设置用户lykchat,密码为!QAZ2wsx,把数据库lykchat的权限分配给用户lykchat @@ -75,7 +75,7 @@ ## 初始化数据库和配置计划任务 /usr/local/python36/bin/python3 /opt/lykchat/manage.py makemigrations - /usr/local/python36/bin/python3 /opt/lykchat/manage.py migrat + /usr/local/python36/bin/python3 /opt/lykchat/manage.py migrate /usr/local/python36/bin/python3 /opt/lykchat/manage.py crontab add crontab -l diff --git "a/doc/\345\276\256\344\277\241\347\231\273\351\231\206\346\227\266\351\227\264\350\266\205\350\277\2071\345\244\251.jpg" "b/doc/\345\276\256\344\277\241\347\231\273\351\231\206\346\227\266\351\227\264\350\266\205\350\277\2071\345\244\251.jpg" index 4b7e52f72aa38824ff400a2b2f87cea3cc74670e..9b12edef5f27049685e094631b63ba0ab8244bc4 100644 GIT binary patch literal 27121 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 literal 25652 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 Date: Thu, 20 Jul 2017 23:33:04 +0800 Subject: [PATCH 08/11] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3cba93a..b5a18bf 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 实现的功能有用户登录管理、微信登陆管理和微信信息发送功能。 +# 注意 +在2017年7月20日晚上,疑似微信web端做了调整,web上不在显示好友微信号,所以即日使用微信号发送信息可能提示无法找到好友等错误提示。 + +解决办法: + 1、使用好友昵称来发送信息 + 2、使用备注名来发送信息 + 但必须只能是数字、字母、符号等,不能为图片等 + ## 特点 1、简单高效 From 5282e88cb49810b8951b71c9d0839b0fd7f51799 Mon Sep 17 00:00:00 2001 From: lykops Date: Thu, 20 Jul 2017 23:33:29 +0800 Subject: [PATCH 09/11] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b5a18bf..81ac97b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 在2017年7月20日晚上,疑似微信web端做了调整,web上不在显示好友微信号,所以即日使用微信号发送信息可能提示无法找到好友等错误提示。 解决办法: + 1、使用好友昵称来发送信息 2、使用备注名来发送信息 但必须只能是数字、字母、符号等,不能为图片等 From f377af0c60713c6b45fdfc6441c53f72566509e7 Mon Sep 17 00:00:00 2001 From: lykops Date: Thu, 20 Jul 2017 23:36:29 +0800 Subject: [PATCH 10/11] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81ac97b..722a38b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 实现的功能有用户登录管理、微信登陆管理和微信信息发送功能。 -# 注意 +# 通知 在2017年7月20日晚上,疑似微信web端做了调整,web上不在显示好友微信号,所以即日使用微信号发送信息可能提示无法找到好友等错误提示。 解决办法: From aa4af83d447bf469be4d6251f32c449ad0019836 Mon Sep 17 00:00:00 2001 From: lykops Date: Tue, 12 Sep 2017 11:25:47 +0800 Subject: [PATCH 11/11] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 722a38b..1be21b3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基 2、信息共享 通过共享用户session和微信登陆信息,保证系统长期稳定运行 3、7*24不间断服务 - 计划任务定时检查微信登陆状态,微信保持登陆超过20天 + 计划任务定时检查微信登陆状态,微信保持登陆超过20天(有用户反映,保持登陆超过30天后,会被微信封掉,解决办法是登陆后2~3个星期退出登陆一次) 4、支持发送多媒体信息 除了支持发送纯文字信息外,还支持发送图片、视频、文件等信息 5、用户管理