python-plugins 0.1.4__tar.gz → 0.1.5__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.5}/.gitignore +0 -1
- {python_plugins-0.1.4 → python_plugins-0.1.5}/CHANGES.rst +7 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/PKG-INFO +3 -1
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/usage.rst +22 -3
- {python_plugins-0.1.4 → python_plugins-0.1.5}/pyproject.toml +2 -1
- {python_plugins-0.1.4 → python_plugins-0.1.5}/requirements/test.in +2 -1
- python_plugins-0.1.5/src/python_plugins/__about__.py +1 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/__init__.py +1 -0
- python_plugins-0.1.5/src/python_plugins/models/mixins/token_minxin.py +8 -0
- python_plugins-0.1.5/src/python_plugins/weixin/biz_data_crypt.py +69 -0
- python_plugins-0.1.5/src/python_plugins/weixin/error_code.py +29 -0
- python_plugins-0.1.5/src/python_plugins/weixin/wechat.py +128 -0
- python_plugins-0.1.5/src/python_plugins/weixin/wechat_crypt.py +141 -0
- python_plugins-0.1.5/src/python_plugins/weixin/weixin_api.py +94 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_sqlalchemy.py +2 -1
- python_plugins-0.1.5/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.5}/.github/workflows/release.yml +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/.readthedocs.yaml +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/LICENSE.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/README.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/Makefile +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/api.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/changes.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/conf.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/examples.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/index.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/make.bat +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/requirements.txt +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/examples/README.rst +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/requirements/build.in +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/convert/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/convert/datetime_str.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/convert/xml.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/crypto/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/crypto/fernet.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/crypto/str_to_list.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/dumps/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/dumps/postgresql_dump.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/email/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/email/smtp.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/hashes/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/hashes/hash.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/jwt/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/jwt/jwt.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/data_mixin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/primary_key_mixin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/timestamp_mixin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/user_minxin.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/update.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/process/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/process/python_venv_process.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/process/sub_process.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/random/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/random/random_str.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/utils/remove_pycache.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/__init__.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/conftest.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_crypto.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_email.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_jwt.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_process.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_random.py +0 -0
- {python_plugins-0.1.4 → python_plugins-0.1.5}/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.5
|
|
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.5"
|
|
@@ -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,128 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import time
|
|
3
|
+
from python_plugins.convert import xml2dict
|
|
4
|
+
from .wechat_crypt import MessageCrypt
|
|
5
|
+
|
|
6
|
+
XML_TEXT_TEMPLATE = """<xml>
|
|
7
|
+
<ToUserName><![CDATA[{touser}]]></ToUserName>
|
|
8
|
+
<FromUserName><![CDATA[{fromuser}]]></FromUserName>
|
|
9
|
+
<CreateTime>{createtime}</CreateTime>
|
|
10
|
+
<MsgType><![CDATA[text]]></MsgType>
|
|
11
|
+
<Content><![CDATA[{content}]]></Content>
|
|
12
|
+
</xml>"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Wechat:
|
|
16
|
+
|
|
17
|
+
def __init__(self, name=None):
|
|
18
|
+
self.name = name
|
|
19
|
+
self.app = self.get_app()
|
|
20
|
+
|
|
21
|
+
# 获取app
|
|
22
|
+
def get_app(self) -> dict:
|
|
23
|
+
raise NotImplementedError()
|
|
24
|
+
|
|
25
|
+
# 记录信息
|
|
26
|
+
def log_data(self):
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
def verify(self, args):
|
|
30
|
+
signature = args["signature"]
|
|
31
|
+
timestamp = args["timestamp"]
|
|
32
|
+
nonce = args["nonce"]
|
|
33
|
+
echostr = args["echostr"]
|
|
34
|
+
token = self.app["token"]
|
|
35
|
+
tmpstr = "".join(sorted([token, timestamp, nonce])).encode("utf8")
|
|
36
|
+
if hashlib.sha1(tmpstr).hexdigest() == signature:
|
|
37
|
+
return echostr
|
|
38
|
+
else:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
def chat(self, args, content):
|
|
42
|
+
self.openid = args.get("openid")
|
|
43
|
+
msg_signature = args.get("msg_signature", "")
|
|
44
|
+
timestamp = args.get("timestamp", "")
|
|
45
|
+
nonce = args.get("nonce", "")
|
|
46
|
+
# 加密标记
|
|
47
|
+
encrypt_type = args.get("encrypt_type")
|
|
48
|
+
xml_dict = xml2dict(content)
|
|
49
|
+
|
|
50
|
+
if not encrypt_type:
|
|
51
|
+
# 未加密
|
|
52
|
+
result = self.dispatch(xml_dict)
|
|
53
|
+
xml_reponse = self.responseText(result)
|
|
54
|
+
else:
|
|
55
|
+
# 解密
|
|
56
|
+
mc = MessageCrypt(self.app["appid"], self.app["token"], self.app["aeskey"])
|
|
57
|
+
xml_decrypted = mc.decrypt_msg(
|
|
58
|
+
timestamp, nonce, xml_dict["Encrypt"], msg_signature
|
|
59
|
+
)
|
|
60
|
+
decrypted_dict = xml2dict(xml_decrypted)
|
|
61
|
+
result = self.dispatch(decrypted_dict)
|
|
62
|
+
unencrypted_xml = self.responseText(result)
|
|
63
|
+
# 加密
|
|
64
|
+
xml_reponse = mc.encrypt_msg(unencrypted_xml, timestamp, nonce)
|
|
65
|
+
|
|
66
|
+
return xml_reponse
|
|
67
|
+
|
|
68
|
+
def responseText(self, content):
|
|
69
|
+
data = {
|
|
70
|
+
"touser": self.fromUser,
|
|
71
|
+
"fromuser": self.toUser,
|
|
72
|
+
"createtime": int(time.time()),
|
|
73
|
+
"content": content,
|
|
74
|
+
}
|
|
75
|
+
return XML_TEXT_TEMPLATE.format(**data)
|
|
76
|
+
|
|
77
|
+
def dispatch(self, data):
|
|
78
|
+
self.toUser = data["ToUserName"]
|
|
79
|
+
self.fromUser = data["FromUserName"]
|
|
80
|
+
msgType = data["MsgType"]
|
|
81
|
+
|
|
82
|
+
if msgType == "text":
|
|
83
|
+
keyword = data["Content"]
|
|
84
|
+
elif msgType == "event":
|
|
85
|
+
event = data["Event"]
|
|
86
|
+
if event == "subscribe":
|
|
87
|
+
# self.onSubscribe()
|
|
88
|
+
keyword = "subscribe"
|
|
89
|
+
elif event == "unsubscribe":
|
|
90
|
+
# self.onUnsubscribe()
|
|
91
|
+
keyword = "unsubscribe"
|
|
92
|
+
elif event == "CLICK":
|
|
93
|
+
eventKey = data["EventKey"]
|
|
94
|
+
keyword = eventKey
|
|
95
|
+
else:
|
|
96
|
+
keyword = "<event:{event}>"
|
|
97
|
+
elif msgType == "image":
|
|
98
|
+
keyword = f"<{msgType}>"
|
|
99
|
+
elif msgType == "voice":
|
|
100
|
+
keyword = f"<{msgType}>"
|
|
101
|
+
elif msgType == "video":
|
|
102
|
+
keyword = f"<{msgType}>"
|
|
103
|
+
elif msgType == "shortvideo":
|
|
104
|
+
keyword = f"<{msgType}>"
|
|
105
|
+
elif msgType == "location":
|
|
106
|
+
location_x = data["location_x"]
|
|
107
|
+
location_y = data["location_y"]
|
|
108
|
+
keyword = f"<{msgType}({location_x},{location_y})>"
|
|
109
|
+
elif msgType == "link":
|
|
110
|
+
keyword = f"<{msgType}>"
|
|
111
|
+
else:
|
|
112
|
+
keyword = f"<{msgType}>"
|
|
113
|
+
|
|
114
|
+
self.keyword = keyword
|
|
115
|
+
answer = self.answer()
|
|
116
|
+
|
|
117
|
+
# 返回前记录下日志,如果实现记录日志的话
|
|
118
|
+
self.log_data()
|
|
119
|
+
|
|
120
|
+
return self.responseText(answer)
|
|
121
|
+
|
|
122
|
+
def answer(self):
|
|
123
|
+
q = self.keyword
|
|
124
|
+
if q == "subscribe":
|
|
125
|
+
r = f"您好,欢迎关注[{self.app['name']}]!"
|
|
126
|
+
else:
|
|
127
|
+
r = q
|
|
128
|
+
return r
|
|
@@ -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.5}/src/python_plugins/models/mixins/data_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/primary_key_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/timestamp_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.4 → python_plugins-0.1.5}/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.5}/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
|