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.
Files changed (67) hide show
  1. {python_plugins-0.1.4 → python_plugins-0.1.6}/.gitignore +0 -1
  2. {python_plugins-0.1.4 → python_plugins-0.1.6}/CHANGES.rst +14 -0
  3. {python_plugins-0.1.4 → python_plugins-0.1.6}/PKG-INFO +3 -1
  4. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/usage.rst +22 -3
  5. {python_plugins-0.1.4 → python_plugins-0.1.6}/pyproject.toml +2 -1
  6. {python_plugins-0.1.4 → python_plugins-0.1.6}/requirements/test.in +2 -1
  7. python_plugins-0.1.6/src/python_plugins/__about__.py +1 -0
  8. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/__init__.py +1 -0
  9. python_plugins-0.1.6/src/python_plugins/models/mixins/token_minxin.py +8 -0
  10. python_plugins-0.1.6/src/python_plugins/weixin/biz_data_crypt.py +69 -0
  11. python_plugins-0.1.6/src/python_plugins/weixin/error_code.py +29 -0
  12. python_plugins-0.1.6/src/python_plugins/weixin/format_response.py +148 -0
  13. python_plugins-0.1.6/src/python_plugins/weixin/wechat.py +135 -0
  14. python_plugins-0.1.6/src/python_plugins/weixin/wechat_crypt.py +141 -0
  15. python_plugins-0.1.6/src/python_plugins/weixin/weixin_api.py +94 -0
  16. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_sqlalchemy.py +2 -1
  17. python_plugins-0.1.6/tests/test_weixin.py +104 -0
  18. python_plugins-0.1.4/src/python_plugins/__about__.py +0 -1
  19. {python_plugins-0.1.4 → python_plugins-0.1.6}/.github/workflows/release.yml +0 -0
  20. {python_plugins-0.1.4 → python_plugins-0.1.6}/.readthedocs.yaml +0 -0
  21. {python_plugins-0.1.4 → python_plugins-0.1.6}/LICENSE.rst +0 -0
  22. {python_plugins-0.1.4 → python_plugins-0.1.6}/README.rst +0 -0
  23. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/Makefile +0 -0
  24. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/api.rst +0 -0
  25. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/changes.rst +0 -0
  26. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/conf.py +0 -0
  27. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/examples.rst +0 -0
  28. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/index.rst +0 -0
  29. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/make.bat +0 -0
  30. {python_plugins-0.1.4 → python_plugins-0.1.6}/docs/requirements.txt +0 -0
  31. {python_plugins-0.1.4 → python_plugins-0.1.6}/examples/README.rst +0 -0
  32. {python_plugins-0.1.4 → python_plugins-0.1.6}/requirements/build.in +0 -0
  33. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/__init__.py +0 -0
  34. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/convert/__init__.py +0 -0
  35. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/convert/datetime_str.py +0 -0
  36. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/convert/xml.py +0 -0
  37. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/crypto/__init__.py +0 -0
  38. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/crypto/fernet.py +0 -0
  39. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/crypto/str_to_list.py +0 -0
  40. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/dumps/__init__.py +0 -0
  41. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/dumps/postgresql_dump.py +0 -0
  42. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/email/__init__.py +0 -0
  43. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/email/smtp.py +0 -0
  44. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/hashes/__init__.py +0 -0
  45. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/hashes/hash.py +0 -0
  46. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/jwt/__init__.py +0 -0
  47. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/jwt/jwt.py +0 -0
  48. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/__init__.py +0 -0
  49. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/data_mixin.py +0 -0
  50. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/primary_key_mixin.py +0 -0
  51. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/timestamp_mixin.py +0 -0
  52. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/mixins/user_minxin.py +0 -0
  53. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/models/update.py +0 -0
  54. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/process/__init__.py +0 -0
  55. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/process/python_venv_process.py +0 -0
  56. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/process/sub_process.py +0 -0
  57. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/random/__init__.py +0 -0
  58. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/random/random_str.py +0 -0
  59. {python_plugins-0.1.4 → python_plugins-0.1.6}/src/python_plugins/utils/remove_pycache.py +0 -0
  60. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/__init__.py +0 -0
  61. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/conftest.py +0 -0
  62. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_crypto.py +0 -0
  63. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_email.py +0 -0
  64. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_jwt.py +0 -0
  65. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_process.py +0 -0
  66. {python_plugins-0.1.4 → python_plugins-0.1.6}/tests/test_random.py +0 -0
  67. {python_plugins-0.1.4 → python_plugins-0.1.6}/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,17 @@
1
+ v0.1.6
2
+ ------
3
+
4
+ Released 2024-10-01
5
+
6
+ - weixin.wechat support news
7
+
8
+ v0.1.5
9
+ ------
10
+
11
+ Released 2024-09-28
12
+
13
+ - weixin.wechat
14
+
1
15
  v0.1.4
2
16
  ------
3
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-plugins
3
- Version: 0.1.4
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,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.6"
@@ -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,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"