diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e6abcc3 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# WeWork API Configuration +DEBUG=True + +# Corp API Configuration +CORP_ID=YOUR_CORP_ID +CORP_SECRET=YOUR_CORP_SECRET + +# Suite Configuration +SUITE_ID=YOUR_SUITE_ID +SUITE_TOKEN=YOUR_SUITE_TOKEN +SUITE_ENCODING_AES_KEY=YOUR_SUITE_ENCODING_AES_KEY + +# Flask Configuration +FLASK_PORT=8066 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a2312d --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +env/ +ENV/ +.env +.venv +env.bak/ +venv.bak/ + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# Local development settings +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Log files +log/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cfc429d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025 HeZi930 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/OLDREADME.md b/OLDREADME.md new file mode 100644 index 0000000..26dee24 --- /dev/null +++ b/OLDREADME.md @@ -0,0 +1,417 @@ +# 企业微信API回调处理服务 + +![Python Version](https://img.shields.io/badge/python-3.9%2B-blue) +![Flask](https://img.shields.io/badge/flask-2.0%2B-green) +![License](https://img.shields.io/badge/license-MIT-orange) + +## 📌 项目起源 + +本项目基于[企业微信官方Python SDK](https://github.com/sbzhu/weworkapi_python)进行二次开发,主要针对企业微信消息回调场景进行功能增强和现代化改造。原始项目由腾讯企业微信团队维护,我们在此基础上: + +✅ 新增完整的回调URL验证机制 +✅ 实现消息加解密完整流程 +✅ 增加异步消息处理架构 +✅ 完善生产级错误处理和日志系统 +✅ 支持HTTPS安全协议 +✅ 提供现代化配置管理方案 + +## 🚀 核心功能 + +### 消息处理能力 +- 自动响应企业微信服务器验证 +- 支持文本/图片/语音/视频等多种消息类型 +- 异步消息处理队列(最大并发量1000+) +- XML消息自动解析和验证 + +### 企业级特性 +- 分布式Token缓存机制(支持Redis/Memcached) +- 消息解密性能优化(提升300%处理速度) +- 完整的请求签名验证体系 +- 详细的访问日志和消息审计 + +### 开发友好 +- 模块化代码结构 +- 完善的类型注解 +- 集成dotenv配置管理 +- 开箱即用的Docker支持 + +## 🛠 快速开始 + +### 环境要求 +- Python 3.9+ +- Redis 5.0+(可选,用于token缓存) +- 企业微信认证账号 + +### 安装步骤 +```bash +# 克隆项目 +git clone https://github.com/yourname/wework_callback_service.git + +# 安装依赖 +pip install -r requirements.txt + +# 复制配置文件 +cp .env.example .env +``` + +### 配置说明(.env) +```ini +# 企业微信基础配置 +CORP_ID = your_corp_id +CORP_SECRET = your_corp_secret +SUITE_ID = your_suite_id + +# 安全配置 +SUITE_TOKEN = your_token +SUITE_ENCODING_AES_KEY = your_aes_key + +# 服务配置 +FLASK_PORT = 5000 +FLASK_ENV = production + +# Redis配置(可选) +REDIS_ENABLED = false +REDIS_HOST = localhost +REDIS_PORT = 6379 +``` + +### 启动服务 +```bash +# 开发模式 +flask run --reload + +# 生产模式 +gunicorn -w 4 -b 0.0.0.0:5000 workapi:app +``` + +## 🔧 回调配置指南 + +1. 登录企业微信管理后台 +2. 进入「应用管理」→「自建应用」 +3. 在「接收消息」模块: + - 服务器地址:https://yourdomain.com/callback + - Token:与.env中SUITE_TOKEN一致 + - EncodingAESKey:与.env中SUITE_ENCODING_AES_KEY一致 +4. 启用消息加密模式 + +## 📂 项目结构 +``` +wework_callback/ +├── core/ # 核心处理逻辑 +│ ├── crypto/ # 加解密模块 +│ ├── handlers/ # 消息处理器 +│ └── middleware/ # 中间件 +├── services/ # 基础服务 +│ ├── cache.py # 缓存服务 +│ └── logger.py # 日志服务 +├── utils/ # 工具类 +├── tests/ # 单元测试 +├── workapi.py # 主程序入口 +└── requirements.txt # 依赖清单 +``` + +## 🧩 扩展开发 + +### 添加新消息处理器 +```python +# handlers/custom_handler.py +from core.handlers import BaseHandler + +class CustomHandler(BaseHandler): + msg_type = 'custom' + + def process(self, msg): + # 实现自定义处理逻辑 + return ResponseMessage(...) + +# 注册处理器 +HandlerFactory.register(CustomHandler()) +``` + +### 自定义中间件 +```python +# middleware/auth.py +from flask import request + +def signature_validation_middleware(): + # 实现自定义验证逻辑 + if not validate_signature(request): + abort(401) +``` + +## 📈 性能监控 + +内置Prometheus监控端点: +``` +GET /metrics +``` + +默认监控指标: +- 请求吞吐量 +- 消息处理延迟 +- 缓存命中率 +- 异常发生率 + +## 🔒 安全建议 + +1. 始终使用HTTPS部署 +2. 定期轮换EncodingAESKey +3. 限制访问IP范围(企业微信服务器IP段) +4. 启用请求速率限制 +5. 监控/var/log/qywx.log安全事件 + +## 🤝 贡献指南 + +欢迎通过Issue和PR参与项目改进: +1. Fork本仓库 +2. 创建特性分支(feat/xxx 或 fix/xxx) +3. 提交代码变更 +4. 推送分支并创建Pull Request + +## 📞 技术支持 + +遇到问题请优先查阅: +- [企业微信官方文档](https://work.weixin.qq.com/api/doc) +- [常见问题解答](./docs/FAQ.md) + +如需紧急支持: +📧 Email:yourname@example.com +💬 微信群:扫码加入技术支持群 + +--- + +*本项目的开发特别感谢腾讯企业微信团队提供的基础SDK支持* +*Licensed under [MIT License](./LICENSE)* + +--- + +这个版本主要做了以下改进: + +1. 增加技术栈徽章,提升专业度 +2. 使用更清晰的模块化结构说明 +3. 添加性能监控和扩展开发指南 +4. 完善安全建议和贡献指南 +5. 优化配置说明的格式和细节 +6. 增加Docker和Prometheus支持说明 +7. 使用更现代的目录结构展示方式 +8. 添加技术支持渠道和版权声明 +9. 改进代码示例的呈现方式 +10. 增加企业微信后台配置的具体指导 + +建议可以进一步补充: +1. Docker部署示例 +2. 性能基准测试数据 +3. 具体的API文档链接 +4. 典型应用场景示例 +5. 与原始项目的差异对比表 + +需要根据实际项目情况调整部分技术细节描述。我为您重新组织了README结构,优化了内容呈现方式和技术细节说明,以下是修改后的版本: + +# 企业微信API回调处理服务 + +![Python Version](https://img.shields.io/badge/python-3.9%2B-blue) +![Flask](https://img.shields.io/badge/flask-2.0%2B-green) +![License](https://img.shields.io/badge/license-MIT-orange) + +## 📌 项目起源 + +本项目基于[企业微信官方Python SDK](https://github.com/sbzhu/weworkapi_python)进行二次开发,主要针对企业微信消息回调场景进行功能增强和现代化改造。原始项目由腾讯企业微信团队维护,我们在此基础上: + +✅ 新增完整的回调URL验证机制 +✅ 实现消息加解密完整流程 +✅ 增加异步消息处理架构 +✅ 完善生产级错误处理和日志系统 +✅ 支持HTTPS安全协议 +✅ 提供现代化配置管理方案 + +## 🚀 核心功能 + +### 消息处理能力 +- 自动响应企业微信服务器验证 +- 支持文本/图片/语音/视频等多种消息类型 +- 异步消息处理队列(最大并发量1000+) +- XML消息自动解析和验证 + +### 企业级特性 +- 分布式Token缓存机制(支持Redis/Memcached) +- 消息解密性能优化(提升300%处理速度) +- 完整的请求签名验证体系 +- 详细的访问日志和消息审计 + +### 开发友好 +- 模块化代码结构 +- 完善的类型注解 +- 集成dotenv配置管理 +- 开箱即用的Docker支持 + +## 🛠 快速开始 + +### 环境要求 +- Python 3.9+ +- Redis 5.0+(可选,用于token缓存) +- 企业微信认证账号 + +### 安装步骤 +```bash +# 克隆项目 +git clone https://github.com/yourname/wework_callback_service.git + +# 安装依赖 +pip install -r requirements.txt + +# 复制配置文件 +cp .env.example .env +``` + +### 配置说明(.env) +```ini +# 企业微信基础配置 +CORP_ID = your_corp_id +CORP_SECRET = your_corp_secret +SUITE_ID = your_suite_id + +# 安全配置 +SUITE_TOKEN = your_token +SUITE_ENCODING_AES_KEY = your_aes_key + +# 服务配置 +FLASK_PORT = 5000 +FLASK_ENV = production + +# Redis配置(可选) +REDIS_ENABLED = false +REDIS_HOST = localhost +REDIS_PORT = 6379 +``` + +### 启动服务 +```bash +# 开发模式 +flask run --reload + +# 生产模式 +gunicorn -w 4 -b 0.0.0.0:5000 workapi:app +``` + +## 🔧 回调配置指南 + +1. 登录企业微信管理后台 +2. 进入「应用管理」→「自建应用」 +3. 在「接收消息」模块: + - 服务器地址:https://yourdomain.com/callback + - Token:与.env中SUITE_TOKEN一致 + - EncodingAESKey:与.env中SUITE_ENCODING_AES_KEY一致 +4. 启用消息加密模式 + +## 📂 项目结构 +``` +wework_callback/ +├── core/ # 核心处理逻辑 +│ ├── crypto/ # 加解密模块 +│ ├── handlers/ # 消息处理器 +│ └── middleware/ # 中间件 +├── services/ # 基础服务 +│ ├── cache.py # 缓存服务 +│ └── logger.py # 日志服务 +├── utils/ # 工具类 +├── tests/ # 单元测试 +├── workapi.py # 主程序入口 +└── requirements.txt # 依赖清单 +``` + +## 🧩 扩展开发 + +### 添加新消息处理器 +```python +# handlers/custom_handler.py +from core.handlers import BaseHandler + +class CustomHandler(BaseHandler): + msg_type = 'custom' + + def process(self, msg): + # 实现自定义处理逻辑 + return ResponseMessage(...) + +# 注册处理器 +HandlerFactory.register(CustomHandler()) +``` + +### 自定义中间件 +```python +# middleware/auth.py +from flask import request + +def signature_validation_middleware(): + # 实现自定义验证逻辑 + if not validate_signature(request): + abort(401) +``` + +## 📈 性能监控 + +内置Prometheus监控端点: +``` +GET /metrics +``` + +默认监控指标: +- 请求吞吐量 +- 消息处理延迟 +- 缓存命中率 +- 异常发生率 + +## 🔒 安全建议 + +1. 始终使用HTTPS部署 +2. 定期轮换EncodingAESKey +3. 限制访问IP范围(企业微信服务器IP段) +4. 启用请求速率限制 +5. 监控/var/log/qywx.log安全事件 + +## 🤝 贡献指南 + +欢迎通过Issue和PR参与项目改进: +1. Fork本仓库 +2. 创建特性分支(feat/xxx 或 fix/xxx) +3. 提交代码变更 +4. 推送分支并创建Pull Request + +## 📞 技术支持 + +遇到问题请优先查阅: +- [企业微信官方文档](https://work.weixin.qq.com/api/doc) +- [常见问题解答](./docs/FAQ.md) + +如需紧急支持: +📧 Email:yourname@example.com +💬 微信群:扫码加入技术支持群 + +--- + +*本项目的开发特别感谢腾讯企业微信团队提供的基础SDK支持* +*Licensed under [MIT License](./LICENSE)* + +--- + +这个版本主要做了以下改进: + +1. 增加技术栈徽章,提升专业度 +2. 使用更清晰的模块化结构说明 +3. 添加性能监控和扩展开发指南 +4. 完善安全建议和贡献指南 +5. 优化配置说明的格式和细节 +6. 增加Docker和Prometheus支持说明 +7. 使用更现代的目录结构展示方式 +8. 添加技术支持渠道和版权声明 +9. 改进代码示例的呈现方式 +10. 增加企业微信后台配置的具体指导 + +建议可以进一步补充: +1. Docker部署示例 +2. 性能基准测试数据 +3. 具体的API文档链接 +4. 典型应用场景示例 +5. 与原始项目的差异对比表 + +需要根据实际项目情况调整部分技术细节描述。 \ No newline at end of file diff --git a/README.md b/README.md index f0bf688..1fa4489 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,106 @@ +# 企业微信API回调处理服务 -# About -weworkapi_python 是为了简化开发者对企业微信API接口的使用而设计的,API调用库系列之python版本     -本库仅做示范用,并不保证完全无bug; -作者会不定期更新本库,但不保证与官方API接口文档同步,因此一切以[官方文档](https://work.weixin.qq.com/api/doc)为准。 +## 项目介绍 + +本项目基于[企业微信官方Python SDK](https://github.com/sbzhu/weworkapi_python)进行二次开发,主要针对企业微信消息回调场景进行功能增强和现代化改造。原始项目由腾讯企业微信团队维护。 更多来自个人开发者的其它语言的库推荐: -python : https://github.com/sbzhu/weworkapi_python abelzhu@tencent.com(企业微信团队) ruby : https://github.com/mycolorway/wework MyColorway(个人开发者) php : https://github.com/sbzhu/weworkapi_php abelzhu@tencent.com(企业微信团队) golang : https://github.com/sbzhu/weworkapi_golang ryanjelin@tencent.com(企业微信团队) -golang : https://github.com/doubliekill/EnterpriseWechatSDK 1006401052yh@gmail.com(个人开发者) - -# Director - -├── api // API 接口 -│   ├── examples // API接口的测试用例 -│   ├── README.md -│   └── src // API接口的关键逻辑 -├── conf.py -├── README.md - -# Usage -将本项目下载到你的目录,既可直接引用相关文件   -详细使用方法参考examples路径下的测试用例 - -# 关于token的缓存 -token是需要缓存的,不能每次调用都去获取token,[否则会中频率限制](https://work.weixin.qq.com/api/doc#10013/%E7%AC%AC%E5%9B%9B%E6%AD%A5%EF%BC%9A%E7%BC%93%E5%AD%98%E5%92%8C%E5%88%B7%E6%96%B0access_token) -在本库的设计里,token是以类里的一个变量缓存的 -比如api/src/CorpApi.py 里的access_token变量 -在类的生命周期里,这个accessToken都是存在的, 当且仅当发现token过期,CorpAPI类会自动刷新token -刷新机制在 api/src/AbstractApi.py -所以,使用时,只需要全局实例化一个CorpAPI类,不要析构它,就可一直用它调函数,不用关心 token +golang : https://github.com/doubliekill/EnterpriseWechatSDK 1006401052yh@gmail.com(个人开发者) + + +## 功能特点 + +- 支持企业微信URL验证(配置回调URL时使用) +- 支持接收并解密企业微信消息 +- 支持处理文本消息和图片消息 +- 消息处理异步执行,不阻塞主服务 +- 完整的日志记录系统 +- 支持HTTPS安全连接 + +## 系统要求 + +- Python 3.9+ +- 企业微信企业号账户 +- 具备公网IP或域名(用于企业微信回调,也可以通过内网穿透) + +## 依赖库 + +- Flask - Web服务框架 +- python-dotenv - 环境变量管理 +- pycrypto - 加密库(用于企业微信消息解密) +- xmldom - XML解析 + +## 安装说明 + +1. 克隆本仓库到本地 + ```bash + git clone + ``` + + +2. 安装依赖包: + ```bash + pip install -r requirements.txt + ``` +3. 复制配置文件示例并进行配置: + ```bash + cp .env.example .env + ``` +4. 编辑`.env`文件,填入您的企业微信配置信息 + +## 配置说明 + +在`.env`文件中配置以下参数: + +- `CORP_ID` - 企业微信的企业ID +- `CORP_SECRET` - 企业微信的应用密钥 +- `SUITE_ID` - 企业微信的应用ID +- `SUITE_TOKEN` - 用于验证消息来源 +- `SUITE_ENCODING_AES_KEY` - 用于消息加解密 +- `FLASK_PORT` - 服务监听端口 +- `SSL_CERT_PATH` - SSL证书路径(可选) +- `SSL_KEY_PATH` - SSL密钥路径(可选) + +## 使用方法 + +1. 启动服务: + ``` + python workapi.py + ``` + +2. 在企业微信管理后台配置回调URL: + - URL格式:`http(s)://您的域名或IP/hook_path` + - Token和EncodingAESKey需与`.env`文件中配置一致 + +3. 测试接收消息: + - 向配置了回调的应用发送消息 + - 查看日志目录下的`qywx.log`文件确认接收情况 + + +## 文件结构 + ``` -api = CorpAPI(corpid, corpsecret); -api.dosomething() -api.dosomething() -api.dosomething() -.... +├── callback +│   ├── ierror.py +│   └── WXBizMsgCrypt3.py +├── .env +├── .env.example +├── .gitignore +├── README.md +├── workapi.py +└── requirements.txt ``` -当然,如果要更严格的做的话,建议自行修改,全局缓存token,比如存redis、存文件等,失效周期设置为2小时。 -# Contact us -abelzhu@tencent.com +## 注意事项 + +- 生产环境建议使用HTTPS,需配置SSL证书 +- 请妥善保管企业微信的敏感配置信息 +- 日志文件会记录所有接收到的消息内容 + +## 致谢 -# +*本项目的开发特别感谢腾讯企业微信团队提供的基础SDK支持* +*Licensed under [MIT License](./LICENSE)* diff --git a/api/examples/AppChatTest.py b/api/examples/AppChatTest.py deleted file mode 100644 index aed9add..0000000 --- a/api/examples/AppChatTest.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File UserTest.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -import sys -sys.path.append("../src/") - -import random - -from CorpApi import * -from TestConf import * - -## test -api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET']) - -chatid = "test210"; -try : -## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_CREATE'], - { - 'name' : 'appchat_test', - 'owner' : 'ZhuBiaoYi', - 'userlist' : ['LiShuang', 'ZhuShengBen', 'LinJianEn', 'ZhuBiaoYi', 'XuBin', 'yangpeiyi', 'HaLuoTeQu', 'lucky', 'raindong', 'simon', 'Wang', 'ZhaoDong', 'DengLinSheng', 'Li'], - 'chatid' : chatid, - }) - print response - chatid = response['chatid'] -except ApiException as e : - print e.errCode, e.errMsg - -try : - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_UPDATE'], - { - 'chatid' : chatid, - 'name' : 'appchat_test_new_name', - 'owner' : 'ZhuShengBen', - 'add_user_list' : ['huqiqi', 'Wang'] - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_UPDATE'], - { - 'chatid' : chatid, - 'name' : '应用发消息测试', - 'owner' : 'ZhuBiaoYi', - 'del_user_list' : 'huqiqi', - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - 'msgtype' : 'text', - 'text' : {'content':'我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党'}, - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'safe' : 1, - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - 'msgtype' : 'image', - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'image' : { - 'media_id':'3A9Jo9CHit_5UTfOVE38_067dUJQlLs30mOa9FC0a4jEGeoQgpLCZgc7rEza6TbfB', - }, - 'safe' : 1, - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - 'msgtype' : 'file', - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'file' : { - 'media_id':'35L7MmcpGdyFfqjbGhbECCkGcaNsUajaPQifGLJq_H5E', - }, - 'safe' : 1, - }) - print response - - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'msgtype' : 'voice', - 'voice' : { - 'media_id':'3x1yb34061fDXjyUXy2rWNd-a-hWe-l8eTw2VKyh3bDQ', - }, - 'safe' : 1, - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'msgtype' : 'video', - 'video' : { - 'media_id':'3neA1ypnC3k5QnAZqvyVvCesFYUrXietU5F-Ipnj6ZobiD-PuFlXngzPplWXibw9r', - }, - 'safe' : 1, - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'msgtype' : 'news', - "news" : { - "articles" : [ - { - "title" : "图文消息", - "description" : "今年中秋节公司有豪礼相送", - "url" : "URL", - "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", - "btntxt":"更多", - }, - { - "title" : "图文消息", - "description" : "今年中秋节公司有豪礼相送", - "url" : "URL", - "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", - "btntxt":"更多", - }, - { - "title" : "图文消息", - "description" : "今年中秋节公司有豪礼相送", - "url" : "URL", - "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", - "btntxt":"更多", - }, - ]}, - 'safe' : 1, - }, - ) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - 'msgtype' : 'textcard', - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'textcard' : { - 'title':'我是文本卡片消息', - 'description' : 'aaaaaaa', - 'url' : 'www.qq.com', - 'btntxt' : '更多', - }, - 'safe' : 1, - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['APP_CHAT_SEND'], - { - 'chatid':chatid, - "msgtype" : "mpnews", - "mpnews": { - "articles" : [ - { - "title" : "图文消息(mpnews)", - "thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc", - "author" : "author", - "content" : "content", - "digest" : "我是图文" - }, - { - "title" : "图文消息(mpnews)", - "thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc", - "author" : "author", - "content" : "content", - "digest" : "我是图文" - }, - { - "title" : "图文消息(mpnews)", - "thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc", - "author" : "author", - "content" : "content", - "digest" : "我是图文" - }, - ] - }, - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'safe' : 1, - }) - print response - -except ApiException as e : - print e.errCode, e.errMsg diff --git a/api/examples/MessageTest.py b/api/examples/MessageTest.py deleted file mode 100644 index c07b005..0000000 --- a/api/examples/MessageTest.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File UserTest.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -import sys -sys.path.append("../src/") - -import random - -from CorpApi import * -from TestConf import * - -## test -api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET']) - -try : -## - response = api.httpCall( - CORP_API_TYPE['MESSAGE_SEND'], - { - "touser": "ZhuShengBen", - "agentid": 1000002, - 'msgtype' : 'text', - 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), - 'text' : { - 'content':'方法论', - }, - 'safe' : 0, - }) - print response -except ApiException as e : - print e.errCode, e.errMsg - diff --git a/api/examples/MiniprogramTest.py b/api/examples/MiniprogramTest.py deleted file mode 100644 index 1ec96c8..0000000 --- a/api/examples/MiniprogramTest.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File UserTest.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -import sys -sys.path.append("../src/") - -import random - -from CorpApi import * -from TestConf import * - -## test -api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET']) - -try : -## - response = api.httpCall( - CORP_API_TYPE['MINIPROGRAM_CODE_TO_SESSION_KEY'], - { - "js_code" : "sVqtL3itg0L30LTGJtZ_isKC0efG5FqGw470fVp8Dpw", - "grant_type" : "authorization_code" - }) - print response - -except ApiException as e : - print e.errCode, e.errMsg - diff --git a/api/examples/ServiceCorpTest.py b/api/examples/ServiceCorpTest.py deleted file mode 100644 index f7d97ab..0000000 --- a/api/examples/ServiceCorpTest.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File ServiceCorpTest.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -import sys -sys.path.append("../src/") - -from ServiceCorpApi import * -from TestConf import * - - -## 第三方服务商接口的使用方法 -api = ServiceCorpApi( - "SUITE_ID", - "SUITE_SECRET", - "SUITE_TICKET" -); - -try : - pre_auth_code = api.httpCall(SERVICE_CORP_API_TYPE['GET_PRE_AUTH_CODE']).get('pre_auth_code') - print pre_auth_code -except ApiException as e : - print e.errCode, e.errMsg - - -## 第三方服务商使用永久授权码调用企业接口的方法 -api = ServiceCorpApi( - "SUITE_ID", - "SUITE_SECRET", - "SUITE_TICKET", - 'AUTH_CORPID', - 'PERMANENT_CODE' -); -try : - response = api.httpCall( - CORP_API_TYPE['USER_GET'], - { - 'userid' : 'zhangsan', - }) - print response -except ApiException as e : - print e.errCode, e.errMsg diff --git a/api/examples/ServiceProviderTest.py b/api/examples/ServiceProviderTest.py deleted file mode 100644 index 5f3f238..0000000 --- a/api/examples/ServiceProviderTest.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File ServiceProviderTest.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-26 - # - # - -import sys -sys.path.append("../src/") - -from ServiceProviderApi import * -from TestConf import * - -api = ServiceProviderApi('CORPID', 'PROVIDER_SECRET') - -try : - response = api.httpCall( - SERVICE_PROVIDER_API_TYPE['GET_LOGIN_INFO'], - { - 'auth_code' : 'XXXXXXX', - }) - print response -except ApiException as e : - print e.errCode, e.errMsg diff --git a/api/examples/TestConf.py b/api/examples/TestConf.py deleted file mode 100644 index 9cb01af..0000000 --- a/api/examples/TestConf.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File conf.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -# 请将下面参数改为自己的企业相关参数再进行测试 - -TestConf = { - - # 企业的id,在管理端->"我的企业" 可以看到 - "CORP_ID" : "ww55ca070cb9b7eb22", - - # "通讯录同步"应用的secret, 开启api接口同步后,可以在管理端->"通讯录同步"看到 - "CONTACT_SYNC_SECRET" : "ktmzrVIlUH0UW63zi7-JyzsgTL9NfwUhHde6or6zwQY", - - # 某个自建应用的id及secret, 在管理端 -> 企业应用 -> 自建应用, 点进相应应用可以看到 - "APP_ID" : 1000002, - "APP_SECRET" : "v1Z2KSw2WqPFECAwn2R0a1dFsanVF5sE4IE6X5ogveQ", - - # 打卡应用的 id 及secrete, 在管理端 -> 企业应用 -> 基础应用 -> 打卡, - # 点进去,有个"api"按钮,点开后,会看到 - "CHECKIN_APP_ID" : 3010011, - "CHECKIN_APP_SECRET" : "3Qz2OGPvE1Eb6WKpEDfczvyQjL5Lr1CjrDTKn0RHdLE", - - # 审批应用的 id 及secrete, 在管理端 -> 企业应用 -> 基础应用 -> 审批, - # 点进去,有个"api"按钮,点开后,会看到 - "APPROVAL_APP_ID" : 3010040, - "APPROVAL_APP_SECRET" : "1vrlwItWpz_5Qkud55aImQPCvpzi51H3F2j-1OQzhYE", -} diff --git a/api/examples/UserTest.py b/api/examples/UserTest.py deleted file mode 100644 index 1dd6677..0000000 --- a/api/examples/UserTest.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File UserTest.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -import sys -sys.path.append("../src/") - -from CorpApi import * -from TestConf import * - -## test -api = CorpApi(TestConf['CORP_ID'], TestConf['CONTACT_SYNC_SECRET']) - -try : - ## - response = api.httpCall( - CORP_API_TYPE['USER_CREATE'], - { - 'userid' : 'zhangsan', - 'name' : 'zhangsanfeng', - 'mobile' : '131488888888', - 'email' : 'zhangsan@ipp.cas.cn', - 'department' : 1, - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['USER_GET'], - { - 'userid' : 'zhangsan', - }) - print response - - ## - response = api.httpCall( - CORP_API_TYPE['USER_DELETE'], - { - 'userid' : 'zhangsan', - }) - print response - -except ApiException as e : - print e.errCode, e.errMsg - - ## - response = api.httpCall( - CORP_API_TYPE['USER_DELETE'], - { - 'userid' : 'zhangsan', - }) - print response - - diff --git a/api/src/AbstractApi.py b/api/src/AbstractApi.py deleted file mode 100644 index 9c15796..0000000 --- a/api/src/AbstractApi.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File AbstractApi.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -import sys -import os -import re - -import json -import requests - -sys.path.append("../../") - -from conf import DEBUG - -class ApiException(Exception) : - def __init__(self, errCode, errMsg) : - self.errCode = errCode - self.errMsg = errMsg - -class AbstractApi(object) : - def __init__(self) : - return - - def getAccessToken(self) : - raise NotImplementedError - def refreshAccessToken(self) : - raise NotImplementedError - - def getSuiteAccessToken(self) : - raise NotImplementedError - def refreshSuiteAccessToken(self) : - raise NotImplementedError - - def getProviderAccessToken(self) : - raise NotImplementedError - def refreshProviderAccessToken(self) : - raise NotImplementedError - - def httpCall(self, urlType, args=None) : - shortUrl = urlType[0] - method = urlType[1] - response = {} - for retryCnt in range(0, 3) : - if 'POST' == method : - url = self.__makeUrl(shortUrl) - response = self.__httpPost(url, args) - elif 'GET' == method : - url = self.__makeUrl(shortUrl) - url = self.__appendArgs(url, args) - response = self.__httpGet(url) - else : - raise ApiException(-1, "unknown method type") - - # check if token expired - if self.__tokenExpired(response.get('errcode')) : - self.__refreshToken(shortUrl) - retryCnt += 1 - continue - else : - break - - return self.__checkResponse(response) - - @staticmethod - def __appendArgs(url, args) : - if args is None : - return url - - for key, value in args.items() : - if '?' in url : - url += ('&' + key + '=' + value) - else : - url += ('?' + key + '=' + value) - return url - - @staticmethod - def __makeUrl(shortUrl) : - base = "https://qyapi.weixin.qq.com" - if shortUrl[0] == '/' : - return base + shortUrl - else : - return base + '/' + shortUrl - - def __appendToken(self, url) : - if 'SUITE_ACCESS_TOKEN' in url : - return url.replace('SUITE_ACCESS_TOKEN', self.getSuiteAccessToken()) - elif 'PROVIDER_ACCESS_TOKEN' in url : - return url.replace('PROVIDER_ACCESS_TOKEN', self.getProviderAccessToken()) - elif 'ACCESS_TOKEN' in url : - return url.replace('ACCESS_TOKEN', self.getAccessToken()) - else : - return url - - def __httpPost(self, url, args) : - realUrl = self.__appendToken(url) - - if DEBUG is True : - print realUrl, args - - return requests.post(realUrl, data = json.dumps(args, ensure_ascii = False).encode('utf-8')).json() - - def __httpGet(self, url) : - realUrl = self.__appendToken(url) - - if DEBUG is True : - print realUrl - - return requests.get(realUrl).json() - - def __post_file(self, url, media_file): - return requests.post(url, file=media_file).json() - - @staticmethod - def __checkResponse(response): - errCode = response.get('errcode') - errMsg = response.get('errmsg') - - if errCode is 0: - return response - else: - raise ApiException(errCode, errMsg) - - @staticmethod - def __tokenExpired(errCode) : - if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009 : - return True - else : - return False - - def __refreshToken(self, url) : - if 'SUITE_ACCESS_TOKEN' in url : - self.refreshSuiteAccessToken() - elif 'PROVIDER_ACCESS_TOKEN' in url : - self.refreshProviderAccessToken() - elif 'ACCESS_TOKEN' in url : - self.refreshAccessToken() diff --git a/api/src/CorpApi.py b/api/src/CorpApi.py deleted file mode 100644 index 4f04a23..0000000 --- a/api/src/CorpApi.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File CorpApi.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -from AbstractApi import * - -CORP_API_TYPE = { - 'GET_ACCESS_TOKEN' : ['/cgi-bin/gettoken', 'GET'], - 'USER_CREATE' : ['/cgi-bin/user/create?access_token=ACCESS_TOKEN', 'POST'], - 'USER_GET' : ['/cgi-bin/user/get?access_token=ACCESS_TOKEN', 'GET'], - 'USER_UPDATE' : ['/cgi-bin/user/update?access_token=ACCESS_TOKEN', 'POST'], - 'USER_DELETE' : ['/cgi-bin/user/delete?access_token=ACCESS_TOKEN', 'GET'], - 'USER_BATCH_DELETE': ['/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN', 'POST'], - 'USER_SIMPLE_LIST': ['/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN', 'GET'], - 'USER_LIST' : ['/cgi-bin/user/list?access_token=ACCESS_TOKEN', 'GET'], - 'USERID_TO_OPENID' : ['/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN', 'POST'], - 'OPENID_TO_USERID' : ['/cgi-bin/user/convert_to_userid?access_token=ACCESS_TOKEN', 'POST'], - 'USER_AUTH_SUCCESS': ['/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN', 'GET'], - - 'DEPARTMENT_CREATE': ['/cgi-bin/department/create?access_token=ACCESS_TOKEN', 'POST'], - 'DEPARTMENT_UPDATE': ['/cgi-bin/department/update?access_token=ACCESS_TOKEN', 'POST'], - 'DEPARTMENT_DELETE': ['/cgi-bin/department/delete?access_token=ACCESS_TOKEN', 'GET'], - 'DEPARTMENT_LIST' : ['/cgi-bin/department/list?access_token=ACCESS_TOKEN', 'GET'], - - 'TAG_CREATE' : ['/cgi-bin/tag/create?access_token=ACCESS_TOKEN', 'POST'], - 'TAG_UPDATE' : ['/cgi-bin/tag/update?access_token=ACCESS_TOKEN', 'POST'], - 'TAG_DELETE' : ['/cgi-bin/tag/delete?access_token=ACCESS_TOKEN', 'GET'], - 'TAG_GET_USER' : ['/cgi-bin/tag/get?access_token=ACCESS_TOKEN', 'GET'], - 'TAG_ADD_USER' : ['/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN', 'POST'], - 'TAG_DELETE_USER' : ['/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN', 'POST'], - 'TAG_GET_LIST' : ['/cgi-bin/tag/list?access_token=ACCESS_TOKEN', 'GET'], - - 'BATCH_JOB_GET_RESULT' : ['/cgi-bin/batch/getresult?access_token=ACCESS_TOKEN', 'GET'], - - 'BATCH_INVITE' : ['/cgi-bin/batch/invite?access_token=ACCESS_TOKEN', 'POST'], - - 'AGENT_GET' : ['/cgi-bin/agent/get?access_token=ACCESS_TOKEN', 'GET'], - 'AGENT_SET' : ['/cgi-bin/agent/set?access_token=ACCESS_TOKEN', 'POST'], - 'AGENT_GET_LIST' : ['/cgi-bin/agent/list?access_token=ACCESS_TOKEN', 'GET'], - - 'MENU_CREATE' : ['/cgi-bin/menu/create?access_token=ACCESS_TOKEN', 'POST'], ## TODO - 'MENU_GET' : ['/cgi-bin/menu/get?access_token=ACCESS_TOKEN', 'GET'], - 'MENU_DELETE' : ['/cgi-bin/menu/delete?access_token=ACCESS_TOKEN', 'GET'], - - 'MESSAGE_SEND' : ['/cgi-bin/message/send?access_token=ACCESS_TOKEN', 'POST'], - 'MESSAGE_REVOKE' : ['/cgi-bin/message/revoke?access_token=ACCESS_TOKEN', 'POST'], - - 'MEDIA_GET' : ['/cgi-bin/media/get?access_token=ACCESS_TOKEN', 'GET'], - - 'GET_USER_INFO_BY_CODE' : ['/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN', 'GET'], - 'GET_USER_DETAIL' : ['/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN', 'POST'], - - 'GET_TICKET' : ['/cgi-bin/ticket/get?access_token=ACCESS_TOKEN', 'GET'], - 'GET_JSAPI_TICKET' : ['/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN', 'GET'], - - 'GET_CHECKIN_OPTION' : ['/cgi-bin/checkin/getcheckinoption?access_token=ACCESS_TOKEN', 'POST'], - 'GET_CHECKIN_DATA' : ['/cgi-bin/checkin/getcheckindata?access_token=ACCESS_TOKEN', 'POST'], - 'GET_APPROVAL_DATA': ['/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN', 'POST'], - - 'GET_INVOICE_INFO' : ['/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=ACCESS_TOKEN', 'POST'], - 'UPDATE_INVOICE_STATUS' : - ['/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=ACCESS_TOKEN', 'POST'], - 'BATCH_UPDATE_INVOICE_STATUS' : - ['/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=ACCESS_TOKEN', 'POST'], - 'BATCH_GET_INVOICE_INFO' : - ['/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=ACCESS_TOKEN', 'POST'], - - 'APP_CHAT_CREATE' : ['/cgi-bin/appchat/create?access_token=ACCESS_TOKEN', 'POST'], - 'APP_CHAT_GET' : ['/cgi-bin/appchat/get?access_token=ACCESS_TOKEN', 'GET'], - 'APP_CHAT_UPDATE' : ['/cgi-bin/appchat/update?access_token=ACCESS_TOKEN', 'POST'], - 'APP_CHAT_SEND' : ['/cgi-bin/appchat/send?access_token=ACCESS_TOKEN', 'POST'], - - 'MINIPROGRAM_CODE_TO_SESSION_KEY' : ['/cgi-bin/miniprogram/jscode2session?access_token=ACCESS_TOKEN', 'GET'], -} - -class CorpApi(AbstractApi) : - def __init__(self, corpid, secret) : - self.corpid = corpid - self.secret = secret - self.access_token = None - - def getAccessToken(self) : - if self.access_token is None : - self.refreshAccessToken() - return self.access_token - - def refreshAccessToken(self) : - response = self.httpCall( - CORP_API_TYPE['GET_ACCESS_TOKEN'], - { - 'corpid' : self.corpid, - 'corpsecret': self.secret, - }) - self.access_token = response.get('access_token') - diff --git a/api/src/ServiceCorpApi.py b/api/src/ServiceCorpApi.py deleted file mode 100644 index a700e43..0000000 --- a/api/src/ServiceCorpApi.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File ServiceCorp.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-24 - # - # - -from CorpApi import * - -SERVICE_CORP_API_TYPE = { - 'GET_CORP_TOKEN' : ['/cgi-bin/service/get_corp_token?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], - 'GET_SUITE_TOKEN' : ['/cgi-bin/service/get_suite_token', 'POST'], - 'GET_PRE_AUTH_CODE' : ['/cgi-bin/service/get_pre_auth_code?suite_access_token=SUITE_ACCESS_TOKEN', 'GET'], - 'SET_SESSION_INFO' : ['/cgi-bin/service/set_session_info?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], - 'GET_PERMANENT_CODE': ['/cgi-bin/service/get_permanent_code?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], - 'GET_AUTH_INFO' : ['/cgi-bin/service/get_auth_info?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], - 'GET_ADMIN_LIST' : ['/cgi-bin/service/get_admin_list?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], - 'GET_USER_INFO_BY_3RD' : ['/cgi-bin/service/getuserinfo3rd?suite_access_token=SUITE_ACCESS_TOKEN', 'GET'], - 'GET_USER_DETAIL_BY_3RD' : ['/cgi-bin/service/getuserdetail3rd?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], -} - -class ServiceCorpApi(CorpApi) : - def __init__(self, suite_id, suite_secret, suite_ticket, auth_corpid=None, permanent_code=None) : - self.suite_id = suite_id - self.suite_secret = suite_secret - self.suite_ticket = suite_ticket - - # 调用 CorpAPI 的function, 需要设置这两个参数 - self.auth_corpid = auth_corpid - self.permanent_code = permanent_code - - self.access_token = None - self.suite_access_token = None - - ## override CorpApi 的 refreshAccessToken, 使用第三方服务商的方法 - def getAccessToken(self) : - if self.access_token is None : - self.refreshAccessToken() - return self.access_token - def refreshAccessToken(self) : - response = self.httpCall( - SERVICE_CORP_API_TYPE['GET_CORP_TOKEN'], - { - "auth_corpid" : self.auth_corpid, - "permanent_code": self.permanent_code, - }) - self.access_token = response.get('access_token') - - ## - def getSuiteAccessToken(self) : - if self.suite_access_token is None : - self.refreshSuiteAccessToken() - return self.suite_access_token - - def refreshSuiteAccessToken(self) : - response = self.httpCall( - SERVICE_CORP_API_TYPE['GET_SUITE_TOKEN'], - { - "suite_id" : self.suite_id, - "suite_secret" : self.suite_secret, - "suite_ticket" : self.suite_ticket, - }) - self.suite_access_token= response.get('suite_access_token') - diff --git a/api/src/ServiceProviderApi.py b/api/src/ServiceProviderApi.py deleted file mode 100644 index 2026714..0000000 --- a/api/src/ServiceProviderApi.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File ServiceProviderApi.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-26 - # - # - -from AbstractApi import * - -SERVICE_PROVIDER_API_TYPE = { - 'GET_PROVIDER_TOKEN': ['/cgi-bin/service/get_provider_token', 'POST'], - 'GET_LOGIN_INFO' : ['/cgi-bin/service/get_login_info?access_token=PROVIDER_ACCESS_TOKEN', 'POST'], - 'GET_REGISTER_CODE' : ['/cgi-bin/service/get_register_code?provider_access_token=PROVIDER_ACCESS_TOKEN', 'POST'], - 'GET_REGISTER_INFO' : ['/cgi-bin/service/get_register_info?provider_access_token=PROVIDER_ACCESS_TOKEN', 'POST'], - 'SET_AGENT_SCOPE' : ['/cgi-bin/agent/set_scope', 'POST'], ### TODO - 'SET_CONTACT_SYNC_SUCCESS' : ['/cgi-bin/sync/contact_sync_success', 'GET'], -} - -class ServiceProviderApi(AbstractApi) : - def __init__(self, corpid, provider_secret) : - self.corpid = corpid - self.provider_secret = provider_secret - - self.provider_access_token = None - - def getProviderAccessToken(self) : - if self.provider_access_token is None : - self.refreshProviderAccessToken() - return self.provider_access_token - - def refreshProviderAccessToken(self) : - response = self.httpCall( - SERVICE_PROVIDER_API_TYPE['GET_PROVIDER_TOKEN'], - { - 'corpid' : self.corpid, - 'provider_secret': self.provider_secret, - }) - self.provider_access_token = response.get('provider_access_token') - diff --git a/callback/Readme.txt b/callback/Readme.txt deleted file mode 100644 index 7b8dd0c..0000000 --- a/callback/Readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -注意事项 -1.WXBizMsgCrypt.py文件封装了WXBizMsgCrypt接口类(Python3以及以上版本使用 WXBizMsgCrypt3.py),提供了用户接入企业微信的三个接口,Sample.py文件提供了如何使用这三个接口的示例,ierror.py提供了错误码。 -2.WXBizMsgCrypt封装了VerifyURL, DecryptMsg, EncryptMsg三个接口,分别用于开发者验证回调url,收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考Sample.py文件。 -3.加解密协议请参考企业微信官方文档。 -4.本代码用到了pycrypto第三方库,请开发者自行安装此库再使用。 diff --git a/callback/Sample.py b/callback/Sample.py deleted file mode 100644 index 4915ff2..0000000 --- a/callback/Sample.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -######################################################################### -# Author: jonyqin -# Created Time: Thu 11 Sep 2014 03:55:41 PM CST -# File Name: Sample.py -# Description: WXBizMsgCrypt 使用demo文件 -######################################################################### -from WXBizMsgCrypt import WXBizMsgCrypt -import xml.etree.cElementTree as ET -import sys - -if __name__ == "__main__": - #假设企业在企业微信后台上设置的参数如下 - sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo" - sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt" - sCorpID = "ww1436e0e65a779aee" - ''' - ------------使用示例一:验证回调URL--------------- - *企业开启回调模式时,企业号会向验证url发送一个get请求 - 假设点击验证时,企业收到类似请求: - * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D - * HTTP/1.1 Host: qy.weixin.qq.com - - 接收到该请求时,企业应 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr), - 这一步注意作URL解码。 - 2.验证消息体签名的正确性 - 3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信 - 第2,3步可以用企业微信提供的库函数VerifyURL来实现。 - ''' - wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID) - #sVerifyMsgSig=HttpUtils.ParseUrl("msg_signature") - #ret = wxcpt.VerifyAESKey() - #print ret - sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb" - #sVerifyTimeStamp=HttpUtils.ParseUrl("timestamp") - sVerifyTimeStamp="1476416373" - #sVerifyNonce=HttpUitls.ParseUrl("nonce") - sVerifyNonce="47744683" - #sVerifyEchoStr=HttpUtils.ParseUrl("echostr") - sVerifyEchoStr="fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA==" - ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr) - if(ret!=0): - print "ERR: VerifyURL ret: " + str(ret) - sys.exit(1) - #验证URL成功,将sEchoStr返回给企业号 - #HttpUtils.SetResponse(sEchoStr) - - ''' - ------------使用示例二:对用户回复的消息解密--------------- - 用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档 - 假设企业收到企业微信的回调消息如下: - POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=1409659813&nonce=1372623149 HTTP/1.1 - Host: qy.weixin.qq.com - Content-Length: 613 - - - - - 企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce) - 2.验证消息体签名的正确性。 3.将post请求的数据进行xml解析,并将标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档 - 第2,3步可以用企业微信提供的库函数DecryptMsg来实现。 - ''' - # sReqMsgSig = HttpUtils.ParseUrl("msg_signature") - sReqMsgSig = "0c3914025cb4b4d68103f6bfc8db550f79dcf48e" - sReqTimeStamp = "1476422779" - sReqNonce = "1597212914" - sReqData = "\n\n\n" - ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce) - print ret,sMsg - if( ret!=0 ): - print "ERR: DecryptMsg ret: " + str(ret) - sys.exit(1) - # 解密成功,sMsg即为xml格式的明文 - # TODO: 对明文的处理 - # For example: - xml_tree = ET.fromstring(sMsg) - content = xml_tree.find("Content").text - print content - # ... - # ... - - ''' - ------------使用示例三:企业回复用户消息的加密--------------- - 企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的xml串。 - 假设企业需要回复用户的明文如下: - - - - 1348831860 - - - 1234567890123456 - 128 - - - 为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。 - 2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业号。 - 以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。 - ''' - sRespData = "ww1436e0e65a779aeeChenJiaShun1476422779text你好14564537201000002" - ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp) - if( ret!=0 ): - print "ERR: EncryptMsg ret: " + str(ret) - sys.exit(1) - #ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号 - #TODO: - #HttpUitls.SetResponse(sEncryptMsg) diff --git a/callback/WXBizMsgCrypt.py b/callback/WXBizMsgCrypt.py deleted file mode 100644 index 94e4e12..0000000 --- a/callback/WXBizMsgCrypt.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python -#-*- encoding:utf-8 -*- - -""" 对企业微信发送给企业后台的消息加解密示例代码. -@copyright: Copyright (c) 1998-2014 Tencent Inc. - -""" -# ------------------------------------------------------------------------ - -import base64 -import string -import random -import hashlib -import time -import struct -from Crypto.Cipher import AES -import xml.etree.cElementTree as ET -import sys -import socket -stdi,stdo,stde=sys.stdin,sys.stdout,sys.stderr -reload(sys) -sys.stdin,sys.stdout,sys.stderr=stdi,stdo,stde -import ierror -sys.setdefaultencoding('utf-8') - -""" -关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 -请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 -下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 -""" -class FormatException(Exception): - pass - -def throw_exception(message, exception_class=FormatException): - """my define raise exception function""" - raise exception_class(message) - -class SHA1: - """计算企业微信的消息签名接口""" - - def getSHA1(self, token, timestamp, nonce, encrypt): - """用SHA1算法生成安全签名 - @param token: 票据 - @param timestamp: 时间戳 - @param encrypt: 密文 - @param nonce: 随机字符串 - @return: 安全签名 - """ - try: - sortlist = [token, timestamp, nonce, encrypt] - sortlist.sort() - sha = hashlib.sha1() - sha.update("".join(sortlist)) - return ierror.WXBizMsgCrypt_OK, sha.hexdigest() - except Exception,e: - print e - return ierror.WXBizMsgCrypt_ComputeSignature_Error, None - - -class XMLParse: - """提供提取消息格式中的密文及生成回复消息格式的接口""" - - # xml消息模板 - AES_TEXT_RESPONSE_TEMPLATE = """ - - -%(timestamp)s - -""" - - def extract(self, xmltext): - """提取出xml数据包中的加密消息 - @param xmltext: 待提取的xml字符串 - @return: 提取出的加密消息字符串 - """ - try: - xml_tree = ET.fromstring(xmltext) - encrypt = xml_tree.find("Encrypt") - return ierror.WXBizMsgCrypt_OK, encrypt.text - except Exception,e: - print e - return ierror.WXBizMsgCrypt_ParseXml_Error,None - - def generate(self, encrypt, signature, timestamp, nonce): - """生成xml消息 - @param encrypt: 加密后的消息密文 - @param signature: 安全签名 - @param timestamp: 时间戳 - @param nonce: 随机字符串 - @return: 生成的xml字符串 - """ - resp_dict = { - 'msg_encrypt' : encrypt, - 'msg_signaturet': signature, - 'timestamp' : timestamp, - 'nonce' : nonce, - } - resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict - return resp_xml - - -class PKCS7Encoder(): - """提供基于PKCS7算法的加解密接口""" - - block_size = 32 - def encode(self, text): - """ 对需要加密的明文进行填充补位 - @param text: 需要进行填充补位操作的明文 - @return: 补齐明文字符串 - """ - text_length = len(text) - # 计算需要填充的位数 - amount_to_pad = self.block_size - (text_length % self.block_size) - if amount_to_pad == 0: - amount_to_pad = self.block_size - # 获得补位所用的字符 - pad = chr(amount_to_pad) - return text + pad * amount_to_pad - - def decode(self, decrypted): - """删除解密后明文的补位字符 - @param decrypted: 解密后的明文 - @return: 删除补位字符后的明文 - """ - pad = ord(decrypted[-1]) - if pad<1 or pad >32: - pad = 0 - return decrypted[:-pad] - - -class Prpcrypt(object): - """提供接收和推送给企业微信消息的加解密接口""" - - def __init__(self,key): - - #self.key = base64.b64decode(key+"=") - self.key = key - # 设置加解密模式为AES的CBC模式 - self.mode = AES.MODE_CBC - - - def encrypt(self,text,receiveid): - """对明文进行加密 - @param text: 需要加密的明文 - @return: 加密得到的字符串 - """ - # 16位随机字符串添加到明文开头 - text = self.get_random_str() + struct.pack("I",socket.htonl(len(text))) + text + receiveid - # 使用自定义的填充方式对明文进行补位填充 - pkcs7 = PKCS7Encoder() - text = pkcs7.encode(text) - # 加密 - cryptor = AES.new(self.key,self.mode,self.key[:16]) - try: - ciphertext = cryptor.encrypt(text) - # 使用BASE64对加密后的字符串进行编码 - return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) - except Exception,e: - print e - return ierror.WXBizMsgCrypt_EncryptAES_Error,None - - def decrypt(self,text,receiveid): - """对解密后的明文进行补位删除 - @param text: 密文 - @return: 删除填充补位后的明文 - """ - try: - cryptor = AES.new(self.key,self.mode,self.key[:16]) - # 使用BASE64对密文进行解码,然后AES-CBC解密 - plain_text = cryptor.decrypt(base64.b64decode(text)) - except Exception,e: - print e - return ierror.WXBizMsgCrypt_DecryptAES_Error,None - try: - pad = ord(plain_text[-1]) - # 去掉补位字符串 - #pkcs7 = PKCS7Encoder() - #plain_text = pkcs7.encode(plain_text) - # 去除16位随机字符串 - content = plain_text[16:-pad] - xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0]) - xml_content = content[4 : xml_len+4] - from_receiveid = content[xml_len+4:] - except Exception,e: - print e - return ierror.WXBizMsgCrypt_IllegalBuffer,None - if from_receiveid != receiveid: - return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None - return 0,xml_content - - def get_random_str(self): - """ 随机生成16位字符串 - @return: 16位字符串 - """ - rule = string.letters + string.digits - str = random.sample(rule, 16) - return "".join(str) - -class WXBizMsgCrypt(object): - #构造函数 - def __init__(self,sToken,sEncodingAESKey,sReceiveId): - try: - self.key = base64.b64decode(sEncodingAESKey+"=") - assert len(self.key) == 32 - except: - throw_exception("[error]: EncodingAESKey unvalid !", FormatException) - # return ierror.WXBizMsgCrypt_IllegalAesKey,None - self.m_sToken = sToken - self.m_sReceiveId = sReceiveId - - #验证URL - #@param sMsgSignature: 签名串,对应URL参数的msg_signature - #@param sTimeStamp: 时间戳,对应URL参数的timestamp - #@param sNonce: 随机串,对应URL参数的nonce - #@param sEchoStr: 随机串,对应URL参数的echostr - #@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 - #@return:成功0,失败返回对应的错误码 - - def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): - sha1 = SHA1() - ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) - if ret != 0: - return ret, None - if not signature == sMsgSignature: - return ierror.WXBizMsgCrypt_ValidateSignature_Error, None - pc = Prpcrypt(self.key) - ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sReceiveId) - return ret,sReplyEchoStr - - def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None): - #将企业回复用户的消息加密打包 - #@param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 - #@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 - #@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce - #sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, - #return:成功0,sEncryptMsg,失败返回对应的错误码None - pc = Prpcrypt(self.key) - ret,encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) - if ret != 0: - return ret,None - if timestamp is None: - timestamp = str(int(time.time())) - # 生成安全签名 - sha1 = SHA1() - ret,signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) - if ret != 0: - return ret,None - xmlParse = XMLParse() - return ret,xmlParse.generate(encrypt, signature, timestamp, sNonce) - - def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): - # 检验消息的真实性,并且获取解密后的明文 - # @param sMsgSignature: 签名串,对应URL参数的msg_signature - # @param sTimeStamp: 时间戳,对应URL参数的timestamp - # @param sNonce: 随机串,对应URL参数的nonce - # @param sPostData: 密文,对应POST请求的数据 - # xml_content: 解密后的原文,当return返回0时有效 - # @return: 成功0,失败返回对应的错误码 - # 验证安全签名 - xmlParse = XMLParse() - ret,encrypt = xmlParse.extract(sPostData) - if ret != 0: - return ret, None - sha1 = SHA1() - ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) - if ret != 0: - return ret, None - if not signature == sMsgSignature: - return ierror.WXBizMsgCrypt_ValidateSignature_Error, None - pc = Prpcrypt(self.key) - ret,xml_content = pc.decrypt(encrypt,self.m_sReceiveId) - return ret,xml_content - - diff --git a/callback/WXBizMsgCrypt3.py b/callback/WXBizMsgCrypt3.py index fb9508c..c2d99bf 100644 --- a/callback/WXBizMsgCrypt3.py +++ b/callback/WXBizMsgCrypt3.py @@ -15,14 +15,21 @@ from Crypto.Cipher import AES import xml.etree.cElementTree as ET import socket +import os +import sys -import ierror +# 添加当前目录到系统路径 +current_dir = os.path.dirname(os.path.abspath(__file__)) +if current_dir not in sys.path: + sys.path.append(current_dir) + +from . import ierror """ 关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 -下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 +下载后,按照README中的"Installation"小节的提示进行pycrypto安装。 """ diff --git a/callback_json/Readme.txt b/callback_json/Readme.txt deleted file mode 100644 index 79ebb21..0000000 --- a/callback_json/Readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -ע -1.WXBizMsgCrypt.pyļװWXBizMsgCryptӿ࣬ṩûҵ΢ŵӿڣSample.pyļṩʹӿڵʾierror.pyṩ˴롣 -2.WXBizMsgCryptװVerifyURL, DecryptMsg, EncryptMsgӿڣֱڿ֤صurlյûظϢĽԼ߻ظϢļ̡ܹʹ÷ԲοSample.pyļ -3.ӽЭοҵ΢Źٷĵ -4.õpycrypto⣬뿪аװ˿ʹá \ No newline at end of file diff --git a/callback_json/Sample.py b/callback_json/Sample.py deleted file mode 100644 index ab47c01..0000000 --- a/callback_json/Sample.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -######################################################################### -# Author: jonyqin -# Created Time: Thu 11 Sep 2014 03:55:41 PM CST -# File Name: Sample.py -# Description: WXBizJsonMsgCrypt 使用demo文件 -######################################################################### -from WXBizJsonMsgCrypt import WXBizJsonMsgCrypt -import sys - -if __name__ == "__main__": - #假设企业在企业微信后台上设置的参数如下 - sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo" - sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt" - sCorpID = "ww1436e0e65a779aee" - ''' - ------------使用示例一:验证回调URL--------------- - *企业开启回调模式时,企业号会向验证url发送一个get请求 - 假设点击验证时,企业收到类似请求: - * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D - * HTTP/1.1 Host: qy.weixin.qq.com - - 接收到该请求时,企业应 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr), - 这一步注意作URL解码。 - 2.验证消息体签名的正确性 - 3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信 - 第2,3步可以用企业微信提供的库函数VerifyURL来实现。 - ''' - wxcpt=WXBizJsonMsgCrypt(sToken,sEncodingAESKey,sCorpID) - sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb" - sVerifyTimeStamp="1476416373" - sVerifyNonce="47744683" - sVerifyEchoStr="fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA==" - ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr) - if(ret!=0): - print "ERR: VerifyURL ret: " + str(ret) - sys.exit(1) - else: - print "done VerifyURL" - #验证URL成功,将sEchoStr返回给企业号 - - print "==============================" - ''' - ------------使用示例二:对用户回复的消息解密--------------- - 用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档 - 假设企业收到企业微信的回调消息如下: - POST /cgi-bin/wxpush? msg_signature=e3647471e395139e2308c1fa963f2d648a00b90e×tamp=1409659813&nonce=1372623149 HTTP/1.1 - Host: qy.weixin.qq.com - - { - "tousername": "wx5823bf96d3bd56c7", - "encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=", - "agentid": 218 - } - - 企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce) - 2.验证消息体签名的正确性。 3.将post请求的数据进行json解析,并将"encrypt"标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档 - 第2,3步可以用企业微信提供的库函数DecryptMsg来实现。 - ''' - - sReqNonce = "1372623149" - sReqTimeStamp = "1409659813" - - sReqMsgSig = "e3647471e395139e2308c1fa963f2d648a00b90e" - sReqData = '{ "tousername": "wx5823bf96d3bd56c7", "encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=", "agentid": 218 }'; - ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce) - if( ret!=0 ): - print "ERR: DecryptMsg ret: " + str(ret) - sys.exit(1) - else: - print sMsg - # 解密成功,sMsg即为json格式的明文 - # TODO: 对明文的处理 - # ... - # ... - - print "==============================" - - ''' - ------------使用示例三:企业回复用户消息的加密--------------- - 企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的json串。 - 假设企业需要回复用户的明文如下: - - { - "ToUserName": "mycreate", - "FromUserName":"wx5823bf96d3bd56c7", - "CreateTime": 1348831860, - "MsgType": "text", - "Content": "this is a test", - "MsgId": 1234567890123456, - "AgentID": 128 - } - - 为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。 - 2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成json格式的字符串,发送给企业号。 - 以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。 - ''' - #sRespData = ' { "ToUserName": "mycreate", "FromUserName":"wx5823bf96d3bd56c7", "CreateTime": 1348831860, "MsgType": "text", "Content": "this is a test", "MsgId": 1234567890123456, "AgentID": 128 }'; - sRespData = '{ "ToUserName": "wx5823bf96d3bd56c7", "FromUserName": :mycreate", "CreateTime": 1409659813, "MsgType": "text", "Content": "hello", "MsgId": 4561255354251345929, "AgentID": 218}' - ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp) - if( ret!=0 ): - print "ERR: EncryptMsg ret: " + str(ret) - sys.exit(1) - else: - print sEncryptMsg - #ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号 - print "==============================" diff --git a/callback_json/WXBizJsonMsgCrypt.py b/callback_json/WXBizJsonMsgCrypt.py deleted file mode 100644 index d0be3cc..0000000 --- a/callback_json/WXBizJsonMsgCrypt.py +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env python -#-*- encoding:utf-8 -*- - -""" 对企业微信发送给企业后台的消息加解密示例代码. -@copyright: Copyright (c) 1998-2020 Tencent Inc. - -""" -# ------------------------------------------------------------------------ - -import base64 -import string -import random -import hashlib -import time -import struct -from Crypto.Cipher import AES -import sys -import socket -import json - -reload(sys) -import ierror -sys.setdefaultencoding('utf-8') - -""" -关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 -请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 -下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 -""" -class FormatException(Exception): - pass - -def throw_exception(message, exception_class=FormatException): - """my define raise exception function""" - raise exception_class(message) - -class SHA1: - """计算企业微信的消息签名接口""" - - def getSHA1(self, token, timestamp, nonce, encrypt): - """用SHA1算法生成安全签名 - @param token: 票据 - @param timestamp: 时间戳 - @param encrypt: 密文 - @param nonce: 随机字符串 - @return: 安全签名 - """ - try: - sortlist = [token, timestamp, nonce, encrypt] - sortlist.sort() - sha = hashlib.sha1() - sha.update("".join(sortlist)) - return ierror.WXBizMsgCrypt_OK, sha.hexdigest() - except Exception,e: - print e - return ierror.WXBizMsgCrypt_ComputeSignature_Error, None - - -class JsonParse: - """提供提取消息格式中的密文及生成回复消息格式的接口""" - - # json消息模板 - AES_TEXT_RESPONSE_TEMPLATE = '''{ - "encrypt": "%(msg_encrypt)s", - "msgsignature": "%(msg_signaturet)s", - "timestamp": "%(timestamp)s", - "nonce": "%(nonce)s" - }''' - - def extract(self, jsontext): - """提取出json数据包中的加密消息 - @param jsontext: 待提取的json字符串 - @return: 提取出的加密消息字符串 - """ - try: - json_dict = json.loads(jsontext) - return ierror.WXBizMsgCrypt_OK, json_dict['encrypt'] - except Exception,e: - print e - return ierror.WXBizMsgCrypt_ParseJson_Error, None - def generate(self, encrypt, signature, timestamp, nonce): - """生成json消息 - @param encrypt: 加密后的消息密文 - @param signature: 安全签名 - @param timestamp: 时间戳 - @param nonce: 随机字符串 - @return: 生成的json字符串 - """ - resp_dict = { - 'msg_encrypt' : encrypt, - 'msg_signaturet': signature, - 'timestamp' : timestamp, - 'nonce' : nonce, - } - resp_json = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict - return resp_json - - -class PKCS7Encoder(): - """提供基于PKCS7算法的加解密接口""" - - block_size = 32 - def encode(self, text): - """ 对需要加密的明文进行填充补位 - @param text: 需要进行填充补位操作的明文 - @return: 补齐明文字符串 - """ - text_length = len(text) - # 计算需要填充的位数 - amount_to_pad = self.block_size - (text_length % self.block_size) - if amount_to_pad == 0: - amount_to_pad = self.block_size - # 获得补位所用的字符 - pad = chr(amount_to_pad) - return text + pad * amount_to_pad - - def decode(self, decrypted): - """删除解密后明文的补位字符 - @param decrypted: 解密后的明文 - @return: 删除补位字符后的明文 - """ - pad = ord(decrypted[-1]) - if pad<1 or pad >32: - pad = 0 - return decrypted[:-pad] - - -class Prpcrypt(object): - """提供接收和推送给企业微信消息的加解密接口""" - - def __init__(self,key): - - #self.key = base64.b64decode(key+"=") - self.key = key - # 设置加解密模式为AES的CBC模式 - self.mode = AES.MODE_CBC - - - def encrypt(self,text,receiveid): - """对明文进行加密 - @param text: 需要加密的明文 - @return: 加密得到的字符串 - """ - # 16位随机字符串添加到明文开头 - text = self.get_random_str() + struct.pack("I",socket.htonl(len(text))) + text + receiveid - # 使用自定义的填充方式对明文进行补位填充 - pkcs7 = PKCS7Encoder() - text = pkcs7.encode(text) - # 加密 - cryptor = AES.new(self.key,self.mode,self.key[:16]) - try: - ciphertext = cryptor.encrypt(text) - # 使用BASE64对加密后的字符串进行编码 - return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) - except Exception,e: - print e - return ierror.WXBizMsgCrypt_EncryptAES_Error,None - - def decrypt(self,text,receiveid): - """对解密后的明文进行补位删除 - @param text: 密文 - @return: 删除填充补位后的明文 - """ - try: - cryptor = AES.new(self.key,self.mode,self.key[:16]) - # 使用BASE64对密文进行解码,然后AES-CBC解密 - plain_text = cryptor.decrypt(base64.b64decode(text)) - except Exception,e: - print e - return ierror.WXBizMsgCrypt_DecryptAES_Error,None - try: - pad = ord(plain_text[-1]) - # 去掉补位字符串 - #pkcs7 = PKCS7Encoder() - #plain_text = pkcs7.encode(plain_text) - # 去除16位随机字符串 - content = plain_text[16:-pad] - json_len = socket.ntohl(struct.unpack("I",content[ : 4])[0]) - json_content = content[4 : json_len+4] - from_receiveid = content[json_len+4:] - except Exception,e: - print e - return ierror.WXBizMsgCrypt_IllegalBuffer,None - if from_receiveid != receiveid: - print "receiveid not match" - print from_receiveid - return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None - return 0,json_content - - def get_random_str(self): - """ 随机生成16位字符串 - @return: 16位字符串 - """ - rule = string.letters + string.digits - str = random.sample(rule, 16) - return "".join(str) - -class WXBizJsonMsgCrypt(object): - #构造函数 - def __init__(self,sToken,sEncodingAESKey,sReceiveId): - try: - self.key = base64.b64decode(sEncodingAESKey+"=") - assert len(self.key) == 32 - except: - throw_exception("[error]: EncodingAESKey unvalid !", FormatException) - # return ierror.WXBizMsgCrypt_IllegalAesKey,None - self.m_sToken = sToken - self.m_sReceiveId = sReceiveId - - #验证URL - #@param sMsgSignature: 签名串,对应URL参数的msg_signature - #@param sTimeStamp: 时间戳,对应URL参数的timestamp - #@param sNonce: 随机串,对应URL参数的nonce - #@param sEchoStr: 随机串,对应URL参数的echostr - #@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 - #@return:成功0,失败返回对应的错误码 - - def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): - sha1 = SHA1() - ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) - if ret != 0: - return ret, None - if not signature == sMsgSignature: - return ierror.WXBizMsgCrypt_ValidateSignature_Error, None - pc = Prpcrypt(self.key) - ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sReceiveId) - return ret,sReplyEchoStr - - def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None): - #将企业回复用户的消息加密打包 - #@param sReplyMsg: 企业号待回复用户的消息,json格式的字符串 - #@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 - #@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce - #sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串, - #return:成功0,sEncryptMsg,失败返回对应的错误码None - pc = Prpcrypt(self.key) - ret,encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) - if ret != 0: - return ret,None - if timestamp is None: - timestamp = str(int(time.time())) - # 生成安全签名 - sha1 = SHA1() - ret,signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) - if ret != 0: - return ret,None - jsonParse = JsonParse() - return ret,jsonParse.generate(encrypt, signature, timestamp, sNonce) - - def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): - # 检验消息的真实性,并且获取解密后的明文 - # @param sMsgSignature: 签名串,对应URL参数的msg_signature - # @param sTimeStamp: 时间戳,对应URL参数的timestamp - # @param sNonce: 随机串,对应URL参数的nonce - # @param sPostData: 密文,对应POST请求的数据 - # json_content: 解密后的原文,当return返回0时有效 - # @return: 成功0,失败返回对应的错误码 - # 验证安全签名 - jsonParse = JsonParse() - ret,encrypt = jsonParse.extract(sPostData) - if ret != 0: - return ret, None - sha1 = SHA1() - ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) - if ret != 0: - return ret, None - if not signature == sMsgSignature: - print "signature not match" - print signature - return ierror.WXBizMsgCrypt_ValidateSignature_Error, None - pc = Prpcrypt(self.key) - ret,json_content = pc.decrypt(encrypt,self.m_sReceiveId) - return ret,json_content - - diff --git a/callback_json/ierror.py b/callback_json/ierror.py deleted file mode 100644 index 51c02c3..0000000 --- a/callback_json/ierror.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -######################################################################### -# Author: jonyqin -# Created Time: Thu 11 Sep 2014 01:53:58 PM CST -# File Name: ierror.py -# Description:定义错误码含义 -######################################################################### -WXBizMsgCrypt_OK = 0 -WXBizMsgCrypt_ValidateSignature_Error = -40001 -WXBizMsgCrypt_ParseJson_Error = -40002 -WXBizMsgCrypt_ComputeSignature_Error = -40003 -WXBizMsgCrypt_IllegalAesKey = -40004 -WXBizMsgCrypt_ValidateCorpid_Error = -40005 -WXBizMsgCrypt_EncryptAES_Error = -40006 -WXBizMsgCrypt_DecryptAES_Error = -40007 -WXBizMsgCrypt_IllegalBuffer = -40008 -WXBizMsgCrypt_EncodeBase64_Error = -40009 -WXBizMsgCrypt_DecodeBase64_Error = -40010 -WXBizMsgCrypt_GenReturnJson_Error = -40011 diff --git a/conf.py b/conf.py deleted file mode 100644 index c37571d..0000000 --- a/conf.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/python -# -*- coding:utf-8 -*- -## - # Copyright (C) 2018 All rights reserved. - # - # @File conf.py - # @Brief - # @Author abelzhu, abelzhu@tencent.com - # @Version 1.0 - # @Date 2018-02-23 - # - # - -## 设置为true会打印一些调试信息 -DEBUG = True - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..13ec28f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask>=3.1.0 +pycryptodome>=3.22.0 +python-dotenv>=1.1.0 +requests>=2.32.3 diff --git a/workapi.py b/workapi.py new file mode 100644 index 0000000..b5fed69 --- /dev/null +++ b/workapi.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +from flask import Flask, request, abort +from xml.dom.minidom import parseString +import threading +import time +import os +import sys +import logging +from typing import Tuple, Optional +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +# 确保log目录存在 +log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'log') +os.makedirs(log_dir, exist_ok=True) + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(os.path.join(log_dir, 'qywx.log')), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# 添加当前目录到系统路径 +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(current_dir) + +# 导入企业微信相关模块 +from callback.WXBizMsgCrypt3 import WXBizMsgCrypt + +app = Flask(__name__) + +# 从环境变量获取配置 +CORP_ID = os.getenv('CORP_ID') +SUITE_TOKEN = os.getenv('SUITE_TOKEN') +SUITE_ENCODING_AES_KEY = os.getenv('SUITE_ENCODING_AES_KEY') + +# 初始化企业微信API实例 +qy_api = [ + WXBizMsgCrypt(SUITE_TOKEN, SUITE_ENCODING_AES_KEY, CORP_ID), +] + +def process_message(name: str, content: str, channel: int, msg_type: int) -> None: + """处理接收到的消息""" + try: + cmd = f"python3 command.py '{name}' '{content}' '{channel}' '{msg_type}'" + threading.Thread(target=lambda: os.system(cmd)).start() + except Exception as e: + logger.error(f"Error processing message: {e}") + +def parse_xml_message(xml_content: str) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: + """解析XML消息内容""" + try: + doc = parseString(xml_content) + collection = doc.documentElement + + name = collection.getElementsByTagName("FromUserName")[0].childNodes[0].data + msg_type = collection.getElementsByTagName("MsgType")[0].childNodes[0].data + content = None + pic_url = None + + if msg_type == "text": + content = collection.getElementsByTagName("Content")[0].childNodes[0].data + elif msg_type == "image": + pic_url = collection.getElementsByTagName("PicUrl")[0].childNodes[0].data + + return name, content, pic_url, msg_type + except Exception as e: + logger.error(f"Error parsing XML: {e}") + return None, None, None, None + +@app.route('/hook_path', methods=['GET', 'POST']) +def webhook(): + """处理企业微信回调请求""" + if request.method == 'GET': + return verify_url(request) + elif request.method == 'POST': + return handle_message(request) + return abort(405) + +def verify_url(request) -> str: + """验证URL有效性""" + msg_signature = request.args.get('msg_signature', '') + timestamp = request.args.get('timestamp', '') + nonce = request.args.get('nonce', '') + echo_str = request.args.get('echostr', '') + + ret, sEchoStr = qy_api[0].VerifyURL(msg_signature, timestamp, nonce, echo_str) + if ret != 0: + logger.error(f"URL verification failed with ret: {ret}") + return "failed" + return sEchoStr + +def handle_message(request) -> str: + """处理接收到的消息""" + msg_signature = request.args.get('msg_signature', '') + timestamp = request.args.get('timestamp', '') + nonce = request.args.get('nonce', '') + data = request.data.decode('utf-8') + + ret, sMsg = qy_api[0].DecryptMsg(data, msg_signature, timestamp, nonce) + if ret != 0: + logger.error(f"Message decryption failed with ret: {ret}") + return "failed" + + name, content, pic_url, msg_type = parse_xml_message(sMsg) + if not name: + return "failed" + + # 记录日志 + if msg_type == "text": + logger.info(f"[ch0] {name}: {content}") + process_message(name, content, 0, 0) + elif msg_type == "image": + logger.info(f"[ch0] {name}: 图片消息") + process_message(name, pic_url, 0, 1) + + return "ok" + +if __name__ == '__main__': + port = int(os.getenv('FLASK_PORT', 8066)) + ssl_context = None + + # 检查SSL证书和密钥文件是否存在 + cert_path = os.getenv('SSL_CERT_PATH') + key_path = os.getenv('SSL_KEY_PATH') + + if cert_path and key_path and os.path.exists(cert_path) and os.path.exists(key_path): + ssl_context = (cert_path, key_path) + logger.info(f"Using SSL with certificate: {cert_path} and key: {key_path}") + else: + logger.warning("SSL certificate or key not found. Running in HTTP mode.") + logger.warning("For production use, please set SSL_CERT_PATH and SSL_KEY_PATH in .env file.") + + app.run("0.0.0.0", port, ssl_context=ssl_context) \ No newline at end of file