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.
Files changed (66) hide show
  1. {python_plugins-0.1.4 → python_plugins-0.1.5}/.gitignore +0 -1
  2. {python_plugins-0.1.4 → python_plugins-0.1.5}/CHANGES.rst +7 -0
  3. {python_plugins-0.1.4 → python_plugins-0.1.5}/PKG-INFO +3 -1
  4. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/usage.rst +22 -3
  5. {python_plugins-0.1.4 → python_plugins-0.1.5}/pyproject.toml +2 -1
  6. {python_plugins-0.1.4 → python_plugins-0.1.5}/requirements/test.in +2 -1
  7. python_plugins-0.1.5/src/python_plugins/__about__.py +1 -0
  8. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/__init__.py +1 -0
  9. python_plugins-0.1.5/src/python_plugins/models/mixins/token_minxin.py +8 -0
  10. python_plugins-0.1.5/src/python_plugins/weixin/biz_data_crypt.py +69 -0
  11. python_plugins-0.1.5/src/python_plugins/weixin/error_code.py +29 -0
  12. python_plugins-0.1.5/src/python_plugins/weixin/wechat.py +128 -0
  13. python_plugins-0.1.5/src/python_plugins/weixin/wechat_crypt.py +141 -0
  14. python_plugins-0.1.5/src/python_plugins/weixin/weixin_api.py +94 -0
  15. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_sqlalchemy.py +2 -1
  16. python_plugins-0.1.5/tests/test_weixin.py +104 -0
  17. python_plugins-0.1.4/src/python_plugins/__about__.py +0 -1
  18. {python_plugins-0.1.4 → python_plugins-0.1.5}/.github/workflows/release.yml +0 -0
  19. {python_plugins-0.1.4 → python_plugins-0.1.5}/.readthedocs.yaml +0 -0
  20. {python_plugins-0.1.4 → python_plugins-0.1.5}/LICENSE.rst +0 -0
  21. {python_plugins-0.1.4 → python_plugins-0.1.5}/README.rst +0 -0
  22. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/Makefile +0 -0
  23. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/api.rst +0 -0
  24. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/changes.rst +0 -0
  25. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/conf.py +0 -0
  26. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/examples.rst +0 -0
  27. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/index.rst +0 -0
  28. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/make.bat +0 -0
  29. {python_plugins-0.1.4 → python_plugins-0.1.5}/docs/requirements.txt +0 -0
  30. {python_plugins-0.1.4 → python_plugins-0.1.5}/examples/README.rst +0 -0
  31. {python_plugins-0.1.4 → python_plugins-0.1.5}/requirements/build.in +0 -0
  32. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/__init__.py +0 -0
  33. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/convert/__init__.py +0 -0
  34. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/convert/datetime_str.py +0 -0
  35. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/convert/xml.py +0 -0
  36. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/crypto/__init__.py +0 -0
  37. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/crypto/fernet.py +0 -0
  38. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/crypto/str_to_list.py +0 -0
  39. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/dumps/__init__.py +0 -0
  40. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/dumps/postgresql_dump.py +0 -0
  41. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/email/__init__.py +0 -0
  42. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/email/smtp.py +0 -0
  43. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/hashes/__init__.py +0 -0
  44. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/hashes/hash.py +0 -0
  45. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/jwt/__init__.py +0 -0
  46. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/jwt/jwt.py +0 -0
  47. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/__init__.py +0 -0
  48. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/data_mixin.py +0 -0
  49. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/primary_key_mixin.py +0 -0
  50. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/timestamp_mixin.py +0 -0
  51. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/mixins/user_minxin.py +0 -0
  52. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/models/update.py +0 -0
  53. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/process/__init__.py +0 -0
  54. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/process/python_venv_process.py +0 -0
  55. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/process/sub_process.py +0 -0
  56. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/random/__init__.py +0 -0
  57. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/random/random_str.py +0 -0
  58. {python_plugins-0.1.4 → python_plugins-0.1.5}/src/python_plugins/utils/remove_pycache.py +0 -0
  59. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/__init__.py +0 -0
  60. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/conftest.py +0 -0
  61. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_crypto.py +0 -0
  62. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_email.py +0 -0
  63. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_jwt.py +0 -0
  64. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_process.py +0 -0
  65. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_random.py +0 -0
  66. {python_plugins-0.1.4 → python_plugins-0.1.5}/tests/test_utils.py +0 -0
@@ -3,7 +3,6 @@
3
3
 
4
4
  ### JupyterNotebooks ###
5
5
  .ipynb_checkpoints
6
- */.ipynb_checkpoints/*
7
6
 
8
7
  ### Python ###
9
8
  # Byte-compiled / optimized / DLL files
@@ -1,3 +1,10 @@
1
+ v0.1.5
2
+ ------
3
+
4
+ Released 2024-09-28
5
+
6
+ - weixin.wechat
7
+
1
8
  v0.1.4
2
9
  ------
3
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-plugins
3
- Version: 0.1.4
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,UserMixin, TimestampMixin,db.models):
52
- pass
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 :: python
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
+
@@ -27,8 +27,9 @@ dependencies = [
27
27
  ]
28
28
 
29
29
  [project.optional-dependencies]
30
- sqlalchemy = ["SQLAlchemy"]
31
30
  cryptography = ["cryptography"]
31
+ requests = ["requests"]
32
+ sqlalchemy = ["SQLAlchemy"]
32
33
  pillow = ["pillow"]
33
34
  qrcode = ["qrcode"]
34
35
  jwt = ["PyJWT"]
@@ -2,4 +2,5 @@ pytest
2
2
  Faker
3
3
  cryptography
4
4
  SQLAlchemy
5
- PyJWT
5
+ PyJWT
6
+ requests
@@ -0,0 +1 @@
1
+ __version__ = "0.1.5"
@@ -1,4 +1,5 @@
1
1
  from .primary_key_mixin import PrimaryKeyMixin
2
2
  from .timestamp_mixin import TimestampMixin, CreateTimestampMixin, UpdateTimestampMixin
3
3
  from .data_mixin import DataMixin
4
+ from .token_minxin import TokenMixin
4
5
  from .user_minxin import UserMixin
@@ -0,0 +1,8 @@
1
+ from typing import Optional
2
+ from sqlalchemy.orm import Mapped
3
+ from sqlalchemy.orm import mapped_column
4
+ from ...random.random_str import secret_token
5
+
6
+
7
+ class TokenMixin:
8
+ token: Mapped[Optional[str]] = mapped_column(unique=True, default=secret_token)
@@ -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"