python-plugins 0.1.5__tar.gz → 0.1.7__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.5 → python_plugins-0.1.7}/CHANGES.rst +14 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/PKG-INFO +1 -1
- python_plugins-0.1.7/docs/conf.py +51 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/docs/index.rst +1 -1
- python_plugins-0.1.7/docs/requirements.txt +2 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/docs/usage.rst +2 -1
- python_plugins-0.1.7/src/python_plugins/__about__.py +1 -0
- python_plugins-0.1.7/src/python_plugins/__init__.py +1 -0
- python_plugins-0.1.7/src/python_plugins/convert/pretty.py +11 -0
- python_plugins-0.1.7/src/python_plugins/crypto/file_to_file.py +112 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/crypto/str_to_list.py +13 -3
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/utils/remove_pycache.py +1 -1
- python_plugins-0.1.7/src/python_plugins/weixin/format_response.py +148 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/weixin/wechat.py +56 -49
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/weixin/wechat_crypt.py +13 -9
- python_plugins-0.1.7/tests/test_crypt_file.py +84 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/test_utils.py +3 -0
- python_plugins-0.1.7/tests/test_weixin.py +132 -0
- python_plugins-0.1.5/docs/conf.py +0 -30
- python_plugins-0.1.5/docs/requirements.txt +0 -2
- python_plugins-0.1.5/src/python_plugins/__about__.py +0 -1
- python_plugins-0.1.5/tests/__init__.py +0 -1
- python_plugins-0.1.5/tests/test_weixin.py +0 -104
- {python_plugins-0.1.5 → python_plugins-0.1.7}/.github/workflows/release.yml +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/.gitignore +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/.readthedocs.yaml +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/LICENSE.rst +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/README.rst +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/docs/Makefile +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/docs/api.rst +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/docs/changes.rst +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/docs/examples.rst +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/docs/make.bat +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/examples/README.rst +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/pyproject.toml +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/requirements/build.in +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/requirements/test.in +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/convert/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/convert/datetime_str.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/convert/xml.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/crypto/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/crypto/fernet.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/dumps/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/dumps/postgresql_dump.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/email/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/email/smtp.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/hashes/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/hashes/hash.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/jwt/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/jwt/jwt.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/data_mixin.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/primary_key_mixin.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/timestamp_mixin.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/token_minxin.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/user_minxin.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/update.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/process/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/process/python_venv_process.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/process/sub_process.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/random/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/random/random_str.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/weixin/biz_data_crypt.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/weixin/error_code.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/weixin/weixin_api.py +0 -0
- {python_plugins-0.1.5/src/python_plugins → python_plugins-0.1.7/tests}/__init__.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/conftest.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/test_crypto.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/test_email.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/test_jwt.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/test_process.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/test_random.py +0 -0
- {python_plugins-0.1.5 → python_plugins-0.1.7}/tests/test_sqlalchemy.py +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Configuration file for the Sphinx documentation builder.
|
|
2
|
+
#
|
|
3
|
+
# For the full list of built-in configuration values, see the documentation:
|
|
4
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
5
|
+
|
|
6
|
+
# -- Project information -----------------------------------------------------
|
|
7
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
8
|
+
|
|
9
|
+
project = 'python-plugins'
|
|
10
|
+
copyright = '2024, David Hua'
|
|
11
|
+
author = 'David Hua'
|
|
12
|
+
|
|
13
|
+
# -- General configuration ---------------------------------------------------
|
|
14
|
+
# -- General configuration
|
|
15
|
+
|
|
16
|
+
extensions = [
|
|
17
|
+
"sphinx.ext.duration",
|
|
18
|
+
"sphinx.ext.doctest",
|
|
19
|
+
"sphinx.ext.autodoc",
|
|
20
|
+
"sphinx.ext.autosummary",
|
|
21
|
+
"sphinx.ext.intersphinx",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# intersphinx_mapping = {
|
|
25
|
+
# "rtd": ("https://docs.readthedocs.io/en/stable/", None),
|
|
26
|
+
# "python": ("https://docs.python.org/3/", None),
|
|
27
|
+
# "sphinx": ("https://www.sphinx-doc.org/en/master/", None),
|
|
28
|
+
# }
|
|
29
|
+
# intersphinx_disabled_domains = ["std"]
|
|
30
|
+
|
|
31
|
+
templates_path = ["_templates"]
|
|
32
|
+
|
|
33
|
+
# -- Options for EPUB output
|
|
34
|
+
epub_show_urls = "footnote"
|
|
35
|
+
|
|
36
|
+
# List of patterns, relative to source directory, that match files and
|
|
37
|
+
# directories to ignore when looking for source files.
|
|
38
|
+
# This pattern also affects html_static_path and html_extra_path.
|
|
39
|
+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|
40
|
+
|
|
41
|
+
# -- Options for HTML output -------------------------------------------------
|
|
42
|
+
|
|
43
|
+
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
44
|
+
# a list of builtin themes.
|
|
45
|
+
#
|
|
46
|
+
html_theme = "sphinx_rtd_theme"
|
|
47
|
+
|
|
48
|
+
# Add any paths that contain custom static files (such as style sheets) here,
|
|
49
|
+
# relative to this directory. They are copied after the builtin static files,
|
|
50
|
+
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
51
|
+
html_static_path = ["_static"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Welcome to python-plugins's documentation!
|
|
2
2
|
============================================
|
|
3
3
|
|
|
4
|
-
**python-plugins** is a
|
|
4
|
+
**python-plugins** is a collection with common python utils.
|
|
5
5
|
|
|
6
6
|
.. toctree::
|
|
7
7
|
:maxdepth: 2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.7"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .utils.remove_pycache import remove_pycache
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from .str_to_list import encrypt_bytes_to_list
|
|
2
|
+
from .str_to_list import encrypt_str_to_list
|
|
3
|
+
from .str_to_list import decrypt_list_to_bytes
|
|
4
|
+
from .str_to_list import decrypt_list_to_str
|
|
5
|
+
from getpass import getpass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def bytes_from_file(fin) -> bytes:
|
|
9
|
+
with open(fin, "rb") as f:
|
|
10
|
+
s = f.read()
|
|
11
|
+
return s
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def bytes_to_file(s: bytes, fout):
|
|
15
|
+
with open(fout, "wb") as f:
|
|
16
|
+
f.write(s)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def str_from_txtfile(fin) -> str:
|
|
20
|
+
with open(fin, encoding="utf-8") as f:
|
|
21
|
+
s = f.read()
|
|
22
|
+
return s
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def str_to_txtfile(s: str, fout):
|
|
26
|
+
with open(fout, "w", encoding="utf-8") as f:
|
|
27
|
+
f.write(s)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def bytes_to_file(s, fout):
|
|
31
|
+
with open(fout, "wb") as f:
|
|
32
|
+
f.write(s)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def encrypt_txt(s: str, prompt=None, accept_password=False):
|
|
36
|
+
if not prompt:
|
|
37
|
+
prompt = input("input prompt=")
|
|
38
|
+
password = None
|
|
39
|
+
if accept_password:
|
|
40
|
+
password = getpass("input password=")
|
|
41
|
+
if password:
|
|
42
|
+
encrypted_list = encrypt_str_to_list(s, password)
|
|
43
|
+
encrypted_list[0] = "-"
|
|
44
|
+
else:
|
|
45
|
+
encrypted_list = encrypt_str_to_list(s)
|
|
46
|
+
s2 = "\n".join([prompt] + encrypted_list)
|
|
47
|
+
return s2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def decrypt_txt(s: str):
|
|
51
|
+
encrypted_list = s.split("\n")
|
|
52
|
+
prompt = encrypted_list[0]
|
|
53
|
+
if encrypted_list[1] == "-":
|
|
54
|
+
print(prompt)
|
|
55
|
+
password = getpass("input password=")
|
|
56
|
+
s2 = decrypt_list_to_str(encrypted_list[1:], password)
|
|
57
|
+
else:
|
|
58
|
+
s2 = decrypt_list_to_str(encrypted_list[1:])
|
|
59
|
+
|
|
60
|
+
return s2
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def encrypt_txtfile(fin, fout, prompt=None, accept_password=False):
|
|
64
|
+
s = str_from_txtfile(fin)
|
|
65
|
+
s2 = encrypt_txt(s, prompt, accept_password)
|
|
66
|
+
str_to_txtfile(s2, fout)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def decrypt_txtfile(fin, fout):
|
|
70
|
+
s = str_from_txtfile(fin)
|
|
71
|
+
s2 = decrypt_txt(s)
|
|
72
|
+
str_to_txtfile(s2, fout)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def encrypt_bytes(s: bytes, prompt=None, accept_password=False):
|
|
76
|
+
if not prompt:
|
|
77
|
+
prompt = input("input prompt=")
|
|
78
|
+
password = None
|
|
79
|
+
if accept_password:
|
|
80
|
+
password = getpass("input password=")
|
|
81
|
+
if password:
|
|
82
|
+
encrypted_list = encrypt_bytes_to_list(s, password)
|
|
83
|
+
encrypted_list[0] = "-"
|
|
84
|
+
else:
|
|
85
|
+
encrypted_list = encrypt_bytes_to_list(s)
|
|
86
|
+
s2 = "\n".join([prompt] + encrypted_list)
|
|
87
|
+
return s2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def decrypt_bytes(s: str):
|
|
91
|
+
encrypted_list = s.split("\n")
|
|
92
|
+
prompt = encrypted_list[0]
|
|
93
|
+
if encrypted_list[1] == "-":
|
|
94
|
+
print(prompt)
|
|
95
|
+
password = getpass("input password=")
|
|
96
|
+
s2 = decrypt_list_to_bytes(encrypted_list[1:], password)
|
|
97
|
+
else:
|
|
98
|
+
s2 = decrypt_list_to_bytes(encrypted_list[1:])
|
|
99
|
+
|
|
100
|
+
return s2
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def encrypt_file(fin, fout, prompt=None, accept_password=False):
|
|
104
|
+
s = bytes_from_file(fin)
|
|
105
|
+
s2 = encrypt_bytes(s, prompt, accept_password)
|
|
106
|
+
str_to_txtfile(s2, fout)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def decrypt_file(fin, fout):
|
|
110
|
+
s = str_from_txtfile(fin)
|
|
111
|
+
s2 = decrypt_bytes(s)
|
|
112
|
+
bytes_to_file(s2, fout)
|
|
@@ -54,7 +54,7 @@ def str_randsplit_to_list(s, n1=10, n2=30):
|
|
|
54
54
|
return r
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def encrypt_bytes_to_list(s: bytes, password=None, prefix=None):
|
|
58
58
|
if password is None:
|
|
59
59
|
password = rand_letter(random.randint(6, 16))
|
|
60
60
|
out_password = password
|
|
@@ -66,7 +66,7 @@ def encrypt_str_to_list(s: str, password=None, prefix=None):
|
|
|
66
66
|
key, safe_salt, times = get_key(password)
|
|
67
67
|
|
|
68
68
|
cipher_suite = Fernet(key)
|
|
69
|
-
encrypted_data = cipher_suite.encrypt(s
|
|
69
|
+
encrypted_data = cipher_suite.encrypt(s)
|
|
70
70
|
safe_data = bytes_to_url64str(encrypted_data)
|
|
71
71
|
|
|
72
72
|
list_out = [out_password, safe_salt, str(times)] + str_randsplit_to_list(safe_data)
|
|
@@ -74,7 +74,7 @@ def encrypt_str_to_list(s: str, password=None, prefix=None):
|
|
|
74
74
|
return list_out
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def
|
|
77
|
+
def decrypt_list_to_bytes(list_in, password=None, prefix=None) -> bytes:
|
|
78
78
|
_password, safe_salt, _times, *_data = list_in
|
|
79
79
|
if password is None:
|
|
80
80
|
password = _password
|
|
@@ -88,4 +88,14 @@ def decrypt_list_to_str(list_in, password=None, prefix=None):
|
|
|
88
88
|
key, _, _ = get_key(password, safe_salt, times)
|
|
89
89
|
cipher_suite = Fernet(key)
|
|
90
90
|
decrypted_bytes = cipher_suite.decrypt(url64str_to_bytes(s))
|
|
91
|
+
return decrypted_bytes
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def encrypt_str_to_list(s: str, password=None, prefix=None):
|
|
95
|
+
arr = encrypt_bytes_to_list(s.encode(), password, prefix)
|
|
96
|
+
return arr
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def decrypt_list_to_str(list_in, password=None, prefix=None):
|
|
100
|
+
decrypted_bytes = decrypt_list_to_bytes(list_in, password, prefix)
|
|
91
101
|
return decrypted_bytes.decode()
|
|
@@ -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
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
-
import time
|
|
3
2
|
from python_plugins.convert import xml2dict
|
|
4
3
|
from .wechat_crypt import MessageCrypt
|
|
5
|
-
|
|
6
|
-
|
|
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>"""
|
|
4
|
+
from .wechat_crypt import get_signature
|
|
5
|
+
from .format_response import get_wechat_xml_response
|
|
13
6
|
|
|
14
7
|
|
|
15
8
|
class Wechat:
|
|
@@ -32,8 +25,7 @@ class Wechat:
|
|
|
32
25
|
nonce = args["nonce"]
|
|
33
26
|
echostr = args["echostr"]
|
|
34
27
|
token = self.app["token"]
|
|
35
|
-
|
|
36
|
-
if hashlib.sha1(tmpstr).hexdigest() == signature:
|
|
28
|
+
if get_signature([token, timestamp, nonce]) == signature:
|
|
37
29
|
return echostr
|
|
38
30
|
else:
|
|
39
31
|
return
|
|
@@ -48,41 +40,37 @@ class Wechat:
|
|
|
48
40
|
xml_dict = xml2dict(content)
|
|
49
41
|
|
|
50
42
|
if not encrypt_type:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
self.input = xml_dict
|
|
44
|
+
self.dispatch()
|
|
45
|
+
self.get_xml_response()
|
|
46
|
+
xml_reponse = self.xml_response
|
|
54
47
|
else:
|
|
55
|
-
#
|
|
48
|
+
# decrypt
|
|
56
49
|
mc = MessageCrypt(self.app["appid"], self.app["token"], self.app["aeskey"])
|
|
57
50
|
xml_decrypted = mc.decrypt_msg(
|
|
58
51
|
timestamp, nonce, xml_dict["Encrypt"], msg_signature
|
|
59
52
|
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
xml_reponse = mc.encrypt_msg(
|
|
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)
|
|
65
58
|
|
|
66
|
-
|
|
59
|
+
# 返回前记录下日志,如果实现记录日志的话
|
|
60
|
+
self.log_data()
|
|
67
61
|
|
|
68
|
-
|
|
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)
|
|
62
|
+
return xml_reponse
|
|
76
63
|
|
|
77
|
-
def dispatch(self
|
|
78
|
-
self.
|
|
79
|
-
self.
|
|
80
|
-
|
|
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"]
|
|
81
69
|
|
|
82
70
|
if msgType == "text":
|
|
83
|
-
keyword =
|
|
71
|
+
keyword = self.input["Content"]
|
|
84
72
|
elif msgType == "event":
|
|
85
|
-
event =
|
|
73
|
+
event = self.input["Event"]
|
|
86
74
|
if event == "subscribe":
|
|
87
75
|
# self.onSubscribe()
|
|
88
76
|
keyword = "subscribe"
|
|
@@ -90,7 +78,7 @@ class Wechat:
|
|
|
90
78
|
# self.onUnsubscribe()
|
|
91
79
|
keyword = "unsubscribe"
|
|
92
80
|
elif event == "CLICK":
|
|
93
|
-
eventKey =
|
|
81
|
+
eventKey = self.input["EventKey"]
|
|
94
82
|
keyword = eventKey
|
|
95
83
|
else:
|
|
96
84
|
keyword = "<event:{event}>"
|
|
@@ -103,8 +91,8 @@ class Wechat:
|
|
|
103
91
|
elif msgType == "shortvideo":
|
|
104
92
|
keyword = f"<{msgType}>"
|
|
105
93
|
elif msgType == "location":
|
|
106
|
-
location_x =
|
|
107
|
-
location_y =
|
|
94
|
+
location_x = self.input["location_x"]
|
|
95
|
+
location_y = self.input["location_y"]
|
|
108
96
|
keyword = f"<{msgType}({location_x},{location_y})>"
|
|
109
97
|
elif msgType == "link":
|
|
110
98
|
keyword = f"<{msgType}>"
|
|
@@ -112,17 +100,36 @@ class Wechat:
|
|
|
112
100
|
keyword = f"<{msgType}>"
|
|
113
101
|
|
|
114
102
|
self.keyword = keyword
|
|
115
|
-
answer = self.answer()
|
|
116
103
|
|
|
117
|
-
|
|
118
|
-
|
|
104
|
+
self.get_answer()
|
|
105
|
+
return
|
|
119
106
|
|
|
120
|
-
|
|
107
|
+
def default_answer(self):
|
|
108
|
+
self.answer = {"type": "text", "content": "I'm sorry, I don't understand."}
|
|
109
|
+
return
|
|
121
110
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
@@ -12,11 +12,15 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
12
12
|
|
|
13
13
|
ENCRYPT_TEXT_RESPONSE_TEMPLATE = """<xml>
|
|
14
14
|
<Encrypt><![CDATA[{msg_encrypt}]]></Encrypt>
|
|
15
|
-
<MsgSignature><![CDATA[{
|
|
15
|
+
<MsgSignature><![CDATA[{msg_signature}]]></MsgSignature>
|
|
16
16
|
<TimeStamp>{timestamp}</TimeStamp>
|
|
17
17
|
<Nonce><![CDATA[{nonce}]]></Nonce>
|
|
18
18
|
</xml>"""
|
|
19
19
|
|
|
20
|
+
def get_signature(arr):
|
|
21
|
+
tmpstr = "".join(sorted(arr)).encode("utf-8")
|
|
22
|
+
return hashlib.sha1(tmpstr).hexdigest()
|
|
23
|
+
|
|
20
24
|
|
|
21
25
|
class MessageCrypt:
|
|
22
26
|
def __init__(self, appid, token, aeskey):
|
|
@@ -24,26 +28,26 @@ class MessageCrypt:
|
|
|
24
28
|
self.token = token
|
|
25
29
|
self.key = base64.b64decode(aeskey + "=")
|
|
26
30
|
|
|
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
31
|
def generate(self, encrypt, signature, timestamp, nonce):
|
|
32
32
|
data = {
|
|
33
33
|
"msg_encrypt": encrypt,
|
|
34
|
-
"
|
|
34
|
+
"msg_signature": signature,
|
|
35
35
|
"timestamp": timestamp,
|
|
36
36
|
"nonce": nonce,
|
|
37
37
|
}
|
|
38
38
|
return ENCRYPT_TEXT_RESPONSE_TEMPLATE.format(**data)
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def get_encrypt_sig(self, msg, timestamp, nonce):
|
|
41
41
|
msg_encrypt = WechatCrypt.encrypt(msg, self.key, self.appid)
|
|
42
|
-
signature =
|
|
42
|
+
signature = get_signature([self.token, timestamp, nonce, msg_encrypt])
|
|
43
|
+
return (msg_encrypt, signature)
|
|
44
|
+
|
|
45
|
+
def encrypt_msg(self, msg, timestamp, nonce):
|
|
46
|
+
msg_encrypt,signature = self.get_encrypt_sig(msg,timestamp,nonce)
|
|
43
47
|
return self.generate(msg_encrypt, signature, timestamp, nonce)
|
|
44
48
|
|
|
45
49
|
def decrypt_msg(self, timestamp, nonce, encrypt, msg_signature):
|
|
46
|
-
signature =
|
|
50
|
+
signature = get_signature([self.token, timestamp, nonce, encrypt])
|
|
47
51
|
if signature != msg_signature:
|
|
48
52
|
return "ValidateSignatureError"
|
|
49
53
|
decrypted = WechatCrypt.decrypt(encrypt, self.key, self.appid)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import os.path as op
|
|
3
|
+
import filecmp
|
|
4
|
+
from python_plugins.random.random_str import rand_sentence
|
|
5
|
+
from python_plugins.crypto.file_to_file import encrypt_txtfile
|
|
6
|
+
from python_plugins.crypto.file_to_file import decrypt_txtfile
|
|
7
|
+
from python_plugins.crypto.file_to_file import encrypt_file
|
|
8
|
+
from python_plugins.crypto.file_to_file import decrypt_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
tmp_path = op.join(os.path.dirname(os.path.abspath(__file__)), "tmp")
|
|
12
|
+
|
|
13
|
+
path_1 = op.join(tmp_path, "test1.txt")
|
|
14
|
+
path_2 = op.join(tmp_path, "test2.txt")
|
|
15
|
+
path_3 = op.join(tmp_path, "test3.txt")
|
|
16
|
+
path_4 = op.join(tmp_path, "test4.txt")
|
|
17
|
+
path_5 = op.join(tmp_path, "test5.txt")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _create_temp():
|
|
21
|
+
if not op.exists(tmp_path):
|
|
22
|
+
os.mkdir(tmp_path)
|
|
23
|
+
return tmp_path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def safe_delete(path):
|
|
27
|
+
try:
|
|
28
|
+
if op.exists(path):
|
|
29
|
+
os.remove(path)
|
|
30
|
+
except:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _remove_testfiles():
|
|
35
|
+
safe_delete(path_1)
|
|
36
|
+
safe_delete(path_2)
|
|
37
|
+
safe_delete(path_3)
|
|
38
|
+
safe_delete(path_4)
|
|
39
|
+
safe_delete(path_5)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_crypto_file():
|
|
43
|
+
create_tmp = _create_temp()
|
|
44
|
+
if create_tmp:
|
|
45
|
+
print(create_tmp)
|
|
46
|
+
|
|
47
|
+
prompt = "test"
|
|
48
|
+
|
|
49
|
+
with open(path_1, "w") as f:
|
|
50
|
+
f.write(rand_sentence(30))
|
|
51
|
+
f.write(rand_sentence(30))
|
|
52
|
+
|
|
53
|
+
encrypt_txtfile(path_1, path_2, prompt=prompt)
|
|
54
|
+
decrypt_txtfile(path_2, path_3)
|
|
55
|
+
cmp_result = filecmp.cmp(path_1, path_3)
|
|
56
|
+
assert cmp_result is True
|
|
57
|
+
|
|
58
|
+
encrypt_file(path_1, path_4, prompt=prompt)
|
|
59
|
+
decrypt_file(path_2, path_5)
|
|
60
|
+
cmp_result = filecmp.cmp(path_1, path_5)
|
|
61
|
+
assert cmp_result is True
|
|
62
|
+
|
|
63
|
+
_remove_testfiles()
|
|
64
|
+
|
|
65
|
+
# pytest with `input()` must using `-s`
|
|
66
|
+
# pytest tests\test_crypt_file.py::test_crypto_file_with_password -s
|
|
67
|
+
def _test_crypto_file_with_password():
|
|
68
|
+
prompt = "test"
|
|
69
|
+
|
|
70
|
+
with open(path_1, "w") as f:
|
|
71
|
+
f.write(rand_sentence(30))
|
|
72
|
+
f.write(rand_sentence(30))
|
|
73
|
+
|
|
74
|
+
encrypt_txtfile(path_1, path_2, accept_password=True)
|
|
75
|
+
decrypt_txtfile(path_2, path_3)
|
|
76
|
+
cmp_result = filecmp.cmp(path_1, path_3)
|
|
77
|
+
assert cmp_result is True
|
|
78
|
+
|
|
79
|
+
encrypt_file(path_1, path_4, accept_password=True)
|
|
80
|
+
decrypt_file(path_2, path_5)
|
|
81
|
+
cmp_result = filecmp.cmp(path_1, path_5)
|
|
82
|
+
assert cmp_result is True
|
|
83
|
+
|
|
84
|
+
_remove_testfiles()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import time
|
|
3
|
+
import hashlib
|
|
4
|
+
from python_plugins.weixin.wechat import Wechat
|
|
5
|
+
from python_plugins.weixin.wechat_crypt import get_signature
|
|
6
|
+
from python_plugins.weixin.wechat_crypt import MessageCrypt
|
|
7
|
+
from python_plugins.random import rand_digit, rand_letter
|
|
8
|
+
from python_plugins.convert.xml import xml2dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
test_wechat_app = {
|
|
12
|
+
"name": "test",
|
|
13
|
+
"appid": "wxabcdefghijklmnop",
|
|
14
|
+
"token": "testtest",
|
|
15
|
+
"aeskey": "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
|
|
16
|
+
"appsecret": "abcdefghijklmnopqrstuvwxyz012345",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
server_id = "gh_1234567890ab"
|
|
20
|
+
openid = "abcdefghijklmnopqrstuvwxyz01"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MyWechat(Wechat):
|
|
24
|
+
def get_app(self) -> dict:
|
|
25
|
+
return test_wechat_app
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestWechat:
|
|
29
|
+
def test_verify(self):
|
|
30
|
+
mywechat = MyWechat()
|
|
31
|
+
timestamp = str(int(time.time()))
|
|
32
|
+
nonce = rand_digit(10)
|
|
33
|
+
token = test_wechat_app["token"]
|
|
34
|
+
signature = get_signature([token, timestamp, nonce])
|
|
35
|
+
echostr = rand_digit(18)
|
|
36
|
+
query = {
|
|
37
|
+
"timestamp": timestamp,
|
|
38
|
+
"nonce": nonce,
|
|
39
|
+
"signature": signature,
|
|
40
|
+
"echostr": echostr,
|
|
41
|
+
}
|
|
42
|
+
r = mywechat.verify(query)
|
|
43
|
+
assert r == echostr
|
|
44
|
+
|
|
45
|
+
def test_chat(self):
|
|
46
|
+
"""chat(明文)"""
|
|
47
|
+
input_text = rand_letter(10)
|
|
48
|
+
timestamp = str(int(time.time()))
|
|
49
|
+
nonce = rand_digit(10)
|
|
50
|
+
|
|
51
|
+
xml = (
|
|
52
|
+
"<xml>"
|
|
53
|
+
f"<ToUserName><![CDATA[{server_id}]]></ToUserName>"
|
|
54
|
+
f"<FromUserName><![CDATA[{openid}]]></FromUserName>"
|
|
55
|
+
f"<CreateTime>{timestamp}</CreateTime>"
|
|
56
|
+
"<MsgType><![CDATA[text]]></MsgType>"
|
|
57
|
+
f"<Content><![CDATA[{input_text}]]></Content>"
|
|
58
|
+
"<MsgId>23533248665819413</MsgId>"
|
|
59
|
+
"</xml>"
|
|
60
|
+
)
|
|
61
|
+
query = {
|
|
62
|
+
"timestamp": timestamp,
|
|
63
|
+
"nonce": nonce,
|
|
64
|
+
"openid": f"{openid}",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
mywechat = MyWechat()
|
|
68
|
+
r = mywechat.chat(query, xml)
|
|
69
|
+
data = xml2dict(r)
|
|
70
|
+
# print(data)
|
|
71
|
+
assert data["ToUserName"] == openid
|
|
72
|
+
assert data["FromUserName"] == server_id
|
|
73
|
+
assert data["MsgType"] == "text"
|
|
74
|
+
assert input_text in data["Content"]
|
|
75
|
+
|
|
76
|
+
def test_chat_aes(self):
|
|
77
|
+
"""chat(密文)"""
|
|
78
|
+
input_text = rand_letter(10)
|
|
79
|
+
timestamp = str(int(time.time()))
|
|
80
|
+
nonce = rand_digit(10)
|
|
81
|
+
mywechat = MyWechat()
|
|
82
|
+
app = mywechat.app
|
|
83
|
+
mc = MessageCrypt(app["appid"], app["token"], app["aeskey"])
|
|
84
|
+
|
|
85
|
+
xml = (
|
|
86
|
+
"<xml>"
|
|
87
|
+
f"<ToUserName><![CDATA[{server_id}]]></ToUserName>"
|
|
88
|
+
f"<FromUserName><![CDATA[{openid}]]></FromUserName>"
|
|
89
|
+
f"<CreateTime>{timestamp}</CreateTime>"
|
|
90
|
+
"<MsgType><![CDATA[text]]></MsgType>"
|
|
91
|
+
f"<Content><![CDATA[{input_text}]]></Content>"
|
|
92
|
+
"<MsgId>23533248665819413</MsgId>"
|
|
93
|
+
"</xml>"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# 获取密文和签名
|
|
97
|
+
encrypt, msg_signature = mc.get_encrypt_sig(xml, timestamp, nonce)
|
|
98
|
+
|
|
99
|
+
xml2 = (
|
|
100
|
+
"<xml>"
|
|
101
|
+
f"<ToUserName><![CDATA[{server_id}]]></ToUserName>"
|
|
102
|
+
f"<FromUserName><![CDATA[{openid}]]></FromUserName>"
|
|
103
|
+
f"<CreateTime>{timestamp}</CreateTime>"
|
|
104
|
+
"<MsgType><![CDATA[text]]></MsgType>"
|
|
105
|
+
f"<Content><![CDATA[{input_text}]]></Content>"
|
|
106
|
+
"<MsgId>6054768590064713728</MsgId>"
|
|
107
|
+
f"<Encrypt><![CDATA[{encrypt}]]></Encrypt>"
|
|
108
|
+
"</xml>"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
query = {
|
|
112
|
+
"timestamp": timestamp,
|
|
113
|
+
"nonce": nonce,
|
|
114
|
+
"encrypt_type": "aes",
|
|
115
|
+
"msg_signature": msg_signature,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
r = mywechat.chat(query, xml2)
|
|
119
|
+
# print(r)
|
|
120
|
+
r_dict = xml2dict(r)
|
|
121
|
+
r_xml = mc.decrypt_msg(
|
|
122
|
+
r_dict["TimeStamp"],
|
|
123
|
+
r_dict["Nonce"],
|
|
124
|
+
r_dict["Encrypt"],
|
|
125
|
+
r_dict["MsgSignature"],
|
|
126
|
+
)
|
|
127
|
+
data = xml2dict(r_xml)
|
|
128
|
+
# print(data)
|
|
129
|
+
assert data["ToUserName"] == openid
|
|
130
|
+
assert data["FromUserName"] == server_id
|
|
131
|
+
assert data["MsgType"] == "text"
|
|
132
|
+
assert input_text in data["Content"]
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Configuration file for the Sphinx documentation builder.
|
|
2
|
-
#
|
|
3
|
-
# For the full list of built-in configuration values, see the documentation:
|
|
4
|
-
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
5
|
-
|
|
6
|
-
# -- Project information -----------------------------------------------------
|
|
7
|
-
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
8
|
-
|
|
9
|
-
project = 'python-plugins'
|
|
10
|
-
copyright = '2024, David Hua'
|
|
11
|
-
author = 'David Hua'
|
|
12
|
-
|
|
13
|
-
# -- General configuration ---------------------------------------------------
|
|
14
|
-
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
15
|
-
|
|
16
|
-
extensions = [
|
|
17
|
-
'sphinx.ext.autodoc',
|
|
18
|
-
'sphinx.ext.intersphinx',
|
|
19
|
-
'sphinx.ext.doctest',
|
|
20
|
-
'sphinx.ext.autosummary',
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
templates_path = ['_templates']
|
|
24
|
-
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
25
|
-
|
|
26
|
-
# -- Options for HTML output -------------------------------------------------
|
|
27
|
-
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
|
28
|
-
|
|
29
|
-
html_theme = 'classic'
|
|
30
|
-
html_title = "python-plugins Documentation"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.5"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,104 +0,0 @@
|
|
|
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
|
|
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.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/data_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/primary_key_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/timestamp_mixin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/token_minxin.py
RENAMED
|
File without changes
|
{python_plugins-0.1.5 → python_plugins-0.1.7}/src/python_plugins/models/mixins/user_minxin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_plugins-0.1.5 → python_plugins-0.1.7}/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
|
|
File without changes
|
|
File without changes
|