python-plugins 0.1.4__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_plugins-0.1.4 → python_plugins-0.1.6}/.gitignore +0 -1
- {python_plugins-0.1.4 → python_plugins-0.1.6}/CHANGES.rst +14 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/PKG-INFO +3 -1
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/usage.rst +22 -3
- {python_plugins-0.1.4 → python_plugins-0.1.6}/pyproject.toml +2 -1
- {python_plugins-0.1.4 → python_plugins-0.1.6}/requirements/test.in +2 -1
- python_plugins-0.1.6/src/python_plugins/__about__.py +1 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/__init__.py +1 -0
- python_plugins-0.1.6/src/python_plugins/models/mixins/token_minxin.py +8 -0
- python_plugins-0.1.6/src/python_plugins/weixin/biz_data_crypt.py +69 -0
- python_plugins-0.1.6/src/python_plugins/weixin/error_code.py +29 -0
- python_plugins-0.1.6/src/python_plugins/weixin/format_response.py +148 -0
- python_plugins-0.1.6/src/python_plugins/weixin/wechat.py +135 -0
- python_plugins-0.1.6/src/python_plugins/weixin/wechat_crypt.py +141 -0
- python_plugins-0.1.6/src/python_plugins/weixin/weixin_api.py +94 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_sqlalchemy.py +2 -1
- python_plugins-0.1.6/tests/test_weixin.py +104 -0
- python_plugins-0.1.4/src/python_plugins/__about__.py +0 -1
- {python_plugins-0.1.4 → python_plugins-0.1.6}/.github/workflows/release.yml +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/.readthedocs.yaml +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/LICENSE.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/README.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/Makefile +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/api.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/changes.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/conf.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/examples.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/index.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/make.bat +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/requirements.txt +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/examples/README.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/requirements/build.in +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/convert/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/convert/datetime_str.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/convert/xml.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/crypto/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/crypto/fernet.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/crypto/str_to_list.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/dumps/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/dumps/postgresql_dump.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/email/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/email/smtp.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/hashes/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/hashes/hash.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/jwt/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/jwt/jwt.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/data_mixin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/primary_key_mixin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/timestamp_mixin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/user_minxin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/update.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/process/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/process/python_venv_process.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/process/sub_process.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/random/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/random/random_str.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/utils/remove_pycache.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/conftest.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_crypto.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_email.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_jwt.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_process.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_random.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-plugins
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A collection of Python functions and classes.
|
|
5
5
|
Project-URL: Documentation, https://python-plugins.readthedocs.io
|
|
6
6
|
Project-URL: Source, https://github.com/ojso/python-plugins
|
|
@@ -46,6 +46,8 @@ Provides-Extra: pillow
|
|
|
46
46
|
Requires-Dist: pillow; extra == 'pillow'
|
|
47
47
|
Provides-Extra: qrcode
|
|
48
48
|
Requires-Dist: qrcode; extra == 'qrcode'
|
|
49
|
+
Provides-Extra: requests
|
|
50
|
+
Requires-Dist: requests; extra == 'requests'
|
|
49
51
|
Provides-Extra: sqlalchemy
|
|
50
52
|
Requires-Dist: sqlalchemy; extra == 'sqlalchemy'
|
|
51
53
|
Description-Content-Type: text/x-rst
|
|
@@ -44,18 +44,37 @@ mixins
|
|
|
44
44
|
from flask_sqlalchemy import SQLAlchemy
|
|
45
45
|
from python_plugins.models.mixins import PrimaryKeyMixin
|
|
46
46
|
from python_plugins.models.mixins import UserMixin
|
|
47
|
+
from python_plugins.models.mixins import DataMixin
|
|
47
48
|
from python_plugins.models.mixins import TimestampMixin
|
|
48
49
|
|
|
49
50
|
db = SQLAlchemy()
|
|
50
51
|
|
|
51
|
-
class User(PrimaryKeyMixin,
|
|
52
|
-
|
|
52
|
+
class User(PrimaryKeyMixin, DataMixin, TimestampMixin, UserMixin, db.models):
|
|
53
|
+
__tablename__ = "users"
|
|
53
54
|
|
|
54
55
|
remove_pycache
|
|
55
56
|
=======================
|
|
56
57
|
|
|
57
|
-
.. code-block
|
|
58
|
+
.. code-block:: python
|
|
58
59
|
|
|
59
60
|
from python_plugins.utils.remove_pycache import remove_pycache
|
|
60
61
|
|
|
61
62
|
remove_pycache(".")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
weixin.wechat
|
|
66
|
+
==================
|
|
67
|
+
|
|
68
|
+
.. code-block:: python
|
|
69
|
+
|
|
70
|
+
from python_plugins.weixin.wechat import Wechat
|
|
71
|
+
|
|
72
|
+
class MyWechat(Wechat):
|
|
73
|
+
def get_app(self) -> dict:
|
|
74
|
+
# may depended on self.name from self.__init__(name)
|
|
75
|
+
return "<your app>"
|
|
76
|
+
|
|
77
|
+
mywechat = MyWechat("name")
|
|
78
|
+
mywechat.verify(query)
|
|
79
|
+
mywechat.chat(query,content)
|
|
80
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.6"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives import padding
|
|
6
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WeixinBizDataCrypt:
|
|
10
|
+
"""Weixin Biz Data Crypt
|
|
11
|
+
微信小程序服务端获取开放数据
|
|
12
|
+
see https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, appid, sessionKey):
|
|
16
|
+
self.appid = appid
|
|
17
|
+
self.sessionKey = sessionKey
|
|
18
|
+
|
|
19
|
+
def decrypt(self, data, iv):
|
|
20
|
+
# base64 decode
|
|
21
|
+
sessionKey = base64.b64decode(self.sessionKey)
|
|
22
|
+
encryptedData = base64.b64decode(data)
|
|
23
|
+
iv = base64.b64decode(iv)
|
|
24
|
+
|
|
25
|
+
# cipher = AES.new(sessionKey, AES.MODE_CBC, iv)
|
|
26
|
+
# decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData)))
|
|
27
|
+
decryptor = Cipher(
|
|
28
|
+
algorithms.AES(sessionKey),
|
|
29
|
+
modes.CBC(iv),
|
|
30
|
+
).decryptor()
|
|
31
|
+
plaintext_padded = decryptor.update(encryptedData)
|
|
32
|
+
plaintext_padded += decryptor.finalize()
|
|
33
|
+
|
|
34
|
+
unpadded = self._unpad(plaintext_padded)
|
|
35
|
+
decrypted = json.loads(unpadded)
|
|
36
|
+
|
|
37
|
+
# 校验签名
|
|
38
|
+
if decrypted["watermark"]["appid"] != self.appid:
|
|
39
|
+
raise Exception("Invalid Buffer")
|
|
40
|
+
|
|
41
|
+
return decrypted
|
|
42
|
+
|
|
43
|
+
def encrypt(self, data, iv) -> str:
|
|
44
|
+
# 添加watermark
|
|
45
|
+
data["watermark"] = {"appid": self.appid, "timestamp": int(time.time())}
|
|
46
|
+
nopadData = json.dumps(data).encode()
|
|
47
|
+
sessionKey = base64.b64decode(self.sessionKey)
|
|
48
|
+
iv = base64.b64decode(iv)
|
|
49
|
+
|
|
50
|
+
encryptor = Cipher(
|
|
51
|
+
algorithms.AES(sessionKey),
|
|
52
|
+
modes.CBC(iv),
|
|
53
|
+
).encryptor()
|
|
54
|
+
|
|
55
|
+
# see https://cryptography.io/en/latest/hazmat/primitives/padding/
|
|
56
|
+
# 填补数据,满足block_size的倍数
|
|
57
|
+
padder = padding.PKCS7(128).padder()
|
|
58
|
+
padded_data = padder.update(nopadData)
|
|
59
|
+
padded_data += padder.finalize()
|
|
60
|
+
|
|
61
|
+
# 加密
|
|
62
|
+
encrypted = encryptor.update(padded_data) + encryptor.finalize()
|
|
63
|
+
# base64.b64encode -> str
|
|
64
|
+
encrypted = base64.b64encode(encrypted).decode()
|
|
65
|
+
return encrypted
|
|
66
|
+
|
|
67
|
+
def _unpad(self, s):
|
|
68
|
+
"""cryptography.hazmat.primitives.padding.PKCS7(128).unpadder()的简化版本"""
|
|
69
|
+
return s[: -ord(s[-1:])]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# /**
|
|
2
|
+
# * error code 说明.
|
|
3
|
+
# * <ul>
|
|
4
|
+
# * <li>-40001: 签名验证错误</li>
|
|
5
|
+
# * <li>-40002: xml解析失败</li>
|
|
6
|
+
# * <li>-40003: sha加密生成签名失败</li>
|
|
7
|
+
# * <li>-40004: encodingAesKey 非法</li>
|
|
8
|
+
# * <li>-40005: appid 校验错误</li>
|
|
9
|
+
# * <li>-40006: aes 加密失败</li>
|
|
10
|
+
# * <li>-40007: aes 解密失败</li>
|
|
11
|
+
# * <li>-40008: 解密后得到的buffer非法</li>
|
|
12
|
+
# * <li>-40009: base64加密失败</li>
|
|
13
|
+
# * <li>-40010: base64解密失败</li>
|
|
14
|
+
# * <li>-40011: 生成xml失败</li>
|
|
15
|
+
# * </ul>
|
|
16
|
+
# */
|
|
17
|
+
class ErrorCode:
|
|
18
|
+
OK = 0
|
|
19
|
+
ValidateSignatureError = -40001
|
|
20
|
+
ParseXmlError = -40002
|
|
21
|
+
ComputeSignatureError = -40003
|
|
22
|
+
IllegalAesKey = -40004
|
|
23
|
+
ValidateAppidError = -40005
|
|
24
|
+
EncryptAESError = -40006
|
|
25
|
+
DecryptAESError = -40007
|
|
26
|
+
IllegalBuffer = -40008
|
|
27
|
+
EncodeBase64Error = -40009
|
|
28
|
+
DecodeBase64Error = -40010
|
|
29
|
+
GenReturnXmlError = -40011
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
# see https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html
|
|
4
|
+
|
|
5
|
+
# 文本消息
|
|
6
|
+
# Content 回复的消息内容(换行:在content中能够换行\n,支持超链接<a href="url">xxx</a>),
|
|
7
|
+
XML_TEXT_TEMPLATE = """<xml>
|
|
8
|
+
<ToUserName><![CDATA[{toUser}]]></ToUserName>
|
|
9
|
+
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
|
|
10
|
+
<CreateTime>{createtime}</CreateTime>
|
|
11
|
+
<MsgType><![CDATA[text]]></MsgType>
|
|
12
|
+
<Content><![CDATA[{content}]]></Content>
|
|
13
|
+
</xml>"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 图片消息
|
|
17
|
+
# MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id
|
|
18
|
+
XML_IMAGE_TEMPLATE = """<xml>
|
|
19
|
+
<ToUserName><![CDATA [{toUser}]]></ToUserName>
|
|
20
|
+
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
|
|
21
|
+
<CreateTime>{createtime}</CreateTime>
|
|
22
|
+
<MsgType><![CDATA[image]]></MsgType>
|
|
23
|
+
<Image>
|
|
24
|
+
<MediaId><![CDATA[{media_id}]]></MediaId>
|
|
25
|
+
</Image>
|
|
26
|
+
</xml>"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# 语音消息
|
|
30
|
+
# MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id
|
|
31
|
+
|
|
32
|
+
XML_VOICE_TEMPLATE = """<xml>
|
|
33
|
+
<ToUserName><![CDATA [{toUser}]]></ToUserName>
|
|
34
|
+
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
|
|
35
|
+
<CreateTime>{createtime}</CreateTime>
|
|
36
|
+
<MsgType><![CDATA[voice]]></MsgType>
|
|
37
|
+
<Voice>
|
|
38
|
+
<MediaId><![CDATA[{media_id}]]></MediaId>
|
|
39
|
+
</Voice>
|
|
40
|
+
</xml>"""
|
|
41
|
+
|
|
42
|
+
# 视频消息
|
|
43
|
+
# MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id
|
|
44
|
+
# Title 否 视频消息的标题
|
|
45
|
+
# Description 否 视频消息的描述
|
|
46
|
+
|
|
47
|
+
XML_VIDEO_TEMPLATE = """<xml>
|
|
48
|
+
<ToUserName><![CDATA [{toUser}]]></ToUserName>
|
|
49
|
+
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
|
|
50
|
+
<CreateTime>{createtime}</CreateTime>
|
|
51
|
+
<MsgType><![CDATA[video]]></MsgType>
|
|
52
|
+
<Video>
|
|
53
|
+
<MediaId><![CDATA[{media_id}]]></MediaId>
|
|
54
|
+
<Title><![CDATA[{title}]]></Title>
|
|
55
|
+
<Description><![CDATA[{description}]]></Description>
|
|
56
|
+
</Video>
|
|
57
|
+
</xml>"""
|
|
58
|
+
|
|
59
|
+
# 音乐消息
|
|
60
|
+
|
|
61
|
+
# Title 否 音乐标题
|
|
62
|
+
# Description 否 音乐描述
|
|
63
|
+
# MusicURL 否 音乐链接
|
|
64
|
+
# HQMusicUrl 否 高质量音乐链接,WIFI环境优先使用该链接播放音乐
|
|
65
|
+
# ThumbMediaId 是 缩略图的媒体id,通过素材管理中的接口上传多媒体文件,得到的id
|
|
66
|
+
|
|
67
|
+
XML_MUSIC_TEMPLATE = """<xml>
|
|
68
|
+
<ToUserName><![CDATA [{toUser}]]></ToUserName>
|
|
69
|
+
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
|
|
70
|
+
<CreateTime>{createtime}</CreateTime>
|
|
71
|
+
<MsgType><![CDATA[music]]></MsgType>
|
|
72
|
+
<Music>
|
|
73
|
+
<Title><![CDATA[{title}]]></Title>
|
|
74
|
+
<Description><![CDATA[{description}]]></Description>
|
|
75
|
+
<MusicUrl><![CDATA[{music_url}]]></MusicUrl>
|
|
76
|
+
<HQMusicUrl><![CDATA[{hq_music_url}]]></HQMusicUrl>
|
|
77
|
+
<ThumbMediaId><![CDATA[{media_id}]]></ThumbMediaId>
|
|
78
|
+
</Music>
|
|
79
|
+
</xml>"""
|
|
80
|
+
|
|
81
|
+
# 图文消息
|
|
82
|
+
|
|
83
|
+
# ArticleCount 是 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
|
|
84
|
+
# Articles 是 图文消息信息,注意,如果图文数超过限制,则将只发限制内的条数
|
|
85
|
+
# Title 是 图文消息标题
|
|
86
|
+
# Description 是 图文消息描述
|
|
87
|
+
# PicUrl 是 图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
|
|
88
|
+
# Url 是 点击图文消息跳转链接
|
|
89
|
+
|
|
90
|
+
XML_NEWS_TEMPLATE_OLD = """<xml>
|
|
91
|
+
<ToUserName><![CDATA [{toUser}]]></ToUserName>
|
|
92
|
+
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
|
|
93
|
+
<CreateTime>{createtime}</CreateTime>
|
|
94
|
+
<MsgType><![CDATA[news]]></MsgType>
|
|
95
|
+
<ArticleCount>{article_count}</ArticleCount>
|
|
96
|
+
<Articles>
|
|
97
|
+
<item>
|
|
98
|
+
<Title><![CDATA[{title}]]></Title>
|
|
99
|
+
<Description><![CDATA[{description}]]></Description>
|
|
100
|
+
<PicUrl><![CDATA[{pic_url}]]></PicUrl>
|
|
101
|
+
<Url><![CDATA[{url}]]></Url>
|
|
102
|
+
</item>
|
|
103
|
+
</Articles>
|
|
104
|
+
</xml>"""
|
|
105
|
+
|
|
106
|
+
XML_ARTICLE_ITEM = """<item>
|
|
107
|
+
<Title><![CDATA[{title}]]></Title>
|
|
108
|
+
<Description><![CDATA[{description}]]></Description>
|
|
109
|
+
<PicUrl><![CDATA[{pic_url}]]></PicUrl>
|
|
110
|
+
<Url><![CDATA[{url}]]></Url>
|
|
111
|
+
</item>"""
|
|
112
|
+
|
|
113
|
+
XML_NEWS_TEMPLATE = """<xml>
|
|
114
|
+
<ToUserName><![CDATA [{toUser}]]></ToUserName>
|
|
115
|
+
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
|
|
116
|
+
<CreateTime>{createtime}</CreateTime>
|
|
117
|
+
<MsgType><![CDATA[news]]></MsgType>
|
|
118
|
+
<ArticleCount>{article_count}</ArticleCount>
|
|
119
|
+
<Articles>
|
|
120
|
+
{xml_articles_items}
|
|
121
|
+
</Articles>
|
|
122
|
+
</xml>"""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_wechat_xml_response(data):
|
|
126
|
+
if "createtime" not in data:
|
|
127
|
+
data["createtime"] = int(time.time())
|
|
128
|
+
|
|
129
|
+
match data["type"]:
|
|
130
|
+
case "text":
|
|
131
|
+
xml = XML_TEXT_TEMPLATE.format(**data)
|
|
132
|
+
case "image":
|
|
133
|
+
xml = XML_IMAGE_TEMPLATE.format(**data)
|
|
134
|
+
case "voice":
|
|
135
|
+
xml = XML_VOICE_TEMPLATE.format(**data)
|
|
136
|
+
case "video":
|
|
137
|
+
xml = XML_VIDEO_TEMPLATE.format(**data)
|
|
138
|
+
case "music":
|
|
139
|
+
xml = XML_MUSIC_TEMPLATE.format(**data)
|
|
140
|
+
case "news":
|
|
141
|
+
xml_articles_items = ""
|
|
142
|
+
for article in data["articles"]:
|
|
143
|
+
xml_articles_items += XML_ARTICLE_ITEM.format(**article)
|
|
144
|
+
data["article_count"] = len(data["articles"])
|
|
145
|
+
data["xml_articles_items"] = xml_articles_items
|
|
146
|
+
xml = XML_NEWS_TEMPLATE.format(**data)
|
|
147
|
+
|
|
148
|
+
return xml
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from python_plugins.convert import xml2dict
|
|
3
|
+
from .wechat_crypt import MessageCrypt
|
|
4
|
+
from .format_response import get_wechat_xml_response
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Wechat:
|
|
8
|
+
|
|
9
|
+
def __init__(self, name=None):
|
|
10
|
+
self.name = name
|
|
11
|
+
self.app = self.get_app()
|
|
12
|
+
|
|
13
|
+
# 获取app
|
|
14
|
+
def get_app(self) -> dict:
|
|
15
|
+
raise NotImplementedError()
|
|
16
|
+
|
|
17
|
+
# 记录信息
|
|
18
|
+
def log_data(self):
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def verify(self, args):
|
|
22
|
+
signature = args["signature"]
|
|
23
|
+
timestamp = args["timestamp"]
|
|
24
|
+
nonce = args["nonce"]
|
|
25
|
+
echostr = args["echostr"]
|
|
26
|
+
token = self.app["token"]
|
|
27
|
+
tmpstr = "".join(sorted([token, timestamp, nonce])).encode()
|
|
28
|
+
if hashlib.sha1(tmpstr).hexdigest() == signature:
|
|
29
|
+
return echostr
|
|
30
|
+
else:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
def chat(self, args, content):
|
|
34
|
+
self.openid = args.get("openid")
|
|
35
|
+
msg_signature = args.get("msg_signature", "")
|
|
36
|
+
timestamp = args.get("timestamp", "")
|
|
37
|
+
nonce = args.get("nonce", "")
|
|
38
|
+
# 加密标记
|
|
39
|
+
encrypt_type = args.get("encrypt_type")
|
|
40
|
+
xml_dict = xml2dict(content)
|
|
41
|
+
|
|
42
|
+
if not encrypt_type:
|
|
43
|
+
self.input = xml_dict
|
|
44
|
+
self.dispatch()
|
|
45
|
+
self.get_xml_response()
|
|
46
|
+
xml_reponse = self.xml_response
|
|
47
|
+
else:
|
|
48
|
+
# decrypt
|
|
49
|
+
mc = MessageCrypt(self.app["appid"], self.app["token"], self.app["aeskey"])
|
|
50
|
+
xml_decrypted = mc.decrypt_msg(
|
|
51
|
+
timestamp, nonce, xml_dict["Encrypt"], msg_signature
|
|
52
|
+
)
|
|
53
|
+
self.input = xml2dict(xml_decrypted)
|
|
54
|
+
self.dispatch()
|
|
55
|
+
self.get_xml_response()
|
|
56
|
+
# encrypt
|
|
57
|
+
xml_reponse = mc.encrypt_msg(self.xml_response, timestamp, nonce)
|
|
58
|
+
|
|
59
|
+
# 返回前记录下日志,如果实现记录日志的话
|
|
60
|
+
self.log_data()
|
|
61
|
+
|
|
62
|
+
return xml_reponse
|
|
63
|
+
|
|
64
|
+
def dispatch(self):
|
|
65
|
+
self.default_answer()
|
|
66
|
+
self.toUser = self.input["ToUserName"]
|
|
67
|
+
self.fromUser = self.input["FromUserName"]
|
|
68
|
+
msgType = self.input["MsgType"]
|
|
69
|
+
|
|
70
|
+
if msgType == "text":
|
|
71
|
+
keyword = self.input["Content"]
|
|
72
|
+
elif msgType == "event":
|
|
73
|
+
event = self.input["Event"]
|
|
74
|
+
if event == "subscribe":
|
|
75
|
+
# self.onSubscribe()
|
|
76
|
+
keyword = "subscribe"
|
|
77
|
+
elif event == "unsubscribe":
|
|
78
|
+
# self.onUnsubscribe()
|
|
79
|
+
keyword = "unsubscribe"
|
|
80
|
+
elif event == "CLICK":
|
|
81
|
+
eventKey = self.input["EventKey"]
|
|
82
|
+
keyword = eventKey
|
|
83
|
+
else:
|
|
84
|
+
keyword = "<event:{event}>"
|
|
85
|
+
elif msgType == "image":
|
|
86
|
+
keyword = f"<{msgType}>"
|
|
87
|
+
elif msgType == "voice":
|
|
88
|
+
keyword = f"<{msgType}>"
|
|
89
|
+
elif msgType == "video":
|
|
90
|
+
keyword = f"<{msgType}>"
|
|
91
|
+
elif msgType == "shortvideo":
|
|
92
|
+
keyword = f"<{msgType}>"
|
|
93
|
+
elif msgType == "location":
|
|
94
|
+
location_x = self.input["location_x"]
|
|
95
|
+
location_y = self.input["location_y"]
|
|
96
|
+
keyword = f"<{msgType}({location_x},{location_y})>"
|
|
97
|
+
elif msgType == "link":
|
|
98
|
+
keyword = f"<{msgType}>"
|
|
99
|
+
else:
|
|
100
|
+
keyword = f"<{msgType}>"
|
|
101
|
+
|
|
102
|
+
self.keyword = keyword
|
|
103
|
+
|
|
104
|
+
self.get_answer()
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def default_answer(self):
|
|
108
|
+
self.answer = {"type": "text", "content": "I'm sorry, I don't understand."}
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
def get_answer(self):
|
|
112
|
+
match self.keyword:
|
|
113
|
+
case "subscribe":
|
|
114
|
+
r = f"Hello, welcome to {self.app['name']}!"
|
|
115
|
+
self.answer = {"type": "text", "content": r}
|
|
116
|
+
case _:
|
|
117
|
+
self.answer = {"type": "text", "content": "a:" + self.keyword}
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
def get_xml_response(self):
|
|
121
|
+
self.data_response = {
|
|
122
|
+
"type" : self.answer["type"],
|
|
123
|
+
"toUser": self.fromUser,
|
|
124
|
+
"fromUser": self.toUser,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
match self.answer["type"]:
|
|
128
|
+
case "text":
|
|
129
|
+
self.data_response["content"] = self.answer["content"]
|
|
130
|
+
case "news":
|
|
131
|
+
self.data_response["articles"] = self.answer["articles"]
|
|
132
|
+
|
|
133
|
+
self.xml_response = get_wechat_xml_response(self.data_response)
|
|
134
|
+
|
|
135
|
+
return
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import string
|
|
3
|
+
import random
|
|
4
|
+
import hashlib
|
|
5
|
+
import struct
|
|
6
|
+
import socket
|
|
7
|
+
|
|
8
|
+
# from Crypto.Cipher import AES
|
|
9
|
+
# 使用 cryptography 替代 pycrypto 作为加密包
|
|
10
|
+
from cryptography.hazmat.primitives import hashes, padding
|
|
11
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
12
|
+
|
|
13
|
+
ENCRYPT_TEXT_RESPONSE_TEMPLATE = """<xml>
|
|
14
|
+
<Encrypt><![CDATA[{msg_encrypt}]]></Encrypt>
|
|
15
|
+
<MsgSignature><![CDATA[{msg_signaturet}]]></MsgSignature>
|
|
16
|
+
<TimeStamp>{timestamp}</TimeStamp>
|
|
17
|
+
<Nonce><![CDATA[{nonce}]]></Nonce>
|
|
18
|
+
</xml>"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageCrypt:
|
|
22
|
+
def __init__(self, appid, token, aeskey):
|
|
23
|
+
self.appid = appid
|
|
24
|
+
self.token = token
|
|
25
|
+
self.key = base64.b64decode(aeskey + "=")
|
|
26
|
+
|
|
27
|
+
def get_sha1(self, token, timestamp, nonce, txt):
|
|
28
|
+
tmpstr = "".join(sorted([token, timestamp, nonce, txt])).encode("utf8")
|
|
29
|
+
return hashlib.sha1(tmpstr).hexdigest()
|
|
30
|
+
|
|
31
|
+
def generate(self, encrypt, signature, timestamp, nonce):
|
|
32
|
+
data = {
|
|
33
|
+
"msg_encrypt": encrypt,
|
|
34
|
+
"msg_signaturet": signature,
|
|
35
|
+
"timestamp": timestamp,
|
|
36
|
+
"nonce": nonce,
|
|
37
|
+
}
|
|
38
|
+
return ENCRYPT_TEXT_RESPONSE_TEMPLATE.format(**data)
|
|
39
|
+
|
|
40
|
+
def encrypt_msg(self, msg, timestamp, nonce):
|
|
41
|
+
msg_encrypt = WechatCrypt.encrypt(msg, self.key, self.appid)
|
|
42
|
+
signature = self.get_sha1(self.token, timestamp, nonce, msg_encrypt)
|
|
43
|
+
return self.generate(msg_encrypt, signature, timestamp, nonce)
|
|
44
|
+
|
|
45
|
+
def decrypt_msg(self, timestamp, nonce, encrypt, msg_signature):
|
|
46
|
+
signature = self.get_sha1(self.token, timestamp, nonce, encrypt)
|
|
47
|
+
if signature != msg_signature:
|
|
48
|
+
return "ValidateSignatureError"
|
|
49
|
+
decrypted = WechatCrypt.decrypt(encrypt, self.key, self.appid)
|
|
50
|
+
return decrypted
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WechatCrypt:
|
|
54
|
+
# mode = AES.MODE_CBC
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def encrypt(cls, text, key, appid):
|
|
58
|
+
# struct
|
|
59
|
+
text_append = (
|
|
60
|
+
(cls.get_random_str()).encode("utf-8")
|
|
61
|
+
+ struct.pack("I", socket.htonl(len(text.encode("utf-8"))))
|
|
62
|
+
+ text.encode("utf-8")
|
|
63
|
+
+ appid.encode("utf-8")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# 使用cryptography中的pad方法填补
|
|
67
|
+
padded_data = PKCS7Encoder.padder(text_append)
|
|
68
|
+
|
|
69
|
+
# 使用cryptography中加密
|
|
70
|
+
iv = key[:16]
|
|
71
|
+
encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
|
|
72
|
+
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
|
|
73
|
+
|
|
74
|
+
# b64encode and decode('utf-8')
|
|
75
|
+
text_base64 = base64.b64encode(ciphertext)
|
|
76
|
+
return text_base64.decode("utf-8")
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def decrypt(cls, text, key, appid):
|
|
80
|
+
ciphertext = base64.b64decode(text)
|
|
81
|
+
|
|
82
|
+
# 解密
|
|
83
|
+
iv = key[:16]
|
|
84
|
+
decryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor()
|
|
85
|
+
plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize()
|
|
86
|
+
|
|
87
|
+
# 使用cryptography中的pad方法反填补
|
|
88
|
+
plain_text = PKCS7Encoder.unpadder(plaintext_padded)
|
|
89
|
+
|
|
90
|
+
# 解构
|
|
91
|
+
content = plain_text[16:]
|
|
92
|
+
xml_len = socket.ntohl(struct.unpack("I", content[:4])[0])
|
|
93
|
+
xml = content[4 : xml_len + 4]
|
|
94
|
+
from_appid = content[xml_len + 4 :]
|
|
95
|
+
|
|
96
|
+
# 校验
|
|
97
|
+
if from_appid == appid.encode("utf-8"):
|
|
98
|
+
return xml
|
|
99
|
+
else:
|
|
100
|
+
raise Exception("ValidateAppidError")
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def get_random_str(cls):
|
|
104
|
+
rule = string.ascii_letters + string.digits
|
|
105
|
+
str = random.sample(rule, 16)
|
|
106
|
+
return "".join(str)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PKCS7Encoder:
|
|
110
|
+
block_size = 32
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def padder(cls, data):
|
|
114
|
+
"""use cryptography, equal encode"""
|
|
115
|
+
padder = padding.PKCS7(cls.block_size * 8).padder()
|
|
116
|
+
padded_data = padder.update(data) + padder.finalize()
|
|
117
|
+
return padded_data
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def unpadder(cls, data):
|
|
121
|
+
"""use cryptography, equal decode"""
|
|
122
|
+
unpadder = padding.PKCS7(cls.block_size * 8).unpadder()
|
|
123
|
+
unpadded_data = unpadder.update(data) + unpadder.finalize()
|
|
124
|
+
return unpadded_data
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def encode(cls, text):
|
|
128
|
+
text_length = len(text)
|
|
129
|
+
amount_to_pad = cls.block_size - (text_length % cls.block_size)
|
|
130
|
+
if amount_to_pad == 0:
|
|
131
|
+
amount_to_pad = cls.block_size
|
|
132
|
+
pad = chr(amount_to_pad)
|
|
133
|
+
return text + (pad * amount_to_pad).encode("utf-8")
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def decode(cls, decrypted):
|
|
137
|
+
# pad = ord(decrypted[-1]) will error # for a bytes object b, b[0] will be an integer
|
|
138
|
+
pad = decrypted[-1]
|
|
139
|
+
if pad < 1 or pad > 32:
|
|
140
|
+
pad = 0
|
|
141
|
+
return decrypted[:-pad]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import datetime, json
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
HTTPS_API_WEIXIN = "https://api.weixin.qq.com/"
|
|
5
|
+
API_WEIXIN_ACCESSTOKEN = (
|
|
6
|
+
HTTPS_API_WEIXIN
|
|
7
|
+
+ "cgi-bin/token?grant_type=client_credential&appid={APPID}&secret={APPSECRET}"
|
|
8
|
+
)
|
|
9
|
+
API_WEIXIN_GETMENU = HTTPS_API_WEIXIN + "cgi-bin/menu/get?access_token={ACCESS_TOKEN}"
|
|
10
|
+
API_WEIXIN_SETMENU = (
|
|
11
|
+
HTTPS_API_WEIXIN + "cgi-bin/menu/create?access_token={ACCESS_TOKEN}"
|
|
12
|
+
)
|
|
13
|
+
API_WEIXIN_CODE2SESSION = (
|
|
14
|
+
HTTPS_API_WEIXIN
|
|
15
|
+
+ "sns/jscode2session?appid={APPID}&secret={SECRET}&js_code={JSCODE}&grant_type=authorization_code"
|
|
16
|
+
)
|
|
17
|
+
API_WEIXIN_MSGSECCHECK = (
|
|
18
|
+
HTTPS_API_WEIXIN + "wxa/msg_sec_check?access_token={ACCESS_TOKEN}"
|
|
19
|
+
)
|
|
20
|
+
API_WEIXIN_IMGSECCHECK = (
|
|
21
|
+
HTTPS_API_WEIXIN + "wxa/img_sec_check?access_token={ACCESS_TOKEN}"
|
|
22
|
+
)
|
|
23
|
+
API_WEIXIN_MEDIACHECKASYNC = (
|
|
24
|
+
HTTPS_API_WEIXIN + "wxa/media_check_async?access_token={ACCESS_TOKEN}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WeixinApi:
|
|
29
|
+
def __init__(self, app):
|
|
30
|
+
self.app = app
|
|
31
|
+
|
|
32
|
+
def get_local_access_token(self) -> dict | None:
|
|
33
|
+
"""从本地获取access_token"""
|
|
34
|
+
raise NotImplementedError()
|
|
35
|
+
|
|
36
|
+
def update_local_access_token(self, token):
|
|
37
|
+
"""更新本地的access_token"""
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
def get_access_token(self):
|
|
41
|
+
"""get access_token from local, if expire then get from api of weixin and update local data."""
|
|
42
|
+
access_token = self.get_access_token_from_local()
|
|
43
|
+
if access_token and access_token["expires_at"] > datetime.datetime.now():
|
|
44
|
+
return access_token["token"]
|
|
45
|
+
r = requests.get(
|
|
46
|
+
API_WEIXIN_ACCESSTOKEN.format(
|
|
47
|
+
APPID=self.app["appid"], APPSECRET=self.app["appsecret"]
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
if r.status_code == 200:
|
|
51
|
+
rtoken = r.json()
|
|
52
|
+
token = rtoken["access_token"]
|
|
53
|
+
expires_at = datetime.datetime.now() + datetime.timedelta(
|
|
54
|
+
seconds=rtoken["expires_in"]
|
|
55
|
+
)
|
|
56
|
+
self.update_local_access_token(token, expires_at)
|
|
57
|
+
return token
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def get_menu(self):
|
|
61
|
+
token = self.get_access_token()
|
|
62
|
+
r = requests.get(API_WEIXIN_GETMENU.format(ACCESS_TOKEN=token))
|
|
63
|
+
return r.json()
|
|
64
|
+
|
|
65
|
+
def set_menu(self, menu):
|
|
66
|
+
token = self.get_access_token()
|
|
67
|
+
r = requests.post(
|
|
68
|
+
API_WEIXIN_SETMENU.format(ACCESS_TOKEN=token),
|
|
69
|
+
data=json.dumps(menu, ensure_ascii=False).encode("utf-8"),
|
|
70
|
+
)
|
|
71
|
+
if r.status_code == 200:
|
|
72
|
+
return r.json()
|
|
73
|
+
else:
|
|
74
|
+
return r
|
|
75
|
+
|
|
76
|
+
def code2Session(self, code):
|
|
77
|
+
"""调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key"""
|
|
78
|
+
response = requests.get(
|
|
79
|
+
API_WEIXIN_CODE2SESSION.format(
|
|
80
|
+
APPID=self.app["appid"], SECRET=self.app["appsecret"], JSCODE=code
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return response.json()
|
|
84
|
+
|
|
85
|
+
def msgseccheck(self, content):
|
|
86
|
+
"""检查一段文本是否含有违法违规内容"""
|
|
87
|
+
token = self.get_access_token()
|
|
88
|
+
r = requests.post(
|
|
89
|
+
API_WEIXIN_MSGSECCHECK.format(ACCESS_TOKEN=token),
|
|
90
|
+
data=json.dumps({"content": content}, ensure_ascii=False).encode("utf-8"),
|
|
91
|
+
)
|
|
92
|
+
if r.status_code == 200:
|
|
93
|
+
return r.json()
|
|
94
|
+
return r
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from sqlalchemy.orm import Mapped
|
|
2
2
|
from sqlalchemy.orm import mapped_column
|
|
3
3
|
from python_plugins.models.mixins import PrimaryKeyMixin
|
|
4
|
+
from python_plugins.models.mixins import TokenMixin
|
|
4
5
|
from python_plugins.models.mixins import DataMixin
|
|
5
6
|
from python_plugins.models.mixins import TimestampMixin
|
|
6
7
|
from sqlalchemy.orm import DeclarativeBase
|
|
@@ -13,7 +14,7 @@ class Base(DeclarativeBase):
|
|
|
13
14
|
pass
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class Demo(PrimaryKeyMixin, DataMixin, TimestampMixin,Base):
|
|
17
|
+
class Demo(PrimaryKeyMixin, TokenMixin, DataMixin, TimestampMixin, Base):
|
|
17
18
|
__tablename__ = "demo"
|
|
18
19
|
|
|
19
20
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import time
|
|
3
|
+
import hashlib
|
|
4
|
+
from python_plugins.weixin.wechat import Wechat
|
|
5
|
+
from python_plugins.random import rand_digit, rand_letter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
test_wechat_app = {
|
|
9
|
+
"name": "test",
|
|
10
|
+
"appid": "wx2c2769f8efd9abc2",
|
|
11
|
+
"token": "spamtest",
|
|
12
|
+
"aeskey": "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
|
|
13
|
+
"appsecret": "45658c05647671e481d75b6fa23e6add",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MyWechat(Wechat):
|
|
18
|
+
def get_app(self) -> dict:
|
|
19
|
+
return test_wechat_app
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestWechat:
|
|
23
|
+
def test_verify(self):
|
|
24
|
+
mywechat = MyWechat()
|
|
25
|
+
# timestamp = "1409735669"
|
|
26
|
+
timestamp = str(int(time.time()))
|
|
27
|
+
# nonce = "1320562132"
|
|
28
|
+
nonce = rand_digit(10)
|
|
29
|
+
token = test_wechat_app["token"]
|
|
30
|
+
signature = hashlib.sha1(
|
|
31
|
+
"".join(sorted([token, timestamp, nonce])).encode("utf8")
|
|
32
|
+
).hexdigest()
|
|
33
|
+
# echostr = "780419598460648693"
|
|
34
|
+
echostr = rand_digit(18)
|
|
35
|
+
query = {
|
|
36
|
+
"timestamp": timestamp,
|
|
37
|
+
"nonce": nonce,
|
|
38
|
+
"signature": signature,
|
|
39
|
+
"echostr": echostr,
|
|
40
|
+
}
|
|
41
|
+
r = mywechat.verify(query)
|
|
42
|
+
# print(r)
|
|
43
|
+
assert r == echostr
|
|
44
|
+
|
|
45
|
+
def test_chat(self):
|
|
46
|
+
"""chat(明文)"""
|
|
47
|
+
text = rand_letter(10)
|
|
48
|
+
timestamp = str(int(time.time()))
|
|
49
|
+
nonce = rand_digit(10)
|
|
50
|
+
xml = (
|
|
51
|
+
"<xml>"
|
|
52
|
+
"<ToUserName><![CDATA[gh_73951532543e]]></ToUserName>"
|
|
53
|
+
"<FromUserName><![CDATA[oMIza6tX35aDNfoXr4JGP02QvM08]]></FromUserName>"
|
|
54
|
+
"<CreateTime>{timestamp}</CreateTime>"
|
|
55
|
+
"<MsgType><![CDATA[text]]></MsgType>"
|
|
56
|
+
f"<Content><![CDATA[{text}]]></Content>"
|
|
57
|
+
"<MsgId>23533248665819413</MsgId>"
|
|
58
|
+
"</xml>"
|
|
59
|
+
)
|
|
60
|
+
query = {
|
|
61
|
+
"timestamp": timestamp,
|
|
62
|
+
"nonce": nonce,
|
|
63
|
+
"signature": "b5fa0bcde34ab0b6dccd6e23034c26c0cf5ecbaa",
|
|
64
|
+
"openid": "oMIza6tX35aDNfoXr4JGP02QvM08",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
mywechat = MyWechat()
|
|
68
|
+
r = mywechat.chat(query, xml)
|
|
69
|
+
# print(r)
|
|
70
|
+
assert query["openid"] in r
|
|
71
|
+
|
|
72
|
+
def test_chat_aes(self):
|
|
73
|
+
"""chat(密文),微信提供的demo"""
|
|
74
|
+
xml = (
|
|
75
|
+
"<xml>"
|
|
76
|
+
"<ToUserName><![CDATA[gh_10f6c3c3ac5a]]></ToUserName>"
|
|
77
|
+
"<FromUserName><![CDATA[oyORnuP8q7ou2gfYjqLzSIWZf0rs]]></FromUserName>"
|
|
78
|
+
"<CreateTime>1409735668</CreateTime>"
|
|
79
|
+
"<MsgType><![CDATA[text]]></MsgType>"
|
|
80
|
+
"<Content><![CDATA[abcdteT]]></Content>"
|
|
81
|
+
"<MsgId>6054768590064713728</MsgId>"
|
|
82
|
+
"<Encrypt>"
|
|
83
|
+
"<![CDATA[hyzAe4OzmOMbd6TvGdIOO6uBmdJoD0Fk53REIHvxYtJlE2B655HuD0m8KUePW"
|
|
84
|
+
"B3+LrPXo87wzQ1QLvbeUgmBM4x6F8PGHQHFVAFmOD2LdJF9FrXpbUAh0B5GIItb52sn896"
|
|
85
|
+
"wVsMSHGuPE328HnRGBcrS7C41IzDWyWNlZkyyXwon8T332jisa+h6tEDYsVticbSnyU8dK"
|
|
86
|
+
"OIbgU6ux5VTjg3yt+WGzjlpKn6NPhRjpA912xMezR4kw6KWwMrCVKSVCZciVGCgavjIQ6X"
|
|
87
|
+
"8tCOp3yZbGpy0VxpAe+77TszTfRd5RJSVO/HTnifJpXgCSUdUue1v6h0EIBYYI1BD1DlD+"
|
|
88
|
+
"C0CR8e6OewpusjZ4uBl9FyJvnhvQl+q5rv1ixrcpCumEPo5MJSgM9ehVsNPfUM669WuMyV"
|
|
89
|
+
"WQLCzpu9GhglF2PE=]]>"
|
|
90
|
+
"</Encrypt>"
|
|
91
|
+
"</xml>"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
query = {
|
|
95
|
+
"timestamp": "1409735669",
|
|
96
|
+
"nonce": "1320562132",
|
|
97
|
+
"encrypt_type": "aes",
|
|
98
|
+
"msg_signature": "5d197aaffba7e9b25a30732f161a50dee96bd5fa",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
mywechat = MyWechat()
|
|
102
|
+
r = mywechat.chat(query, xml)
|
|
103
|
+
# print(r)
|
|
104
|
+
assert f'<Nonce><![CDATA[{query["nonce"]}]]></Nonce>' in r
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/data_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/primary_key_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/timestamp_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/user_minxin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/process/python_venv_process.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|