diff --git a/README.md b/README.md index 09d6a2e..1be21b3 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,67 @@ # lykchat信息发送系统 lykchat信息发送系统是Python3开发的,通过模拟微信网页端,基于个人微信号,为系统管理人员提供信息发送工具。 + 实现的功能有用户登录管理、微信登陆管理和微信信息发送功能。 -## 特点 ## +# 通知 +在2017年7月20日晚上,疑似微信web端做了调整,web上不在显示好友微信号,所以即日使用微信号发送信息可能提示无法找到好友等错误提示。 + +解决办法: + + 1、使用好友昵称来发送信息 + 2、使用备注名来发送信息 + 但必须只能是数字、字母、符号等,不能为图片等 + +## 特点 1、简单高效 基于个人微信号,模拟微信web端,部署和维护简单 web管理页面实现可视化管理微信登陆 接口采用URL,简化调用复杂度,返回结果均为json格式 - 2、信息共享 - 通过共享用户session和微信登陆信息,保证系统长期稳定运行 + 2、信息共享 + 通过共享用户session和微信登陆信息,保证系统长期稳定运行 3、7*24不间断服务 - 计划任务定时检查微信登陆状态,微信保持登陆超过20天 - 4、用户管理 + 计划任务定时检查微信登陆状态,微信保持登陆超过20天(有用户反映,保持登陆超过30天后,会被微信封掉,解决办法是登陆后2~3个星期退出登陆一次) + 4、支持发送多媒体信息 + 除了支持发送纯文字信息外,还支持发送图片、视频、文件等信息 + 5、用户管理 通过用户隔离微信个人号,不同用户管理不同微信号 用户密码分为管理密码和接口密码,保证用户信息安全性 - 5、微信信息安全 + 6、微信信息安全 不会监控和存储微信聊天信息 不会增加和删除好友 -## 截图 ## - -管理页面--等待扫码 -![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/web管理--登陆.jpg) - +## 截图 管理页面--功能展示 + ![等待扫码 截图](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) + 接口-发送信息成功 -![等待扫码 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/接口-发送信息成功.jpg) - +![发送信息成功 截图](https://raw.githubusercontent.com/lykops/lykchat/master/doc/接口-发送信息成功.jpg) + + +## 发送信息接口使用说明 +[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 "模块和工作流程") - 1、管理web页面:可视化管理微信个人号 - 用户登录和认证 - 微信号登陆管理 - 发送信息给好友 - 2、发送信息接口: - 通过接口方式为其他业务系统发送信息给指定好友 - 3、计划任务检测微信登陆状态: - 获取所有登录微信成功的用户,通过调用检测微信登陆接口 - 4、会话保持模块: - 存储微信登陆信息和会话信息,同用户在任何地方登陆,保证微信登陆状态一致 - 5、模拟微信web端模块: - 通过微信登陆信息,访问微信web端接口,实现管理登陆、发送信息等功能。 - - -## V2.0.0版本说明 ## - - 1、修复bug: - 微信登陆时间超过12小时自动退出,测试过程中测得最大登陆时长20天 - 2、完善功能: - 1)、微信会话保持机制: - 保存位置:之前保存在数据库中,修改为数据库只记录用户名,所有信息保持到文件中,减少数据库的查询、写入、加解密压力 - 动态更新微信登陆信息 - 调整会话信息内容 - 2)、优化微信检测登陆流程,大大缩短各个页面执行时间 - 3)、完善获取好友流程 - 3、新增功能: - 1)、增加用户管理机制 - 2)、好友信息缓存机制 - 4、取消功能: - 接受和处理新信息 - - -## 发送信息接口 ## - 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端进行开发的 @@ -109,6 +78,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 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 a27bb97..9b12ede 100644 Binary files "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" and "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" differ diff --git a/install/requirements.txt b/install/requirements.txt index 33732ff..e1d2b04 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -8,4 +8,5 @@ djangorestframework django-template-utils mysqlclient Pillow -requests \ No newline at end of file +requests +requests-toolbelt \ No newline at end of file diff --git a/library/config/wechat.py b/library/config/wechat.py index 474e429..a979929 100644 --- a/library/config/wechat.py +++ b/library/config/wechat.py @@ -1,7 +1,7 @@ import os, platform from lykchat.settings import BASE_DIR -version = '2.0.0' +version = '2.1.0' base_url = 'https://wx2.qq.com' os_type = platform.system() # Windows, Linux, Darwin curr_dir = os.getcwd() @@ -68,14 +68,15 @@ }, } - SESSION_COOKIE_AGE = 60 * 60 * 1 +max_upload_size = 1024 * 1024 * 5 +# 上传文件最大值,单位bytes,默认5M CRONJOBS = ( ('*/2 * * * *', 'library.cron.checklogin.check_login', '>>/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/lykchat/settings.py b/lykchat/settings.py index 4393939..b425b8e 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', @@ -70,6 +70,7 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.static' ], }, }, @@ -123,5 +124,8 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ - STATIC_URL = '/static/' + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "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 '
' + send_result + '
' 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..14646a4 --- /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)