diff --git a/.env.dev b/.env.dev
index e225f8b8..e7257740 100644
--- a/.env.dev
+++ b/.env.dev
@@ -10,11 +10,18 @@ NICKNAME=["真寻", "小真寻", "绪山真寻", "小寻子"]
SESSION_EXPIRE_TIMEOUT=30
+PLATFORM_SUPERUSERS = '
+ {
+ "qq": [""],
+ "dodo": [""]
+ }
+'
+
# DRIVER=~fastapi
DRIVER=~fastapi+~httpx+~websockets
# kook adapter toekn
-kaiheila_bots =[{""}]
+kaiheila_bots =[{"token": ""}]
# discode adapter
DISCORD_BOTS='
@@ -41,6 +48,8 @@ DODO_BOTS='
]
'
+# application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令
+# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册
LOG_LEVEL=DEBUG
# 服务器和端口
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 784fa6a0..b23be7bc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,9 +7,15 @@
"Alconna",
"arclet",
"Arparma",
+ "displayname",
"getbbox",
"httpx",
+ "kaiheila",
"nonebot",
+ "onebot",
+ "tobytes",
+ "userinfo",
"zhenxun"
- ]
-}
\ No newline at end of file
+ ],
+ "python.analysis.autoImportCompletions": true
+}
diff --git a/poetry.lock b/poetry.lock
index f30884aa..58022200 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,5 +1,21 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+[[package]]
+name = "aiofiles"
+version = "23.2.1"
+description = "File support for asyncio."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"},
+ {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"},
+]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "aiosqlite"
version = "0.17.0"
@@ -267,6 +283,22 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "cachetools"
+version = "5.3.2"
+description = "Extensible memoizing collections and decorators"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"},
+ {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"},
+]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "cashews"
version = "6.4.0"
@@ -534,6 +566,25 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "emoji"
+version = "2.10.1"
+description = "Emoji for Python"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "emoji-2.10.1-py2.py3-none-any.whl", hash = "sha256:11fb369ea79d20c14efa4362c732d67126df294a7959a2c98bfd7447c12a218e"},
+ {file = "emoji-2.10.1.tar.gz", hash = "sha256:16287283518fb7141bde00198f9ffff4e1c1cb570efb68b2f1ec50975c3a581d"},
+]
+
+[package.extras]
+dev = ["coverage", "coveralls", "pytest"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "exceptiongroup"
version = "1.2.0"
@@ -553,6 +604,30 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "fastapi"
+version = "0.109.2"
+description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"},
+ {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"},
+]
+
+[package.dependencies]
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
+starlette = ">=0.36.3,<0.37.0"
+typing-extensions = ">=4.8.0"
+
+[package.extras]
+all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "filelock"
version = "3.13.1"
@@ -574,21 +649,6 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
-[[package]]
-name = "fleep"
-version = "1.0.1"
-description = "File format determination library"
-optional = false
-python-versions = ">=3.1"
-files = [
- {file = "fleep-1.0.1.tar.gz", hash = "sha256:c8f62b258ee5364d7f6c1ed1f3f278e99020fc3f0a60a24ad1e10846e31d104c"},
-]
-
-[package.source]
-type = "legacy"
-url = "https://mirrors.aliyun.com/pypi/simple"
-reference = "ali"
-
[[package]]
name = "greenlet"
version = "3.0.3"
@@ -707,6 +767,59 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "httptools"
+version = "0.6.1"
+description = "A collection of framework independent HTTP protocol utils."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"},
+ {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"},
+ {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"},
+ {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"},
+ {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"},
+ {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"},
+ {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"},
+ {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"},
+ {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"},
+ {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"},
+ {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"},
+ {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"},
+ {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"},
+ {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"},
+ {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"},
+ {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"},
+ {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"},
+ {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"},
+ {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"},
+ {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"},
+ {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"},
+ {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"},
+ {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"},
+ {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"},
+ {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"},
+ {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"},
+ {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"},
+ {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"},
+ {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"},
+ {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"},
+ {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"},
+ {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"},
+ {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"},
+ {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"},
+ {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"},
+ {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"},
+]
+
+[package.extras]
+test = ["Cython (>=0.29.24,<0.30.0)"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "httpx"
version = "0.26.0"
@@ -813,6 +926,26 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "markdown"
+version = "3.5.2"
+description = "Python implementation of John Gruber's Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"},
+ {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"},
+]
+
+[package.extras]
+docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+testing = ["coverage", "pyyaml"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@@ -1223,19 +1356,18 @@ reference = "ali"
[[package]]
name = "nonebot-plugin-alconna"
-version = "0.36.0"
+version = "0.36.3"
description = "Alconna Adapter for Nonebot"
optional = false
python-versions = ">=3.8"
files = [
- {file = "nonebot_plugin_alconna-0.36.0-py3-none-any.whl", hash = "sha256:37f8afc272924802fe75146df5f68b44e8e5537420cbb983d2d9d65195e625e7"},
- {file = "nonebot_plugin_alconna-0.36.0.tar.gz", hash = "sha256:e524fac76ee0f1a08817007e649c2b491b44094e0262a3d36fcef3e1259edfa2"},
+ {file = "nonebot_plugin_alconna-0.36.3-py3-none-any.whl", hash = "sha256:8f26f96c711d3adadc538ebf40d51ba2249c18fe1689bf36baed0e4d1e05246a"},
+ {file = "nonebot_plugin_alconna-0.36.3.tar.gz", hash = "sha256:ed8e4f2fd845d0c3d8becdd68678c203ee76109b9104a3b1c18f63525e85c6d4"},
]
[package.dependencies]
-arclet-alconna = ">=1.7.38,<2.0.0"
-arclet-alconna-tools = ">=0.6.7,<0.7.0"
-fleep = ">=1.0.1"
+arclet-alconna = ">=1.7.42,<2.0.0"
+arclet-alconna-tools = ">=0.6.11,<0.7.0"
nepattern = ">=0.5.14,<0.6.0"
nonebot2 = ">=2.1.0"
@@ -1264,6 +1396,32 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "nonebot-plugin-htmlrender"
+version = "0.3.0"
+description = "通过浏览器渲染图片"
+optional = false
+python-versions = "<4.0,>=3.8"
+files = [
+ {file = "nonebot_plugin_htmlrender-0.3.0-py3-none-any.whl", hash = "sha256:c05588bad4738421a49a47a7db974359adeb624c1ed6af49d6237023fa014bcf"},
+ {file = "nonebot_plugin_htmlrender-0.3.0.tar.gz", hash = "sha256:34b4ff5b898ea47480d3488a2a0b01c46e0ca3d938ab4b891d1db91a70d83d2d"},
+]
+
+[package.dependencies]
+aiofiles = ">=0.8.0"
+jinja2 = ">=3.0.3"
+markdown = ">=3.3.6"
+nonebot2 = {version = ">=2.2.0", extras = ["fastapi"]}
+playwright = ">=1.17.2"
+Pygments = ">=2.10.0"
+pymdown-extensions = ">=9.1"
+python-markdown-math = ">=0.8"
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "nonebot-plugin-send-anything-anywhere"
version = "0.5.0"
@@ -1306,23 +1464,49 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "nonebot-plugin-userinfo"
+version = "0.1.3"
+description = "Nonebot2 用户信息获取插件"
+optional = false
+python-versions = ">=3.8,<4.0"
+files = [
+ {file = "nonebot_plugin_userinfo-0.1.3-py3-none-any.whl", hash = "sha256:e20b22c81e86e81f7953560bd8ce0a54559a87ad615358c613b78cb5a4918191"},
+ {file = "nonebot_plugin_userinfo-0.1.3.tar.gz", hash = "sha256:d0a4d64c612486df63cd16950446072f8dfd2063ea28f15d56305a585a6b0b6e"},
+]
+
+[package.dependencies]
+cachetools = ">=5.0.0,<6.0.0"
+emoji = ">=2.0.0,<3.0.0"
+httpx = ">=0.20.0,<1.0.0"
+nonebot2 = {version = ">=2.0.0,<3.0.0", extras = ["fastapi"]}
+strenum = ">=0.4.8,<0.5.0"
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "nonebot2"
-version = "2.1.3"
+version = "2.2.0"
description = "An asynchronous python bot framework."
optional = false
python-versions = ">=3.8,<4.0"
files = [
- {file = "nonebot2-2.1.3-py3-none-any.whl", hash = "sha256:c36c1a60ce4355d9777fee431c08619f22ffd60f7060993fbbbd1fe67b6368f7"},
- {file = "nonebot2-2.1.3.tar.gz", hash = "sha256:e750e615f1ad2503721ce055fbe55ec3b061277135d995be112fecd27f7232e5"},
+ {file = "nonebot2-2.2.0-py3-none-any.whl", hash = "sha256:447fa63d384414c0e610f4ce6d2b3999db81ac2becd8d86716c4117013dc032f"},
+ {file = "nonebot2-2.2.0.tar.gz", hash = "sha256:138800846fa3dc635bda9f2ddc589519ee8d9d3b401013fbb95e47676fc830fb"},
]
[package.dependencies]
+fastapi = {version = ">=0.93.0,<1.0.0", optional = true, markers = "extra == \"fastapi\" or extra == \"all\""}
loguru = ">=0.6.0,<1.0.0"
-pydantic = {version = ">=1.10.0,<2.0.0", extras = ["dotenv"]}
+pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
pygtrie = ">=2.4.1,<3.0.0"
+python-dotenv = ">=0.21.0,<2.0.0"
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.4.0,<5.0.0"
+uvicorn = {version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true, markers = "extra == \"quart\" or extra == \"fastapi\" or extra == \"all\""}
yarl = ">=1.7.2,<2.0.0"
[package.extras]
@@ -1551,7 +1735,6 @@ files = [
]
[package.dependencies]
-python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""}
typing-extensions = ">=4.2.0"
[package.extras]
@@ -1637,6 +1820,29 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "pymdown-extensions"
+version = "10.7"
+description = "Extension pack for Python Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pymdown_extensions-10.7-py3-none-any.whl", hash = "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c"},
+ {file = "pymdown_extensions-10.7.tar.gz", hash = "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb"},
+]
+
+[package.dependencies]
+markdown = ">=3.5"
+pyyaml = "*"
+
+[package.extras]
+extra = ["pygments (>=2.12)"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "pypika-tortoise"
version = "0.1.6"
@@ -1691,6 +1897,25 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "python-markdown-math"
+version = "0.8"
+description = "Math extension for Python-Markdown"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "python-markdown-math-0.8.tar.gz", hash = "sha256:8564212af679fc18d53f38681f16080fcd3d186073f23825c7ce86fadd3e3635"},
+ {file = "python_markdown_math-0.8-py3-none-any.whl", hash = "sha256:c685249d84b5b697e9114d7beb352bd8ca2e07fd268fd4057ffca888c14641e5"},
+]
+
+[package.dependencies]
+Markdown = ">=3.0"
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "python-slugify"
version = "8.0.2"
@@ -1820,6 +2045,25 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "retrying"
+version = "1.3.4"
+description = "Retrying"
+optional = false
+python-versions = "*"
+files = [
+ {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"},
+ {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"},
+]
+
+[package.dependencies]
+six = ">=1.7.0"
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "rich"
version = "13.7.0"
@@ -1962,6 +2206,28 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "starlette"
+version = "0.36.3"
+description = "The little ASGI library that shines."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"},
+ {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"},
+]
+
+[package.dependencies]
+anyio = ">=3.4.0,<5"
+
+[package.extras]
+full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "strenum"
version = "0.4.15"
@@ -2287,6 +2553,86 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "uvicorn"
+version = "0.27.1"
+description = "The lightning-fast ASGI server."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"},
+ {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
+h11 = ">=0.8"
+httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""}
+python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
+pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
+typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
+uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
+watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
+websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
+
+[package.extras]
+standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
+[[package]]
+name = "uvloop"
+version = "0.19.0"
+description = "Fast implementation of asyncio event loop on top of libuv"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"},
+ {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"},
+ {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"},
+ {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"},
+ {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"},
+ {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"},
+ {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"},
+ {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"},
+ {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"},
+ {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"},
+ {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"},
+ {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"},
+ {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"},
+ {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"},
+ {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"},
+ {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"},
+ {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"},
+ {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"},
+ {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"},
+ {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"},
+ {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"},
+ {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"},
+ {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"},
+ {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"},
+ {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"},
+ {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"},
+ {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"},
+ {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"},
+ {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"},
+ {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"},
+ {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"},
+]
+
+[package.extras]
+docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
+test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "virtualenv"
version = "20.25.0"
@@ -2420,6 +2766,92 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "websockets"
+version = "12.0"
+description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"},
+ {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"},
+ {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"},
+ {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"},
+ {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"},
+ {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"},
+ {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"},
+ {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"},
+ {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"},
+ {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"},
+ {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"},
+ {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"},
+ {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"},
+ {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"},
+ {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"},
+ {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"},
+ {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"},
+ {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"},
+ {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"},
+ {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"},
+ {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"},
+ {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"},
+ {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"},
+ {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"},
+ {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"},
+ {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"},
+ {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"},
+ {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"},
+ {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"},
+ {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"},
+ {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"},
+ {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"},
+ {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"},
+ {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"},
+ {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"},
+ {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"},
+ {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"},
+ {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"},
+ {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"},
+ {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"},
+ {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"},
+ {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"},
+ {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"},
+ {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"},
+ {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"},
+ {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"},
+ {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"},
+ {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"},
+ {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"},
+ {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"},
+ {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"},
+ {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"},
+ {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"},
+ {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"},
+ {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"},
+ {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"},
+ {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"},
+ {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"},
+ {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"},
+ {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"},
+ {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"},
+ {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"},
+ {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"},
+ {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"},
+ {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"},
+ {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"},
+ {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"},
+ {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"},
+ {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"},
+ {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"},
+ {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"},
+ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
+]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "win32-setctime"
version = "1.1.0"
@@ -2550,4 +2982,4 @@ reference = "ali"
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "bc2932cc9955e05badaaf34f0bda8031edd80d3a832ccd05f9c079fadc4c5cdf"
+content-hash = "2e5c4963196533949601dff69762b6f5586056a8775419c2ee1aef0df91b016a"
diff --git a/pyproject.toml b/pyproject.toml
index b79c47c6..41f7f799 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,6 +29,10 @@ nonebot2 = "^2.1.3"
nonebot-adapter-discord = "^0.1.3"
nonebot-adapter-dodo = "^0.1.4"
pillow = "9.5"
+retrying = "^1.3.4"
+aiofiles = "^23.2.1"
+nonebot-plugin-htmlrender = "^0.3.0"
+nonebot-plugin-userinfo = "^0.1.3"
[tool.poetry.dev-dependencies]
diff --git a/resources/image/sign/sign_res/bar.png b/resources/image/sign/sign_res/bar.png
index 4fc99ec3..18b898d1 100644
Binary files a/resources/image/sign/sign_res/bar.png and b/resources/image/sign/sign_res/bar.png differ
diff --git a/resources/image/sign/sign_res/bar_white.png b/resources/image/sign/sign_res/bar_white.png
index c09c4635..2f3bcae4 100644
Binary files a/resources/image/sign/sign_res/bar_white.png and b/resources/image/sign/sign_res/bar_white.png differ
diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py
index 6448d28d..38776d55 100644
--- a/zhenxun/builtin_plugins/__init__.py
+++ b/zhenxun/builtin_plugins/__init__.py
@@ -1,3 +1,5 @@
+import os
+
from nonebot import require
require("nonebot_plugin_apscheduler")
@@ -8,3 +10,10 @@ require("nonebot_plugin_saa")
from nonebot_plugin_saa import enable_auto_select_bot
enable_auto_select_bot()
+from pathlib import Path
+
+import nonebot
+
+path = Path(__file__).parent / "platform"
+for d in os.listdir(path):
+ nonebot.load_plugins(str((path / d).resolve()))
diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py
new file mode 100644
index 00000000..6cad3cc0
--- /dev/null
+++ b/zhenxun/builtin_plugins/admin/admin_help.py
@@ -0,0 +1,165 @@
+import nonebot
+from arclet.alconna import Args, Option
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
+from nonebot_plugin_alconna.matcher import AlconnaMatcher
+from nonebot_plugin_saa import Image, Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.models.task_info import TaskInfo
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.exception import EmptyError
+from zhenxun.utils.image_utils import (
+ BuildImage,
+ build_sort_image,
+ group_image,
+ text2image,
+)
+from zhenxun.utils.rules import admin_check, ensure_group
+
+base_config = Config.get("admin_bot_manage")
+
+__plugin_meta__ = PluginMetadata(
+ name="群组管理员帮助",
+ description="管理员帮助列表",
+ usage="""
+ 管理员帮助
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.ADMIN,
+ admin_level=1,
+ ).dict(),
+)
+
+_matcher = on_alconna(
+ Alconna("管理员帮助"),
+ rule=admin_check(1) & ensure_group,
+ priority=5,
+ block=True,
+)
+
+
+ADMIN_HELP_IMAGE = IMAGE_PATH / "ADMIN_HELP.png"
+if ADMIN_HELP_IMAGE.exists():
+ ADMIN_HELP_IMAGE.unlink()
+
+
+async def build_help() -> BuildImage:
+ """构造管理员帮助图片
+
+ 异常:
+ EmptyError: 管理员帮助为空
+
+ 返回:
+ BuildImage: 管理员帮助图片
+ """
+ plugin_list = await PluginInfo.filter(
+ plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN]
+ ).all()
+ data_list = []
+ for plugin in plugin_list:
+ if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path):
+ if _plugin.metadata:
+ data_list.append({"plugin": plugin, "metadata": _plugin.metadata})
+ font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
+ image_list = []
+ for data in data_list:
+ plugin = data["plugin"]
+ metadata = data["metadata"]
+ try:
+ usage = None
+ description = None
+ if metadata.usage:
+ usage = await text2image(
+ metadata.usage,
+ padding=5,
+ color=(255, 255, 255),
+ font_color=(0, 0, 0),
+ )
+ if metadata.description:
+ description = await text2image(
+ metadata.description,
+ padding=5,
+ color=(255, 255, 255),
+ font_color=(0, 0, 0),
+ )
+ width = 0
+ height = 100
+ if usage:
+ width = usage.width
+ height += usage.height
+ if description and description.width > width:
+ width = description.width
+ height += description.height
+ font_width, font_height = BuildImage.get_text_size(
+ plugin.name + f"[{plugin.level}]", font
+ )
+ if font_width > width:
+ width = font_width
+ A = BuildImage(width + 30, height + 120, "#EAEDF2")
+ await A.text((15, 10), plugin.name + f"[{plugin.level}]")
+ await A.text((15, 70), "简介:")
+ if not description:
+ description = BuildImage(A.width - 30, 30, (255, 255, 255))
+ await description.circle_corner(10)
+ await A.paste(description, (15, 100))
+ if not usage:
+ usage = BuildImage(A.width - 30, 30, (255, 255, 255))
+ await usage.circle_corner(10)
+ await A.text((15, description.height + 115), "用法:")
+ await A.paste(usage, (15, description.height + 145))
+ await A.circle_corner(10)
+ image_list.append(A)
+ except Exception as e:
+ logger.warning(
+ f"获取群管理员插件 {plugin.module}: {plugin.name} 设置失败...",
+ "管理员帮助",
+ e=e,
+ )
+ if task_list := await TaskInfo.all():
+ task_str = "\n".join([task.name for task in task_list])
+ task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str
+ task_image = await text2image(task_str, padding=5, color=(255, 255, 255))
+ await task_image.circle_corner(10)
+ A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2")
+ await A.text((25, 10), "被动技能")
+ await A.paste(task_image, (25, 50))
+ await A.circle_corner(10)
+ image_list.append(A)
+ if not image_list:
+ raise EmptyError()
+ image_group, _ = group_image(image_list)
+ A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160)
+ text = await BuildImage.build_text_image(
+ "群管理员帮助",
+ size=40,
+ )
+ tip = await BuildImage.build_text_image(
+ "注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red"
+ )
+ await A.paste(text, (50, 30))
+ await A.paste(tip, (50, 90))
+ await A.save(ADMIN_HELP_IMAGE)
+ return BuildImage(1, 1)
+
+
+@_matcher.handle()
+async def _(
+ session: EventSession,
+ matcher: AlconnaMatcher,
+ arparma: Arparma,
+):
+ if not ADMIN_HELP_IMAGE.exists():
+ try:
+ await build_help()
+ except EmptyError:
+ await Text("管理员帮助为空").finish(reply=True)
+ await Image(ADMIN_HELP_IMAGE).send()
+ logger.info("查看管理员帮助", arparma.header_result, session=session)
diff --git a/zhenxun/builtin_plugins/admin/admin_watch.py b/zhenxun/builtin_plugins/admin/admin_watch.py
index 4fc274ea..02fe9417 100644
--- a/zhenxun/builtin_plugins/admin/admin_watch.py
+++ b/zhenxun/builtin_plugins/admin/admin_watch.py
@@ -9,11 +9,6 @@ from zhenxun.models.level_user import LevelUser
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
-__zx_plugin_name__ = "群管理员变动监测 [Hidden]"
-__plugin_version__ = 0.1
-__plugin_author__ = "HibiKier"
-
-
__plugin_meta__ = PluginMetadata(
name="群管理员变动监测",
description="检测群管理员变动, 添加与删除管理员默认权限, 当配置项 ADMIN_DEFAULT_AUTH 为空时, 不会添加管理员权限",
@@ -40,20 +35,25 @@ async def _(event: GroupAdminNoticeEvent):
admin_default_auth = base_config.get("ADMIN_DEFAULT_AUTH")
if admin_default_auth is not None:
await LevelUser.set_level(
- event.user_id,
- event.group_id,
+ str(event.user_id),
+ str(event.group_id),
admin_default_auth,
)
logger.info(
f"成为管理员,添加权限: {admin_default_auth}",
"群管理员变动监测",
- event.user_id,
- event.group_id,
+ session=event.user_id,
+ group_id=event.group_id,
)
else:
logger.warning(
f"配置项 MODULE: [admin_bot_manage] | KEY: [ADMIN_DEFAULT_AUTH] 为空"
)
elif event.sub_type == "unset":
- await LevelUser.delete_level(event.user_id, event.group_id)
- logger.info("撤销群管理员, 取消权限等级", "群管理员变动监测", event.user_id, event.group_id)
+ await LevelUser.delete_level(str(event.user_id), str(event.group_id))
+ logger.info(
+ "撤销群管理员, 取消权限等级",
+ "群管理员变动监测",
+ session=event.user_id,
+ group_id=event.group_id,
+ )
diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py
new file mode 100644
index 00000000..289ef6b6
--- /dev/null
+++ b/zhenxun/builtin_plugins/admin/ban/__init__.py
@@ -0,0 +1,196 @@
+from arclet.alconna import Args
+from nonebot.adapters import Bot
+from nonebot.permission import SUPERUSER
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import (
+ Alconna,
+ Arparma,
+ At,
+ Match,
+ Option,
+ Subcommand,
+ on_alconna,
+ store_true,
+)
+from nonebot_plugin_saa import Image, Mention, MessageFactory, Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.rules import admin_check
+
+from ._data_source import BanManage
+
+base_config = Config.get("ban")
+
+__plugin_meta__ = PluginMetadata(
+ name="封禁用户/群组",
+ description="你被逮捕了!丢进小黑屋!封禁用户以及群组,屏蔽消息",
+ usage="""
+ .ban [at] ?[小时] ?[分钟]
+ .unban
+ 示例:.ban @user
+ 示例:.ban @user 6
+ 示例:.ban @user 3 10
+ 示例:.unban @user
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.SUPER_AND_ADMIN,
+ admin_level=base_config.get("BAN_LEVEL", 5),
+ configs=[
+ RegisterConfig(
+ key="BAN_LEVEL",
+ value=5,
+ help="ban/unban所需要的管理员权限等级",
+ default_value=5,
+ type=int,
+ )
+ ],
+ ).dict(),
+)
+
+
+_matcher = on_alconna(
+ Alconna(
+ "ban-console",
+ Subcommand(
+ "ban",
+ Args["user?", [str, At]]["duration?", int],
+ Option("-g|--group", Args["group_id", str]),
+ ),
+ Subcommand(
+ "unban",
+ Args["user?", [str, At]],
+ Option("-g|--group", Args["group_id", str]),
+ ),
+ ),
+ rule=admin_check("ban", "BAN_LEVEL"),
+ priority=5,
+ block=True,
+)
+
+_status_matcher = on_alconna(
+ Alconna(
+ "ban-status",
+ Option("-u|--user", Args["user_id", str]),
+ Option("-g|--group", Args["group_id", str]),
+ ),
+ permission=SUPERUSER,
+ priority=1,
+ block=True,
+)
+# TODO: shortcut
+
+
+@_status_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ user_id: Match[str],
+ group_id: Match[str],
+):
+ _user_id = user_id.result if user_id.available else None
+ _group_id = group_id.result if group_id.available else None
+ if image := await BanManage.build_ban_image(_user_id, _group_id):
+ await Image(image.pic2bs4()).finish(reply=True)
+ else:
+ await Text("数据为空捏...").finish(reply=True)
+
+
+@_matcher.assign("ban")
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ user: Match[str | At],
+ duration: Match[int],
+ group_id: Match[str],
+):
+ user_id = None
+ if user.available:
+ if isinstance(user.result, At):
+ user_id = user.result.target
+ else:
+ user_id = user.result
+ _duration = duration.result * 60 if duration.available else -1
+ if gid := session.id3 or session.id2:
+ if group_id.available:
+ gid = group_id.result
+ await BanManage.ban(
+ user_id, gid, _duration, session, session.id1 in bot.config.superusers
+ )
+ logger.info(
+ f"管理员Ban",
+ arparma.header_result,
+ session=session,
+ target=f"{gid}:{user_id}",
+ )
+ await MessageFactory(
+ [
+ Text("对 "),
+ Mention(user_id), # type: ignore
+ Text(f" 狠狠惩戒了一番,一脚踢进了小黑屋!"),
+ ]
+ ).finish(reply=True)
+ elif session.id1 in bot.config.superusers:
+ _group_id = group_id.result if group_id.available else None
+ await BanManage.ban(user_id, _group_id, _duration, session, True)
+ logger.info(
+ f"超级用户Ban",
+ arparma.header_result,
+ session=session,
+ target=f"{_group_id}:{user_id}",
+ )
+ at_msg = user_id if user_id else f"群组:{_group_id}"
+ await Text(f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!").finish(reply=True)
+
+
+@_matcher.assign("unban")
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ user: Match[str | At],
+ group_id: Match[str],
+):
+ user_id = None
+ if user.available:
+ if isinstance(user.result, At):
+ user_id = user.result.target
+ else:
+ user_id = user.result
+ if gid := session.id3 or session.id2:
+ if group_id.available:
+ gid = group_id.result
+ await BanManage.unban(
+ user_id, gid, session, session.id1 in bot.config.superusers
+ )
+ logger.info(
+ f"管理员UnBan",
+ arparma.header_result,
+ session=session,
+ target=f"{gid}:{user_id}",
+ )
+ await MessageFactory(
+ [
+ Text("将 "),
+ Mention(user_id), # type: ignore
+ Text(f" 从黑屋中拉了出来并急救了一下!"),
+ ]
+ ).finish(reply=True)
+ elif session.id1 in bot.config.superusers:
+ _group_id = group_id.result if group_id.available else None
+ await BanManage.unban(user_id, _group_id, session, True)
+ logger.info(
+ f"超级用户UnBan",
+ arparma.header_result,
+ session=session,
+ target=f"{_group_id}:{user_id}",
+ )
+ at_msg = user_id if user_id else f"群组:{_group_id}"
+ await Text(f"对 {at_msg} 从黑屋中拉了出来并急救了一下!").finish(reply=True)
diff --git a/zhenxun/builtin_plugins/admin/ban/_data_source.py b/zhenxun/builtin_plugins/admin/ban/_data_source.py
new file mode 100644
index 00000000..7418eb47
--- /dev/null
+++ b/zhenxun/builtin_plugins/admin/ban/_data_source.py
@@ -0,0 +1,120 @@
+import time
+
+from nonebot_plugin_session import EventSession
+
+from zhenxun.models.ban_console import BanConsole
+from zhenxun.models.level_user import LevelUser
+from zhenxun.utils.image_utils import ImageTemplate
+
+
+class BanManage:
+
+ @classmethod
+ async def build_ban_image(cls, user_id: str | None, group_id: str | None):
+ data_list = None
+ if not user_id and not group_id:
+ data_list = await BanConsole.all()
+ elif user_id:
+ if group_id:
+ data_list = await BanConsole.filter(
+ user_id=user_id, group_id=group_id
+ ).all()
+ else:
+ data_list = await BanConsole.filter(
+ user_id=user_id, group_id__isnull=True
+ ).all()
+ else:
+ if group_id:
+ data_list = await BanConsole.filter(
+ user_id__isnull=True, group_id=group_id
+ ).all()
+ if not data_list:
+ return None
+ column_name = [
+ "ID",
+ "用户ID",
+ "群组ID",
+ "BAN LEVEL",
+ "剩余时长(分钟)",
+ "操作员ID",
+ ]
+ row_data = []
+ for data in data_list:
+ duration = int((data.ban_time + data.duration - time.time()) / 60)
+ if duration < 0:
+ duration = 0
+ row_data.append(
+ [
+ data.id,
+ data.user_id,
+ data.group_id,
+ data.ban_level,
+ duration,
+ data.operator,
+ ]
+ )
+ return await ImageTemplate.table_page(
+ "Ban / UnBan 列表", "在黑屋中狠狠调教!", column_name, row_data
+ )
+
+ @classmethod
+ async def is_ban(cls, user_id: str, group_id: str | None):
+ """判断用户是否被ban
+
+ 参数:
+ user_id: 用户id
+
+ 返回:
+ bool: 是否被ban
+ """
+ return await BanConsole.is_ban(user_id, group_id)
+
+ @classmethod
+ async def unban(
+ cls,
+ user_id: str | None,
+ group_id: str | None,
+ session: EventSession,
+ is_superuser: bool = False,
+ ) -> bool:
+ """ban掉目标用户
+
+ 参数:
+ user_id: 用户id
+ group_id: 群组id
+ session: Session
+ is_superuser: 是否为超级用户操作
+
+ 返回:
+ bool: 是否unban成功
+ """
+ user_level = 9999
+ if not is_superuser and user_id and session.id1:
+ user_level = await LevelUser.get_user_level(session.id1, group_id)
+ if await BanConsole.check_ban_level(user_id, group_id, user_level):
+ await BanConsole.unban(user_id, group_id)
+ return True
+ return False
+
+ @classmethod
+ async def ban(
+ cls,
+ user_id: str | None,
+ group_id: str | None,
+ duration: int,
+ session: EventSession,
+ is_superuser: bool,
+ ):
+ """ban掉目标用户
+
+ 参数:
+ user_id: 用户id
+ group_id: 群组id
+ duration: 时长,秒
+ session: Session
+ is_superuser: 是否为超级用户操作
+ """
+ level = 9999
+ if not is_superuser and user_id and session.id1:
+ level = await LevelUser.get_user_level(session.id1, group_id)
+ await BanConsole.ban(user_id, group_id, level, duration, session.id1)
diff --git a/zhenxun/builtin_plugins/admin/group_member_update/__init__.py b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py
new file mode 100644
index 00000000..1a7bfe4a
--- /dev/null
+++ b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py
@@ -0,0 +1,63 @@
+from nonebot import on_notice
+from nonebot.adapters import Bot
+from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
+from nonebot_plugin_saa import Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.utils import PluginExtraData
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.rules import admin_check, ensure_group
+
+from ._data_source import MemberUpdateManage
+
+__plugin_meta__ = PluginMetadata(
+ name="更新群组成员列表",
+ description="更新群组成员列表",
+ usage="""
+ 更新群组成员的基本信息
+ 指令:
+ 更新群组成员信息
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.SUPER_AND_ADMIN,
+ admin_level=1,
+ ).dict(),
+)
+
+
+_matcher = on_alconna(
+ Alconna("更新群组成员信息"),
+ rule=admin_check(1) & ensure_group,
+ priority=5,
+ block=True,
+)
+
+
+@_matcher.handle()
+async def _(bot: Bot, session: EventSession, arparma: Arparma):
+ if gid := session.id3 or session.id2:
+ logger.info("更新群组成员信息", arparma.header_result, session=session)
+ await MemberUpdateManage.update(bot, gid)
+ await Text("已经成功更新了群组成员信息!").finish(reply=True)
+ await Text("群组id为空...").send()
+
+
+_notice = on_notice(priority=1, block=False)
+
+
+@_notice.handle()
+async def _(bot: Bot, event: GroupIncreaseNoticeEvent):
+ # TODO: 其他适配器的加群自动更新群组成员信息
+ if str(event.user_id) == bot.self_id:
+ await MemberUpdateManage.update(bot, str(event.group_id))
+ logger.info(
+ "{NICKNAME}加入群聊更新群组信息",
+ "更新群组成员列表",
+ session=event.user_id,
+ group_id=event.group_id,
+ )
diff --git a/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py
new file mode 100644
index 00000000..442d337e
--- /dev/null
+++ b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py
@@ -0,0 +1,177 @@
+import time
+from datetime import datetime, timedelta, timezone
+
+from nonebot.adapters import Bot
+from nonebot.adapters.discord import Bot as DiscordBot
+from nonebot.adapters.dodo import Bot as DodoBot
+from nonebot.adapters.dodo.models import MemberInfo
+from nonebot.adapters.kaiheila import Bot as KaiheilaBot
+from nonebot.adapters.onebot.v11 import Bot as v11Bot
+from nonebot.adapters.onebot.v12 import Bot as v12Bot
+
+from zhenxun.configs.config import Config
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.models.level_user import LevelUser
+from zhenxun.services.log import logger
+
+
+class MemberUpdateManage:
+
+ @classmethod
+ async def update(cls, bot: Bot, group_id: str):
+ if isinstance(bot, v11Bot):
+ await cls.v11(bot, group_id)
+ elif isinstance(bot, v12Bot):
+ await cls.v12(bot, group_id)
+ elif isinstance(bot, KaiheilaBot):
+ await cls.kaiheila(bot, group_id)
+ elif isinstance(bot, DodoBot):
+ await cls.dodo(bot, group_id)
+ elif isinstance(bot, DiscordBot):
+ await cls.discord(bot, group_id)
+
+ @classmethod
+ async def discord(cls, bot: DiscordBot, group_id: str):
+ # TODO: discord更新群组成员信息
+ pass
+
+ @classmethod
+ async def dodo(cls, bot: DodoBot, group_id: str):
+ page_size = 100
+ result_size = 100
+ max_id = 0
+ exist_member_list = []
+ group_member_list: list[MemberInfo] = []
+ while result_size == page_size:
+ group_member_data = await bot.get_member_list(
+ island_source_id=group_id, page_size=page_size
+ )
+ result_size = len(group_member_data.list)
+ group_member_list += group_member_data.list
+ max_id = group_member_data.max_id
+ if group_member_list:
+ for user in group_member_list:
+ exist_member_list.append(user.dodo_source_id)
+ await GroupInfoUser.update_or_create(
+ user_id=user.dodo_source_id,
+ group_id=group_id,
+ defaults={
+ "user_name": user.nick_name or user.personal_nick_name,
+ "user_join_time": user.join_time,
+ "platform": "dodo",
+ },
+ )
+ if delete_member_list := list(
+ set(exist_member_list).difference(
+ set(await GroupInfoUser.get_group_member_id_list(group_id))
+ )
+ ):
+ await GroupInfoUser.filter(
+ user_id__in=delete_member_list, group_id=group_id
+ ).delete()
+ logger.info(
+ f"删除已退群用户",
+ "更新群组成员信息",
+ group_id=group_id,
+ platform="dodo",
+ )
+
+ @classmethod
+ async def kaiheila(cls, bot: KaiheilaBot, group_id: str):
+ # TODO: kaiheila 更新群组成员信息
+ pass
+
+ @classmethod
+ async def v11(cls, bot: v11Bot, group_id: str):
+ exist_member_list = []
+ default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH")
+ group_member_list = await bot.get_group_member_list(group_id=int(group_id))
+ for user_info in group_member_list:
+ user_id = user_info["user_id"]
+ nickname = user_info["card"] or user_info["nickname"]
+ role = user_info["role"]
+ if default_auth:
+ if role in ["owner", "admin"] and not LevelUser.is_group_flag(
+ str(user_id), group_id
+ ):
+ await LevelUser.set_level(user_id, group_id, default_auth)
+ if str(user_id) in bot.config.superusers:
+ await LevelUser.set_level(str(user_id), group_id, 9)
+ join_time = datetime.strptime(
+ time.strftime(
+ "%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"])
+ ),
+ "%Y-%m-%d %H:%M:%S",
+ )
+ await GroupInfoUser.update_or_create(
+ user_id=str(user_id),
+ group_id=group_id,
+ defaults={
+ "user_name": nickname,
+ "user_join_time": join_time.replace(
+ tzinfo=timezone(timedelta(hours=8))
+ ),
+ "platform": "qq",
+ },
+ )
+ exist_member_list.append(str(user_id))
+ logger.debug(
+ "更新成功", "更新群组成员信息", session=user_id, group_id=group_id
+ )
+ if delete_member_list := list(
+ set(exist_member_list).difference(
+ set(await GroupInfoUser.get_group_member_id_list(group_id))
+ )
+ ):
+ await GroupInfoUser.filter(
+ user_id__in=delete_member_list, group_id=group_id
+ ).delete()
+ logger.info(
+ f"删除已退群用户", "更新群组成员信息", group_id=group_id, platform="qq"
+ )
+
+ @classmethod
+ async def v12(cls, bot: v12Bot, group_id: str):
+ # TODO: v12更新群组成员信息
+ pass
+ # exist_member_list = []
+ # default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH")
+ # group_member_list: list[GetGroupMemberInfoResp] = await bot.get_group_member_list(
+ # group_id=group_id
+ # )
+ # for user_info in group_member_list:
+ # user_id = user_info.user_id
+ # nickname = user_info.user_displayname or user_info.user_name
+ # role = user_info["role"]
+ # if default_auth:
+ # if role in ["owner", "admin"] and not LevelUser.is_group_flag(
+ # str(user_id), group_id
+ # ):
+ # await LevelUser.set_level(user_id, group_id, default_auth)
+ # if str(user_id) in bot.config.superusers:
+ # await LevelUser.set_level(str(user_id), group_id, 9)
+ # join_time = datetime.strptime(
+ # time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"])),
+ # "%Y-%m-%d %H:%M:%S",
+ # )
+ # await GroupInfoUser.update_or_create(
+ # user_id=str(user_id),
+ # group_id=group_id,
+ # defaults={
+ # "user_name": nickname,
+ # "user_join_time": join_time.replace(
+ # tzinfo=timezone(timedelta(hours=8))
+ # ),
+ # },
+ # )
+ # exist_member_list.append(str(user_id))
+ # logger.debug("更新成功", "更新群组成员信息", session=user_id, group_id=group_id)
+ # if delete_member_list := list(
+ # set(exist_member_list).difference(
+ # set(await GroupInfoUser.get_group_member_id_list(group_id))
+ # )
+ # ):
+ # await GroupInfoUser.filter(
+ # user_id__in=delete_member_list, group_id=group_id
+ # ).delete()
+ # logger.info(f"删除已退群用户", "更新群组成员信息", group_id=group_id)
diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py
new file mode 100644
index 00000000..42da314f
--- /dev/null
+++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py
@@ -0,0 +1,162 @@
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import (
+ Alconna,
+ Args,
+ Arparma,
+ Match,
+ Option,
+ Subcommand,
+ on_alconna,
+ store_true,
+)
+from nonebot_plugin_saa import Image, Text
+from nonebot_plugin_session import EventSession
+from requests import session
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import BlockType, PluginType
+from zhenxun.utils.rules import admin_check, ensure_group
+
+from ._data_source import PluginManage, build_plugin, build_task
+
+base_config = Config.get("admin_bot_manage")
+
+
+__plugin_meta__ = PluginMetadata(
+ name="功能开关",
+ description="对群组内的功能限制,超级用户可以对群组以及全局的功能被动开关限制",
+ usage="""
+ 开启/关闭[功能]
+ 群被动状态
+ 开启全部被动
+ 关闭全部被动
+ 醒来/休息吧
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.SUPER_AND_ADMIN,
+ admin_level=base_config.get("CHANGE_GROUP_SWITCH_LEVEL", 2),
+ configs=[
+ RegisterConfig(
+ key="CHANGE_GROUP_SWITCH_LEVEL",
+ value=2,
+ help="开关群功能权限",
+ default_value=2,
+ type=int,
+ )
+ ],
+ ).dict(),
+)
+
+
+_status_matcher = on_alconna(
+ Alconna(
+ "switch",
+ Option("-t|--task", action=store_true, help_text="被动技能"),
+ Subcommand(
+ "open",
+ Args["name", str],
+ Option(
+ "-g|--group",
+ Args["group_id", str],
+ ),
+ ),
+ Subcommand(
+ "close",
+ Args["name", str],
+ Option(
+ "-t|--type",
+ Args["block_type", ["all", "a", "private", "p", "group", "g"]],
+ ),
+ Option(
+ "-g|--group",
+ Args["group_id", str],
+ ),
+ ),
+ ),
+ rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"),
+ priority=5,
+ block=True,
+)
+
+# TODO: shortcut
+
+_group_status_matcher = on_alconna(
+ Alconna("group-status", Args["status", ["sleep", "wake"]]),
+ rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") & ensure_group,
+ priority=5,
+ block=True,
+)
+
+
+@_status_matcher.assign("$main")
+async def _(bot: Bot, session: EventSession, arparma: Arparma):
+ image = None
+ if arparma.find("task"):
+ image = await build_task(session.id3 or session.id2)
+ elif session.id1 in bot.config.superusers:
+ image = await build_plugin()
+ if image:
+ await Image(image.pic2bs4()).send(reply=True)
+ logger.info(
+ f"查看{'被动' if arparma.find('task') else '功能'}列表",
+ arparma.header_result,
+ session=session,
+ )
+
+
+@_status_matcher.assign("open")
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ name: str,
+ group: Match[str],
+):
+ if gid := session.id3 or session.id2:
+ result = await PluginManage.block_group_plugin(name, gid)
+ await Text(result).send(reply=True)
+ logger.info(f"开启功能 {name}", arparma.header_result, session=session)
+ elif session.id1 in bot.config.superusers:
+ result = await PluginManage.superuser_block(name, None, group.result)
+ await Text(result).send(reply=True)
+ logger.info(
+ f"超级用户开启功能 {name}",
+ arparma.header_result,
+ session=session,
+ target=group.result,
+ )
+
+
+@_status_matcher.assign("close")
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ name: str,
+ block_type: Match[str],
+ group: Match[str],
+):
+ if gid := session.id3 or session.id2:
+ result = await PluginManage.unblock_group_plugin(name, gid)
+ await Text(result).send(reply=True)
+ logger.info(f"关闭功能 {name}", arparma.header_result, session=session)
+ elif session.id1 in bot.config.superusers:
+ _type = BlockType.ALL
+ if block_type.available:
+ if block_type.result in ["p", "private"]:
+ _type = BlockType.FRIEND
+ elif block_type.result in ["g", "group"]:
+ _type = BlockType.GROUP
+ result = await PluginManage.superuser_block(name, _type, group.result)
+ await Text(result).send(reply=True)
+ logger.info(
+ f"超级用户关闭功能 {name}, 禁用类型: {_type}",
+ arparma.header_result,
+ session=session,
+ target=group.result,
+ )
diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py
new file mode 100644
index 00000000..bd7fd937
--- /dev/null
+++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py
@@ -0,0 +1,244 @@
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.models.task_info import TaskInfo
+from zhenxun.utils.enum import BlockType, PluginType
+from zhenxun.utils.exception import GroupInfoNotFound
+from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle
+
+
+def plugin_row_style(column: str, text: str) -> RowStyle:
+ """被动技能文本风格
+
+ 参数:
+ column: 表头
+ text: 文本内容
+
+ 返回:
+ RowStyle: RowStyle
+ """
+ style = RowStyle()
+ if column == "全局状态":
+ if text == "开启":
+ style.font_color = "#67C23A"
+ else:
+ style.font_color = "#F56C6C"
+ if column == "加载状态":
+ if text == "SUCCESS":
+ style.font_color = "#67C23A"
+ else:
+ style.font_color = "#F56C6C"
+ return style
+
+
+async def build_plugin() -> BuildImage:
+ column_name = [
+ "ID",
+ "模块",
+ "名称",
+ "全局状态",
+ "禁用类型",
+ "加载状态",
+ "菜单分类",
+ "作者",
+ "版本",
+ "金币花费",
+ ]
+ plugin_list = await PluginInfo.filter(plugin_type__not=PluginType.HIDDEN).all()
+ column_data = []
+ for plugin in plugin_list:
+ column_data.append(
+ [
+ plugin.id,
+ plugin.module,
+ plugin.name,
+ "开启" if plugin.status else "关闭",
+ plugin.block_type,
+ "SUCCESS" if plugin.load_status else "ERROR",
+ plugin.menu_type,
+ plugin.author,
+ plugin.version,
+ plugin.cost_gold,
+ ]
+ )
+ return await ImageTemplate.table_page(
+ "Plugin",
+ "插件状态",
+ column_name,
+ column_data,
+ text_style=plugin_row_style,
+ )
+
+
+def task_row_style(column: str, text: str) -> RowStyle:
+ """被动技能文本风格
+
+ 参数:
+ column: 表头
+ text: 文本内容
+
+ 返回:
+ RowStyle: RowStyle
+ """
+ style = RowStyle()
+ if column in ["群组状态", "全局状态"]:
+ if text == "开启":
+ style.font_color = "#67C23A"
+ else:
+ style.font_color = "#F56C6C"
+ return style
+
+
+async def build_task(group_id: str | None) -> BuildImage:
+ """构造被动技能状态图片
+
+ 参数:
+ group_id: 群组id
+
+ 异常:
+ GroupInfoNotFound: 未找到群组
+
+ 返回:
+ BuildImage: 被动技能状态图片
+ """
+ task_list = await TaskInfo.all()
+ column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"]
+ group = None
+ if group_id:
+ group = await GroupConsole.get_or_none(group_id=group_id)
+ if not group:
+ raise GroupInfoNotFound()
+ else:
+ column_name.remove("群组状态")
+ column_data = []
+ for task in task_list:
+ if group:
+ column_data.append(
+ [
+ task.id,
+ task.module,
+ task.name,
+ "开启" if task.module not in group.block_task else "关闭",
+ "开启" if task.status else "关闭",
+ task.run_time,
+ ]
+ )
+ else:
+ column_data.append(
+ [
+ task.id,
+ task.module,
+ task.name,
+ "开启" if task.status else "关闭",
+ task.run_time,
+ ]
+ )
+ return await ImageTemplate.table_page(
+ "Task",
+ "被动技能状态",
+ column_name,
+ column_data,
+ text_style=task_row_style,
+ )
+
+
+class PluginManage:
+
+ @classmethod
+ async def block(cls, module: str):
+ await PluginInfo.filter(module=module).update(status=False)
+
+ @classmethod
+ async def unblock(cls, module: str):
+ await PluginInfo.filter(module=module).update(status=True)
+
+ @classmethod
+ async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str:
+ """禁用群组插件
+
+ 参数:
+ plugin_name: 插件名称
+ group_id: 群组id
+
+ 返回:
+ str: 返回信息
+ """
+ return await cls._change_group_plugin(plugin_name, group_id, True)
+
+ @classmethod
+ async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str:
+ """启用群组插件
+
+ 参数:
+ plugin_name: 插件名称
+ group_id: 群组id
+
+ 返回:
+ str: 返回信息
+ """
+ return await cls._change_group_plugin(plugin_name, group_id, False)
+
+ @classmethod
+ async def _change_group_plugin(
+ cls, plugin_name: str, group_id: str, status: bool
+ ) -> str:
+ """修改群组插件状态
+
+ 参数:
+ plugin_name: 插件名称
+ group_id: 群组id
+ status: 插件状态
+
+ 返回:
+ str: 返回信息
+ """
+ status_str = "开启" if status else "关闭"
+ if plugin := await PluginInfo.get_or_none(name=plugin_name):
+ group, _ = await GroupConsole.get_or_create(group_id=group_id)
+ if status:
+ if plugin.module in group.block_plugin:
+ group.block_plugin = group.block_plugin.replace(
+ f"{plugin.module},", ""
+ )
+ await group.save(update_fields=["block_plugin"])
+ return f"已成功{status_str} {plugin_name} 功能!"
+ else:
+ if plugin.module not in group.block_plugin:
+ group.block_plugin += f"{plugin.module},"
+ await group.save(update_fields=["block_plugin"])
+ return f"已成功{status_str} {plugin_name} 功能!"
+ return f"该功能已经{status_str}了喔,不要重复{status_str}..."
+ return "没有找到这个功能喔..."
+
+ @classmethod
+ async def superuser_block(
+ cls, plugin_name: str, block_type: BlockType | None, group_id: str | None
+ ) -> str:
+ """超级用户禁用
+
+ 参数:
+ plugin_name: 插件名称
+ block_type: 禁用类型
+ group_id: 群组id
+
+ 返回:
+ str: 返回信息
+ """
+ if plugin := await PluginInfo.get_or_none(name=plugin_name):
+ if group_id:
+ if group := await GroupConsole.get_or_none(group_id=group_id):
+ if f"super:{plugin_name}," not in group.block_plugin:
+ group.block_plugin += f"super:{plugin_name},"
+ await group.save(update_fields=["block_plugin"])
+ return (
+ f"已成功关闭群组 {group.group_name} 的 {plugin_name} 功能!"
+ )
+ return "此群组该功能已被超级用户关闭,不要重复关闭..."
+ return "群组信息未更新,请先更新群组信息..."
+ plugin.block_type = block_type
+ plugin.status = not bool(block_type)
+ await plugin.save(update_fields=["status", "block_type"])
+ if not block_type:
+ return f"已成功将 {plugin_name} 全局启用!"
+ else:
+ return f"已成功将 {plugin_name} 全局关闭!"
+ return "没有找到这个功能喔..."
diff --git a/zhenxun/builtin_plugins/admin/welcome_message.py b/zhenxun/builtin_plugins/admin/welcome_message.py
index 96c47949..7909a036 100644
--- a/zhenxun/builtin_plugins/admin/welcome_message.py
+++ b/zhenxun/builtin_plugins/admin/welcome_message.py
@@ -1,26 +1,22 @@
+import os
import shutil
-from typing import Dict
+from typing import Annotated, Dict
import ujson as json
-from arclet.alconna import Args, Option
+from nonebot import on_command
+from nonebot.params import Command
from nonebot.plugin import PluginMetadata
-from nonebot_plugin_alconna import (
- Alconna,
- AlconnaMatch,
- Arparma,
- Match,
- on_alconna,
- store_true,
-)
-from nonebot_plugin_alconna.matcher import AlconnaMatcher
-from nonebot_plugin_saa import Text
+from nonebot_plugin_alconna import Image
+from nonebot_plugin_alconna import Text as alcText
+from nonebot_plugin_alconna import UniMsg
from nonebot_plugin_session import EventSession
from zhenxun.configs.config import Config
from zhenxun.configs.path_config import DATA_PATH
-from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
+from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.rules import admin_check, ensure_group
base_config = Config.get("admin_bot_manage")
@@ -48,18 +44,15 @@ __plugin_meta__ = PluginMetadata(
).dict(),
)
-_matcher = on_alconna(
- Alconna(
- "设置欢迎消息",
- Args["message", str],
- Option("-at", action=store_true, help_text="是否at新入群用户"),
- ),
+_matcher = on_command(
+ "设置欢迎消息",
rule=admin_check("admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL")
& ensure_group,
priority=5,
block=True,
)
+
BASE_PATH = DATA_PATH / "welcome_message"
BASE_PATH.mkdir(parents=True, exist_ok=True)
@@ -86,31 +79,43 @@ if old_file.exists():
@_matcher.handle()
async def _(
session: EventSession,
- matcher: AlconnaMatcher,
- arparma: Arparma,
- message: str,
+ message: UniMsg,
+ command: Annotated[tuple[str, ...], Command()],
):
- file = (
- BASE_PATH
- / f"{session.platform or session.bot_type}"
- / f"{session.id2}"
- / "text.json"
- )
+ path = BASE_PATH / f"{session.platform or session.bot_type}" / f"{session.id2}"
if session.id3:
- file = (
+ path = (
BASE_PATH
/ f"{session.platform or session.bot_type}"
/ f"{session.id3}"
/ f"{session.id2}"
- / "text.json"
)
+ file = path / "text.json"
+ idx = 0
+ text = ""
+ for f in os.listdir(path):
+ (path / f).unlink()
+ message[0].text = message[0].text.replace(command[0], "").strip()
+ for msg in message:
+ if isinstance(msg, alcText):
+ text += msg.text
+ elif isinstance(msg, Image):
+ if msg.url:
+ text += f"[image:{idx}]"
+ await AsyncHttpx.download_file(msg.url, path / f"{idx}.png")
+ idx += 1
+ else:
+ logger.debug("图片 URL 为空...", command[0])
if not file.exists():
file.parent.mkdir(exist_ok=True, parents=True)
+ is_at = "-at" in message
+ text = text.replace("-at", "")
json.dump(
- {"at": arparma.find("at"), "message": message},
+ {"at": is_at, "message": text},
file.open("w"),
ensure_ascii=False,
indent=4,
)
- logger.info(f"设置群欢迎消息成功: {message}", arparma.header_result, session=session)
- await Text(f"设置欢迎消息成功: \n{message}").send()
+ uni_msg = alcText("设置欢迎消息成功: \n") + message
+ await uni_msg.send()
+ logger.info(f"设置群欢迎消息成功: {text}", command[0], session=session)
diff --git a/zhenxun/builtin_plugins/chat_history/__init__.py b/zhenxun/builtin_plugins/chat_history/__init__.py
new file mode 100644
index 00000000..838488cf
--- /dev/null
+++ b/zhenxun/builtin_plugins/chat_history/__init__.py
@@ -0,0 +1,4 @@
+import nonebot
+from pathlib import Path
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
diff --git a/zhenxun/builtin_plugins/chat_history/chat_message.py b/zhenxun/builtin_plugins/chat_history/chat_message.py
new file mode 100644
index 00000000..602a0199
--- /dev/null
+++ b/zhenxun/builtin_plugins/chat_history/chat_message.py
@@ -0,0 +1,83 @@
+from nonebot import on_message
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import UniMsg
+from nonebot_plugin_apscheduler import scheduler
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.models.chat_history import ChatHistory
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+
+__plugin_meta__ = PluginMetadata(
+ name="消息存储",
+ description="消息存储,被动存储群消息",
+ usage="",
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.HIDDEN,
+ configs=[
+ RegisterConfig(
+ module="chat_history",
+ key="FLAG",
+ value=True,
+ help="是否开启消息自从存储",
+ default_value=True,
+ type=bool,
+ )
+ ],
+ ).dict(),
+)
+
+
+def rule(message: UniMsg) -> bool:
+ return bool(Config.get_config("chat_history", "FLAG") and message)
+
+
+chat_history = on_message(rule=rule, priority=1, block=False)
+
+
+TEMP_LIST = []
+
+
+@chat_history.handle()
+async def _(message: UniMsg, session: EventSession):
+ group_id = session.id3 or session.id2
+ TEMP_LIST.append(
+ ChatHistory(
+ user_id=session.id1,
+ group_id=group_id,
+ text=str(message),
+ plain_text=message.extract_plain_text(),
+ bot_id=session.bot_id,
+ platform=session.platform,
+ )
+ )
+
+
+@scheduler.scheduled_job(
+ "interval",
+ minutes=1,
+)
+async def _():
+ try:
+ message_list = TEMP_LIST.copy()
+ TEMP_LIST.clear()
+ if message_list:
+ await ChatHistory.bulk_create(message_list)
+ logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务")
+ except Exception as e:
+ logger.error(f"定时批量添加聊天记录", "定时任务", e=e)
+
+
+# @test.handle()
+# async def _(event: MessageEvent):
+# print(await ChatHistory.get_user_msg(event.user_id, "private"))
+# print(await ChatHistory.get_user_msg_count(event.user_id, "private"))
+# print(await ChatHistory.get_user_msg(event.user_id, "group"))
+# print(await ChatHistory.get_user_msg_count(event.user_id, "group"))
+# print(await ChatHistory.get_group_msg(event.group_id))
+# print(await ChatHistory.get_group_msg_count(event.group_id))
diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py
new file mode 100644
index 00000000..8b8cb697
--- /dev/null
+++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py
@@ -0,0 +1,116 @@
+from datetime import datetime, timedelta
+
+import pytz
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import (
+ Alconna,
+ Args,
+ Arparma,
+ Match,
+ Option,
+ on_alconna,
+ store_true,
+)
+from nonebot_plugin_saa import Image, Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.utils import PluginExtraData
+from zhenxun.models.chat_history import ChatHistory
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.image_utils import ImageTemplate
+
+__plugin_meta__ = PluginMetadata(
+ name="消息统计查询",
+ description="消息统计查询",
+ usage="",
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.NORMAL,
+ menu_type="数据统计",
+ ).dict(),
+)
+
+# TODO: shortcut
+
+_matcher = on_alconna(
+ Alconna(
+ "消息排行",
+ Option("--des", default=False, action=store_true),
+ Args["type?", ["日", "周", "月", "年"]]["count?", int, 10],
+ ),
+ priority=5,
+ block=True,
+)
+
+
+@_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ type: Match[str],
+ count: Match[int],
+):
+ group_id = session.id3 or session.id2
+ if not group_id:
+ await Text("群组id为空...").finish()
+ time_now = datetime.now()
+ date_scope = None
+ zero_today = time_now - timedelta(
+ hours=time_now.hour, minutes=time_now.minute, seconds=time_now.second
+ )
+ date = type.result if type.available else None
+ if date:
+ if date in ["日"]:
+ date_scope = (zero_today, time_now)
+ elif date in ["周"]:
+ date_scope = (time_now - timedelta(days=7), time_now)
+ elif date in ["月"]:
+ date_scope = (time_now - timedelta(days=30), time_now)
+ column_name = ["名次", "昵称", "发言次数"]
+ if rank_data := await ChatHistory.get_group_msg_rank(
+ group_id, count.result, "DES" if arparma.find("des") else "DESC", date_scope
+ ):
+ idx = 1
+ data_list = []
+ for uid, num in rank_data:
+ if user := await GroupInfoUser.filter(
+ user_id=uid, group_id=group_id
+ ).first():
+ user_name = user.user_name
+ else:
+ user_name = uid
+ data_list.append([idx, user_name, num])
+ idx += 1
+ if not date_scope:
+ if date_scope := await ChatHistory.get_group_first_msg_datetime(group_id):
+ date_scope = date_scope.astimezone(
+ pytz.timezone("Asia/Shanghai")
+ ).replace(microsecond=0)
+ else:
+ date_scope = time_now.replace(microsecond=0)
+ date_str = f"{date_scope} - 至今"
+ else:
+ date_str = f"{date_scope[0].replace(microsecond=0)} - {date_scope[1].replace(microsecond=0)}"
+ A = await ImageTemplate.table_page(
+ f"消息排行({count.result})", date_str, column_name, data_list
+ )
+ logger.info(
+ f"查看消息排行 数量={count.result}", arparma.header_result, session=session
+ )
+ await Image(A.pic2bs4()).finish(reply=True)
+ await Text("群组消息记录为空...").finish()
+
+
+# # @test.handle()
+# # async def _(event: MessageEvent):
+# # print(await ChatHistory.get_user_msg(event.user_id, "private"))
+# # print(await ChatHistory.get_user_msg_count(event.user_id, "private"))
+# # print(await ChatHistory.get_user_msg(event.user_id, "group"))
+# # print(await ChatHistory.get_user_msg_count(event.user_id, "group"))
+# # print(await ChatHistory.get_group_msg(event.group_id))
+# # print(await ChatHistory.get_group_msg_count(event.group_id))
diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py
new file mode 100644
index 00000000..41c190b2
--- /dev/null
+++ b/zhenxun/builtin_plugins/help/__init__.py
@@ -0,0 +1,80 @@
+from nonebot.plugin import PluginMetadata
+from nonebot.rule import to_me
+from nonebot_plugin_alconna import Alconna, Args, Match, on_alconna
+from nonebot_plugin_saa import Image, Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+
+from ._data_source import create_help_img, get_plugin_help
+from ._utils import GROUP_HELP_PATH
+
+__plugin_meta__ = PluginMetadata(
+ name="帮助",
+ description="帮助",
+ usage="",
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.HIDDEN,
+ configs=[
+ RegisterConfig(
+ key="type",
+ value="normal",
+ help="帮助图片样式 ['normal', 'HTML']",
+ default_value="normal",
+ )
+ ],
+ ).dict(),
+)
+
+
+SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png"
+if SIMPLE_HELP_IMAGE.exists():
+ SIMPLE_HELP_IMAGE.unlink()
+
+_matcher = on_alconna(
+ Alconna(
+ "功能",
+ Args["name?", str],
+ ),
+ aliases={"help", "帮助"},
+ rule=to_me(),
+ priority=1,
+ block=True,
+)
+
+# TODO: 插件使用详情 图片形式的帮助回复
+
+
+@_matcher.handle()
+async def _(
+ name: Match[str],
+ session: EventSession,
+):
+
+ if name.available:
+ if text := await get_plugin_help(name.result):
+ await Text(text).send(reply=True)
+ else:
+ await Text("没有此功能的帮助信息...").send()
+ logger.info(
+ f"查看帮助详情: {name.result}",
+ "帮助",
+ session=session,
+ )
+ else:
+ if gid := session.id3 or session.id2:
+ _image_path = GROUP_HELP_PATH / f"{gid}.png"
+ if not _image_path.exists():
+ await create_help_img(gid)
+ await Image(_image_path).finish()
+ else:
+ if not SIMPLE_HELP_IMAGE.exists():
+ if SIMPLE_HELP_IMAGE.exists():
+ SIMPLE_HELP_IMAGE.unlink()
+ await create_help_img(None)
+ await Image(SIMPLE_HELP_IMAGE).finish()
diff --git a/zhenxun/builtin_plugins/help/_config.py b/zhenxun/builtin_plugins/help/_config.py
new file mode 100644
index 00000000..b38bf066
--- /dev/null
+++ b/zhenxun/builtin_plugins/help/_config.py
@@ -0,0 +1,13 @@
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ plugin_name: str
+ sta: int
+
+
+class PluginList(BaseModel):
+ plugin_type: str
+ icon: str
+ logo: str
+ items: list[Item]
diff --git a/zhenxun/builtin_plugins/help/_data_source.py b/zhenxun/builtin_plugins/help/_data_source.py
new file mode 100644
index 00000000..75d8b66e
--- /dev/null
+++ b/zhenxun/builtin_plugins/help/_data_source.py
@@ -0,0 +1,35 @@
+import nonebot
+
+from zhenxun.configs.path_config import IMAGE_PATH
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.utils.image_utils import BuildImage
+
+from ._utils import HelpImageBuild
+
+random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help"
+
+background = IMAGE_PATH / "background" / "0.png"
+
+
+async def create_help_img(group_id: int | None):
+ """
+ 说明:
+ 生成帮助图片
+ 参数:
+ :param group_id: 群号
+ """
+ await HelpImageBuild().build_image(group_id)
+
+
+async def get_plugin_help(name: str) -> str:
+ """获取功能的帮助信息
+
+ 参数:
+ name: 插件名称
+ """
+ if plugin := await PluginInfo.get_or_none(name=name):
+ _plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
+ if _plugin and _plugin.metadata:
+ return _plugin.metadata.usage
+ return "糟糕! 该功能没有帮助喔..."
+ return "没有查找到这个功能噢..."
diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py
new file mode 100644
index 00000000..b3f8d3a4
--- /dev/null
+++ b/zhenxun/builtin_plugins/help/_utils.py
@@ -0,0 +1,242 @@
+import os
+import random
+from typing import Dict
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH, TEMPLATE_PATH
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.utils.enum import BlockType, PluginType
+from zhenxun.utils.image_utils import BuildImage, build_sort_image, group_image
+
+from ._config import Item
+
+GROUP_HELP_PATH = DATA_PATH / "group_help"
+GROUP_HELP_PATH.mkdir(exist_ok=True, parents=True)
+for f in os.listdir(GROUP_HELP_PATH):
+ group_help_image = GROUP_HELP_PATH / f
+ group_help_image.unlink()
+
+BACKGROUND_PATH = IMAGE_PATH / "background" / "help" / "simple_help"
+
+LOGO_PATH = TEMPLATE_PATH / "menu" / "res" / "logo"
+
+
+class HelpImageBuild:
+ def __init__(self):
+ self._data: list[PluginInfo] = []
+ self._sort_data: Dict[str, list[PluginInfo]] = {}
+ self._image_list = []
+ self.icon2str = {
+ "normal": "fa fa-cog",
+ "原神相关": "fa fa-circle-o",
+ "常规插件": "fa fa-cubes",
+ "联系管理员": "fa fa-envelope-o",
+ "抽卡相关": "fa fa-credit-card-alt",
+ "来点好康的": "fa fa-picture-o",
+ "数据统计": "fa fa-bar-chart",
+ "一些工具": "fa fa-shopping-cart",
+ "商店": "fa fa-shopping-cart",
+ "其它": "fa fa-tags",
+ "群内小游戏": "fa fa-gamepad",
+ }
+
+ async def sort_type(self):
+ """
+ 对插件按照菜单类型分类
+ """
+ if not self._data:
+ self._data = await PluginInfo.filter(plugin_type=PluginType.NORMAL)
+ if not self._sort_data:
+ for plugin in self._data:
+ menu_type = plugin.menu_type or "normal"
+ if not self._sort_data.get(menu_type):
+ self._sort_data[menu_type] = []
+ self._sort_data[menu_type].append(plugin)
+
+ async def build_image(self, group_id: int | None):
+ if group_id:
+ help_image = GROUP_HELP_PATH / f"{group_id}.png"
+ else:
+ help_image = IMAGE_PATH / f"SIMPLE_HELP.png"
+ build_type = Config.get_config("help", "TYPE")
+ if build_type == "HTML":
+ byt = await self.build_html_image(group_id)
+ with open(help_image, "wb") as f:
+ f.write(byt)
+ else:
+ img = await self.build_pil_image(group_id)
+ await img.save(help_image)
+
+ async def build_html_image(self, group_id: int | None) -> bytes:
+ from nonebot_plugin_htmlrender import template_to_pic
+
+ await self.sort_type()
+ classify = {}
+ for menu in self._sort_data:
+ for plugin in self._sort_data[menu]:
+ sta = 0
+ if not plugin.status:
+ if group_id and plugin.block_type in [
+ BlockType.ALL,
+ BlockType.GROUP,
+ ]:
+ sta = 2
+ if not group_id and plugin.block_type in [
+ BlockType.ALL,
+ BlockType.FRIEND,
+ ]:
+ sta = 2
+ if group_id and (
+ group := await GroupConsole.get_or_none(group_id=group_id)
+ ):
+ if f"{plugin.module}:super," in group.block_plugin:
+ sta = 2
+ if f"{plugin.module}," in group.block_plugin:
+ sta = 1
+ if classify.get(menu):
+ classify[menu].append(Item(plugin_name=plugin.name, sta=sta))
+ else:
+ classify[menu] = [Item(plugin_name=plugin.name, sta=sta)]
+ max_len = 0
+ flag_index = -1
+ max_data = None
+ plugin_list = []
+ for index, plu in enumerate(classify.keys()):
+ if plu in self.icon2str.keys():
+ icon = self.icon2str[plu]
+ else:
+ icon = "fa fa-pencil-square-o"
+ logo = LOGO_PATH / random.choice(os.listdir(LOGO_PATH))
+ data = {
+ "name": plu if plu != "normal" else "功能",
+ "items": classify[plu],
+ "icon": icon,
+ "logo": str(logo.absolute()),
+ }
+ if len(classify[plu]) > max_len:
+ max_len = len(classify[plu])
+ flag_index = index
+ max_data = data
+ plugin_list.append(data)
+ del plugin_list[flag_index]
+ plugin_list.insert(0, max_data)
+ pic = await template_to_pic(
+ template_path=str((TEMPLATE_PATH / "menu").absolute()),
+ template_name="zhenxun_menu.html",
+ templates={"plugin_list": plugin_list},
+ pages={
+ "viewport": {"width": 1903, "height": 975},
+ "base_url": f"file://{TEMPLATE_PATH}",
+ },
+ wait=2,
+ )
+ return pic
+
+ async def build_pil_image(self, group_id: int | None) -> BuildImage:
+ """构造帮助图片
+
+ 参数:
+ group_id: 群号
+ """
+ self._image_list = []
+ await self.sort_type()
+ font_size = 24
+ build_type = Config.get_config("help", "TYPE")
+ _image = BuildImage.build_text_image("1", size=font_size)
+ font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
+ for idx, menu_type in enumerate(self._sort_data.keys()):
+ plugin_list = self._sort_data[menu_type]
+ wh_list = [BuildImage.get_text_size(x.name, font) for x in plugin_list]
+ wh_list.append(BuildImage.get_text_size(menu_type, font))
+ # sum_height = sum([x[1] for x in wh_list])
+ if build_type == "VV":
+ sum_height = 50 * len(plugin_list) + 10
+ else:
+ sum_height = (font_size + 6) * len(plugin_list) + 10
+ max_width = max([x[0] for x in wh_list]) + 20
+ bk = BuildImage(
+ max_width + 40,
+ sum_height + 50,
+ font_size=30,
+ color="#a7d1fc",
+ font="CJGaoDeGuo.otf",
+ )
+ title_size = bk.getsize(menu_type)
+ max_width = max_width if max_width > title_size[0] else title_size[0]
+ B = BuildImage(
+ max_width + 40,
+ sum_height,
+ font_size=font_size,
+ color="white" if not idx % 2 else "black",
+ )
+ curr_h = 10
+ if group := await GroupConsole.get_or_none(group_id=group_id):
+ for i, plugin in enumerate(plugin_list):
+ text_color = (255, 255, 255) if idx % 2 else (0, 0, 0)
+ if f"{plugin.module}," in group.block_plugin:
+ text_color = (252, 75, 13)
+ pos = None
+ # 禁用状态划线
+ if (
+ plugin.block_type in [BlockType.ALL, BlockType.GROUP]
+ or f"{plugin.module}:super," in group.block_plugin
+ ):
+ w = curr_h + int(B.getsize(plugin.name)[1] / 2) + 2
+ pos = (
+ 7,
+ w,
+ B.getsize(plugin.name)[0] + 35,
+ w,
+ )
+ if build_type == "VV":
+ name_image = await self.build_name_image( # type: ignore
+ max_width,
+ plugin.name,
+ "black" if not idx % 2 else "white",
+ text_color,
+ pos,
+ )
+ await B.paste(name_image, (0, curr_h), center_type="width")
+ curr_h += name_image.h + 5
+ else:
+ await B.text((10, curr_h), f"{i + 1}.{plugin.name}", text_color)
+ if pos:
+ await B.line(pos, (236, 66, 7), 3)
+ curr_h += font_size + 5
+ if menu_type == "normal":
+ menu_type = "功能"
+ await bk.text((0, 14), menu_type, center_type="width")
+ await bk.paste(B, (0, 50))
+ await bk.transparent(2)
+ # await bk.acircle_corner(point_list=['lt', 'rt'])
+ self._image_list.append(bk)
+ image_group, h = group_image(self._image_list)
+ B = await build_sort_image(
+ image_group,
+ h,
+ background_path=BACKGROUND_PATH,
+ background_handle=lambda image: image.filter("GaussianBlur", 5),
+ )
+ w = 10
+ h = 10
+ for msg in [
+ "目前支持的功能列表:",
+ "可以通过 ‘帮助[功能名称]’ 来获取对应功能的使用方法",
+ ]:
+ text = await BuildImage.build_text_image(msg, "HYWenHei-85W.ttf", 24)
+ await B.paste(text, (w, h))
+ h += 50
+ if msg == "目前支持的功能列表:":
+ w += 50
+ text = await BuildImage.build_text_image(
+ "注: 红字代表功能被群管理员禁用,红线代表功能正在维护",
+ "HYWenHei-85W.ttf",
+ 24,
+ (231, 74, 57),
+ )
+ await B.paste(
+ text,
+ (300, 10),
+ )
+ return B
diff --git a/zhenxun/builtin_plugins/hooks/__init__.py b/zhenxun/builtin_plugins/hooks/__init__.py
new file mode 100644
index 00000000..80aa7181
--- /dev/null
+++ b/zhenxun/builtin_plugins/hooks/__init__.py
@@ -0,0 +1,43 @@
+from pathlib import Path
+
+import nonebot
+
+from zhenxun.configs.config import Config
+
+Config.add_plugin_config(
+ "hook",
+ "CHECK_NOTICE_INFO_CD",
+ 300,
+ help="群检测,个人权限检测等各种检测提示信息cd",
+ default_value=300,
+ type=int,
+)
+
+Config.add_plugin_config(
+ "hook",
+ "MALICIOUS_BAN_TIME",
+ 30,
+ help="恶意命令触发检测触发后ban的时长(分钟)",
+ default_value=30,
+ type=int,
+)
+
+Config.add_plugin_config(
+ "hook",
+ "MALICIOUS_CHECK_TIME",
+ 5,
+ help="恶意命令触发检测规定时间内(秒)",
+ default_value=5,
+ type=int,
+)
+
+Config.add_plugin_config(
+ "hook",
+ "MALICIOUS_BAN_COUNT",
+ 6,
+ help="恶意命令触发检测最大触发次数",
+ default_value=6,
+ type=int,
+)
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
diff --git a/zhenxun/builtin_plugins/hooks/ban_hook.py b/zhenxun/builtin_plugins/hooks/ban_hook.py
new file mode 100644
index 00000000..3f10e078
--- /dev/null
+++ b/zhenxun/builtin_plugins/hooks/ban_hook.py
@@ -0,0 +1,61 @@
+from nonebot.adapters import Bot, Event
+from nonebot.exception import IgnoredException
+from nonebot.matcher import Matcher
+from nonebot.message import run_preprocessor
+from nonebot.typing import T_State
+from nonebot_plugin_saa import Mention, MessageFactory, Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.models.ban_console import BanConsole
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.utils import FreqLimiter
+
+Config.add_plugin_config(
+ "hook",
+ "BAN_RESULT",
+ "才不会给你发消息.",
+ help="对被ban用户发送的消息",
+)
+
+_flmt = FreqLimiter(300)
+
+
+# 检查是否被ban
+@run_preprocessor
+async def _(
+ matcher: Matcher, bot: Bot, event: Event, state: T_State, session: EventSession
+):
+ if plugin := matcher.plugin:
+ if metadata := plugin.metadata:
+ extra = metadata.extra
+ if extra.get("plugin_type") == PluginType.HIDDEN:
+ return
+ user_id = session.id1
+ group_id = session.id3 or session.id2
+ if user_id:
+ ban_result = Config.get_config("hook", "BAN_RESULT")
+ if user_id in bot.config.superusers:
+ return
+ if await BanConsole.is_ban(user_id) or await BanConsole.is_ban(
+ user_id, group_id
+ ):
+ time = await BanConsole.check_ban_time(user_id)
+ if time == -1:
+ time_str = "∞"
+ else:
+ time = abs(int(time))
+ if time < 60:
+ time_str = str(time) + " 秒"
+ else:
+ time_str = str(int(time / 60)) + " 分钟"
+ if ban_result and _flmt.check(user_id):
+ _flmt.start_cd(user_id)
+ await MessageFactory(
+ [
+ Mention(user_id),
+ Text(f"{ban_result}\n在..在 {time_str} 后才会理你喔"),
+ ]
+ ).send()
+ raise IgnoredException("用户处于黑名单中")
diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py
new file mode 100644
index 00000000..fffc1b80
--- /dev/null
+++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py
@@ -0,0 +1,104 @@
+import time
+from collections import defaultdict
+
+from click import command
+from nonebot.adapters.onebot.v11 import ActionFailed, Bot, GroupMessageEvent
+from nonebot.exception import IgnoredException
+from nonebot.matcher import Matcher
+from nonebot.message import run_preprocessor
+from nonebot.typing import T_State
+from nonebot_plugin_alconna import Arparma
+from nonebot_plugin_saa import Mention, MessageFactory, Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.models.ban_console import BanConsole
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+
+malicious_check_time = Config.get_config("hook", "MALICIOUS_CHECK_TIME")
+malicious_ban_count = Config.get_config("hook", "MALICIOUS_BAN_COUNT")
+
+if not malicious_check_time:
+ raise ValueError("模块: [hook], 配置项: [MALICIOUS_CHECK_TIME] 为空或小于0")
+if not malicious_ban_count:
+ raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_COUNT] 为空或小于0")
+
+
+class BanCheckLimiter:
+ """
+ 恶意命令触发检测
+ """
+
+ def __init__(self, default_check_time: float = 5, default_count: int = 4):
+ self.mint = defaultdict(int)
+ self.mtime = defaultdict(float)
+ self.default_check_time = default_check_time
+ self.default_count = default_count
+
+ def add(self, key: str | int | float):
+ if self.mint[key] == 1:
+ self.mtime[key] = time.time()
+ self.mint[key] += 1
+
+ def check(self, key: str | int | float) -> bool:
+ if time.time() - self.mtime[key] > self.default_check_time:
+ self.mtime[key] = time.time()
+ self.mint[key] = 0
+ return False
+ if (
+ self.mint[key] >= self.default_count
+ and time.time() - self.mtime[key] < self.default_check_time
+ ):
+ self.mtime[key] = time.time()
+ self.mint[key] = 0
+ return True
+ return False
+
+
+_blmt = BanCheckLimiter(
+ malicious_check_time,
+ malicious_ban_count,
+)
+
+
+# 恶意触发命令检测
+@run_preprocessor
+async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State):
+ if plugin := matcher.plugin:
+ if metadata := plugin.metadata:
+ extra = metadata.extra
+ if extra.get("plugin_type") == PluginType.HIDDEN:
+ return
+ user_id = session.id1
+ group_id = session.id3 or session.id2
+ malicious_ban_time = Config.get_config("hook", "MALICIOUS_BAN_TIME")
+ if not malicious_ban_time:
+ raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0")
+ if user_id:
+ command = state["_prefix"]["raw_command"]
+ if state["_alc_result"]:
+ command = state["_alc_result"].source.command
+ if command:
+ if _blmt.check(f"{user_id}__{command}"):
+ await BanConsole.ban(
+ user_id, group_id, 9, malicious_ban_time * 60, bot.self_id
+ )
+ logger.info(
+ f"触发了恶意触发检测: {matcher.plugin_name}",
+ "HOOK",
+ session=session,
+ )
+ await MessageFactory(
+ [
+ Mention(user_id),
+ Text(f"检测到恶意触发命令,您将被封禁 30 分钟"),
+ ]
+ ).send()
+ logger.debug(
+ f"触发了恶意触发检测: {matcher.plugin_name}",
+ "HOOK",
+ session=session,
+ )
+ raise IgnoredException("检测到恶意触发命令")
+ _blmt.add(f"{user_id}__{command}")
diff --git a/zhenxun/builtin_plugins/hooks/withdraw_hook.py b/zhenxun/builtin_plugins/hooks/withdraw_hook.py
new file mode 100644
index 00000000..eab5267a
--- /dev/null
+++ b/zhenxun/builtin_plugins/hooks/withdraw_hook.py
@@ -0,0 +1,46 @@
+import asyncio
+from typing import Optional
+
+from nonebot.adapters import Bot
+from nonebot.adapters.discord import Bot as DiscordBot
+from nonebot.adapters.dodo import Bot as DodoBot
+from nonebot.adapters.kaiheila import Bot as KaiheilaBot
+from nonebot.adapters.onebot.v11 import Bot as v11Bot
+from nonebot.adapters.onebot.v12 import Bot as v12Bot
+from nonebot.matcher import Matcher
+from nonebot.message import run_postprocessor
+
+from zhenxun.services.log import logger
+from zhenxun.utils.utils import WithdrawManager
+
+# TODO: 其他平台撤回消息
+
+
+# 消息撤回
+@run_postprocessor
+async def _(
+ matcher: Matcher,
+ exception: Optional[Exception],
+ bot: Bot,
+):
+ tasks = []
+ for message_id in WithdrawManager._data:
+ second = WithdrawManager._data[message_id]
+ tasks.append(asyncio.ensure_future(_withdraw_message(bot, message_id, second)))
+ WithdrawManager.remove(message_id)
+ await asyncio.gather(*tasks)
+
+
+async def _withdraw_message(bot: Bot, message_id: str, time: int):
+ await asyncio.sleep(time)
+ logger.debug(f"撤回消息ID: {message_id}", "HOOK")
+ if isinstance(bot, v11Bot):
+ await bot.delete_msg(message_id=int(message_id))
+ elif isinstance(bot, v12Bot):
+ await bot.delete_message(message_id=message_id)
+ elif isinstance(bot, DodoBot):
+ pass
+ elif isinstance(bot, KaiheilaBot):
+ pass
+ elif isinstance(bot, DiscordBot):
+ pass
diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py
index 2a19cebc..e80fc8f6 100644
--- a/zhenxun/builtin_plugins/init/init_plugin.py
+++ b/zhenxun/builtin_plugins/init/init_plugin.py
@@ -1,26 +1,15 @@
-from pathlib import Path
-from typing import List
-
import nonebot
from nonebot import get_loaded_plugins
from nonebot.drivers import Driver
from nonebot.plugin import Plugin
-from ruamel import yaml
-from ruamel.yaml import YAML, round_trip_dump, round_trip_load
-from ruamel.yaml.comments import CommentedMap
+from ruamel.yaml import YAML
-from zhenxun.configs.config import Config
-from zhenxun.configs.path_config import DATA_PATH
-from zhenxun.configs.utils import (
- BaseBlock,
- PluginExtraData,
- PluginSetting,
- RegisterConfig,
-)
+from zhenxun.configs.utils import PluginExtraData, PluginSetting
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.plugin_limit import PluginLimit
+from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger
-from zhenxun.utils.enum import PluginLimitType
+from zhenxun.utils.enum import PluginType
_yaml = YAML(pure=True)
_yaml.allow_unicode = True
@@ -29,8 +18,11 @@ _yaml.indent = 2
driver: Driver = nonebot.get_driver()
-def _handle_setting(
- plugin: Plugin, plugin_list: List[PluginInfo], limit_list: List[PluginLimit]
+async def _handle_setting(
+ plugin: Plugin,
+ plugin_list: list[PluginInfo],
+ limit_list: list[PluginLimit],
+ task_list: list[TaskInfo],
):
"""处理插件设置
@@ -45,6 +37,8 @@ def _handle_setting(
extra_data = PluginExtraData(**extra)
logger.debug(f"{metadata.name}:{plugin.name} -> {extra}", "初始化插件数据")
setting = extra_data.setting or PluginSetting()
+ if metadata.type == "library":
+ extra_data.plugin_type = PluginType.HIDDEN
plugin_list.append(
PluginInfo(
module=plugin.name,
@@ -76,6 +70,16 @@ def _handle_setting(
max_count=getattr(limit, "max_count", None),
)
)
+ if extra_data.tasks:
+ for task in extra_data.tasks:
+ task_list.append(
+ TaskInfo(
+ module=task.module,
+ name=task.name,
+ status=task.status,
+ run_time=task.run_time,
+ )
+ )
@driver.on_startup
@@ -83,14 +87,15 @@ async def _():
"""
初始化插件数据配置
"""
- plugin_list: List[PluginInfo] = []
- limit_list: List[PluginLimit] = []
+ plugin_list: list[PluginInfo] = []
+ limit_list: list[PluginLimit] = []
+ task_list: list[TaskInfo] = []
module2id = {}
if module_list := await PluginInfo.all().values("id", "module_path"):
module2id = {m["module_path"]: m["id"] for m in module_list}
for plugin in get_loaded_plugins():
if plugin.metadata:
- _handle_setting(plugin, plugin_list, limit_list)
+ await _handle_setting(plugin, plugin_list, limit_list, task_list)
create_list = []
update_list = []
for plugin in plugin_list:
@@ -124,3 +129,23 @@ async def _():
limit_create.append(limit)
if limit_create:
await PluginLimit.bulk_create(limit_create, 10)
+ if task_list:
+ module_dict = {
+ t[1]: t[0] for t in await TaskInfo.all().values_list("id", "module")
+ }
+ create_list = []
+ update_list = []
+ for task in task_list:
+ if task.module not in module_list:
+ create_list.append(task)
+ else:
+ task.id = module_dict[task.module]
+ update_list.append(task)
+ if create_list:
+ await TaskInfo.bulk_create(create_list, 10)
+ if update_list:
+ await TaskInfo.bulk_update(
+ update_list,
+ ["run_time", "status", "name"],
+ 10,
+ )
diff --git a/zhenxun/builtin_plugins/nickname.py b/zhenxun/builtin_plugins/nickname.py
new file mode 100644
index 00000000..d672db69
--- /dev/null
+++ b/zhenxun/builtin_plugins/nickname.py
@@ -0,0 +1,240 @@
+import random
+from typing import Any, List
+
+from nonebot import on_regex
+from nonebot.adapters import Bot
+from nonebot.matcher import Matcher
+from nonebot.params import Depends, RegexGroup
+from nonebot.plugin import PluginMetadata
+from nonebot.rule import to_me
+from nonebot_plugin_alconna import Alconna, Option, UniMsg, on_alconna, store_true
+from nonebot_plugin_saa import Text
+from nonebot_plugin_session import EventSession
+from nonebot_plugin_userinfo import EventUserInfo, UserInfo
+
+from zhenxun.configs.config import NICKNAME, Config
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.models.ban_console import BanConsole
+from zhenxun.models.friend_user import FriendUser
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+
+__plugin_meta__ = PluginMetadata(
+ name="昵称系统",
+ description="区区昵称,才不想叫呢!",
+ usage=f"""
+ 个人昵称,将替换{NICKNAME}称呼你的名称,群聊 与 私聊 昵称相互独立,全局昵称设置将更改您目前所有群聊中及私聊的昵称
+ 指令:
+ 以后叫我 [昵称]: 设置当前群聊/私聊的昵称
+ 全局昵称设置 [昵称]: 设置当前所有群聊和私聊的昵称
+ {NICKNAME}我是谁
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.NORMAL,
+ menu_type="商店",
+ configs=[
+ RegisterConfig(
+ key="BLACK_WORD",
+ value=["爸", "爹", "爷", "父"],
+ help="昵称所屏蔽的关键词,已设置的昵称会被替换为 *,未设置的昵称会在设置时提示",
+ default_value=None,
+ type=List[str],
+ )
+ ],
+ ).dict(),
+)
+
+_nickname_matcher = on_regex(
+ "(?:以后)?(?:叫我|请叫我|称呼我)(.*)",
+ rule=to_me(),
+ priority=5,
+ block=True,
+)
+
+_global_nickname_matcher = on_regex(
+ "设置全局昵称(.*)", rule=to_me(), priority=5, block=True
+)
+
+_matcher = on_alconna(
+ Alconna(
+ "nickname",
+ Option("--name", action=store_true, help_text="用户昵称"),
+ Option("--cancel", action=store_true, help_text="取消昵称"),
+ ),
+ rule=to_me(),
+ priority=5,
+ block=True,
+)
+
+
+CALL_NAME = [
+ "好啦好啦,我知道啦,{},以后就这么叫你吧",
+ f"嗯嗯,{NICKNAME}" + "记住你的昵称了哦,{}",
+ "好突然,突然要叫你昵称什么的...{}..",
+ f"{NICKNAME}" + "会好好记住{}的,放心吧",
+ "好..好.,那窝以后就叫你{}了.",
+]
+
+REMIND = [
+ "我肯定记得你啊,你是{}啊",
+ "我不会忘记你的,你也不要忘记我!{}",
+ f"哼哼,{NICKNAME}" + "记忆力可是很好的,{}",
+ "嗯?你是失忆了嘛...{}..",
+ f"不要小看{NICKNAME}" + "的记忆力啊!笨蛋{}!QAQ",
+ "哎?{}..怎么了吗..突然这样问..",
+]
+
+CANCEL = [
+ f"呜..{NICKNAME}" + "睡一觉就会忘记的..和梦一样..{}",
+ "窝知道了..{}..",
+ f"是{NICKNAME}" + "哪里做的不好嘛..好吧..晚安{}",
+ "呃,{},下次我绝对绝对绝对不会再忘记你!",
+ "可..可恶!{}!太可恶了!呜",
+]
+
+
+def CheckNickname():
+ """
+ 检查名称是否合法
+ """
+
+ async def dependency(
+ bot: Bot,
+ matcher: Matcher,
+ session: EventSession,
+ message: UniMsg,
+ reg_group: tuple[Any, ...] = RegexGroup(),
+ ):
+ black_word = Config.get_config("nickname", "BLACK_WORD")
+ (name,) = reg_group
+ logger.debug(f"昵称检查: {name}", "昵称设置", session=session)
+ if not name:
+ await Text("叫你空白?叫你虚空?叫你无名??").finish(at_sender=True)
+ if session.id1 in bot.config.superusers:
+ logger.debug(
+ f"超级用户设置昵称, 跳过合法检测: {name}", "昵称设置", session=session
+ )
+ return
+ if len(name) > 20:
+ await Text("昵称可不能超过20个字!").finish(at_sender=True)
+ if name in bot.config.nickname:
+ await Text("笨蛋!休想占用我的名字! #").finish(at_sender=True)
+ if black_word:
+ for x in name:
+ if x in black_word:
+ logger.debug("昵称设置禁止字符: [{x}]", "昵称设置", session=session)
+ await Text(f"字符 [{x}] 为禁止字符!").finish(at_sender=True)
+ for word in black_word:
+ if word in name:
+ logger.debug(
+ "昵称设置禁止字符: [{word}]", "昵称设置", session=session
+ )
+ await Text(f"字符 [{x}] 为禁止字符!").finish(at_sender=True)
+
+ return Depends(dependency)
+
+
+@_nickname_matcher.handle(parameterless=[CheckNickname()])
+async def _(
+ session: EventSession,
+ user_info: UserInfo = EventUserInfo(),
+ reg_group: tuple[Any, ...] = RegexGroup(),
+):
+ if session.id1:
+ (name,) = reg_group
+ if len(name) < 5:
+ if random.random() < 0.3:
+ name = "~".join(name)
+ if gid := session.id3 or session.id2:
+ await GroupInfoUser.set_user_nickname(
+ session.id1,
+ gid,
+ name,
+ user_info.user_displayname
+ or user_info.user_remark
+ or user_info.user_name,
+ session.platform,
+ )
+ logger.info(f"设置群昵称成功: {name}", "昵称设置", session=session)
+ await Text(random.choice(CALL_NAME).format(name)).finish(reply=True)
+ else:
+ await FriendUser.set_user_nickname(
+ session.id1,
+ name,
+ user_info.user_displayname
+ or user_info.user_remark
+ or user_info.user_name,
+ session.platform,
+ )
+ logger.info(f"设置私聊昵称成功: {name}", "昵称设置", session=session)
+ await Text(random.choice(CALL_NAME).format(name)).finish(reply=True)
+ await Text("用户id为空...").send()
+
+
+@_global_nickname_matcher.handle(parameterless=[CheckNickname()])
+async def _(
+ session: EventSession,
+ user_info: UserInfo = EventUserInfo(),
+ reg_group: tuple[Any, ...] = RegexGroup(),
+):
+ if session.id1:
+ (name,) = reg_group
+ await FriendUser.set_user_nickname(
+ session.id1,
+ name,
+ user_info.user_displayname or user_info.user_remark or user_info.user_name,
+ session.platform,
+ )
+ await GroupInfoUser.filter(user_id=session.id1).update(nickname=name)
+ logger.info(f"设置全局昵称成功: {name}", "设置全局昵称", session=session)
+ await Text(random.choice(CALL_NAME).format(name)).finish(reply=True)
+ await Text("用户id为空...").send()
+
+
+@_matcher.assign("name")
+async def _(session: EventSession, user_info: UserInfo = EventUserInfo()):
+ if session.id1:
+ if gid := session.id3 or session.id2:
+ nickname = await GroupInfoUser.get_user_nickname(session.id1, gid)
+ card = user_info.user_displayname or user_info.user_name
+ else:
+ nickname = await FriendUser.get_user_nickname(session.id1)
+ card = user_info.user_name
+ if nickname:
+ await Text(random.choice(REMIND).format(nickname)).finish(reply=True)
+ else:
+ await Text(
+ random.choice(
+ [
+ "没..没有昵称嘛,{}",
+ "啊,你是{}啊,我想叫你的昵称!",
+ "是{}啊,有什么事吗?",
+ "你是{}?",
+ ]
+ ).format(card)
+ ).finish(reply=True)
+ await Text("用户id为空...").send()
+
+
+@_matcher.assign("cancel")
+async def _(bot: Bot, session: EventSession, user_info: UserInfo = EventUserInfo()):
+ if session.id1:
+ gid = session.id3 or session.id2
+ if gid:
+ nickname = await GroupInfoUser.get_user_nickname(session.id1, gid)
+ else:
+ nickname = await FriendUser.get_user_nickname(session.id1)
+ if nickname:
+ await Text(random.choice(CANCEL).format(nickname)).send(reply=True)
+ if gid:
+ await GroupInfoUser.set_user_nickname(session.id1, gid, "")
+ else:
+ await FriendUser.set_user_nickname(session.id1, "")
+ await BanConsole.ban(session.id1, gid, 9, 60, bot.self_id)
+ return
+ else:
+ await Text("你在做梦吗?你没有昵称啊").finish(reply=True)
+ await Text("用户id为空...").send()
diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py
new file mode 100644
index 00000000..141b58bc
--- /dev/null
+++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py
@@ -0,0 +1,297 @@
+import os
+import random
+import re
+from datetime import datetime
+
+import nonebot
+import ujson as json
+from nonebot import on_notice, on_request
+from nonebot.adapters import Bot
+from nonebot.adapters.onebot.v11 import (
+ GroupDecreaseNoticeEvent,
+ GroupIncreaseNoticeEvent,
+)
+from nonebot.adapters.onebot.v12 import (
+ GroupMemberDecreaseEvent,
+ GroupMemberIncreaseEvent,
+)
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_saa import Image, Mention, MessageFactory, Text
+
+from zhenxun.configs.config import NICKNAME, Config
+from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
+from zhenxun.models.fg_request import FgRequest
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.models.level_user import LevelUser
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType
+from zhenxun.utils.utils import FreqLimiter
+
+__plugin_meta__ = PluginMetadata(
+ name="QQ群事件处理",
+ description="群事件处理",
+ usage="",
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.HIDDEN,
+ configs=[
+ RegisterConfig(
+ module="invite_manager",
+ key="message",
+ value=f"请不要未经同意就拉{NICKNAME}入群!告辞!",
+ help="强制拉群后进群回复的内容",
+ ),
+ RegisterConfig(
+ module="invite_manager",
+ key="flag",
+ value=True,
+ help="强制拉群后进群回复的内容",
+ default_value=True,
+ type=bool,
+ ),
+ RegisterConfig(
+ module="invite_manager",
+ key="welcome_msg_cd",
+ value=5,
+ help="群欢迎消息cd",
+ default_value=5,
+ type=int,
+ ),
+ RegisterConfig(
+ module="_task",
+ key="DEFAULT_GROUP_WELCOME",
+ value=True,
+ help="被动 进群欢迎 进群默认开关状态",
+ default_value=True,
+ type=bool,
+ ),
+ RegisterConfig(
+ module="_task",
+ key="DEFAULT_REFUND_GROUP_REMIND",
+ value=True,
+ help="被动 退群提醒 进群默认开关状态",
+ default_value=True,
+ type=bool,
+ ),
+ ],
+ tasks=[
+ Task(module="group_welcome", name="进群欢迎"),
+ Task(module="refund_group_remind", name="退群提醒"),
+ ],
+ ).dict(),
+)
+
+
+superuser = nonebot.get_driver().config.platform_superusers["qq"][0]
+
+base_config = Config.get("invite_manager")
+
+
+limit_cd = base_config.get("welcome_msg_cd")
+
+_flmt = FreqLimiter(limit_cd)
+
+
+group_increase_handle = on_notice(priority=1, block=False)
+"""群员增加处理"""
+group_decrease_handle = on_notice(priority=1, block=False)
+"""群员减少处理"""
+add_group = on_request(priority=1, block=False)
+"""加群同意请求"""
+
+
+@group_increase_handle.handle()
+async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent):
+ user_id = str(event.user_id)
+ group_id = str(event.group_id)
+ if user_id == bot.self_id:
+ """新成员为bot本身"""
+ group = await GroupConsole.get_or_none(group_id=group_id)
+ if (not group or group.group_flag == 0) and base_config.get("flag"):
+ """群聊不存在或被强制拉群,退出该群"""
+ try:
+ if result_msg := base_config.get("message"):
+ await bot.send_group_msg(
+ group_id=event.group_id, message=result_msg
+ )
+ await bot.set_group_leave(group_id=event.group_id)
+ await bot.send_private_msg(
+ user_id=int(superuser),
+ message=f"触发强制入群保护,已成功退出群聊 {group_id}...",
+ )
+ logger.info(
+ f"强制拉群或未有群信息,退出群聊成功",
+ "入群检测",
+ group_id=event.group_id,
+ )
+ if req := await FgRequest.get_or_none(group_id=group_id):
+ req.handle_type = RequestHandleType.IGNORE
+ await req.save(update_fields=["handle_type"])
+ except Exception as e:
+ logger.error(
+ f"强制拉群或未有群信息,退出群聊失败",
+ "入群检测",
+ group_id=event.group_id,
+ e=e,
+ )
+ await bot.send_private_msg(
+ user_id=int(superuser),
+ message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...",
+ )
+ elif group_id not in await GroupConsole.all().values_list(
+ "group_id", flat=True
+ ):
+ """默认群功能开关"""
+ block_plugin = ""
+ if plugin_list := await PluginInfo.filter(default_status=False).all():
+ for plugin in plugin_list:
+ block_plugin += f"{plugin.module},"
+ group_info = await bot.get_group_info(group_id=event.group_id)
+ await GroupConsole.create(
+ group_id=group_info["group_id"],
+ group_name=group_info["group_name"],
+ max_member_count=group_info["max_member_count"],
+ member_count=group_info["member_count"],
+ group_flag=1,
+ block_plugin=block_plugin,
+ platform="qq",
+ )
+ admin_default_auth = Config.get_config(
+ "admin_bot_manage", "ADMIN_DEFAULT_AUTH"
+ )
+ # 即刻刷新权限
+ for user_info in await bot.get_group_member_list(group_id=event.group_id):
+ """即刻刷新权限"""
+ if (
+ user_info["role"]
+ in [
+ "owner",
+ "admin",
+ ]
+ and not await LevelUser.is_group_flag(
+ user_info["user_id"], group_id
+ )
+ and admin_default_auth is not None
+ ):
+ await LevelUser.set_level(
+ user_info["user_id"],
+ user_info["group_id"],
+ admin_default_auth,
+ )
+ logger.debug(
+ f"添加默认群管理员权限: {admin_default_auth}",
+ "入群检测",
+ session=user_info["user_id"],
+ group_id=user_info["group_id"],
+ )
+ if str(user_info["user_id"]) in bot.config.superusers:
+ await LevelUser.set_level(
+ user_info["user_id"], user_info["group_id"], 9
+ )
+ logger.debug(
+ f"添加超级用户权限: 9",
+ "入群检测",
+ session=user_info["user_id"],
+ group_id=user_info["group_id"],
+ )
+ else:
+ join_time = datetime.now()
+ user_info = await bot.get_group_member_info(
+ group_id=event.group_id, user_id=event.user_id
+ )
+ await GroupInfoUser.update_or_create(
+ user_id=str(user_info["user_id"]),
+ group_id=str(user_info["group_id"]),
+ defaults={"user_name": user_info["nickname"], "user_join_time": join_time},
+ )
+ logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新成功")
+
+ if _flmt.check(group_id):
+ """群欢迎消息"""
+ _flmt.start_cd(group_id)
+ path = DATA_PATH / "welcome_message" / "qq" / f"{group_id}"
+ data = json.load((path / "text.json").open())
+ message = data["message"]
+ msg_split = re.split(r"\[image:\d+\]", message)
+ msg_list = []
+ if data["at"]:
+ msg_list.append(Mention(user_id))
+ for i, text in enumerate(msg_split):
+ msg_list.append(Text(text))
+ img_file = path / f"{i}.png"
+ if img_file.exists():
+ msg_list.append(Image(img_file))
+ if GroupConsole.is_block_task(group_id, "group_welcome"):
+ logger.info(f"发送群欢迎消息...", "入群检测", group_id=group_id)
+ if msg_list:
+ await MessageFactory(msg_list).send()
+ else:
+ await MessageFactory(
+ [
+ Text("新人快跑啊!!本群现状↓(快使用自定义!)"),
+ Image(
+ IMAGE_PATH
+ / "qxz"
+ / random.choice(os.listdir(IMAGE_PATH / "qxz"))
+ ),
+ ]
+ ).send()
+
+
+@group_decrease_handle.handle()
+async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent):
+ if event.sub_type == "kick_me":
+ """踢出Bot"""
+ group_id = event.group_id
+ operator_id = event.operator_id
+ if user := await GroupInfoUser.get_or_none(
+ user_id=str(event.operator_id), group_id=str(event.group_id)
+ ):
+ operator_name = user.user_name
+ else:
+ operator_name = "None"
+ group = await GroupConsole.filter(group_id=str(group_id)).first()
+ group_name = group.group_name if group else ""
+ coffee = int(list(bot.config.superusers)[0])
+ await bot.send_private_msg(
+ user_id=coffee,
+ message=f"****呜..一份踢出报告****\n"
+ f"我被 {operator_name}({operator_id})\n"
+ f"踢出了 {group_name}({group_id})\n"
+ f"日期:{str(datetime.now()).split('.')[0]}",
+ )
+ return
+ if str(event.user_id) == bot.self_id:
+ """踢出Bot"""
+ await GroupConsole.filter(group_id=str(event.group_id)).delete()
+ return
+ if user := await GroupInfoUser.get_or_none(
+ user_id=str(event.user_id), group_id=str(event.group_id)
+ ):
+ user_name = user.user_name
+ else:
+ user_name = f"{event.user_id}"
+ await GroupInfoUser.filter(
+ user_id=str(event.user_id), group_id=str(event.group_id)
+ ).delete()
+ logger.info(
+ f"名称: {user_name} 退出群聊",
+ "group_decrease_handle",
+ session=event.user_id,
+ group_id=event.group_id,
+ )
+ result = ""
+ if event.sub_type == "leave":
+ result = f"{user_name}离开了我们..."
+ if event.sub_type == "kick":
+ operator = await bot.get_group_member_info(
+ user_id=event.operator_id, group_id=event.group_id
+ )
+ operator_name = operator["card"] if operator["card"] else operator["nickname"]
+ result = f"{user_name} 被 {operator_name} 送走了."
+ if GroupConsole.is_block_task(str(event.group_id), "refund_group_remind"):
+ await group_decrease_handle.send(f"{result}")
diff --git a/zhenxun/builtin_plugins/record_request.py b/zhenxun/builtin_plugins/record_request.py
index 2743e662..367031fc 100644
--- a/zhenxun/builtin_plugins/record_request.py
+++ b/zhenxun/builtin_plugins/record_request.py
@@ -2,7 +2,8 @@ import time
from datetime import datetime
from typing import Dict
-from nonebot import on_message, on_request
+import nonebot
+from nonebot import drivers, on_message, on_request
from nonebot.adapters.onebot.v11 import (
ActionFailed,
Bot,
@@ -18,7 +19,7 @@ from zhenxun.configs.config import NICKNAME, Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.models.fg_request import FgRequest
from zhenxun.models.friend_user import FriendUser
-from zhenxun.models.group_info import GroupInfo
+from zhenxun.models.group_console import GroupConsole
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType, RequestType
@@ -26,7 +27,7 @@ base_config = Config.get("invite_manager")
__plugin_meta__ = PluginMetadata(
name="记录请求",
- description="自定义群欢迎消息",
+ description="记录 好友/群组 请求",
usage="",
extra=PluginExtraData(
author="HibiKier",
@@ -61,6 +62,8 @@ class Timer:
cls.data = {k: v for k, v in cls.data.items() if v - now < 5 * 60}
+# TODO: 其他平台请求
+
friend_req = on_request(priority=5, block=True)
group_req = on_request(priority=5, block=True)
_t = on_message(priority=999, block=False, rule=lambda: False)
@@ -69,13 +72,14 @@ _t = on_message(priority=999, block=False, rule=lambda: False)
@friend_req.handle()
async def _(bot: Bot, event: FriendRequestEvent, session: EventSession):
if event.user_id and Timer.check(event.user_id):
+ superuser = nonebot.get_driver().config.platform_superusers["qq"][0]
logger.debug(f"收录好友请求...", "好友请求", target=event.user_id)
user = await bot.get_stranger_info(user_id=event.user_id)
nickname = user["nickname"]
# sex = user["sex"]
# age = str(user["age"])
comment = event.comment
- superuser = int(list(bot.config.superusers)[0])
+ superuser = int(superuser)
await Text(
f"*****一份好友申请*****\n"
f"昵称:{nickname}({event.user_id})\n"
@@ -84,7 +88,11 @@ async def _(bot: Bot, event: FriendRequestEvent, session: EventSession):
f"备注:{event.comment}"
).send_to(target=TargetQQPrivate(user_id=superuser), bot=bot)
if base_config.get("AUTO_ADD_FRIEND"):
- logger.debug(f"已开启好友请求自动同意,成功通过该请求", "好友请求", target=event.user_id)
+ logger.debug(
+ f"已开启好友请求自动同意,成功通过该请求",
+ "好友请求",
+ target=event.user_id,
+ )
await bot.set_friend_add_request(flag=event.flag, approve=True)
await FriendUser.create(
user_id=str(user["user_id"]), user_name=user["nickname"]
@@ -119,7 +127,7 @@ async def _(bot: Bot, event: GroupRequestEvent, session: EventSession):
flag=event.flag, sub_type="invite", approve=True
)
group_info = await bot.get_group_info(group_id=event.group_id)
- await GroupInfo.update_or_create(
+ await GroupConsole.update_or_create(
group_id=str(group_info["group_id"]),
defaults={
"group_name": group_info["group_name"],
diff --git a/zhenxun/builtin_plugins/scheduler/__init__.py b/zhenxun/builtin_plugins/scheduler/__init__.py
new file mode 100644
index 00000000..eb35e275
--- /dev/null
+++ b/zhenxun/builtin_plugins/scheduler/__init__.py
@@ -0,0 +1,5 @@
+from pathlib import Path
+
+import nonebot
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
diff --git a/zhenxun/builtin_plugins/scheduler/auto_backup.py b/zhenxun/builtin_plugins/scheduler/auto_backup.py
new file mode 100644
index 00000000..92f1119b
--- /dev/null
+++ b/zhenxun/builtin_plugins/scheduler/auto_backup.py
@@ -0,0 +1,62 @@
+import shutil
+from pathlib import Path
+
+from nonebot_plugin_apscheduler import scheduler
+
+from zhenxun.configs.config import Config
+from zhenxun.services.log import logger
+
+Config.add_plugin_config(
+ "_backup",
+ "BACKUP_FLAG",
+ True,
+ help="是否开启文件备份",
+ default_value=True,
+ type=bool,
+)
+
+Config.add_plugin_config(
+ "_backup",
+ "BACKUP_DIR_OR_FILE",
+ [
+ "data/black_word",
+ "data/configs",
+ "data/statistics",
+ "data/word_bank",
+ "data/manager",
+ "configs",
+ ],
+ help="备份的文件夹或文件",
+ default_value=[],
+ type=list[str],
+)
+
+
+# 自动备份
+@scheduler.scheduled_job(
+ "cron",
+ hour=3,
+ minute=25,
+)
+async def _():
+ if Config.get_config("_backup", "BACKUP_FLAG"):
+ _backup_path = Path() / "backup"
+ _backup_path.mkdir(exist_ok=True, parents=True)
+ if backup_dir_or_file := Config.get_config("_backup", "BACKUP_DIR_OR_FILE"):
+ for path_file in backup_dir_or_file:
+ try:
+ path = Path(path_file)
+ _p = _backup_path / path_file
+ if path.exists():
+ if path.is_dir():
+ if _p.exists():
+ shutil.rmtree(_p, ignore_errors=True)
+ shutil.copytree(path_file, _p)
+ else:
+ if _p.exists():
+ _p.unlink()
+ shutil.copy(path_file, _p)
+ logger.debug(f"已完成自动备份:{path_file}", "自动备份")
+ except Exception as e:
+ logger.error(f"自动备份文件 {path_file} 发生错误", "自动备份", e=e)
+ logger.info("自动备份成功...", "自动备份")
diff --git a/zhenxun/builtin_plugins/scheduler/auto_update_group.py b/zhenxun/builtin_plugins/scheduler/auto_update_group.py
new file mode 100644
index 00000000..1cfdacb3
--- /dev/null
+++ b/zhenxun/builtin_plugins/scheduler/auto_update_group.py
@@ -0,0 +1,68 @@
+import nonebot
+from nonebot_plugin_apscheduler import scheduler
+
+from zhenxun.models.friend_user import FriendUser
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.services.log import logger
+
+# TODO: 其他平台更新
+
+
+# 自动更新群组信息
+@scheduler.scheduled_job(
+ "cron",
+ hour=3,
+ minute=1,
+)
+async def _():
+ bots = nonebot.get_bots()
+ _used_group = []
+ for bot in bots.values():
+ try:
+ group_list = await bot.get_group_list()
+ gl = [g["group_id"] for g in group_list if g["group_id"] not in _used_group]
+ for g in gl:
+ _used_group.append(g)
+ group_info = await bot.get_group_info(group_id=g)
+ await GroupConsole.update_or_create(
+ group_id=str(group_info["group_id"]),
+ defaults={
+ "group_name": group_info["group_name"],
+ "max_member_count": group_info["max_member_count"],
+ "member_count": group_info["member_count"],
+ "group_flag": 1,
+ },
+ )
+ logger.debug("自动更新群组信息成功", "自动更新群组", group_id=g)
+ except Exception as e:
+ logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e)
+ logger.info("自动更新群组成员信息成功...")
+
+
+# 自动更新好友信息
+@scheduler.scheduled_job(
+ "cron",
+ hour=3,
+ minute=1,
+)
+async def _():
+ bots = nonebot.get_bots()
+ for key in bots:
+ try:
+ bot = bots[key]
+ fl = await bot.get_friend_list()
+ for f in fl:
+ if FriendUser.exists(user_id=str(f["user_id"])):
+ await FriendUser.create(
+ user_id=str(f["user_id"]), user_name=f["nickname"]
+ )
+ logger.debug(
+ f"更新好友信息成功", "自动更新好友", session=f["user_id"]
+ )
+ else:
+ logger.debug(
+ f"好友信息已存在", "自动更新好友", session=f["user_id"]
+ )
+ except Exception as e:
+ logger.error(f"自动更新好友信息错误", "自动更新好友", e=e)
+ logger.info("自动更新好友信息成功...")
diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py
new file mode 100644
index 00000000..62964462
--- /dev/null
+++ b/zhenxun/builtin_plugins/scheduler/morning.py
@@ -0,0 +1,28 @@
+from nonebot_plugin_apscheduler import scheduler
+
+# TODO: 消息发送
+
+# # 早上好
+# @scheduler.scheduled_job(
+# "cron",
+# hour=6,
+# minute=1,
+# )
+# async def _():
+# img = image(IMAGE_PATH / "zhenxun" / "zao.jpg")
+# await broadcast_group("[[_task|zwa]]早上好" + img, log_cmd="被动早晚安")
+# logger.info("每日早安发送...")
+
+
+# # 睡觉了
+# @scheduler.scheduled_job(
+# "cron",
+# hour=23,
+# minute=59,
+# )
+# async def _():
+# img = image(IMAGE_PATH / "zhenxun" / "sleep.jpg")
+# await broadcast_group(
+# f"[[_task|zwa]]{NICKNAME}要睡觉了,你们也要早点睡呀" + img, log_cmd="被动早晚安"
+# )
+# logger.info("每日晚安发送...")
diff --git a/zhenxun/builtin_plugins/scripts.py b/zhenxun/builtin_plugins/scripts.py
new file mode 100644
index 00000000..13589454
--- /dev/null
+++ b/zhenxun/builtin_plugins/scripts.py
@@ -0,0 +1,59 @@
+from asyncio.exceptions import TimeoutError
+
+import nonebot
+import ujson as json
+from nonebot.drivers import Driver
+from nonebot_plugin_apscheduler import scheduler
+
+from zhenxun.configs.path_config import TEXT_PATH
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+
+driver: Driver = nonebot.get_driver()
+
+
+@driver.on_startup
+async def update_city():
+ """
+ 部分插件需要中国省份城市
+ 这里直接更新,避免插件内代码重复
+ """
+ china_city = TEXT_PATH / "china_city.json"
+ data = {}
+ if not china_city.exists():
+ try:
+ logger.debug("开始更新城市列表...")
+ res = await AsyncHttpx.get(
+ "http://www.weather.com.cn/data/city3jdata/china.html", timeout=5
+ )
+ res.encoding = "utf8"
+ provinces_data = json.loads(res.text)
+ for province in provinces_data.keys():
+ data[provinces_data[province]] = []
+ res = await AsyncHttpx.get(
+ f"http://www.weather.com.cn/data/city3jdata/provshi/{province}.html",
+ timeout=5,
+ )
+ res.encoding = "utf8"
+ city_data = json.loads(res.text)
+ for city in city_data.keys():
+ data[provinces_data[province]].append(city_data[city])
+ with open(china_city, "w", encoding="utf8") as f:
+ json.dump(data, f, indent=4, ensure_ascii=False)
+ logger.info("自动更新城市列表完成...")
+ except TimeoutError as e:
+ logger.warning("自动更新城市列表超时...", e=e)
+ except ValueError as e:
+ logger.warning("自动城市列表失败...", e=e)
+ except Exception as e:
+ logger.error(f"自动城市列表未知错误...", e=e)
+
+
+# 自动更新城市列表
+@scheduler.scheduled_job(
+ "cron",
+ hour=6,
+ minute=1,
+)
+async def _():
+ await update_city()
diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py
new file mode 100644
index 00000000..9ea6a6cb
--- /dev/null
+++ b/zhenxun/builtin_plugins/shop/__init__.py
@@ -0,0 +1,102 @@
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
+from nonebot_plugin_saa import Image, Text
+from nonebot_plugin_session import EventSession
+from nonebot_plugin_userinfo import EventUserInfo, UserInfo
+
+from zhenxun.configs.utils import BaseBlock, PluginExtraData
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import BlockType, PluginType
+
+from ._data_source import ShopManage
+
+__plugin_meta__ = PluginMetadata(
+ name="商店",
+ description="商店系统[金币回收计划]",
+ usage="""
+ 商品操作
+ 指令:
+ 添加商品 name:[名称] price:[价格] des:[描述] ?discount:[折扣](小数) ?limit_time:[限时时间](小时)
+ 删除商品 [名称或序号]
+ 修改商品 name:[名称或序号] price:[价格] des:[描述] discount:[折扣] limit_time:[限时]
+ 示例:添加商品 name:萝莉酒杯 price:9999 des:普通的酒杯,但是里面.. discount:0.4 limit_time:90
+ 示例:添加商品 name:可疑的药 price:5 des:效果未知
+ 示例:删除商品 2
+ 示例:修改商品 name:1 price:900 修改序号为1的商品的价格为900
+ * 修改商品只需添加需要值即可 *
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.NORMAL,
+ menu_type="商店",
+ limits=[BaseBlock(check_type=BlockType.GROUP)],
+ ).dict(),
+)
+
+# TODO: 修改操作,shortcut
+
+_matcher = on_alconna(
+ Alconna(
+ "shop",
+ Subcommand("my-cost", help_text="我的金币"),
+ Subcommand("my-props", help_text="我的道具"),
+ Subcommand("buy", Args["name", str]["num", int, 1], help_text="购买道具"),
+ Subcommand("use", Args["name", str]["num?", int, 1], help_text="使用道具"),
+ ),
+ priority=5,
+ block=True,
+)
+
+
+@_matcher.assign("$main")
+async def _(session: EventSession, arparma: Arparma):
+ image = await ShopManage.build_shop_image()
+ logger.info("查看商店", arparma.header_result, session=session)
+ await Image(image.pic2bs4()).send()
+
+
+@_matcher.assign("my-cost")
+async def _(session: EventSession, arparma: Arparma):
+ if session.id1:
+ logger.info("查看金币", arparma.header_result, session=session)
+ gold = await ShopManage.my_cost(session.id1, session.platform)
+ await Text(f"你的当前余额: {gold}").send(reply=True)
+ else:
+ await Text(f"用户id为空...").send(reply=True)
+
+
+@_matcher.assign("my-props")
+async def _(
+ session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo()
+):
+ if session.id1:
+ logger.info("查看道具", arparma.header_result, session=session)
+ if image := await ShopManage.my_props(
+ session.id1,
+ user_info.user_displayname or user_info.user_name,
+ session.platform,
+ ):
+ await Image(image.pic2bs4()).finish(reply=True)
+ return await Text(f"你的道具为空捏...").send(reply=True)
+ else:
+ await Text(f"用户id为空...").send(reply=True)
+
+
+@_matcher.assign("buy")
+async def _(session: EventSession, arparma: Arparma, name: str, num: int):
+ if session.id1:
+ logger.info(
+ f"购买道具 {name}, 数量: {num}",
+ arparma.header_result,
+ session=session,
+ )
+ result = await ShopManage.buy_prop(session.id1, name, num, session.platform)
+ await Text(result).send(reply=True)
+ else:
+ await Text(f"用户id为空...").send(reply=True)
+
+
+@_matcher.assign("use")
+async def _(session: EventSession, arparma: Arparma, name: str, num: int):
+ pass
diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py
new file mode 100644
index 00000000..96bed3f0
--- /dev/null
+++ b/zhenxun/builtin_plugins/shop/_data_source.py
@@ -0,0 +1,319 @@
+import time
+from typing import Dict
+
+from zhenxun.configs.path_config import IMAGE_PATH
+from zhenxun.models.goods_info import GoodsInfo
+from zhenxun.models.user_console import UserConsole
+from zhenxun.models.user_gold_log import UserGoldLog
+from zhenxun.models.user_props_log import UserPropsLog
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import GoldHandle, PropHandle
+from zhenxun.utils.image_utils import BuildImage, ImageTemplate, text2image
+
+ICON_PATH = IMAGE_PATH / "shop_icon"
+
+
+class ShopManage:
+
+ @classmethod
+ async def buy_prop(
+ cls, user_id: str, name: str, num: int = 1, platform: str | None = None
+ ) -> str:
+ if name == "神秘药水":
+ return "你们看看就好啦,这是不可能卖给你们的~"
+ if num < 0:
+ return "购买的数量要大于0!"
+ goods_list = await GoodsInfo.annotate().order_by("-id").all()
+ goods_list = [
+ goods
+ for goods in goods_list
+ if goods.goods_limit_time > time.time() or goods.goods_limit_time == 0
+ ]
+ if name.isdigit():
+ goods = goods_list[int(name) - 1]
+ else:
+ if filter_goods := [g for g in goods_list if g.goods_name == name]:
+ goods = filter_goods[0]
+ else:
+ return "道具名称不存在..."
+ user, _ = await UserConsole.get_or_create(
+ user_id=user_id, defaults={"platform": platform}
+ )
+ price = goods.goods_price * num * goods.goods_discount
+ if user.gold < price:
+ return "糟糕! 您的金币好像不太够哦..."
+ count = await UserPropsLog.filter(
+ user_id=user_id, handle=PropHandle.BUY
+ ).count()
+ if goods.daily_limit and count >= goods.daily_limit:
+ return "今天的购买已达限制了喔!"
+ await UserGoldLog.create(user_id=user_id, gold=price, handle=GoldHandle.BUY)
+ await UserPropsLog.create(
+ user_id=user_id, uuid=goods.uuid, gold=price, num=num, handle=PropHandle.BUY
+ )
+ logger.info(
+ f"花费 {price} 金币购买 {goods.goods_name} ×{num} 成功!",
+ "购买道具",
+ session=user_id,
+ )
+ user.gold -= int(price)
+ if goods.uuid not in user.props:
+ user.props[goods.uuid] = 0
+ user.props[goods.uuid] += num
+ await user.save(update_fields=["gold", "props"])
+ return f"花费 {price} 金币购买 {goods.goods_name} ×{num} 成功!"
+
+ @classmethod
+ async def my_props(
+ cls, user_id: str, name: str, platform: str | None = None
+ ) -> BuildImage | None:
+ """获取道具背包
+
+ 参数:
+ user_id: 用户id
+ name: 用户昵称
+ platform: 平台.
+
+ 返回:
+ BuildImage | None: 道具背包图片
+ """
+ user, _ = await UserConsole.get_or_create(
+ user_id=user_id, defaults={"platform": platform}
+ )
+ if not user.props:
+ return None
+ result = await GoodsInfo.filter(uuid__in=user.props.keys()).all()
+ data_list = []
+ uuid2goods = {item.uuid: item for item in result}
+ column_name = ["-", "使用ID", "名称", "数量", "简介"]
+ for i, p in enumerate(user.props):
+ prop = uuid2goods[p]
+ data_list.append(
+ [
+ (ICON_PATH / prop.icon, 33, 33) if prop.icon else "",
+ i,
+ prop.goods_name,
+ user.props[p],
+ prop.goods_description,
+ ]
+ )
+
+ return await ImageTemplate.table_page(
+ f"{name}的道具仓库", "", column_name, data_list
+ )
+
+ @classmethod
+ async def my_cost(cls, user_id: str, platform: str | None = None) -> int:
+ """用户金币
+
+ 参数:
+ user_id: 用户id
+ platform: 平台.
+
+ 返回:
+ int: 金币数量
+ """
+ user, _ = await UserConsole.get_or_create(
+ user_id=user_id, defaults={"platform": platform}
+ )
+ return user.gold
+
+ @classmethod
+ async def build_shop_image(cls) -> BuildImage:
+ """制作商店图片
+
+ 返回:
+ BuildImage: 商店图片
+ """
+ goods_lst = await GoodsInfo.get_all_goods()
+ _dc = {}
+ font_h = BuildImage.get_text_size("正")[1]
+ h = 10
+ _list: list[GoodsInfo] = []
+ for goods in goods_lst:
+ if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time:
+ _list.append(goods)
+ # A = BuildImage(1100, h, color="#f9f6f2")
+ total_n = 0
+ image_list = []
+ for idx, goods in enumerate(_list):
+ name_image = BuildImage(
+ 580, 40, font_size=25, color="#e67b6b", font="CJGaoDeGuo.otf"
+ )
+ await name_image.text(
+ (15, 0), f"{idx + 1}.{goods.goods_name}", center_type="height"
+ )
+ await name_image.line((380, -5, 280, 45), "#a29ad6", 5)
+ await name_image.text((390, 0), "售价:", center_type="height")
+ if goods.goods_discount != 1:
+ discount_price = int(goods.goods_discount * goods.goods_price)
+ old_price_image = await BuildImage.build_text_image(
+ str(goods.goods_price), font_color=(194, 194, 194), size=15
+ )
+ await old_price_image.line(
+ (
+ 0,
+ int(old_price_image.height / 2),
+ old_price_image.width + 1,
+ int(old_price_image.height / 2),
+ ),
+ (0, 0, 0),
+ )
+ await name_image.paste(old_price_image, (440, 0))
+ await name_image.text((440, 15), str(discount_price), (255, 255, 255))
+ else:
+ await name_image.text(
+ (440, 0),
+ str(goods.goods_price),
+ (255, 255, 255),
+ center_type="height",
+ )
+ _tmp = await BuildImage.build_text_image(str(goods.goods_price), size=25)
+ await name_image.text(
+ (
+ 440 + _tmp.width,
+ 0,
+ ),
+ f" 金币",
+ center_type="height",
+ )
+ des_image = None
+ font_img = BuildImage(600, 80, font_size=20, color="#a29ad6")
+ p = font_img.getsize("简介:")[0] + 20
+ if goods.goods_description:
+ des_list = goods.goods_description.split("\n")
+ desc = ""
+ for des in des_list:
+ if font_img.getsize(des)[0] > font_img.width - p - 20:
+ msg = ""
+ tmp = ""
+ for i in range(len(des)):
+ if font_img.getsize(tmp)[0] < font_img.width - p - 20:
+ tmp += des[i]
+ else:
+ msg += tmp + "\n"
+ tmp = des[i]
+ desc += msg
+ if tmp:
+ desc += tmp
+ else:
+ desc += des + "\n"
+ if desc[-1] == "\n":
+ desc = desc[:-1]
+ des_image = await text2image(desc, color="#a29ad6")
+ goods_image = BuildImage(
+ 600,
+ (50 + des_image.height) if des_image else 50,
+ font_size=20,
+ color="#a29ad6",
+ font="CJGaoDeGuo.otf",
+ )
+ if des_image:
+ await goods_image.text((15, 50), "简介:")
+ await goods_image.paste(des_image, (p, 50))
+ await name_image.circle_corner(5)
+ await goods_image.paste(name_image, (0, 5), center_type="width")
+ await goods_image.circle_corner(20)
+ bk = BuildImage(
+ 1180,
+ (50 + des_image.height) if des_image else 50,
+ font_size=15,
+ color="#f9f6f2",
+ font="CJGaoDeGuo.otf",
+ )
+ if goods.icon and (ICON_PATH / goods.icon).exists():
+ icon = BuildImage(70, 70, background=ICON_PATH / goods.icon)
+ await bk.paste(icon)
+ await bk.paste(goods_image, (70, 0))
+ n = 0
+ _w = 650
+ # 添加限时图标和时间
+ if goods.goods_limit_time > 0:
+ n += 140
+ _limit_time_logo = BuildImage(
+ 40, 40, background=f"{IMAGE_PATH}/other/time.png"
+ )
+ await bk.paste(_limit_time_logo, (_w + 50, 0))
+ _time_img = await BuildImage.build_text_image("限时!", size=23)
+ await bk.paste(
+ _time_img,
+ (_w + 90, 10),
+ )
+ limit_time = time.strftime(
+ "%Y-%m-%d %H:%M", time.localtime(goods.goods_limit_time)
+ ).split()
+ y_m_d = limit_time[0]
+ _h_m = limit_time[1].split(":")
+ h_m = _h_m[0] + "时 " + _h_m[1] + "分"
+ await bk.text((_w + 55, 38), str(y_m_d))
+ await bk.text((_w + 65, 57), str(h_m))
+ _w += 140
+ if goods.goods_discount != 1:
+ n += 140
+ _discount_logo = BuildImage(
+ 30, 30, background=f"{IMAGE_PATH}/other/discount.png"
+ )
+ await bk.paste(_discount_logo, (_w + 50, 10))
+ _tmp = await BuildImage.build_text_image("折扣!", size=23)
+ await bk.paste(_tmp, (_w + 90, 15))
+ _tmp = await BuildImage.build_text_image(
+ f"{10 * goods.goods_discount:.1f} 折",
+ size=30,
+ font_color=(85, 156, 75),
+ )
+ await bk.paste(_tmp, (_w + 50, 44))
+ _w += 140
+ if goods.daily_limit != 0:
+ n += 140
+ _daily_limit_logo = BuildImage(
+ 35, 35, background=f"{IMAGE_PATH}/other/daily_limit.png"
+ )
+ await bk.paste(_daily_limit_logo, (_w + 50, 10))
+ _tmp = await BuildImage.build_text_image(
+ "限购!",
+ size=23,
+ )
+ await bk.paste(_tmp, (_w + 90, 20))
+ _tmp = await BuildImage.build_text_image(
+ f"{goods.daily_limit}", size=30
+ )
+ await bk.paste(_tmp, (_w + 72, 45))
+ if total_n < n:
+ total_n = n
+ if n:
+ await bk.line((650, -1, 650 + n, -1), "#a29ad6", 5)
+ # await bk.aline((650, 80, 650 + n, 80), "#a29ad6", 5)
+
+ # 添加限时图标和时间
+ image_list.append(bk)
+ # await A.apaste(bk, (0, current_h), True)
+ # current_h += 90
+ h = 0
+ current_h = 0
+ for img in image_list:
+ h += img.height + 10
+ A = BuildImage(1100, h, color="#f9f6f2")
+ for img in image_list:
+ await A.paste(img, (0, current_h))
+ current_h += img.height + 10
+ w = 950
+ if total_n:
+ w += total_n
+ h = A.height + 230 + 100
+ h = 1000 if h < 1000 else h
+ shop_logo = BuildImage(100, 100, background=f"{IMAGE_PATH}/other/shop_text.png")
+ shop = BuildImage(w, h, font_size=20, color="#f9f6f2")
+ await shop.paste(A, (20, 230))
+ await shop.paste(shop_logo, (450, 30))
+ await shop.text(
+ (
+ int((1000 - shop.getsize("注【通过 序号 或者 商品名称 购买】")[0]) / 2),
+ 170,
+ ),
+ "注【通过 序号 或者 商品名称 购买】",
+ )
+ await shop.text(
+ (20, h - 100),
+ "神秘药水\t\t售价:9999999金币\n\t\t鬼知道会有什么效果~",
+ )
+ return shop
diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py
new file mode 100644
index 00000000..cb1155f2
--- /dev/null
+++ b/zhenxun/builtin_plugins/sign_in/__init__.py
@@ -0,0 +1,148 @@
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import (
+ Alconna,
+ Args,
+ Arparma,
+ Option,
+ on_alconna,
+ store_true,
+)
+from nonebot_plugin_apscheduler import scheduler
+from nonebot_plugin_saa import Image, Text
+from nonebot_plugin_session import EventSession
+from nonebot_plugin_userinfo import EventUserInfo, UserInfo
+
+from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+
+from ._data_source import SignManage
+from .goods_register import driver
+from .utils import clear_sign_data_pic
+
+__plugin_meta__ = PluginMetadata(
+ name="签到",
+ description="每日签到,证明你在这里",
+ usage="""
+ 每日签到
+ 会影响色图概率和开箱次数,以及签到的随机道具获取
+ 指令:
+ 我的签到
+ 好感度排行
+ * 签到时有 3% 概率 * 2 *
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ configs=[
+ RegisterConfig(
+ module="send_setu",
+ key="INITIAL_SETU_PROBABILITY",
+ value=0.7,
+ help="初始色图概率,总概率 = 初始色图概率 + 好感度",
+ default_value=0.7,
+ type=float,
+ ),
+ RegisterConfig(
+ key="MAX_SIGN_GOLD",
+ value=200,
+ help="签到好感度加成额外获得的最大金币数",
+ default_value=200,
+ type=int,
+ ),
+ RegisterConfig(
+ key="SIGN_CARD1_PROB",
+ value=0.2,
+ help="签到好感度双倍加持卡Ⅰ掉落概率",
+ default_value=0.2,
+ type=float,
+ ),
+ RegisterConfig(
+ key="SIGN_CARD2_PROB",
+ value=0.09,
+ help="签到好感度双倍加持卡Ⅲ掉落概率",
+ default_value=0.09,
+ type=float,
+ ),
+ RegisterConfig(
+ key="SIGN_CARD3_PROB",
+ value=0.05,
+ help="签到好感度双倍加持卡Ⅲ掉落概率",
+ default_value=0.05,
+ type=float,
+ ),
+ ],
+ limits=[PluginCdBlock()],
+ ).dict(),
+)
+
+
+_sign_matcher = on_alconna(
+ Alconna(
+ "签到",
+ Option("--my", action=store_true, help_text="我的签到"),
+ Option(
+ "-l|--list", Args["num", int, 10], action=store_true, help_text="好感度排行"
+ ),
+ ),
+ priority=5,
+ block=True,
+)
+
+# TODO: shortcut
+
+
+@_sign_matcher.assign("$main")
+async def _(
+ session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo()
+):
+ nickname = (
+ user_info.user_displayname or user_info.user_remark or user_info.user_name
+ )
+ if session.id1:
+ if path := await SignManage.sign(session, nickname):
+ logger.info("签到成功", arparma.header_result, session=session)
+ await Image(path).finish(reply=True)
+ return Text("用户id为空...").send()
+
+
+@_sign_matcher.assign("my")
+async def _(
+ session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo()
+):
+ nickname = (
+ user_info.user_displayname or user_info.user_remark or user_info.user_name
+ )
+ if session.id1:
+ if image := await SignManage.sign(session, nickname, True):
+ logger.info("查看我的签到", arparma.header_result, session=session)
+ await Image(image).finish(reply=True)
+ return Text("用户id为空...").send()
+
+
+@_sign_matcher.assign("list")
+async def _(
+ session: EventSession,
+ arparma: Arparma,
+ num: int,
+ user_info: UserInfo = EventUserInfo(),
+):
+ nickname = (
+ user_info.user_displayname or user_info.user_remark or user_info.user_name
+ )
+ if session.id1:
+ if image := await SignManage.rank(session.id1, num):
+ logger.info("查看签到排行", arparma.header_result, session=session)
+ await Image(image.pic2bs4()).finish()
+ return Text("用户id为空...").send()
+
+
+@scheduler.scheduled_job(
+ "interval",
+ hours=1,
+)
+async def _():
+ try:
+ clear_sign_data_pic()
+ logger.info("清理日常签到图片数据数据完成...", "签到")
+ except Exception as e:
+ logger.error(f"清理日常签到图片数据数据失败...", e=e)
diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py
new file mode 100644
index 00000000..07f3fcbe
--- /dev/null
+++ b/zhenxun/builtin_plugins/sign_in/_data_source.py
@@ -0,0 +1,175 @@
+import os
+import random
+import secrets
+from datetime import datetime
+from pathlib import Path
+
+import pytz
+from nonebot_plugin_session import EventSession
+from tortoise.functions import Count
+
+from zhenxun.configs.path_config import IMAGE_PATH
+from zhenxun.models.friend_user import FriendUser
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.models.sign_log import SignLog
+from zhenxun.models.sign_user import SignUser
+from zhenxun.models.user_console import UserConsole
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage, ImageTemplate
+from zhenxun.utils.utils import get_user_avatar
+
+from ._random_event import random_event
+from .utils import SIGN_TODAY_CARD_PATH, get_card
+
+ICON_PATH = IMAGE_PATH / "_icon"
+
+PLATFORM_PATH = {
+ "dodo": ICON_PATH / "dodo.png",
+ "discord": ICON_PATH / "discord.png",
+ "kaiheila": ICON_PATH / "kook.png",
+ "qq": ICON_PATH / "qq.png",
+}
+
+
+class SignManage:
+
+ @classmethod
+ async def rank(cls, user_id: str, num: int) -> BuildImage:
+ all_list = (
+ await SignUser.annotate()
+ .order_by("impression")
+ .values_list("user_id", flat=True)
+ )
+ index = all_list.index(user_id) + 1 # type: ignore
+ user_list = await SignUser.annotate().order_by("impression").limit(num).all()
+ user_id_list = [u.user_id for u in user_list]
+ log_list = (
+ await SignLog.filter(user_id__in=user_id_list)
+ .annotate(count=Count("id"))
+ .group_by("user_id")
+ .values_list("user_id", "count")
+ )
+ uid2cnt = {l[0]: l[1] for l in log_list}
+ column_name = ["排名", "-", "名称", "好感度", "签到次数", "平台"]
+ friend_list = await FriendUser.filter(user_id__in=user_id_list).values_list(
+ "user_id", "user_name"
+ )
+ uid2name = {f[0]: f[1] for f in friend_list}
+ group_member_list = await GroupInfoUser.filter(
+ user_id__in=user_id_list
+ ).values_list("user_id", "user_name")
+ for gm in group_member_list:
+ uid2name[gm[0]] = gm[1]
+ data_list = []
+ for i, user in enumerate(user_list):
+ bytes = await get_user_avatar(user.user_id)
+ data_list.append(
+ [
+ f"{i+1}",
+ (bytes, 30, 30) if user.platform == "qq" else "",
+ uid2name.get(user.user_id),
+ user.impression,
+ uid2cnt.get(user.user_id) or 0,
+ (PLATFORM_PATH.get(user.platform), 30, 30),
+ ]
+ )
+ return await ImageTemplate.table_page(
+ "好感度排行", f"你的排名在第 {index} 位哦!", column_name, data_list
+ )
+
+ @classmethod
+ async def sign(
+ cls, session: EventSession, nickname: str, is_view_card: bool = False
+ ) -> Path | None:
+ """签到
+
+ 参数:
+ session: Session
+ nickname: 用户昵称
+ is_view_card: 是否展示卡片
+
+ 返回:
+ Path: 卡片路径
+ """
+ if not session.id1:
+ return None
+ now = datetime.now(pytz.timezone("Asia/Shanghai"))
+ user_console, _ = await UserConsole.get_or_create(
+ user_id=session.id1,
+ defaults={
+ "uid": await UserConsole.get_new_uid(),
+ "platform": session.platform,
+ },
+ )
+ user, _ = await SignUser.get_or_create(
+ user_id=session.id1,
+ defaults={"user_console": user_console, "platform": session.platform},
+ )
+ new_log = await SignLog.filter(user_id=session.id1).first()
+ file_name = f"{user}_sign_{datetime.now().date()}.png"
+ if (
+ user.sign_count != 0
+ or (new_log and now > new_log.create_time)
+ or file_name in os.listdir(SIGN_TODAY_CARD_PATH)
+ ):
+ user_console, _ = await UserConsole.get_or_create(user_id=session.id1)
+ path = await get_card(user, nickname, -1, user_console.gold, "")
+ else:
+ path = await cls._handle_sign_in(user, nickname, session, is_view_card)
+ return path
+
+ @classmethod
+ async def _handle_sign_in(
+ cls,
+ user: SignUser,
+ nickname: str,
+ session: EventSession,
+ is_view_card: bool,
+ ) -> Path:
+ """签到处理
+
+ 参数:
+ user: SignUser
+ nickname: 用户昵称
+ session: Session
+ is_view_card: 是否展示卡片
+
+ 返回:
+ Path: 卡片路径
+ """
+ impression_added = (secrets.randbelow(99) + 1) / 100
+ rand = random.random()
+ add_probability = float(user.add_probability)
+ specify_probability = user.specify_probability
+ if rand + add_probability > 0.97:
+ impression_added *= 2
+ elif rand < specify_probability:
+ impression_added *= 2
+ await SignUser.sign(user, impression_added, session.bot_id, session.platform)
+ gold = random.randint(1, 100)
+ gift = random_event(float(user.impression))
+ if isinstance(gift, int):
+ gold += gift
+ await UserConsole.add_gold(
+ user.user_id, gold + gift, "sign_in", session.platform
+ )
+ gift = f"额外金币 +{gift}"
+ else:
+ await UserConsole.add_gold(user.user_id, gold, "sign_in", session.platform)
+ await UserConsole.add_props(user.user_id, gift, 1, session.platform)
+ gift += " + 1"
+ logger.info(
+ f"签到成功. score: {user.impression:.2f} "
+ f"(+{impression_added:.2f}).获取金币/道具: {gold}",
+ "签到",
+ session=session,
+ )
+ return await get_card(
+ user,
+ nickname,
+ impression_added,
+ gold,
+ gift,
+ rand + add_probability > 0.97 or rand < specify_probability,
+ is_view_card,
+ )
diff --git a/zhenxun/builtin_plugins/sign_in/_random_event.py b/zhenxun/builtin_plugins/sign_in/_random_event.py
new file mode 100644
index 00000000..32d133c1
--- /dev/null
+++ b/zhenxun/builtin_plugins/sign_in/_random_event.py
@@ -0,0 +1,33 @@
+import random
+
+from zhenxun.configs.config import Config
+
+PROB_DATA = None
+
+
+def random_event(impression: float) -> str | int:
+ """签到随机事件
+
+ 参数:
+ impression: 好感度
+
+ 返回:
+ 额外奖励 和 类型
+ """
+ global PROB_DATA
+ if not PROB_DATA:
+ PROB_DATA = {
+ Config.get_config("sign_in", "SIGN_CARD3_PROB"): "好感度双倍加持卡Ⅲ",
+ Config.get_config("sign_in", "SIGN_CARD2_PROB"): "好感度双倍加持卡Ⅱ",
+ Config.get_config("sign_in", "SIGN_CARD1_PROB"): "好感度双倍加持卡Ⅰ",
+ }
+ rand = random.random() - impression / 1000
+ for prob in PROB_DATA.keys():
+ if rand <= prob:
+ return PROB_DATA[prob]
+ gold = random.randint(
+ 1, random.randint(1, int(1 if impression < 1 else impression))
+ )
+ max_sign_gold = Config.get_config("sign_in", "MAX_SIGN_GOLD")
+ gold = max_sign_gold if gold > max_sign_gold else gold
+ return gold
diff --git a/zhenxun/builtin_plugins/sign_in/config.py b/zhenxun/builtin_plugins/sign_in/config.py
new file mode 100644
index 00000000..e2bfdbc6
--- /dev/null
+++ b/zhenxun/builtin_plugins/sign_in/config.py
@@ -0,0 +1,49 @@
+from zhenxun.configs.path_config import IMAGE_PATH
+
+SIGN_RESOURCE_PATH = IMAGE_PATH / "sign" / "sign_res"
+SIGN_TODAY_CARD_PATH = IMAGE_PATH / "sign" / "today_card"
+SIGN_BORDER_PATH = SIGN_RESOURCE_PATH / "border"
+SIGN_BACKGROUND_PATH = SIGN_RESOURCE_PATH / "background"
+
+SIGN_BORDER_PATH.mkdir(exist_ok=True, parents=True)
+SIGN_BACKGROUND_PATH.mkdir(exist_ok=True, parents=True)
+
+
+lik2relation = {
+ "0": "路人",
+ "1": "陌生",
+ "2": "初识",
+ "3": "普通",
+ "4": "熟悉",
+ "5": "信赖",
+ "6": "相知",
+ "7": "厚谊",
+ "8": "亲密",
+}
+
+level2attitude = {
+ "0": "排斥",
+ "1": "警惕",
+ "2": "可以交流",
+ "3": "一般",
+ "4": "是个好人",
+ "5": "好朋友",
+ "6": "可以分享小秘密",
+ "7": "喜欢",
+ "8": "恋人",
+}
+
+weekdays = {1: "Mon", 2: "Tue", 3: "Wed", 4: "Thu", 5: "Fri", 6: "Sat", 7: "Sun"}
+
+lik2level = {
+ 9999: "9",
+ 400: "8",
+ 270: "7",
+ 200: "6",
+ 140: "5",
+ 90: "4",
+ 50: "3",
+ 25: "2",
+ 10: "1",
+ 0: "0",
+}
diff --git a/zhenxun/builtin_plugins/sign_in/goods_register.py b/zhenxun/builtin_plugins/sign_in/goods_register.py
new file mode 100644
index 00000000..3af6514f
--- /dev/null
+++ b/zhenxun/builtin_plugins/sign_in/goods_register.py
@@ -0,0 +1,75 @@
+from decimal import Decimal
+
+import nonebot
+from nonebot.drivers import Driver
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.models.sign_user import SignUser
+from zhenxun.models.user_console import UserConsole
+from zhenxun.utils.decorator.shop import NotMeetUseConditionsException, shop_register
+
+driver: Driver = nonebot.get_driver()
+
+
+@driver.on_startup
+async def _():
+ """
+ 导入内置的三个商品
+ """
+
+ @shop_register(
+ name=("好感度双倍加持卡Ⅰ", "好感度双倍加持卡Ⅱ", "好感度双倍加持卡Ⅲ"),
+ price=(30, 150, 250),
+ des=(
+ "下次签到双倍好感度概率 + 10%(谁才是真命天子?)(同类商品将覆盖)",
+ "下次签到双倍好感度概率 + 20%(平平庸庸)(同类商品将覆盖)",
+ "下次签到双倍好感度概率 + 30%(金币才是真命天子!)(同类商品将覆盖)",
+ ),
+ load_status=bool(Config.get_config("shop", "IMPORT_DEFAULT_SHOP_GOODS")),
+ icon=(
+ "favorability_card_1.png",
+ "favorability_card_2.png",
+ "favorability_card_3.png",
+ ),
+ **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, # type: ignore
+ )
+ async def _(session: EventSession, user_id: int, group_id: int, prob: float):
+ user_console, _ = await UserConsole.get_or_create(
+ user_id=session.id1,
+ defaults={
+ "uid": await UserConsole.get_new_uid(),
+ "platform": session.platform,
+ },
+ )
+ user, _ = await SignUser.get_or_create(
+ user_id=user_id,
+ defaults={"platform": session.platform, "user_console": user_console},
+ )
+ user.add_probability = Decimal(prob)
+ await user.save(update_fields=["add_probability"])
+
+ @shop_register(
+ name="测试道具A",
+ price=99,
+ des="随便侧而出",
+ load_status=False,
+ icon="sword.png",
+ )
+ async def _(user_id: int, group_id: int):
+ print(user_id, group_id, "使用测试道具")
+
+ @shop_register.before_handle(name="测试道具A", load_status=False)
+ async def _(user_id: int, group_id: int):
+ print(user_id, group_id, "第一个使用前函数(before handle)")
+
+ @shop_register.before_handle(name="测试道具A", load_status=False)
+ async def _(user_id: int, group_id: int):
+ print(user_id, group_id, "第二个使用前函数(before handle)222")
+ raise NotMeetUseConditionsException(
+ "太笨了!"
+ ) # 抛出异常,阻断使用,并返回信息
+
+ @shop_register.after_handle(name="测试道具A", load_status=False)
+ async def _(user_id: int, group_id: int):
+ print(user_id, group_id, "第一个使用后函数(after handle)")
diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py
new file mode 100644
index 00000000..950a7aab
--- /dev/null
+++ b/zhenxun/builtin_plugins/sign_in/utils.py
@@ -0,0 +1,326 @@
+import os
+import random
+from datetime import datetime
+from io import BytesIO
+from pathlib import Path
+
+import nonebot
+import pytz
+from nonebot.drivers import Driver
+
+from zhenxun.configs.config import NICKNAME, Config
+from zhenxun.configs.path_config import IMAGE_PATH
+from zhenxun.models.sign_log import SignLog
+from zhenxun.models.sign_user import SignUser
+from zhenxun.utils.image_utils import BuildImage
+from zhenxun.utils.utils import get_user_avatar
+
+from .config import (
+ SIGN_BACKGROUND_PATH,
+ SIGN_BORDER_PATH,
+ SIGN_RESOURCE_PATH,
+ SIGN_TODAY_CARD_PATH,
+ level2attitude,
+ lik2level,
+ lik2relation,
+)
+
+driver: Driver = nonebot.get_driver()
+
+
+@driver.on_startup
+async def init_image():
+ SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True)
+ SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True)
+ await generate_progress_bar_pic()
+ clear_sign_data_pic()
+
+
+async def get_card(
+ user: SignUser,
+ nickname: str,
+ add_impression: float,
+ gold: int | None,
+ gift: str,
+ is_double: bool = False,
+ is_card_view: bool = False,
+) -> Path:
+ """获取好感度卡片
+
+ 参数:
+ user: SignUser
+ nickname: 用户昵称
+ impression: 新增的好感度
+ gold: 金币
+ gift: 礼物
+ is_double: 是否触发双倍.
+ is_card_view: 是否展示好感度卡片.
+
+ 返回:
+ Path: 卡片路径
+ """
+ user_id = user.user_id
+ date = datetime.now().date()
+ _type = "view" if is_card_view else "sign"
+ file_name = f"{user_id}_{_type}_{date}.png"
+ view_name = f"{user_id}_view_{date}.png"
+ card_file = Path(SIGN_TODAY_CARD_PATH) / file_name
+ if card_file.exists():
+ return IMAGE_PATH / "sign" / "today_card" / file_name
+ else:
+ if add_impression == -1:
+ card_file = Path(SIGN_TODAY_CARD_PATH) / view_name
+ if card_file.exists():
+ return card_file
+ is_card_view = True
+ return await _generate_card(
+ user, nickname, add_impression, gold, gift, is_double, is_card_view
+ )
+
+
+async def _generate_card(
+ user: SignUser,
+ nickname: str,
+ impression: float,
+ gold: int | None,
+ gift: str,
+ is_double: bool = False,
+ is_card_view: bool = False,
+) -> Path:
+ """生成签到卡片
+
+ 参数:
+ user: SignUser
+ nickname: 用户昵称
+ impression: 新增的好感度
+ gold: 金币
+ gift: 礼物
+ is_double: 是否触发双倍.
+ is_card_view: 是否展示好感度卡片.
+
+ 返回:
+ Path: 卡片路径
+ """
+ ava_bk = BuildImage(140, 140, (255, 255, 255, 0))
+ ava_border = BuildImage(
+ 140,
+ 140,
+ background=SIGN_BORDER_PATH / "ava_border_01.png",
+ )
+ if user.platform == "qq" and (byt := await get_user_avatar(user.user_id)):
+ ava = BuildImage(107, 107, background=BytesIO(byt))
+ else:
+ ava = BuildImage(107, 107, (0, 0, 0))
+ await ava.circle()
+ await ava_bk.paste(ava, (19, 18))
+ await ava_bk.paste(ava_border, center_type="center")
+ add_impression = impression
+ impression = float(user.impression)
+ info_img = BuildImage(250, 150, color=(255, 255, 255, 0), font_size=15)
+ level, next_impression, previous_impression = get_level_and_next_impression(
+ impression
+ )
+ interpolation = next_impression - impression
+ if level == "9":
+ level = "8"
+ interpolation = 0
+ await info_img.text((0, 0), f"· 好感度等级:{level} [{lik2relation[level]}]")
+ await info_img.text((0, 20), f"· {NICKNAME}对你的态度:{level2attitude[level]}")
+ await info_img.text((0, 40), f"· 距离升级还差 {interpolation:.2f} 好感度")
+
+ bar_bk = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar_white.png")
+ bar = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar.png")
+ ratio = 1 - (next_impression - user.impression) / (
+ next_impression - previous_impression
+ )
+ if next_impression == 0:
+ ratio = 0
+ await bar.resize(width=int(bar.width * ratio) or bar.width, height=bar.height)
+ await bar_bk.paste(bar)
+ font_size = 30
+ if "好感度双倍加持卡" in gift:
+ font_size = 20
+ gift_border = BuildImage(
+ 270,
+ 100,
+ background=SIGN_BORDER_PATH / "gift_border_02.png",
+ font_size=font_size,
+ )
+ await gift_border.text((0, 0), gift, center_type="center")
+
+ bk = BuildImage(
+ 876,
+ 424,
+ background=SIGN_BACKGROUND_PATH
+ / random.choice(os.listdir(SIGN_BACKGROUND_PATH)),
+ font_size=25,
+ )
+ A = BuildImage(876, 274, background=SIGN_RESOURCE_PATH / "white.png")
+ line = BuildImage(2, 180, color="black")
+ await A.transparent(2)
+ await A.paste(ava_bk, (25, 80))
+ await A.paste(line, (200, 70))
+ nickname_img = await BuildImage.build_text_image(
+ nickname, size=50, font_color=(255, 255, 255)
+ )
+ user_console = await user.user_console.first()
+ if user_console and user_console.uid:
+ uid = f"{user_console.uid}".rjust(12, "0")
+ uid = uid[:4] + " " + uid[4:8] + " " + uid[8:]
+ else:
+ uid = "XXXX XXXX XXXX"
+ uid_img = await BuildImage.build_text_image(
+ f"UID: {uid}", size=30, font_color=(255, 255, 255)
+ )
+ sign_count = await SignLog.filter(user_id=user.user_id).count()
+ sign_day_img = await BuildImage.build_text_image(
+ f"{sign_count}", size=40, font_color=(211, 64, 33)
+ )
+ lik_text1_img = await BuildImage.build_text_image("当前", size=20)
+ lik_text2_img = await BuildImage.build_text_image(
+ f"好感度:{user.impression:.2f}", size=30
+ )
+ watermark = await BuildImage.build_text_image(
+ f"{NICKNAME}@{datetime.now().year}", size=15, font_color=(155, 155, 155)
+ )
+ today_data = BuildImage(300, 300, color=(255, 255, 255, 0), font_size=20)
+ if is_card_view:
+ today_sign_text_img = await BuildImage.build_text_image("", size=30)
+ value_list = (
+ await SignUser.annotate()
+ .order_by("impression")
+ .values_list("user_id", flat=True)
+ )
+ index = value_list.index(user.user_id) + 1 # type: ignore
+ rank_img = await BuildImage.build_text_image(
+ f"* 好感度排名第 {index} 位", size=30
+ )
+ await A.paste(rank_img, ((A.width - rank_img.width - 32), 20))
+ last_log = (
+ await SignLog.filter(user_id=user.user_id).order_by("create_time").first()
+ )
+ last_date = "从未"
+ if last_log:
+ last_date = last_log.create_time.astimezone(
+ pytz.timezone("Asia/Shanghai")
+ ).date()
+ await today_data.text(
+ (0, 0),
+ f"上次签到日期:{last_date}",
+ )
+ await today_data.text((0, 25), f"总金币:{gold}")
+ default_setu_prob = (
+ Config.get_config("send_setu", "INITIAL_SETU_PROBABILITY") * 100 # type: ignore
+ )
+ await today_data.text(
+ (0, 50),
+ f"色图概率:{(default_setu_prob + float(user.impression) if user.impression < 100 else 100):.2f}%",
+ )
+ await today_data.text((0, 75), f"开箱次数:{(20 + int(user.impression / 3))}")
+ _type = "view"
+ else:
+ await A.paste(gift_border, (570, 140))
+ today_sign_text_img = await BuildImage.build_text_image("今日签到", size=30)
+ if is_double:
+ await today_data.text((0, 0), f"好感度 + {add_impression / 2:.2f} × 2")
+ else:
+ await today_data.text((0, 0), f"好感度 + {add_impression:.2f}")
+ await today_data.text((0, 25), f"金币 + {gold}")
+ _type = "sign"
+ current_date = datetime.now()
+ current_datetime_str = current_date.strftime("%Y-%m-%d %a %H:%M:%S")
+ data = current_date.date()
+ data_img = await BuildImage.build_text_image(
+ f"时间:{current_datetime_str}", size=20
+ )
+ await bk.paste(nickname_img, (30, 15))
+ await bk.paste(uid_img, (30, 85))
+ await bk.paste(A, (0, 150))
+ await bk.text((30, 167), "Accumulative check-in for")
+ _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.width + 45
+ await bk.paste(sign_day_img, (380, 158))
+ await bk.text((_x, 167), "days")
+ await bk.paste(data_img, (220, 370))
+ await bk.paste(lik_text1_img, (220, 240))
+ await bk.paste(lik_text2_img, (262, 234))
+ await bk.paste(bar_bk, (225, 275))
+ await bk.paste(info_img, (220, 305))
+ await bk.paste(today_sign_text_img, (550, 180))
+ await bk.paste(today_data, (580, 220))
+ await bk.paste(watermark, (15, 400))
+ await bk.save(SIGN_TODAY_CARD_PATH / f"{user.user_id}_{_type}_{data}.png")
+ return IMAGE_PATH / "sign" / "today_card" / f"{user.user_id}_{_type}_{data}.png"
+
+
+async def generate_progress_bar_pic():
+ """
+ 初始化进度条图片
+ """
+ bg_2 = (254, 1, 254)
+ bg_1 = (0, 245, 246)
+
+ bk = BuildImage(1000, 50)
+ img_x = BuildImage(50, 50, color=bg_2)
+ await img_x.circle()
+ await img_x.crop((25, 0, 50, 50))
+ img_y = BuildImage(50, 50, color=bg_1)
+ await img_y.circle()
+ await img_y.crop((0, 0, 25, 50))
+ A = BuildImage(950, 50)
+ width, height = A.size
+
+ step_r = (bg_2[0] - bg_1[0]) / width
+ step_g = (bg_2[1] - bg_1[1]) / width
+ step_b = (bg_2[2] - bg_1[2]) / width
+
+ for y in range(0, width):
+ bg_r = round(bg_1[0] + step_r * y)
+ bg_g = round(bg_1[1] + step_g * y)
+ bg_b = round(bg_1[2] + step_b * y)
+ for x in range(0, height):
+ await A.point((y, x), fill=(bg_r, bg_g, bg_b))
+ await bk.paste(img_y, (0, 0))
+ await bk.paste(A, (25, 0))
+ await bk.paste(img_x, (975, 0))
+ await bk.save(SIGN_RESOURCE_PATH / "bar.png")
+
+ A = BuildImage(950, 50)
+ bk = BuildImage(1000, 50)
+ img_x = BuildImage(50, 50)
+ await img_x.circle()
+ await img_x.crop((25, 0, 50, 50))
+ img_y = BuildImage(50, 50)
+ await img_y.circle()
+ await img_y.crop((0, 0, 25, 50))
+ await bk.paste(img_y, (0, 0))
+ await bk.paste(A, (25, 0))
+ await bk.paste(img_x, (975, 0))
+ await bk.save(SIGN_RESOURCE_PATH / "bar_white.png")
+
+
+def get_level_and_next_impression(impression: float) -> tuple[str, int, int]:
+ """获取当前好感等级与下一等级的差距
+
+ 参数:
+ impression: 好感度
+
+ 返回:
+ tuple[str, int, int]: 好感度等级中文,好感度等级,下一等级好感差距
+ """
+ if impression == 0:
+ return lik2level[10], 10, 0
+ keys = list(lik2level.keys())
+ for i in range(len(keys)):
+ if impression > keys[i]:
+ return lik2level[keys[i]], keys[i - 1], keys[i]
+ return lik2level[10], 10, 0
+
+
+def clear_sign_data_pic():
+ """
+ 清空当前签到图片数据
+ """
+ date = datetime.now().date()
+ for file in os.listdir(SIGN_TODAY_CARD_PATH):
+ if str(date) not in file:
+ os.remove(SIGN_TODAY_CARD_PATH / file)
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py
new file mode 100644
index 00000000..271652a7
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py
@@ -0,0 +1,61 @@
+from typing import Annotated
+
+from nonebot import on_command
+from nonebot.adapters import Bot
+from nonebot.params import Command
+from nonebot.permission import SUPERUSER
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import UniMsg
+from nonebot_plugin_saa import Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+
+from ._data_source import BroadcastManage
+
+__plugin_meta__ = PluginMetadata(
+ name="广播",
+ description="昭告天下!",
+ usage="""
+ 广播 [消息] [图片]
+ 示例:广播 你们好!
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.SUPERUSER,
+ configs=[
+ RegisterConfig(
+ module="_task",
+ key="DEFAULT_BROADCAST",
+ value=True,
+ help="被动 广播 进群默认开关状态",
+ default_value=True,
+ type=bool,
+ )
+ ],
+ tasks=[Task(module="broadcast", name="广播")],
+ ).dict(),
+)
+
+_matcher = on_command("广播", priority=1, permission=SUPERUSER, block=True)
+
+
+@_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ message: UniMsg,
+ command: Annotated[tuple[str, ...], Command()],
+):
+ message[0].text = message[0].text.replace(command[0], "").strip()
+ # await Text("正在发送..请等一下哦!").send()
+ count, error_count = await BroadcastManage.send(bot, message, session)
+ result = f"成功广播 {count} 个群组"
+ if error_count:
+ result += f"\n广播失败 {error_count} 个群组"
+ await Text(f"发送广播完成!\n{result}").send(reply=True)
+ logger.info(f"发送广播信息: {message}", "广播", session=session)
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py
new file mode 100644
index 00000000..967fee82
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py
@@ -0,0 +1,117 @@
+import nonebot_plugin_alconna as alc
+from nonebot.adapters import Bot
+from nonebot.adapters.discord import Bot as DiscordBot
+from nonebot.adapters.dodo import Bot as DodoBot
+from nonebot.adapters.kaiheila import Bot as KaiheilaBot
+from nonebot.adapters.onebot.v11 import Bot as v11Bot
+from nonebot.adapters.onebot.v12 import Bot as v12Bot
+from nonebot_plugin_alconna import UniMsg
+from nonebot_plugin_saa import (
+ Image,
+ MessageFactory,
+ TargetDoDoChannel,
+ TargetQQGroup,
+ Text,
+)
+from nonebot_plugin_session import EventSession
+from pydantic import BaseModel
+
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.services.log import logger
+
+
+class GroupChannel(BaseModel):
+
+ group_id: str
+ """群组id"""
+ channel_id: str | None = None
+ """频道id"""
+
+
+class BroadcastManage:
+
+ @classmethod
+ async def send(
+ cls, bot: Bot, message: UniMsg, session: EventSession
+ ) -> tuple[int, int]:
+ """发送广播消息
+
+ 参数:
+ bot: Bot
+ message: 消息内容
+ session: Session
+
+ 返回:
+ tuple[int, int]: 发送成功的群组数量, 发送失败的群组数量
+ """
+ message_list = []
+ for msg in message:
+ if isinstance(msg, alc.Image) and msg.url:
+ message_list.append(Image(msg.url))
+ elif isinstance(msg, alc.Text):
+ message_list.append(Text(msg.text))
+ if group_list := await cls.__get_group_list(bot):
+ error_count = 0
+ for group in group_list:
+ try:
+ if not await GroupConsole.is_block_task(
+ group.group_id, "broadcast", group.channel_id
+ ):
+ if isinstance(bot, (v11Bot, v12Bot)):
+ target = TargetQQGroup(group_id=int(group.group_id))
+ elif isinstance(bot, DodoBot):
+ target = TargetDoDoChannel(channel_id=group.channel_id) # type: ignore
+ await MessageFactory(message_list).send_to(target, bot)
+ logger.debug(
+ "发送成功",
+ "广播",
+ session=session,
+ target=f"{group.group_id}:{group.channel_id}",
+ )
+ except Exception as e:
+ error_count += 1
+ logger.error(
+ "发送失败",
+ "广播",
+ session=session,
+ target=f"{group.group_id}:{group.channel_id}",
+ e=e,
+ )
+ return len(group_list) - error_count, error_count
+ return 0, 0
+
+ @classmethod
+ async def __get_group_list(cls, bot: Bot) -> list[GroupChannel]:
+ """获取群组id列表
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ list[str]: 群组id列表
+ """
+ if isinstance(bot, (v11Bot, v12Bot)):
+ group_list = await bot.get_group_list()
+ return [GroupChannel(group_id=str(g["group_id"])) for g in group_list]
+ if isinstance(bot, DodoBot):
+ island_list = await bot.get_island_list()
+ source_id_list = [
+ g.island_source_id for g in island_list if g.island_source_id
+ ]
+ channel_id_list = []
+ for id in source_id_list:
+ channel_list = await bot.get_channel_list(island_source_id=id)
+ channel_id_list += [
+ GroupChannel(group_id=id, channel_id=c.channel_id)
+ for c in channel_list
+ ]
+ return channel_id_list
+ if isinstance(bot, KaiheilaBot):
+ pass
+ # group_list = await bot.guild_list()
+ # if group_list.guilds:
+ # return [g.open_id for g in group_list.guilds if g.open_id]
+ if isinstance(bot, DiscordBot):
+ # TODO: discord获取群组列表
+ pass
+ return []
diff --git a/zhenxun/builtin_plugins/superuser/fg_manage.py b/zhenxun/builtin_plugins/superuser/fg_manage.py
index 9e6e79d1..fe17f8f3 100644
--- a/zhenxun/builtin_plugins/superuser/fg_manage.py
+++ b/zhenxun/builtin_plugins/superuser/fg_manage.py
@@ -1,21 +1,9 @@
-
-
from nonebot.adapters import Bot
from nonebot.adapters.kaiheila.exception import ApiNotAvailable
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
-from nonebot_plugin_alconna import (
- Alconna,
- AlconnaMatch,
- Arparma,
- Match,
- Query,
- Subcommand,
- UniMessage,
- on_alconna,
- store_true,
-)
+from nonebot_plugin_alconna import Alconna, on_alconna
from nonebot_plugin_alconna.matcher import AlconnaMatcher
from nonebot_plugin_saa import Text
from nonebot_plugin_session import EventSession
diff --git a/zhenxun/builtin_plugins/superuser/super_help.py b/zhenxun/builtin_plugins/superuser/super_help.py
new file mode 100644
index 00000000..a47943d7
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/super_help.py
@@ -0,0 +1,161 @@
+import nonebot
+from arclet.alconna import Args, Option
+from nonebot.permission import SUPERUSER
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
+from nonebot_plugin_alconna.matcher import AlconnaMatcher
+from nonebot_plugin_saa import Image, Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.models.task_info import TaskInfo
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.exception import EmptyError
+from zhenxun.utils.image_utils import (
+ BuildImage,
+ build_sort_image,
+ group_image,
+ text2image,
+)
+from zhenxun.utils.rules import admin_check, ensure_group
+
+__plugin_meta__ = PluginMetadata(
+ name="超级用户帮助",
+ description="超级用户帮助",
+ usage="""
+ 超级用户帮助
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.SUPERUSER,
+ ).dict(),
+)
+
+_matcher = on_alconna(
+ Alconna("超级用户帮助"),
+ permission=SUPERUSER,
+ priority=5,
+ block=True,
+)
+
+
+SUPERUSER_HELP_IMAGE = IMAGE_PATH / "SUPERUSER_HELP.png"
+if SUPERUSER_HELP_IMAGE.exists():
+ SUPERUSER_HELP_IMAGE.unlink()
+
+
+async def build_help() -> BuildImage:
+ """构造超级用户帮助图片
+
+ 异常:
+ EmptyError: 超级用户帮助为空
+
+ 返回:
+ BuildImage: 超级用户帮助图片
+ """
+ plugin_list = await PluginInfo.filter(plugin_type=PluginType.SUPERUSER).all()
+ data_list = []
+ for plugin in plugin_list:
+ if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path):
+ if _plugin.metadata:
+ data_list.append({"plugin": plugin, "metadata": _plugin.metadata})
+ font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
+ image_list = []
+ for data in data_list:
+ plugin = data["plugin"]
+ metadata = data["metadata"]
+ try:
+ usage = None
+ description = None
+ if metadata.usage:
+ usage = await text2image(
+ metadata.usage,
+ padding=5,
+ color=(255, 255, 255),
+ font_color=(0, 0, 0),
+ )
+ if metadata.description:
+ description = await text2image(
+ metadata.description,
+ padding=5,
+ color=(255, 255, 255),
+ font_color=(0, 0, 0),
+ )
+ width = 0
+ height = 100
+ if usage:
+ width = usage.width
+ height += usage.height
+ if description and description.width > width:
+ width = description.width
+ height += description.height
+ font_width, font_height = BuildImage.get_text_size(
+ plugin.name + f"[{plugin.level}]", font
+ )
+ if font_width > width:
+ width = font_width
+ A = BuildImage(width + 30, height + 120, "#EAEDF2")
+ await A.text((15, 10), plugin.name + f"[{plugin.level}]")
+ await A.text((15, 70), "简介:")
+ if not description:
+ description = BuildImage(A.width - 30, 30, (255, 255, 255))
+ await description.circle_corner(10)
+ await A.paste(description, (15, 100))
+ if not usage:
+ usage = BuildImage(A.width - 30, 30, (255, 255, 255))
+ await usage.circle_corner(10)
+ await A.text((15, description.height + 115), "用法:")
+ await A.paste(usage, (15, description.height + 145))
+ await A.circle_corner(10)
+ image_list.append(A)
+ except Exception as e:
+ logger.warning(
+ f"获取超级用户管理员插件 {plugin.module}: {plugin.name} 设置失败...",
+ "超级用户帮助",
+ e=e,
+ )
+ if task_list := await TaskInfo.all():
+ task_str = "\n".join([task.name for task in task_list])
+ task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str
+ task_image = await text2image(task_str, padding=5, color=(255, 255, 255))
+ await task_image.circle_corner(10)
+ A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2")
+ await A.text((25, 10), "被动技能")
+ await A.paste(task_image, (25, 50))
+ await A.circle_corner(10)
+ image_list.append(A)
+ if not image_list:
+ raise EmptyError()
+ image_group, _ = group_image(image_list)
+ A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160)
+ text = await BuildImage.build_text_image(
+ "超级用户帮助",
+ size=40,
+ )
+ tip = await BuildImage.build_text_image(
+ "注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red"
+ )
+ await A.paste(text, (50, 30))
+ await A.paste(tip, (50, 90))
+ await A.save(SUPERUSER_HELP_IMAGE)
+ return BuildImage(1, 1)
+
+
+@_matcher.handle()
+async def _(
+ session: EventSession,
+ matcher: AlconnaMatcher,
+ arparma: Arparma,
+):
+ if not SUPERUSER_HELP_IMAGE.exists():
+ try:
+ await build_help()
+ except EmptyError:
+ await Text("超级用户帮助为空").finish(reply=True)
+ await Image(SUPERUSER_HELP_IMAGE).send()
+ logger.info("查看超级用户帮助", arparma.header_result, session=session)
diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info.py b/zhenxun/builtin_plugins/superuser/update_fg_info.py
deleted file mode 100644
index 18cf31cb..00000000
--- a/zhenxun/builtin_plugins/superuser/update_fg_info.py
+++ /dev/null
@@ -1,119 +0,0 @@
-from nonebot.adapters import Bot
-from nonebot.adapters.kaiheila.exception import ApiNotAvailable
-from nonebot.permission import SUPERUSER
-from nonebot.plugin import PluginMetadata
-from nonebot.rule import to_me
-from nonebot_plugin_alconna import Alconna, Arparma, At, Match, on_alconna
-from nonebot_plugin_saa import Mention, MessageFactory, Text
-from nonebot_plugin_session import EventSession, SessionLevel
-
-from zhenxun.configs.config import Config
-from zhenxun.configs.utils import PluginExtraData
-from zhenxun.models.friend_user import FriendUser
-from zhenxun.models.group_info import GroupInfo
-from zhenxun.models.level_user import LevelUser
-from zhenxun.services.log import logger
-from zhenxun.utils.enum import PluginType
-
-__plugin_meta__ = PluginMetadata(
- name="更新群组/好友信息",
- description="更新群组/好友信息",
- usage="""
- 更新群组信息
- 更新好友信息
- """.strip(),
- extra=PluginExtraData(
- author="HibiKier",
- version="0.1",
- plugin_type=PluginType.SUPERUSER,
- ).dict(),
-)
-
-
-_group_matcher = on_alconna(
- Alconna(
- "更新群组信息",
- ),
- permission=SUPERUSER,
- rule=to_me(),
- priority=1,
- block=True,
-)
-
-_friend_matcher = on_alconna(
- Alconna(
- "更新好友信息",
- ),
- permission=SUPERUSER,
- rule=to_me(),
- priority=1,
- block=True,
-)
-
-# TODO: 其他adapter的更新操作
-
-@_group_matcher.handle()
-async def _(
- bot: Bot,
- session: EventSession,
- arparma: Arparma,
-):
- try:
- gl = await bot.get_group_list()
- gl = [g["group_id"] for g in gl]
- num = 0
- for g in gl:
- try:
- group_info = await bot.get_group_info(group_id=g)
- await GroupInfo.update_or_create(
- group_id=str(group_info["group_id"]),
- defaults={
- "group_name": group_info["group_name"],
- "max_member_count": group_info["max_member_count"],
- "member_count": group_info["member_count"],
- },
- )
- num += 1
- logger.debug(
- "群聊信息更新成功", "更新群信息", session=session, target=group_info["group_id"]
- )
- except Exception as e:
- logger.error(
- f"更新群聊信息失败",
- arparma.header_result,
- session=session,
- target=g,
- )
- await Text(f"成功更新了 {len(gl)} 个群的信息").send()
- logger.info(
- f"更新群聊信息完成,共更新了 {len(gl)} 个群的信息", arparma.header_result, session=session
- )
- except (ApiNotAvailable, AttributeError) as e:
- await Text("Api未实现...").send()
- except Exception as e:
- logger.error("更新好友信息发生错误", arparma.header_result, session=session, e=e)
- await Text("其他未知错误...").send()
-
-
-@_friend_matcher.assign("delete")
-async def _(
- bot: Bot,
- session: EventSession,
- arparma: Arparma,
-):
- num = 0
- error_list = []
- fl = await bot.get_friend_list()
- for f in fl:
- try:
- await FriendUser.update_or_create(
- user_id=str(f["user_id"]), defaults={"nickname": f["nickname"]}
- )
- logger.debug(f"更新好友信息成功", "更新好友信息", session=session, target=f["user_id"])
- num += 1
- except Exception as e:
- logger.error(f"更新好友信息失败", "更新好友信息", session=session, target=f["user_id"], e=e)
- await Text(f"成功更新了 {num} 个好友的信息!").send()
- if error_list:
- await Text(f"以下好友更新失败:\n" + "\n".join(error_list)).send()
- logger.info(f"更新好友信息完成,共更新了 {num} 个群的信息", arparma.header_result, session=session)
diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py b/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py
new file mode 100644
index 00000000..2c8bcca0
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py
@@ -0,0 +1,92 @@
+from nonebot.adapters import Bot
+from nonebot.adapters.kaiheila.exception import ApiNotAvailable
+from nonebot.permission import SUPERUSER
+from nonebot.plugin import PluginMetadata
+from nonebot.rule import to_me
+from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
+from nonebot_plugin_saa import Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.utils import PluginExtraData
+from zhenxun.models.friend_user import FriendUser
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+
+from ._data_source import FgUpdateManage
+
+__plugin_meta__ = PluginMetadata(
+ name="更新群组/好友信息",
+ description="更新群组/好友信息",
+ usage="""
+ 更新群组信息
+ 更新好友信息
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.SUPERUSER,
+ ).dict(),
+)
+
+
+_group_matcher = on_alconna(
+ Alconna(
+ "更新群组信息",
+ ),
+ permission=SUPERUSER,
+ rule=to_me(),
+ priority=1,
+ block=True,
+)
+
+_friend_matcher = on_alconna(
+ Alconna(
+ "更新好友信息",
+ ),
+ permission=SUPERUSER,
+ rule=to_me(),
+ priority=1,
+ block=True,
+)
+
+
+@_group_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+):
+ try:
+ num = await FgUpdateManage.update_group(bot, session.platform)
+ logger.info(
+ f"更新群聊信息完成,共更新了 {num} 个群组的信息!",
+ arparma.header_result,
+ session=session,
+ )
+ await Text(f"成功更新了 {num} 个群组的信息").send()
+ except Exception as e:
+ logger.error(
+ "更新群组信息发生错误", arparma.header_result, session=session, e=e
+ )
+ await Text("其他未知错误...").send()
+
+
+@_friend_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+):
+ try:
+ num = await FgUpdateManage.update_friend(bot, session.platform)
+ logger.info(
+ f"更新好友信息完成,共更新了 {num} 个好友的信息!",
+ arparma.header_result,
+ session=session,
+ )
+ await Text(f"成功更新了 {num} 个好友的信息").send()
+ except Exception as e:
+ logger.error(
+ "更新好友信息发生错误", arparma.header_result, session=session, e=e
+ )
+ await Text("其他未知错误...").send()
diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py b/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py
new file mode 100644
index 00000000..646a1136
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py
@@ -0,0 +1,156 @@
+from nonebot.adapters import Bot
+from nonebot.adapters.discord import Bot as DiscordBot
+from nonebot.adapters.dodo import Bot as DodoBot
+from nonebot.adapters.kaiheila import Bot as KaiheilaBot
+from nonebot.adapters.onebot.v11 import Bot as v11Bot
+from nonebot.adapters.onebot.v12 import Bot as v12Bot
+
+from zhenxun.models.friend_user import FriendUser
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.services.log import logger
+
+
+class FgUpdateManage:
+
+ @classmethod
+ async def update_group(cls, bot: Bot, platform: str) -> int:
+ """更新群组信息
+
+ 参数:
+ bot: Bot
+ platform: 平台
+
+ 返回:
+ int: 更新个数
+ """
+ create_list = []
+ if group_list := await cls.__get_group_list(bot):
+ exists_group_list = await GroupConsole.all().values_list(
+ "group_id", "channel_id"
+ )
+ for group in group_list:
+ group.platform = platform
+ if (group.group_id, group.channel_id) not in exists_group_list:
+ create_list.append(group)
+ logger.debug(
+ "群聊信息更新成功",
+ "更新群信息",
+ target=f"{group.group_id}:{group.channel_id}",
+ )
+ if create_list:
+ await GroupConsole.bulk_create(create_list, 10)
+ return len(create_list)
+
+ @classmethod
+ async def __get_group_list(cls, bot: Bot) -> list[GroupConsole]:
+ """获取群组列表
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ list[GroupConsole]: 群组列表
+ """
+ if isinstance(bot, v11Bot):
+ group_list = await bot.get_group_list()
+ return [
+ GroupConsole(
+ group_id=str(g["group_id"]),
+ group_name=g["group_name"],
+ max_member_count=g["max_member_count"],
+ member_count=g["member_count"],
+ )
+ for g in group_list
+ ]
+ if isinstance(bot, v12Bot):
+ group_list = await bot.get_group_list()
+ return [
+ GroupConsole(
+ group_id=g.group_id, # type: ignore
+ user_name=g.group_name, # type: ignore
+ )
+ for g in group_list
+ ]
+ if isinstance(bot, DodoBot):
+ island_list = await bot.get_island_list()
+ source_id_list = [
+ (g.island_source_id, g.island_name)
+ for g in island_list
+ if g.island_source_id
+ ]
+ group_list = []
+ for id, name in source_id_list:
+ channel_list = await bot.get_channel_list(island_source_id=id)
+ group_list.append(GroupConsole(group_id=id, group_name=name))
+ group_list += [
+ GroupConsole(
+ group_id=id, group_name=c.channel_name, channel_id=c.channel_id
+ )
+ for c in channel_list
+ ]
+ return group_list
+ if isinstance(bot, KaiheilaBot):
+ # TODO: kaiheila群组列表
+ pass
+ if isinstance(bot, DiscordBot):
+ # TODO: discord群组列表
+ pass
+ return []
+
+ @classmethod
+ async def update_friend(cls, bot: Bot, platform: str) -> int:
+ """更新好友信息
+
+ 参数:
+ bot: Bot
+ platform: 平台
+
+ 返回:
+ int: 更新个数
+ """
+ create_list = []
+ if friend_list := await cls.__get_friend_list(bot):
+ user_id_list = await FriendUser.all().values_list("user_id", flat=True)
+ for friend in friend_list:
+ friend.platform = platform
+ if friend.user_id not in user_id_list:
+ create_list.append(friend)
+ if create_list:
+ await FriendUser.bulk_create(create_list, 10)
+ return len(create_list)
+
+ @classmethod
+ async def __get_friend_list(cls, bot: Bot) -> list[FriendUser]:
+ """获取好友列表
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ list[FriendUser]: 好友列表
+ """
+ if isinstance(bot, v11Bot):
+ friend_list = await bot.get_friend_list()
+ return [
+ FriendUser(user_id=str(f["user_id"]), user_name=f["nickname"])
+ for f in friend_list
+ ]
+ if isinstance(bot, v12Bot):
+ friend_list = await bot.get_friend_list()
+ return [
+ FriendUser(
+ user_id=f.user_id, # type: ignore
+ user_name=f.user_displayname or f.user_remark or f.user_name, # type: ignore
+ )
+ for f in friend_list
+ ]
+ if isinstance(bot, DodoBot):
+ # TODO: dodo好友列表
+ pass
+ if isinstance(bot, KaiheilaBot):
+ # TODO: kaiheila好友列表
+ pass
+ if isinstance(bot, DiscordBot):
+ # TODO: discord好友列表
+ pass
+ return []
diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py
index 8ed7cd1e..7ef9e0f4 100644
--- a/zhenxun/configs/utils/__init__.py
+++ b/zhenxun/configs/utils/__init__.py
@@ -1,6 +1,6 @@
import copy
from pathlib import Path
-from typing import Any, Callable, Dict, List, Type, Union
+from typing import Any, Callable, Dict, Type
import cattrs
from pydantic import BaseModel
@@ -17,7 +17,6 @@ _yaml.allow_unicode = True
class RegisterConfig(BaseModel):
-
"""
注册配置项
"""
@@ -39,7 +38,6 @@ class RegisterConfig(BaseModel):
class ConfigModel(BaseModel):
-
"""
配置项
"""
@@ -57,7 +55,6 @@ class ConfigModel(BaseModel):
class ConfigGroup(BaseModel):
-
"""
配置组
"""
@@ -72,7 +69,7 @@ class ConfigGroup(BaseModel):
def get(self, c: str, default: Any = None) -> Any:
cfg = self.configs.get(c)
if cfg is not None:
- return cfg
+ return cfg.value
return default
@@ -130,6 +127,17 @@ class PluginSetting(BaseModel):
"""调用插件花费金币"""
+class Task(BaseBlock):
+ module: str
+ """被动技能模块名"""
+ name: str
+ """被动技能名称"""
+ status: bool = True
+ """全局开关状态"""
+ run_time: str | None = None
+ """运行时间"""
+
+
class PluginExtraData(BaseModel):
"""
插件扩展信息
@@ -139,18 +147,20 @@ class PluginExtraData(BaseModel):
"""作者"""
version: str | None = None
"""版本"""
- plugin_type: PluginType | None = None
+ plugin_type: PluginType = PluginType.NORMAL
"""插件类型"""
menu_type: str = "功能"
"""菜单类型"""
admin_level: int | None = None
"""管理员插件所需权限等级"""
- configs: List[RegisterConfig] | None = None
+ configs: list[RegisterConfig] | None = None
"""插件配置"""
setting: PluginSetting | None = None
"""插件基本配置"""
- limits: List[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None
+ limits: list[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None
"""插件限制"""
+ tasks: list[Task] | None = None
+ """技能被动"""
class NoSuchConfig(Exception):
@@ -287,7 +297,9 @@ class ConfigsManager:
if not config:
config = self._data[module].configs.get(f"{key} [LEVEL]")
if not config:
- raise NoSuchConfig(f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]")
+ raise NoSuchConfig(
+ f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]"
+ )
if config.arg_parser:
value = config.arg_parser(value or config.default_value)
else:
diff --git a/zhenxun/models/bag_user.py b/zhenxun/models/bag_user.py
new file mode 100644
index 00000000..7f3b0322
--- /dev/null
+++ b/zhenxun/models/bag_user.py
@@ -0,0 +1,160 @@
+# from typing import Dict
+
+# from services.db_context import Model
+# from tortoise import fields
+
+# from .goods_info import GoodsInfo
+
+
+# class BagUser(Model):
+
+# id = fields.IntField(pk=True, generated=True, auto_increment=True)
+# """自增id"""
+# user_id = fields.CharField(255)
+# """用户id"""
+# group_id = fields.CharField(255)
+# """群聊id"""
+# gold = fields.IntField(default=100)
+# """金币数量"""
+# spend_total_gold = fields.IntField(default=0)
+# """花费金币总数"""
+# get_total_gold = fields.IntField(default=0)
+# """获取金币总数"""
+# get_today_gold = fields.IntField(default=0)
+# """今日获取金币"""
+# spend_today_gold = fields.IntField(default=0)
+# """今日获取金币"""
+# property: Dict[str, int] = fields.JSONField(default={}) # type: ignore
+# """道具"""
+
+# class Meta:
+# table = "bag_users"
+# table_description = "用户道具数据表"
+# unique_together = ("user_id", "group_id")
+
+# @classmethod
+# async def get_gold(cls, user_id: str, group_id: str) -> int:
+# """获取当前金币
+
+# 参数:
+# user_id: 用户id
+# group_id: 所在群组id
+
+# 返回:
+# int: 金币数量
+# """
+# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id)
+# return user.gold
+
+# @classmethod
+# async def get_property(
+# cls, user_id: str, group_id: str, only_active: bool = False
+# ) -> Dict[str, int]:
+# """获取当前道具
+
+# 参数:
+# user_id: 用户id
+# group_id: 所在群组id
+# only_active: 仅仅获取主动使用的道具
+
+# 返回:
+# Dict[str, int]: 道具名称与数量
+# """
+# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id)
+# if only_active and user.property:
+# data = {}
+# name_list = [
+# x.goods_name
+# for x in await GoodsInfo.get_all_goods()
+# if not x.is_passive
+# ]
+# for key in [x for x in user.property if x in name_list]:
+# data[key] = user.property[key]
+# return data
+# return user.property
+
+# @classmethod
+# async def add_gold(cls, user_id: str, group_id: str, num: int):
+# """增加金币
+
+# 参数:
+# user_id: 用户id
+# group_id: 所在群组id
+# num: 金币数量
+# """
+# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id)
+# user.gold = user.gold + num
+# user.get_total_gold = user.get_total_gold + num
+# user.get_today_gold = user.get_today_gold + num
+# await user.save(update_fields=["gold", "get_today_gold", "get_total_gold"])
+
+# @classmethod
+# async def spend_gold(cls, user_id: str, group_id: str, num: int):
+# """花费金币
+
+# 参数:
+# user_id: 用户id
+# group_id: 所在群组id
+# num: 金币数量
+# """
+# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id))
+# user.gold = user.gold - num
+# user.spend_total_gold = user.spend_total_gold + num
+# user.spend_today_gold = user.spend_today_gold + num
+# await user.save(update_fields=["gold", "spend_total_gold", "spend_today_gold"])
+
+# @classmethod
+# async def add_property(cls, user_id: str, group_id: str, name: str, num: int = 1):
+# """增加道具
+
+# 参数:
+# user_id: 用户id
+# group_id: 所在群组id
+# name: 道具名称
+# num: 道具数量
+# """
+# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id))
+# property_ = user.property
+# if property_.get(name) is None:
+# property_[name] = 0
+# property_[name] += num
+# user.property = property_
+# await user.save(update_fields=["property"])
+
+# @classmethod
+# async def delete_property(
+# cls, user_id: str, group_id: str, name: str, num: int = 1
+# ) -> bool:
+# """使用/删除 道具
+
+# 参数:
+# user_id: 用户id
+# group_id: 所在群组id
+# name: 道具名称
+# num: 使用个数
+
+# 返回:
+# bool: 是否使用/删除成功
+# """
+# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id))
+# property_ = user.property
+# if name in property_:
+# if (n := property_.get(name, 0)) < num:
+# return False
+# if n == num:
+# del property_[name]
+# else:
+# property_[name] -= num
+# await user.save(update_fields=["property"])
+# return True
+# return False
+
+# @classmethod
+# async def _run_script(cls):
+# return [
+# "ALTER TABLE bag_users DROP props;", # 删除 props 字段
+# "ALTER TABLE bag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id
+# "ALTER TABLE bag_users ALTER COLUMN user_id TYPE character varying(255);",
+# # 将user_id字段类型改为character varying(255)
+# "ALTER TABLE bag_users ALTER COLUMN group_id TYPE character varying(255);",
+# ]
diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py
new file mode 100644
index 00000000..5996e55f
--- /dev/null
+++ b/zhenxun/models/ban_console.py
@@ -0,0 +1,172 @@
+import time
+
+from tortoise import fields
+from typing_extensions import Self
+
+from zhenxun.services.db_context import Model
+from zhenxun.services.log import logger
+from zhenxun.utils.exception import UserAndGroupIsNone
+
+
+class BanConsole(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255, null=True)
+ """用户id"""
+ group_id = fields.CharField(255, null=True)
+ """群组id"""
+ ban_level = fields.IntField()
+ """使用ban命令的用户等级"""
+ ban_time = fields.BigIntField()
+ """ban开始的时间"""
+ duration = fields.BigIntField()
+ """ban时长"""
+ operator = fields.CharField(255)
+ """使用Ban命令的用户"""
+
+ class Meta:
+ table = "ban_console"
+ table_description = ".ban/b了 封禁人员/群组数据表"
+
+ @classmethod
+ async def _get_data(cls, user_id: str | None, group_id: str | None) -> Self | None:
+ """获取数据
+
+ 参数:
+ user_id: 用户id
+ group_id: 群组id
+
+ 异常:
+ UserAndGroupIsNone: 用户id和群组id都为空
+
+ 返回:
+ Self | None: Self
+ """
+ if not user_id and not group_id:
+ raise UserAndGroupIsNone()
+ user = None
+ if user_id:
+ if group_id:
+ user = await cls.get_or_none(user_id=user_id, group_id=group_id)
+ else:
+ user = await cls.get_or_none(user_id=user_id, group_id__isnull=True)
+ else:
+ if group_id:
+ user = await cls.get_or_none(user_id__isnull=True, group_id=group_id)
+ return user
+
+ @classmethod
+ async def check_ban_level(
+ cls, user_id: str | None, group_id: str | None, level: int
+ ) -> bool:
+ """检测ban掉目标的用户与unban用户的权限等级大小
+
+ 参数:
+ user_id: 用户id
+ group_id: 群组id
+ level: 权限等级
+
+ 返回:
+ bool: 权限判断
+ """
+ user = await cls._get_data(user_id, group_id)
+ if user:
+ logger.debug(
+ f"检测用户被ban等级,user_level: {user.ban_level},level: {level}",
+ target=f"{group_id}:{user_id}",
+ )
+ return bool(user and user.ban_level >= level)
+ return False
+
+ @classmethod
+ async def check_ban_time(
+ cls, user_id: str | None, group_id: str | None = None
+ ) -> int:
+ """检测用户被ban时长
+
+ 参数:
+ user_id: 用户id
+
+ 返回:
+ int: ban剩余时长,-1时为永久ban,0表示未被ban
+ """
+ logger.debug(f"获取用户ban时长", target=f"{group_id}:{user_id}")
+ user = await cls._get_data(user_id, group_id)
+ if user:
+ if user.duration == -1:
+ return -1
+ _time = time.time() - (user.ban_time + user.duration)
+ if _time > 0:
+ return 0
+ return int(time.time() - user.ban_time - user.duration)
+ return 0
+
+ @classmethod
+ async def is_ban(cls, user_id: str | None, group_id: str | None = None) -> bool:
+ """判断用户是否被ban
+
+ 参数:
+ user_id: 用户id
+
+ 返回:
+ bool: 是否被ban
+ """
+ logger.debug(f"检测是否被ban", target=f"{group_id}:{user_id}")
+ if await cls.check_ban_time(user_id, group_id):
+ return True
+ else:
+ await cls.unban(user_id, group_id)
+ return False
+
+ @classmethod
+ async def ban(
+ cls,
+ user_id: str | None,
+ group_id: str | None,
+ ban_level: int,
+ duration: int,
+ operator: str | None,
+ ):
+ """ban掉目标用户
+
+ 参数:
+ user_id: 用户id
+ group_id: 群组id
+ ban_level: 使用命令者的权限等级
+ duration: 时长,秒
+ operator: 操作者id
+ """
+ logger.debug(
+ f"封禁用户,等级:{ban_level},时长: {duration}",
+ target=f"{group_id}:{user_id}",
+ )
+ user = await cls._get_data(user_id, group_id)
+ if user:
+ await cls.unban(user_id, group_id)
+ await cls.create(
+ user_id=user_id,
+ group_id=group_id,
+ ban_level=ban_level,
+ ban_time=int(time.time()),
+ duration=duration,
+ operator=operator or 0,
+ )
+
+ @classmethod
+ async def unban(cls, user_id: str | None, group_id: str | None = None) -> bool:
+ """unban用户
+
+ 参数:
+ user_id: 用户id
+ group_id: 群组id
+
+ 返回:
+ bool: 是否被ban
+ """
+ user = await cls._get_data(user_id, group_id)
+ if user:
+ logger.debug("解除封禁", target=f"{group_id}:{user_id}")
+ await user.delete()
+ return True
+ return False
diff --git a/zhenxun/models/chat_history.py b/zhenxun/models/chat_history.py
new file mode 100644
index 00000000..117397ce
--- /dev/null
+++ b/zhenxun/models/chat_history.py
@@ -0,0 +1,125 @@
+from datetime import datetime, timedelta
+from typing import Literal, Tuple
+
+from tortoise import fields
+from tortoise.functions import Count
+from typing_extensions import Self
+
+from zhenxun.services.db_context import Model
+
+
+class ChatHistory(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255)
+ """用户id"""
+ group_id = fields.CharField(255, null=True)
+ """群聊id"""
+ text = fields.TextField(null=True)
+ """文本内容"""
+ plain_text = fields.TextField(null=True)
+ """纯文本"""
+ create_time = fields.DatetimeField(auto_now_add=True)
+ """创建时间"""
+ bot_id = fields.CharField(255, null=True)
+ """bot记录id"""
+ platform = fields.CharField(255, null=True)
+ """平台"""
+
+ class Meta:
+ table = "chat_history"
+ table_description = "聊天记录数据表"
+
+ @classmethod
+ async def get_group_msg_rank(
+ cls,
+ gid: str,
+ limit: int = 10,
+ order: str = "DESC",
+ date_scope: tuple[datetime, datetime] | None = None,
+ ) -> list[Self]:
+ """获取排行数据
+
+ 参数:
+ gid: 群号
+ limit: 获取数量
+ order: 排序类型,desc,des
+ date_scope: 日期范围
+ """
+ o = "-" if order == "DESC" else ""
+ query = cls.filter(group_id=gid)
+ if date_scope:
+ query = query.filter(create_time__range=date_scope)
+ return list(
+ await query.annotate(count=Count("user_id"))
+ .order_by(o + "count")
+ .group_by("user_id")
+ .limit(limit)
+ .values_list("user_id", "count")
+ ) # type: ignore
+
+ @classmethod
+ async def get_group_first_msg_datetime(cls, group_id: str) -> datetime | None:
+ """获取群第一条记录消息时间
+
+ 参数:
+ group_id: 群组id
+ """
+ if (
+ message := await cls.filter(group_id=group_id)
+ .order_by("create_time")
+ .first()
+ ):
+ return message.create_time
+
+ @classmethod
+ async def get_message(
+ cls,
+ uid: str,
+ gid: str,
+ type_: Literal["user", "group"],
+ msg_type: Literal["private", "group"] | None = None,
+ days: int | Tuple[datetime, datetime] | None = None,
+ ) -> list[Self]:
+ """获取消息查询query
+
+ 参数:
+ uid: 用户id
+ gid: 群聊id
+ type_: 类型,私聊或群聊
+ msg_type: 消息类型,用户或群聊
+ days: 限制日期
+ """
+ if type_ == "user":
+ query = cls.filter(user_id=uid)
+ if msg_type == "private":
+ query = query.filter(group_id__isnull=True)
+ elif msg_type == "group":
+ query = query.filter(group_id__not_isnull=True)
+ else:
+ query = cls.filter(group_id=gid)
+ if uid:
+ query = query.filter(user_id=uid)
+ if days:
+ if isinstance(days, int):
+ query = query.filter(
+ create_time__gte=datetime.now() - timedelta(days=days)
+ )
+ elif isinstance(days, tuple):
+ query = query.filter(create_time__range=days)
+ return await query.all() # type: ignore
+
+ @classmethod
+ async def _run_script(cls):
+ return [
+ "alter table chat_history alter group_id drop not null;", # 允许 group_id 为空
+ "alter table chat_history alter text drop not null;", # 允许 text 为空
+ "alter table chat_history alter plain_text drop not null;", # 允许 plain_text 为空
+ "ALTER TABLE chat_history RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id
+ "ALTER TABLE chat_history ALTER COLUMN user_id TYPE character varying(255);",
+ "ALTER TABLE chat_history ALTER COLUMN group_id TYPE character varying(255);",
+ "ALTER TABLE chat_history ADD bot_id VARCHAR(255);", # 添加bot_id字段
+ "ALTER TABLE chat_history ALTER COLUMN bot_id TYPE character varying(255);",
+ "ALTER TABLE chat_history ADD COLUMN platform character varying(255);",
+ ]
diff --git a/zhenxun/models/friend_user.py b/zhenxun/models/friend_user.py
index f871e389..597ce4f1 100644
--- a/zhenxun/models/friend_user.py
+++ b/zhenxun/models/friend_user.py
@@ -15,32 +15,32 @@ class FriendUser(Model):
"""用户名称"""
nickname = fields.CharField(max_length=255, null=True, description="用户自定义昵称")
"""私聊下自定义昵称"""
+ platform = fields.CharField(255, null=True, description="平台")
+ """平台"""
class Meta:
table = "friend_users"
table_description = "好友信息数据表"
@classmethod
- async def get_user_name(cls, user_id: Union[int, str]) -> str:
- """
- 说明:
- 获取好友用户名称
+ async def get_user_name(cls, user_id: str) -> str:
+ """获取好友用户名称
+
参数:
- :param user_id: 用户id
+ user_id: 用户id
"""
- if user := await cls.get_or_none(user_id=str(user_id)):
+ if user := await cls.get_or_none(user_id=user_id):
return user.user_name
return ""
@classmethod
- async def get_user_nickname(cls, user_id: Union[int, str]) -> str:
- """
- 说明:
- 获取用户昵称
+ async def get_user_nickname(cls, user_id: str) -> str:
+ """获取用户昵称
+
参数:
- :param user_id: 用户id
+ user_id: 用户id
"""
- if user := await cls.get_or_none(user_id=str(user_id)):
+ if user := await cls.get_or_none(user_id=user_id):
if user.nickname:
_tmp = ""
if black_word := Config.get_config("nickname", "BLACK_WORD"):
@@ -50,21 +50,34 @@ class FriendUser(Model):
return ""
@classmethod
- async def set_user_nickname(cls, user_id: Union[int, str], nickname: str):
- """
- 说明:
- 设置用户昵称
+ async def set_user_nickname(
+ cls,
+ user_id: str,
+ nickname: str,
+ uname: str | None = None,
+ platform: str | None = None,
+ ):
+ """设置用户昵称
+
参数:
- :param user_id: 用户id
- :param nickname: 昵称
+ user_id: 用户id
+ nickname: 昵称
+ uname: 用户昵称
+ platform: 平台
"""
+ defaults = {"nickname": nickname}
+ if uname is not None:
+ defaults["user_name"] = uname
+ if platform is not None:
+ defaults["platform"] = platform
await cls.update_or_create(
- user_id=str(user_id), defaults={"nickname": nickname}
+ user_id=user_id,
+ defaults=defaults,
)
@classmethod
- async def _run_script(cls):
- await cls.raw(
- "ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);"
- )
- # 将user_id字段类型改为character varying(255))
+ def _run_script(cls):
+ return [
+ "ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);",
+ "ALTER TABLE friend_users ADD COLUMN platform character varying(255) default 'qq';",
+ ]
diff --git a/zhenxun/models/goods_info.py b/zhenxun/models/goods_info.py
new file mode 100644
index 00000000..60f64c4b
--- /dev/null
+++ b/zhenxun/models/goods_info.py
@@ -0,0 +1,162 @@
+import uuid
+from typing import Dict
+
+from tortoise import fields
+from typing_extensions import Self
+
+from zhenxun.services.db_context import Model
+
+
+class GoodsInfo(Model):
+ __tablename__ = "goods_info"
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ uuid = fields.CharField(255, null=True)
+ """uuid"""
+ goods_name = fields.CharField(255, unique=True)
+ """商品名称"""
+ goods_price = fields.IntField()
+ """价格"""
+ goods_description = fields.TextField()
+ """描述"""
+ goods_discount = fields.FloatField(default=1)
+ """折扣"""
+ goods_limit_time = fields.BigIntField(default=0)
+ """限时"""
+ daily_limit = fields.IntField(default=0)
+ """每日限购"""
+ is_passive = fields.BooleanField(default=False)
+ """是否为被动道具"""
+ icon = fields.TextField(null=True)
+ """图标路径"""
+
+ class Meta:
+ table = "goods_info"
+ table_description = "商品数据表"
+
+ @classmethod
+ async def add_goods(
+ cls,
+ goods_name: str,
+ goods_price: int,
+ goods_description: str,
+ goods_discount: float = 1,
+ goods_limit_time: int = 0,
+ daily_limit: int = 0,
+ is_passive: bool = False,
+ icon: str | None = None,
+ ):
+ """添加商品
+
+ 参数:
+ goods_name: 商品名称
+ goods_price: 商品价格
+ goods_description: 商品简介
+ goods_discount: 商品折扣
+ goods_limit_time: 商品限时
+ daily_limit: 每日购买限制
+ is_passive: 是否为被动道具
+ icon: 图标
+ """
+ if not await cls.exists(goods_name=goods_name):
+ await cls.create(
+ uuid=uuid.uuid1(),
+ goods_name=goods_name,
+ goods_price=goods_price,
+ goods_description=goods_description,
+ goods_discount=goods_discount,
+ goods_limit_time=goods_limit_time,
+ daily_limit=daily_limit,
+ is_passive=is_passive,
+ icon=icon,
+ )
+
+ @classmethod
+ async def delete_goods(cls, goods_name: str) -> bool:
+ """删除商品
+
+ 参数:
+ goods_name: 商品名称
+
+ 返回:
+ bool: 是否删除成功
+ """
+ if goods := await cls.get_or_none(goods_name=goods_name):
+ await goods.delete()
+ return True
+ return False
+
+ @classmethod
+ async def update_goods(
+ cls,
+ goods_name: str,
+ goods_price: int | None = None,
+ goods_description: str | None = None,
+ goods_discount: float | None = None,
+ goods_limit_time: int | None = None,
+ daily_limit: int | None = None,
+ is_passive: bool | None = None,
+ icon: str | None = None,
+ ):
+ """更新商品信息
+
+ 参数:
+ goods_name: 商品名称
+ goods_price: 商品价格
+ goods_description: 商品简介
+ goods_discount: 商品折扣
+ goods_limit_time: 商品限时时间
+ daily_limit: 每日次数限制
+ is_passive: 是否为被动
+ icon: 图标
+ """
+ if goods := await cls.get_or_none(goods_name=goods_name):
+ await cls.update_or_create(
+ goods_name=goods_name,
+ defaults={
+ "goods_price": goods_price or goods.goods_price,
+ "goods_description": goods_description or goods.goods_description,
+ "goods_discount": goods_discount or goods.goods_discount,
+ "goods_limit_time": (
+ goods_limit_time
+ if goods_limit_time is not None
+ else goods.goods_limit_time
+ ),
+ "daily_limit": (
+ daily_limit if daily_limit is not None else goods.daily_limit
+ ),
+ "is_passive": (
+ is_passive if is_passive is not None else goods.is_passive
+ ),
+ "icon": icon or goods.icon,
+ },
+ )
+
+ @classmethod
+ async def get_all_goods(cls) -> list[Self]:
+ """
+ 获得全部有序商品对象
+ """
+ query = await cls.all()
+ id_lst = [x.id for x in query]
+ goods_lst = []
+ for _ in range(len(query)):
+ min_id = min(id_lst)
+ goods_lst.append([x for x in query if x.id == min_id][0])
+ id_lst.remove(min_id)
+ return goods_lst
+
+ @classmethod
+ async def _run_script(cls):
+ if goods_list := await cls.filter(uuid__isnull=True).all():
+ for goods in goods_list:
+ goods.uuid = uuid.uuid1()
+ await cls.bulk_update(goods_list, ["uuid"], 10)
+ return [
+ "ALTER TABLE goods_info ADD uuid VARCHAR(255);",
+ "ALTER TABLE goods_info ADD daily_limit Integer DEFAULT 0;",
+ "ALTER TABLE goods_info ADD is_passive boolean DEFAULT False;",
+ "ALTER TABLE goods_info ADD icon VARCHAR(255);",
+ "ALTER TABLE goods_info DROP daily_purchase_limit;", # 删除 daily_purchase_limit 字段
+ ]
diff --git a/zhenxun/models/group_console.py b/zhenxun/models/group_console.py
new file mode 100644
index 00000000..a0ff26fe
--- /dev/null
+++ b/zhenxun/models/group_console.py
@@ -0,0 +1,54 @@
+from tortoise import fields
+
+from zhenxun.services.db_context import Model
+
+
+class GroupConsole(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ group_id = fields.CharField(255, description="群组id")
+ """群聊id"""
+ channel_id = fields.CharField(255, null=True, description="频道id")
+ """频道id"""
+ group_name = fields.TextField(default="", description="群组名称")
+ """群聊名称"""
+ max_member_count = fields.IntField(default=0, description="最大人数")
+ """最大人数"""
+ member_count = fields.IntField(default=0, description="当前人数")
+ """当前人数"""
+ group_flag = fields.IntField(default=0, description="群认证标记")
+ """群认证标记"""
+ block_plugin = fields.TextField(default="", description="禁用插件")
+ """禁用插件"""
+ block_task = fields.TextField(default="", description="禁用插件")
+ """禁用插件"""
+ platform = fields.CharField(255, default="qq", description="所属平台")
+ """所属平台"""
+
+ class Meta:
+ table = "group_console"
+ table_description = "群组信息表"
+ unique_together = ("group_id", "channel_id")
+
+ @classmethod
+ async def is_block_task(
+ cls, group_id: str, task: str, channel_id: str | None = None
+ ) -> bool:
+ """查看群组是否禁用被动
+
+ 参数:
+ group_id: 群组id
+ task: 任务模块
+ channel_id: 频道id
+
+ 返回:
+ bool: 是否禁用被动
+ """
+ return await cls.exists(
+ group_id=group_id, channel_id=channel_id, block_task__contains=f"{task},"
+ )
+
+ @classmethod
+ def _run_script(cls):
+ return []
diff --git a/zhenxun/models/group_info copy.py b/zhenxun/models/group_info copy.py
deleted file mode 100644
index 64df8c5a..00000000
--- a/zhenxun/models/group_info copy.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from typing import List, Optional
-
-from tortoise import fields
-
-from zhenxun.services.db_context import Model
-from zhenxun.services.log import logger
-
-
-class GroupInfo(Model):
- group_id = fields.CharField(255, pk=True)
- """群聊id"""
- group_name = fields.TextField(default="")
- """群聊名称"""
- max_member_count = fields.IntField(default=0)
- """最大人数"""
- member_count = fields.IntField(default=0)
- """当前人数"""
- group_flag = fields.IntField(default=0)
- """群认证标记"""
-
- class Meta:
- table = "group_info"
- table_description = "群聊信息表"
-
- @classmethod
- def _run_script(cls):
- return [
- "ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag
- "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);"
- # 将group_id字段类型改为character varying(255)
- ]
diff --git a/zhenxun/models/group_info.py b/zhenxun/models/group_info.py
index e8249c44..8183bf16 100644
--- a/zhenxun/models/group_info.py
+++ b/zhenxun/models/group_info.py
@@ -1,5 +1,3 @@
-from typing import List, Optional
-
from tortoise import fields
from zhenxun.services.db_context import Model
@@ -8,6 +6,8 @@ from zhenxun.services.db_context import Model
class GroupInfo(Model):
group_id = fields.CharField(255, pk=True, description="群组id")
"""群聊id"""
+ # channel_id = fields.CharField(255, description="群组id")
+ # """频道id"""
group_name = fields.TextField(default="", description="群组名称")
"""群聊名称"""
max_member_count = fields.IntField(default=0, description="最大人数")
@@ -16,15 +16,36 @@ class GroupInfo(Model):
"""当前人数"""
group_flag = fields.IntField(default=0, description="群认证标记")
"""群认证标记"""
+ block_plugin = fields.TextField(default="", description="禁用插件")
+ """禁用插件"""
+ block_task = fields.TextField(default="", description="禁用插件")
+ """禁用插件"""
+ platform = fields.CharField(255, default="qq", description="所属平台")
+ """所属平台"""
class Meta:
table = "group_info"
table_description = "群聊信息表"
+ @classmethod
+ async def is_block_task(cls, group_id: str, task: str) -> bool:
+ """查看群组是否禁用被动
+
+ 参数:
+ group_id: 群组id
+ task: 任务模块
+
+ 返回:
+ bool: 是否禁用被动
+ """
+ return await cls.exists(group_id=group_id, block_task__contains=f"{task},")
+
@classmethod
def _run_script(cls):
return [
"ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag
- "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);"
- # 将group_id字段类型改为character varying(255)
+ "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);",
+ "ALTER TABLE group_info ADD block_plugin Text NOT NULL DEFAULT '';",
+ "ALTER TABLE group_info ADD block_task Text NOT NULL DEFAULT '';",
+ "ALTER TABLE group_info ADD platform character varying(255) NOT NULL DEFAULT 'qq';",
]
diff --git a/zhenxun/models/group_member_info.py b/zhenxun/models/group_member_info.py
index e7bbe586..cf73f14d 100644
--- a/zhenxun/models/group_member_info.py
+++ b/zhenxun/models/group_member_info.py
@@ -1,9 +1,8 @@
-from datetime import datetime
-from typing import List, Optional, Set, Union
+from typing import Set
from tortoise import fields
-
from zhenxun.configs.config import Config
+
from zhenxun.services.db_context import Model
from zhenxun.services.log import logger
@@ -23,6 +22,8 @@ class GroupInfoUser(Model):
"""群聊昵称"""
uid = fields.BigIntField(null=True)
"""用户uid"""
+ platform = fields.CharField(255, null=True, description="平台")
+ """平台"""
class Meta:
table = "group_info_users"
@@ -30,59 +31,65 @@ class GroupInfoUser(Model):
unique_together = ("user_id", "group_id")
@classmethod
- async def get_group_member_id_list(cls, group_id: Union[int, str]) -> Set[int]:
- """
- 说明:
- 获取该群所有用户id
+ async def get_group_member_id_list(cls, group_id: str) -> Set[int]:
+ """获取该群所有用户id
+
参数:
- :param group_id: 群号
+ group_id: 群号
"""
return set(
- await cls.filter(group_id=str(group_id)).values_list("user_id", flat=True)
+ await cls.filter(group_id=group_id).values_list("user_id", flat=True)
) # type: ignore
@classmethod
async def set_user_nickname(
- cls, user_id: Union[int, str], group_id: Union[int, str], nickname: str
+ cls,
+ user_id: str,
+ group_id: str,
+ nickname: str,
+ uname: str | None = None,
+ platform: str | None = None,
):
- """
- 说明:
- 设置群员在该群内的昵称
+ """设置群员在该群内的昵称
+
参数:
- :param user_id: 用户id
- :param group_id: 群号
- :param nickname: 昵称
+ user_id: 用户id
+ group_id: 群号
+ nickname: 昵称
+ uname: 用户昵称
+ platform: 平台
"""
+ defaults = {"nickname": nickname}
+ if uname is not None:
+ defaults["user_name"] = uname
+ if platform is not None:
+ defaults["platform"] = platform
await cls.update_or_create(
- user_id=str(user_id),
- group_id=str(group_id),
- defaults={"nickname": nickname},
+ user_id=user_id,
+ group_id=group_id,
+ defaults=defaults,
)
@classmethod
- async def get_user_all_group(cls, user_id: Union[int, str]) -> List[int]:
- """
- 说明:
- 获取该用户所在的所有群聊
+ async def get_user_all_group(cls, user_id: str) -> list[int]:
+ """获取该用户所在的所有群聊
+
参数:
- :param user_id: 用户id
+ user_id: 用户id
"""
return list(
await cls.filter(user_id=str(user_id)).values_list("group_id", flat=True)
) # type: ignore
@classmethod
- async def get_user_nickname(
- cls, user_id: Union[int, str], group_id: Union[int, str]
- ) -> str:
- """
- 说明:
- 获取用户在该群的昵称
+ async def get_user_nickname(cls, user_id: str, group_id: str) -> str:
+ """获取用户在该群的昵称
+
参数:
- :param user_id: 用户id
- :param group_id: 群号
+ user_id: 用户id
+ group_id: 群号
"""
- if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)):
+ if user := await cls.get_or_none(user_id=user_id, group_id=group_id):
if user.nickname:
nickname = ""
if black_word := Config.get_config("nickname", "BLACK_WORD"):
@@ -92,28 +99,6 @@ class GroupInfoUser(Model):
return user.nickname
return ""
- @classmethod
- async def get_group_member_uid(
- cls, user_id: Union[int, str], group_id: Union[int, str]
- ) -> Optional[int]:
- logger.debug(
- f"GroupInfoUser 尝试获取 用户[{user_id}] 群聊[{group_id}] UID"
- )
- user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id))
- _max_uid_user, _ = await cls.get_or_create(user_id="114514", group_id="114514")
- _max_uid = _max_uid_user.uid
- if not user.uid:
- all_user = await cls.filter(user_id=str(user_id)).all()
- for x in all_user:
- if x.uid:
- return x.uid
- user.uid = _max_uid + 1
- _max_uid_user.uid = _max_uid + 1
- await cls.bulk_update([user, _max_uid_user], ["uid"])
- logger.debug(
- f"GroupInfoUser 获取 用户[{user_id}] 群聊[{group_id}] UID: {user.uid}"
- )
- return user.uid
@classmethod
async def _run_script(cls):
@@ -124,4 +109,5 @@ class GroupInfoUser(Model):
"ALTER TABLE group_info_users ALTER COLUMN user_id TYPE character varying(255);",
# 将user_id字段类型改为character varying(255)
"ALTER TABLE group_info_users ALTER COLUMN group_id TYPE character varying(255);",
+ "ALTER TABLE group_info_users ADD COLUMN platform character varying(255) default 'qq';",
]
diff --git a/zhenxun/models/level_user.py b/zhenxun/models/level_user.py
index 1495a315..da229216 100644
--- a/zhenxun/models/level_user.py
+++ b/zhenxun/models/level_user.py
@@ -24,9 +24,8 @@ class LevelUser(Model):
unique_together = ("user_id", "group_id")
@classmethod
- async def get_user_level(cls, user_id: int | str, group_id: int | str) -> int:
- """
- 获取用户在群内的等级
+ async def get_user_level(cls, user_id: str, group_id: str | None) -> int:
+ """获取用户在群内的等级
参数:
user_id: 用户id
@@ -35,20 +34,21 @@ class LevelUser(Model):
返回:
int: 权限等级
"""
- if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)):
+ if not group_id:
+ return 0
+ if user := await cls.get_or_none(user_id=user_id, group_id=group_id):
return user.user_level
return 0
@classmethod
async def set_level(
cls,
- user_id: int | str,
- group_id: int | str,
+ user_id: str,
+ group_id: str,
level: int,
group_flag: int = 0,
):
- """
- 设置用户在群内的权限
+ """设置用户在群内的权限
参数:
user_id: 用户id
@@ -57,8 +57,8 @@ class LevelUser(Model):
group_flag: 是否被自动更新刷新权限 0:是, 1:否.
"""
await cls.update_or_create(
- user_id=str(user_id),
- group_id=str(group_id),
+ user_id=user_id,
+ group_id=group_id,
defaults={
"user_level": level,
"group_flag": group_flag,
@@ -66,9 +66,8 @@ class LevelUser(Model):
)
@classmethod
- async def delete_level(cls, user_id: int | str, group_id: int | str) -> bool:
- """
- 删除用户权限
+ async def delete_level(cls, user_id: str, group_id: str) -> bool:
+ """删除用户权限
参数:
user_id: 用户id
@@ -77,17 +76,14 @@ class LevelUser(Model):
返回:
bool: 是否含有用户权限
"""
- if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)):
+ if user := await cls.get_or_none(user_id=user_id, group_id=group_id):
await user.delete()
return True
return False
@classmethod
- async def check_level(
- cls, user_id: int | str, group_id: int | str, level: int
- ) -> bool:
- """
- 检查用户权限等级是否大于 level
+ async def check_level(cls, user_id: str, group_id: str, level: int) -> bool:
+ """检查用户权限等级是否大于 level
参数:
user_id: 用户id
@@ -98,20 +94,17 @@ class LevelUser(Model):
bool: 是否大于level
"""
if group_id:
- if user := await cls.get_or_none(
- user_id=str(user_id), group_id=str(group_id)
- ):
+ if user := await cls.get_or_none(user_id=user_id, group_id=group_id):
return user.user_level >= level
else:
- user_list = await cls.filter(user_id=str(user_id)).all()
+ user_list = await cls.filter(user_id=user_id).all()
user = max(user_list, key=lambda x: x.user_level)
return user.user_level >= level
return False
@classmethod
- async def is_group_flag(cls, user_id: int | str, group_id: int | str) -> bool:
- """
- 检测是否会被自动更新刷新权限
+ async def is_group_flag(cls, user_id: str, group_id: str) -> bool:
+ """检测是否会被自动更新刷新权限
参数:
user_id: 用户id
@@ -120,7 +113,7 @@ class LevelUser(Model):
返回:
bool: 是否会被自动更新权限刷新
"""
- if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)):
+ if user := await cls.get_or_none(user_id=user_id, group_id=group_id):
return user.group_flag == 1
return False
diff --git a/zhenxun/models/plugin_info.py b/zhenxun/models/plugin_info.py
index 15eb2a0a..0eaea950 100644
--- a/zhenxun/models/plugin_info.py
+++ b/zhenxun/models/plugin_info.py
@@ -17,7 +17,7 @@ class PluginInfo(Model):
"""插件名称"""
status = fields.BooleanField(default=True, description="全局开关状态")
"""全局开关状态"""
- block_type = fields.CharEnumField(
+ block_type: BlockType | None = fields.CharEnumField(
BlockType, default=None, null=True, description="禁用类型"
)
"""禁用类型"""
diff --git a/zhenxun/models/plugin_limit.py b/zhenxun/models/plugin_limit.py
index 89247886..9bf4259f 100644
--- a/zhenxun/models/plugin_limit.py
+++ b/zhenxun/models/plugin_limit.py
@@ -22,6 +22,7 @@ class PluginLimit(Model):
on_delete=fields.CASCADE,
description="所属插件",
)
+ """所属插件"""
limit_type = fields.CharEnumField(PluginLimitType, description="限制类型")
"""限制类型"""
watch_type = fields.CharEnumField(LimitWatchType, description="监听类型")
diff --git a/zhenxun/models/sign_log.py b/zhenxun/models/sign_log.py
new file mode 100644
index 00000000..fffdee2c
--- /dev/null
+++ b/zhenxun/models/sign_log.py
@@ -0,0 +1,26 @@
+from datetime import datetime
+from typing import List, Literal, Optional, Tuple, Union
+
+from tortoise import fields
+
+from zhenxun.services.db_context import Model
+
+
+class SignLog(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255, unique=True, description="用户id")
+ """用户id"""
+ impression = fields.DecimalField(10, 3, default=0, description="好感度")
+ """好感度"""
+ create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
+ """创建时间"""
+ bot_id = fields.CharField(255, null=True, description="botId")
+ """bot记录id"""
+ platform = fields.CharField(255, null=True, description="平台")
+ """平台"""
+
+ class Meta:
+ table = "sign_log"
+ table_description = "用户签到记录表"
diff --git a/zhenxun/models/sign_user.py b/zhenxun/models/sign_user.py
new file mode 100644
index 00000000..eb25b6cb
--- /dev/null
+++ b/zhenxun/models/sign_user.py
@@ -0,0 +1,72 @@
+from tortoise import fields
+from typing_extensions import Self
+
+from zhenxun.services.db_context import Model
+
+from .sign_log import SignLog
+from .user_console import UserConsole
+
+
+class SignUser(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255, unique=True, description="用户id")
+ """用户id"""
+ sign_count = fields.IntField(default=0, description="签到次数")
+ """签到次数"""
+ impression = fields.DecimalField(10, 3, default=0, description="好感度")
+ """好感度"""
+ user_console: fields.OneToOneRelation[UserConsole] = fields.OneToOneField(
+ "models.UserConsole", related_name="user_console", description="用户数据"
+ )
+ """用户数据"""
+ add_probability = fields.DecimalField(
+ 10, 3, default=0, description="双倍签到增加概率"
+ )
+ """双倍签到增加概率"""
+ specify_probability = fields.DecimalField(
+ 10, 3, default=0, description="指定双倍概率"
+ )
+ """使用指定双倍概率"""
+ platform = fields.CharField(255, null=True, description="平台")
+ """平台"""
+
+ class Meta:
+ table = "sign_users"
+ table_description = "用户签到数据表"
+
+ @classmethod
+ async def sign(
+ cls,
+ user_id: str | Self,
+ impression: float,
+ bot_id: str | None = None,
+ platform: str | None = None,
+ ) -> Self:
+ """签到
+
+ 参数:
+ user_id: 用户id
+ impression: 好感度
+ bot_id: bot Id
+ platform: 平台
+ """
+ if isinstance(user_id, SignUser):
+ user = user_id
+ else:
+ user, _ = await cls.get_or_create(
+ user_id=user_id, defaults={"platform": platform}
+ )
+ user.impression = float(user.impression) + impression
+ user.add_probability = 0
+ user.specify_probability = 0
+ user.sign_count += 1
+ await user.save()
+ await SignLog.create(
+ user_id=user.user_id,
+ impression=impression,
+ bot_id=bot_id,
+ platform=platform,
+ )
+ return user
diff --git a/zhenxun/models/task_info.py b/zhenxun/models/task_info.py
new file mode 100644
index 00000000..7e02a3d0
--- /dev/null
+++ b/zhenxun/models/task_info.py
@@ -0,0 +1,22 @@
+from tortoise import fields
+
+from zhenxun.services.db_context import Model
+
+
+class TaskInfo(Model):
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ module = fields.CharField(255, description="被动技能模块名")
+ """被动技能模块名"""
+ name = fields.CharField(255, description="被动技能名称")
+ """被动技能名称"""
+ status = fields.BooleanField(default=True, description="全局开关状态")
+ """全局开关状态"""
+ run_time = fields.CharField(255, null=True, description="运行时间")
+ """运行时间"""
+ run_count = fields.IntField(default=0, description="运行次数")
+ """运行次数"""
+
+ class Meta:
+ table = "task_info"
+ table_description = "被动技能基本信息"
diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py
new file mode 100644
index 00000000..59b643f4
--- /dev/null
+++ b/zhenxun/models/user_console.py
@@ -0,0 +1,79 @@
+from typing import Dict
+
+from tortoise import fields
+
+from zhenxun.services.db_context import Model
+from zhenxun.utils.enum import GoldHandle
+
+from .user_gold_log import UserGoldLog
+
+
+class UserConsole(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255, unique=True, description="用户id")
+ """用户id"""
+ uid = fields.IntField(description="UID")
+ """UID"""
+ gold = fields.IntField(default=100, description="金币数量")
+ """金币数量"""
+ sign = fields.ReverseRelation["SignUser"] # type: ignore
+ """好感度"""
+ props: Dict[str, int] = fields.JSONField(default={}) # type: ignore
+ """道具"""
+ platform = fields.CharField(255, null=True, description="平台")
+ """平台"""
+ create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
+ """创建时间"""
+
+ class Meta:
+ table = "user_console"
+ table_description = "用户数据表"
+
+ @classmethod
+ async def get_new_uid(cls):
+ if user := await cls.annotate().order_by("uid").first():
+ return user.uid + 1
+ return 1
+
+ @classmethod
+ async def add_gold(
+ cls, user_id: str, gold: int, source: str, platform: str | None = None
+ ):
+ """添加金币
+
+ 参数:
+ user_id: 用户id
+ gold: 金币
+ source: 来源
+ platform: 平台.
+ """
+ user, _ = await cls.get_or_create(
+ user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()}
+ )
+ user.gold += gold
+ await user.save(update_fields=["gold"])
+ await UserGoldLog.create(
+ user_id=user_id, gold=gold, handle=GoldHandle.GET, source=source
+ )
+
+ @classmethod
+ async def add_props(
+ cls, user_id: str, goods_uuid: str, num: int = 1, platform: str | None = None
+ ):
+ """添加道具
+
+ 参数:
+ user_id: 用户id
+ goods_uuid: 道具uuid
+ num: 道具数量.
+ platform: 平台.
+ """
+ user, _ = await cls.get_or_create(
+ user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()}
+ )
+ if goods_uuid not in user.props:
+ user.props[goods_uuid] = 0
+ user.props[goods_uuid] += num
+ await user.save(update_fields=["props"])
diff --git a/zhenxun/models/user_gold_log.py b/zhenxun/models/user_gold_log.py
new file mode 100644
index 00000000..30e83242
--- /dev/null
+++ b/zhenxun/models/user_gold_log.py
@@ -0,0 +1,24 @@
+from tortoise import fields
+
+from zhenxun.services.db_context import Model
+from zhenxun.utils.enum import GoldHandle
+
+
+class UserGoldLog(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255, description="用户id")
+ """用户id"""
+ gold = fields.IntField(description="金币")
+ """金币"""
+ handle = fields.CharEnumField(GoldHandle, default=None, description="道具处理类型")
+ """金币处理类型"""
+ source = fields.CharField(255, null=True, description="来源插件")
+ """来源插件"""
+ create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
+ """创建时间"""
+
+ class Meta:
+ table = "user_gold_log"
+ table_description = "用户金币记录表"
diff --git a/zhenxun/models/user_props.py b/zhenxun/models/user_props.py
new file mode 100644
index 00000000..3e3a5e2b
--- /dev/null
+++ b/zhenxun/models/user_props.py
@@ -0,0 +1,25 @@
+from typing import Dict
+
+from tortoise import fields
+
+from zhenxun.services.db_context import Model
+
+from .sign_user import SignUser
+
+
+class UserProps(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255, unique=True, description="用户id")
+ """用户id"""
+ name = fields.CharField(255, description="道具名称")
+ """道具名称"""
+ property: Dict[str, int] = fields.JSONField(default={}) # type: ignore
+ """道具"""
+ platform = fields.CharField(255, null=True)
+ """平台"""
+
+ class Meta:
+ table = "user_props"
+ table_description = "用户道具表"
diff --git a/zhenxun/models/user_props_log.py b/zhenxun/models/user_props_log.py
new file mode 100644
index 00000000..16ae405c
--- /dev/null
+++ b/zhenxun/models/user_props_log.py
@@ -0,0 +1,30 @@
+from typing import Dict
+
+from tortoise import fields
+
+from zhenxun.services.db_context import Model
+from zhenxun.utils.enum import PropHandle
+
+from .sign_user import SignUser
+
+
+class UserPropsLog(Model):
+
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+ user_id = fields.CharField(255, description="用户id")
+ """用户id"""
+ uuid = fields.CharField(255, description="道具uuid")
+ """道具uuid"""
+ num = fields.IntField(null=True, description="道具金币")
+ """数量"""
+ gold = fields.IntField(null=True, description="道具金币")
+ """道具金币"""
+ handle = fields.CharEnumField(PropHandle, default=None, description="道具处理类型")
+ """道具处理类型"""
+ create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
+ """创建时间"""
+
+ class Meta:
+ table = "user_props_log"
+ table_description = "用户道具记录表"
diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py
index a0f9c7dc..3e2e4649 100644
--- a/zhenxun/services/db_context.py
+++ b/zhenxun/services/db_context.py
@@ -1,10 +1,7 @@
-from typing import List
-
from nonebot.utils import is_coroutine_callable
from tortoise import Tortoise, fields
from tortoise.connection import connections
from tortoise.models import Model as Model_
-from tortoise.queryset import RawSQLQuery
from zhenxun.configs.config import (
address,
@@ -18,7 +15,7 @@ from zhenxun.configs.config import (
from .log import logger
-MODELS: List[str] = []
+MODELS: list[str] = []
SCRIPT_METHOD = []
@@ -60,12 +57,14 @@ async def init():
modules={"models": MODELS},
# timezone="Asia/Shanghai"
)
- await Tortoise.generate_schemas()
logger.info(f"Database loaded successfully!")
# except Exception as e:
# raise Exception(f"数据库连接错误... {type(e)}: {e}")
if SCRIPT_METHOD:
- logger.debug(f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个...")
+ db = Tortoise.get_connection("default")
+ logger.debug(
+ f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个..."
+ )
sql_list = []
for module, func in SCRIPT_METHOD:
try:
@@ -75,15 +74,16 @@ async def init():
sql = func()
if sql:
sql_list += sql
-
except Exception as e:
logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e)
for sql in sql_list:
logger.debug(f"执行SQL: {sql}")
try:
- await TestSQL.raw(sql)
+ await db.execute_query_dict(sql)
+ # await TestSQL.raw(sql)
except Exception as e:
logger.debug(f"执行SQL: {sql} 错误...", e=e)
+ await Tortoise.generate_schemas()
async def disconnect():
diff --git a/zhenxun/services/log.py b/zhenxun/services/log.py
index 8220ca01..e7bfed4a 100644
--- a/zhenxun/services/log.py
+++ b/zhenxun/services/log.py
@@ -40,6 +40,7 @@ class logger:
TEMPLATE_USER = "用户[{}] "
TEMPLATE_GROUP = "群聊[{}] "
TEMPLATE_COMMAND = "CMD[{}] "
+ TEMPLATE_PLATFORM = "平台[{}] "
TEMPLATE_TARGET = "[Target]([{}]) "
SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]"
@@ -59,8 +60,8 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
- ):
- ...
+ platform: str | None = None,
+ ): ...
@overload
@classmethod
@@ -71,8 +72,8 @@ class logger:
*,
session: Session | None = None,
target: Any = None,
- ):
- ...
+ platform: str | None = None,
+ ): ...
@classmethod
def info(
@@ -84,16 +85,20 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
):
user_id: str | None = session # type: ignore
group_id = None
if type(session) == Session:
user_id = session.id1
adapter = session.bot_type
- if session.id3 or session.id2:
+ if session.id3:
group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
template = cls.__parser_template(
- info, command, user_id, group_id, adapter, target
+ info, command, user_id, group_id, adapter, target, platform
)
logger_.opt(colors=True).info(template)
@@ -123,9 +128,9 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
- ):
- ...
+ ): ...
@overload
@classmethod
@@ -137,9 +142,9 @@ class logger:
session: Session | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
- ):
- ...
+ ): ...
@classmethod
def warning(
@@ -151,6 +156,7 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
):
user_id: str | None = session # type: ignore
@@ -158,10 +164,13 @@ class logger:
if type(session) == Session:
user_id = session.id1
adapter = session.bot_type
- if session.id3 or session.id2:
+ if session.id3:
group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
template = cls.__parser_template(
- info, command, user_id, group_id, adapter, target
+ info, command, user_id, group_id, adapter, target, platform
)
if e:
template += f" || 错误{type(e)}: {e}"
@@ -178,9 +187,9 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
- ):
- ...
+ ): ...
@overload
@classmethod
@@ -191,9 +200,9 @@ class logger:
*,
session: Session | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
- ):
- ...
+ ): ...
@classmethod
def error(
@@ -205,6 +214,7 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
):
user_id: str | None = session # type: ignore
@@ -212,10 +222,13 @@ class logger:
if type(session) == Session:
user_id = session.id1
adapter = session.bot_type
- if session.id3 or session.id2:
+ if session.id3:
group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
template = cls.__parser_template(
- info, command, user_id, group_id, adapter, target
+ info, command, user_id, group_id, adapter, target, platform
)
if e:
template += f" || 错误 {type(e)}: {e}"
@@ -232,9 +245,9 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
- ):
- ...
+ ): ...
@overload
@classmethod
@@ -245,9 +258,9 @@ class logger:
*,
session: Session | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
- ):
- ...
+ ): ...
@classmethod
def debug(
@@ -259,6 +272,7 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
e: Exception | None = None,
):
user_id: str | None = session # type: ignore
@@ -266,10 +280,13 @@ class logger:
if type(session) == Session:
user_id = session.id1
adapter = session.bot_type
- if session.id3 or session.id2:
+ if session.id3:
group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
template = cls.__parser_template(
- info, command, user_id, group_id, adapter, target
+ info, command, user_id, group_id, adapter, target, platform
)
if e:
template += f" || 错误 {type(e)}: {e}"
@@ -284,12 +301,16 @@ class logger:
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
+ platform: str | None = None,
) -> str:
arg_list = []
template = ""
if adapter is not None:
template += cls.TEMPLATE_ADAPTER
arg_list.append(adapter)
+ if platform is not None:
+ template += cls.TEMPLATE_PLATFORM
+ arg_list.append(platform)
if group_id is not None:
template += cls.TEMPLATE_GROUP
arg_list.append(group_id)
diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py
index 6649e542..bcb83dc7 100644
--- a/zhenxun/utils/_build_image.py
+++ b/zhenxun/utils/_build_image.py
@@ -1,8 +1,9 @@
import base64
import math
+import uuid
from io import BytesIO
from pathlib import Path
-from typing import List, Literal, Tuple, TypeAlias, overload
+from typing import Literal, Tuple, TypeAlias, overload
from nonebot.utils import run_sync
from PIL import Image, ImageDraw, ImageFilter, ImageFont
@@ -40,12 +41,13 @@ class BuildImage:
self,
width: int = 0,
height: int = 0,
- color: ColorAlias = None,
+ color: ColorAlias = (255, 255, 255),
mode: ModeType = "RGBA",
font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf",
font_size: int = 20,
background: str | BytesIO | Path | None = None,
) -> None:
+ self.uid = uuid.uuid1()
self.width = width
self.height = height
self.color = color
@@ -72,7 +74,7 @@ class BuildImage:
async def build_text_image(
cls,
text: str,
- font: str | Path = "HYWenHei-85W.ttf",
+ font: str | FreeTypeFont | Path = "HYWenHei-85W.ttf",
size: int = 10,
font_color: str | Tuple[int, int, int] = (0, 0, 0),
color: ColorAlias = None,
@@ -91,12 +93,16 @@ class BuildImage:
返回:
Self: Self
"""
- _font = cls.load_font(font, size)
- width, height = cls.get_text_size(text, _font)
- if type(padding) == int:
+ _font = None
+ if isinstance(font, FreeTypeFont):
+ _font = font
+ elif isinstance(font, (str, Path)):
+ _font = cls.load_font(font, size)
+ width, height = cls.get_text_size(text or "A", _font)
+ if isinstance(padding, int):
width += padding * 2
height += padding * 2
- elif type(padding) == tuple:
+ elif isinstance(padding, tuple):
width += padding[1] + padding[3]
height += padding[0] + padding[2]
markImg = cls(width, height, color)
@@ -112,9 +118,9 @@ class BuildImage:
row: int,
space: int = 10,
padding: int = 50,
- color: ColorAlias = (255, 255, 255, 0),
+ color: ColorAlias = (255, 255, 255),
background: str | BytesIO | Path | None = None,
- ) -> Self | None:
+ ) -> Self:
"""自动贴图
参数:
@@ -129,11 +135,15 @@ class BuildImage:
Self: Self
"""
if not img_list:
- return None
+ raise ValueError("贴图类别为空...")
width, height = img_list[0].size
background_width = width * row + space * (row - 1) + padding * 2
- column = math.ceil(len(img_list) / row)
- background_height = height * column + space * (column - 1) + padding * 2
+ row_count = math.ceil(len(img_list) / row)
+ if row_count == 1:
+ background_width = (
+ sum([img.width for img in img_list]) + space * (row - 1) + padding * 2
+ )
+ background_height = height * row_count + space * (row_count - 1) + padding * 2
background_image = cls(
background_width, background_height, color=color, background=background
)
@@ -141,15 +151,16 @@ class BuildImage:
for img in img_list:
await background_image.paste(img, (_cur_width, _cur_height))
_cur_width += space + img.width
- _cur_height += space + img.height
if _cur_width + padding >= background_image.width:
+ _cur_height += space + img.height
_cur_width = padding
return background_image
@classmethod
- def load_font(cls, font: str | Path, font_size: int) -> FreeTypeFont:
- """
- 加载字体
+ def load_font(
+ cls, font: str | Path = "HYWenHei-85W.ttf", font_size: int = 10
+ ) -> FreeTypeFont:
+ """加载字体
参数:
font: 字体名称
@@ -165,19 +176,20 @@ class BuildImage:
@classmethod
def get_text_size(
cls, text: str, font: FreeTypeFont | None = None
- ) -> Tuple[int, int]:
- ...
+ ) -> Tuple[int, int]: ...
@overload
@classmethod
def get_text_size(
cls, text: str, font: str | None = None, font_size: int = 10
- ) -> Tuple[int, int]:
- ...
+ ) -> Tuple[int, int]: ...
@classmethod
def get_text_size(
- cls, text: str, font: str | FreeTypeFont | None = None, font_size: int = 10
+ cls,
+ text: str,
+ font: str | FreeTypeFont | None = "HYWenHei-85W.ttf",
+ font_size: int = 10,
) -> Tuple[int, int]:
"""获取该字体下文本需要的长宽
@@ -192,7 +204,7 @@ class BuildImage:
_font = font
if font and type(font) == str:
_font = cls.load_font(font, font_size)
- return _font.getsize(text) # type: ignore
+ return _font.getsize(str(text)) # type: ignore
def getsize(self, msg: str) -> Tuple[int, int]:
"""
@@ -265,7 +277,10 @@ class BuildImage:
_image = image.markImg
if _image.width and _image.height and center_type:
pos = self.__center_xy(pos, _image.width, _image.height, center_type)
- self.markImg.paste(_image, pos, _image) # type: ignore
+ try:
+ self.markImg.paste(_image, pos, _image) # type: ignore
+ except ValueError:
+ self.markImg.paste(_image, pos) # type: ignore
return self
@run_sync
@@ -335,6 +350,7 @@ class BuildImage:
异常:
ValueError: 居中类型错误
"""
+ text = str(text)
if center_type and center_type not in ["center", "height", "width"]:
raise ValueError("center_type must be 'center', 'width' or 'height'")
width, height = 0, 0
@@ -484,7 +500,7 @@ class BuildImage:
@run_sync
def polygon(
self,
- xy: List[Tuple[int, int]],
+ xy: list[Tuple[int, int]],
fill: Tuple[int, int, int] = (0, 0, 0),
outline: int = 1,
) -> Self:
@@ -560,7 +576,7 @@ class BuildImage:
def circle_corner(
self,
radii: int = 30,
- point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"],
+ point_list: list[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"],
) -> Self:
"""
矩形四角变圆
@@ -654,3 +670,11 @@ class BuildImage:
self.markImg = self.markImg.filter(_type)
self.draw = ImageDraw.Draw(self.markImg)
return self
+
+ def tobytes(self) -> bytes:
+ """转换为bytes
+
+ 返回:
+ bytes: bytes
+ """
+ return self.markImg.tobytes()
diff --git a/zhenxun/utils/_image_template.py b/zhenxun/utils/_image_template.py
new file mode 100644
index 00000000..3c894cab
--- /dev/null
+++ b/zhenxun/utils/_image_template.py
@@ -0,0 +1,156 @@
+from email.mime import image
+from io import BytesIO
+from pathlib import Path
+from typing import Any, Callable
+
+from nonebot.plugin import PluginMetadata
+from PIL.ImageFont import FreeTypeFont
+from pydantic import BaseModel
+
+from ._build_image import BuildImage
+
+
+class RowStyle(BaseModel):
+
+ font: FreeTypeFont | str | Path | None = "HYWenHei-85W.ttf"
+ """字体"""
+ font_size: int = 20
+ """字体大小"""
+ font_color: str | tuple[int, int, int] = (0, 0, 0)
+ """字体颜色"""
+
+ class Config:
+ arbitrary_types_allowed = True
+
+
+class ImageTemplate:
+
+ @classmethod
+ async def table_page(
+ cls,
+ head_text: str,
+ tip_text: str | None,
+ column_name: list[str],
+ data_list: list[list[str]],
+ row_space: int = 35,
+ column_space: int = 30,
+ padding: int = 5,
+ text_style: Callable[[str, str], RowStyle] | None = None,
+ ) -> BuildImage:
+ """表格页
+
+ 参数:
+ head_text: 标题文本.
+ tip_text: 标题注释.
+ column_name: 表头列表.
+ data_list: 数据列表.
+ row_space: 行间距.
+ column_space: 列间距.
+ padding: 文本内间距.
+ text_style: 文本样式.
+
+ 返回:
+ BuildImage: 表格图片
+ """
+ table = await cls.table(
+ column_name, data_list, row_space, column_space, padding, text_style
+ )
+ await table.circle_corner()
+ table_bk = BuildImage(table.width + 100, table.height + 50, "#EAEDF2")
+ await table_bk.paste(table, center_type="center")
+ height = table_bk.height + 200
+ background = BuildImage(table_bk.width, height, (255, 255, 255), font_size=50)
+ await background.paste(table_bk, (0, 200))
+ await background.text((0, 50), head_text, "#334762", center_type="width")
+ if tip_text:
+ text_image = await BuildImage.build_text_image(tip_text, size=22)
+ await background.paste(text_image, (0, 110), center_type="width")
+ return background
+
+ @classmethod
+ async def table(
+ cls,
+ column_name: list[str],
+ data_list: list[list[str | tuple[Path, int, int]]],
+ row_space: int = 25,
+ column_space: int = 10,
+ padding: int = 5,
+ text_style: Callable[[str, str], RowStyle] | None = None,
+ ) -> BuildImage:
+ """表格
+
+ 参数:
+ column_name: 表头列表
+ data_list: 数据列表
+ row_space: 行间距.
+ column_space: 列间距.
+ padding: 文本内间距.
+ text_style: 文本样式.
+
+ 返回:
+ BuildImage: 表格图片
+ """
+ font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
+ column_num = max([len(l) for l in data_list])
+ list_data = []
+ column_data = []
+ for i in range(len(column_name)):
+ c = []
+ for l in data_list:
+ if len(l) > i:
+ c.append(l[i])
+ else:
+ c.append("")
+ column_data.append(c)
+ build_data_list = []
+ _, base_h = BuildImage.get_text_size("A", font)
+ for i, column_list in enumerate(column_data):
+ name_width, name_height = BuildImage.get_text_size(column_name[i], font)
+ _temp = {"width": name_width, "data": column_list}
+ for s in column_list:
+ if isinstance(s, tuple):
+ w = s[1]
+ else:
+ w, _ = BuildImage.get_text_size(s, font)
+ if w > _temp["width"]:
+ _temp["width"] = w
+ build_data_list.append(_temp)
+ column_image_list = []
+ for i, data in enumerate(build_data_list):
+ width = data["width"] + padding * 2
+ height = (base_h + row_space) * (len(data["data"]) + 1) + padding * 2
+ background = BuildImage(width, height, (255, 255, 255))
+ column_name_image = await BuildImage.build_text_image(
+ column_name[i], font, 12, "#C8CCCF"
+ )
+ await background.paste(column_name_image, (0, 20), center_type="width")
+ cur_h = column_name_image.height + row_space + 20
+ for item in data["data"]:
+ style = RowStyle(font=font)
+ if text_style:
+ style = text_style(column_name[i], item)
+ if isinstance(item, tuple):
+ """图片"""
+ data, width, height = item
+ if isinstance(data, Path):
+ image_ = BuildImage(width, height, background=data)
+ elif isinstance(data, bytes):
+ image_ = BuildImage(width, height, background=BytesIO(data))
+ elif isinstance(data, BuildImage):
+ image_ = data
+ await background.paste(image_, (padding, cur_h))
+ else:
+ await background.text(
+ (padding, cur_h),
+ item if item is not None else "",
+ style.font_color,
+ font=style.font,
+ font_size=style.font_size,
+ )
+ cur_h += base_h + row_space
+ column_image_list.append(background)
+ height = max([bk.height for bk in column_image_list])
+ width = sum([bk.width for bk in column_image_list])
+ return await BuildImage.auto_paste(
+ column_image_list, len(column_image_list), column_space
+ )
diff --git a/zhenxun/utils/browser.py b/zhenxun/utils/browser.py
index 88e252b0..c7d89727 100644
--- a/zhenxun/utils/browser.py
+++ b/zhenxun/utils/browser.py
@@ -1,14 +1,12 @@
-from typing import Optional
-
from nonebot import get_driver
from playwright.async_api import Browser, Playwright, async_playwright
-from services.log import logger
+from zhenxun.services.log import logger
driver = get_driver()
-_playwright: Optional[Playwright] = None
-_browser: Optional[Browser] = None
+_playwright: Playwright | None = None
+_browser: Browser | None = None
@driver.on_startup
diff --git a/zhenxun/utils/decorator/shop.py b/zhenxun/utils/decorator/shop.py
new file mode 100644
index 00000000..5105e4c2
--- /dev/null
+++ b/zhenxun/utils/decorator/shop.py
@@ -0,0 +1,204 @@
+from typing import Callable, Union, Tuple, Optional
+from nonebot.adapters.onebot.v11 import MessageSegment, Message
+from nonebot.plugin import require
+
+
+class ShopRegister(dict):
+ def __init__(self, *args, **kwargs):
+ super(ShopRegister, self).__init__(*args, **kwargs)
+ self._data = {}
+ self._flag = True
+
+ def before_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True):
+ """
+ 说明:
+ 使用前检查方法
+ 参数:
+ :param name: 道具名称
+ :param load_status: 加载状态
+ """
+ def register_before_handle(name_list: Tuple[str, ...], func: Callable):
+ if load_status:
+ for name_ in name_list:
+ if not self._data[name_]:
+ self._data[name_] = {}
+ if not self._data[name_].get('before_handle'):
+ self._data[name_]['before_handle'] = []
+ self._data[name]['before_handle'].append(func)
+ _name = (name,) if isinstance(name, str) else name
+ return lambda func: register_before_handle(_name, func)
+
+ def after_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True):
+ """
+ 说明:
+ 使用后执行方法
+ 参数:
+ :param name: 道具名称
+ :param load_status: 加载状态
+ """
+ def register_after_handle(name_list: Tuple[str, ...], func: Callable):
+ if load_status:
+ for name_ in name_list:
+ if not self._data[name_]:
+ self._data[name_] = {}
+ if not self._data[name_].get('after_handle'):
+ self._data[name_]['after_handle'] = []
+ self._data[name_]['after_handle'].append(func)
+ _name = (name,) if isinstance(name, str) else name
+ return lambda func: register_after_handle(_name, func)
+
+ def register(
+ self,
+ name: Tuple[str, ...],
+ price: Tuple[float, ...],
+ des: Tuple[str, ...],
+ discount: Tuple[float, ...],
+ limit_time: Tuple[int, ...],
+ load_status: Tuple[bool, ...],
+ daily_limit: Tuple[int, ...],
+ is_passive: Tuple[bool, ...],
+ icon: Tuple[str, ...],
+ **kwargs,
+ ):
+ def add_register_item(func: Callable):
+ if name in self._data.keys():
+ raise ValueError("该商品已注册,请替换其他名称!")
+ for n, p, d, dd, l, s, dl, pa, i in zip(
+ name, price, des, discount, limit_time, load_status, daily_limit, is_passive, icon
+ ):
+ if s:
+ _temp_kwargs = {}
+ for key, value in kwargs.items():
+ if key.startswith(f"{n}_"):
+ _temp_kwargs[key.split("_", maxsplit=1)[-1]] = value
+ else:
+ _temp_kwargs[key] = value
+ temp = self._data.get(n, {})
+ temp.update({
+ "price": p,
+ "des": d,
+ "discount": dd,
+ "limit_time": l,
+ "daily_limit": dl,
+ "icon": i,
+ "is_passive": pa,
+ "func": func,
+ "kwargs": _temp_kwargs,
+ })
+ self._data[n] = temp
+ return func
+
+ return lambda func: add_register_item(func)
+
+ async def load_register(self):
+ require("use")
+ require("shop_handle")
+ from basic_plugins.shop.use.data_source import register_use, func_manager
+ from basic_plugins.shop.shop_handle.data_source import register_goods
+ # 统一进行注册
+ if self._flag:
+ # 只进行一次注册
+ self._flag = False
+ for name in self._data.keys():
+ await register_goods(
+ name,
+ self._data[name]["price"],
+ self._data[name]["des"],
+ self._data[name]["discount"],
+ self._data[name]["limit_time"],
+ self._data[name]["daily_limit"],
+ self._data[name]["is_passive"],
+ self._data[name]["icon"],
+ )
+ register_use(
+ name, self._data[name]["func"], **self._data[name]["kwargs"]
+ )
+ func_manager.register_use_before_handle(name, self._data[name].get('before_handle', []))
+ func_manager.register_use_after_handle(name, self._data[name].get('after_handle', []))
+
+ def __call__(
+ self,
+ name: Union[str, Tuple[str, ...]], # 名称
+ price: Union[float, Tuple[float, ...]], # 价格
+ des: Union[str, Tuple[str, ...]], # 简介
+ discount: Union[float, Tuple[float, ...]] = 1, # 折扣
+ limit_time: Union[int, Tuple[int, ...]] = 0, # 限时
+ load_status: Union[bool, Tuple[bool, ...]] = True, # 加载状态
+ daily_limit: Union[int, Tuple[int, ...]] = 0, # 每日限购
+ is_passive: Union[bool, Tuple[bool, ...]] = False, # 被动道具(无法被'使用道具'命令消耗)
+ icon: Union[str, Tuple[str, ...]] = False, # 图标
+ **kwargs,
+ ):
+ _tuple_list = []
+ _current_len = -1
+ for x in [name, price, des, discount, limit_time, load_status]:
+ if isinstance(x, tuple):
+ if _current_len == -1:
+ _current_len = len(x)
+ if _current_len != len(x):
+ raise ValueError(
+ f"注册商品 {name} 中 name,price,des,discount,limit_time,load_status,daily_limit 数量不符!"
+ )
+ _current_len = _current_len if _current_len > -1 else 1
+ _name = self.__get(name, _current_len)
+ _price = self.__get(price, _current_len)
+ _discount = self.__get(discount, _current_len)
+ _limit_time = self.__get(limit_time, _current_len)
+ _des = self.__get(des, _current_len)
+ _load_status = self.__get(load_status, _current_len)
+ _daily_limit = self.__get(daily_limit, _current_len)
+ _is_passive = self.__get(is_passive, _current_len)
+ _icon = self.__get(icon, _current_len)
+ return self.register(
+ _name,
+ _price,
+ _des,
+ _discount,
+ _limit_time,
+ _load_status,
+ _daily_limit,
+ _is_passive,
+ _icon,
+ **kwargs,
+ )
+
+ def __get(self, value, _current_len):
+ return value if isinstance(value, tuple) else tuple([value for _ in range(_current_len)])
+
+ def __setitem__(self, key, value):
+ self._data[key] = value
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+ def __contains__(self, key):
+ return key in self._data
+
+ def __str__(self):
+ return str(self._data)
+
+ def keys(self):
+ return self._data.keys()
+
+ def values(self):
+ return self._data.values()
+
+ def items(self):
+ return self._data.items()
+
+
+class NotMeetUseConditionsException(Exception):
+
+ """
+ 不满足条件异常类
+ """
+
+ def __init__(self, info: Optional[Union[str, MessageSegment, Message]]):
+ super().__init__(self)
+ self._info = info
+
+ def get_info(self):
+ return self._info
+
+
+shop_register = ShopRegister()
diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py
index 5de75bf4..32270316 100644
--- a/zhenxun/utils/enum.py
+++ b/zhenxun/utils/enum.py
@@ -1,15 +1,38 @@
from strenum import StrEnum
+class GoldHandle(StrEnum):
+ """
+ 金币处理
+ """
+
+ BUY = "BUY"
+ """购买"""
+ GET = "GET"
+ """获取"""
+
+
+class PropHandle(StrEnum):
+ """
+ 道具处理
+ """
+
+ BUY = "BUY"
+ """购买"""
+ USE = "USE"
+ """使用"""
+
+
class PluginType(StrEnum):
"""
插件类型
"""
- SUPERUSER = "超级管理员插件"
- ADMIN = "管理员插件"
- NORMAL = "普通插件"
- HIDDEN = "被动插件"
+ SUPERUSER = "SUPERUSER"
+ ADMIN = "ADMIN"
+ SUPER_AND_ADMIN = "ADMIN_SUPER"
+ NORMAL = "NORMAL"
+ HIDDEN = "HIDDEN"
class BlockType(StrEnum):
diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py
index c901a5c9..0998e776 100644
--- a/zhenxun/utils/exception.py
+++ b/zhenxun/utils/exception.py
@@ -1,2 +1,14 @@
class NotFoundError(Exception):
pass
+
+
+class GroupInfoNotFound(Exception):
+ pass
+
+
+class EmptyError(Exception):
+ pass
+
+
+class UserAndGroupIsNone(Exception):
+ pass
diff --git a/zhenxun/utils/http_utils.py b/zhenxun/utils/http_utils.py
new file mode 100644
index 00000000..ea9516ef
--- /dev/null
+++ b/zhenxun/utils/http_utils.py
@@ -0,0 +1,379 @@
+import asyncio
+from asyncio.exceptions import TimeoutError
+from contextlib import asynccontextmanager
+from pathlib import Path
+from typing import Any, AsyncGenerator, Dict, Literal
+
+import aiofiles
+import httpx
+import rich
+from httpx import ConnectTimeout, Response
+from nonebot import require
+from playwright.async_api import Page
+from retrying import retry
+
+from zhenxun.configs.config import SYSTEM_PROXY
+from zhenxun.services.log import logger
+from zhenxun.utils.user_agent import get_user_agent
+
+from .browser import get_browser
+
+require("nonebot_plugin_saa")
+
+from nonebot_plugin_saa import Image
+
+
+class AsyncHttpx:
+
+ proxy = {"http://": SYSTEM_PROXY, "https://": SYSTEM_PROXY}
+
+ @classmethod
+ @retry(stop_max_attempt_number=3)
+ async def get(
+ cls,
+ url: str,
+ *,
+ params: Dict[str, Any] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
+ verify: bool = True,
+ use_proxy: bool = True,
+ proxy: Dict[str, str] | None = None,
+ timeout: int = 30,
+ **kwargs,
+ ) -> Response:
+ """Get
+
+ 参数:
+ url: url
+ params: params
+ headers: 请求头
+ cookies: cookies
+ verify: verify
+ use_proxy: 使用默认代理
+ proxy: 指定代理
+ timeout: 超时时间
+ """
+ if not headers:
+ headers = get_user_agent()
+ _proxy = proxy if proxy else cls.proxy if use_proxy else None
+ async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore
+ return await client.get(
+ url,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ timeout=timeout,
+ **kwargs,
+ )
+
+ @classmethod
+ async def post(
+ cls,
+ url: str,
+ *,
+ data: Dict[str, str] | None = None,
+ content: Any = None,
+ files: Any = None,
+ verify: bool = True,
+ use_proxy: bool = True,
+ proxy: Dict[str, str] | None = None,
+ json: Dict[str, Any] | None = None,
+ params: Dict[str, str] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
+ timeout: int = 30,
+ **kwargs,
+ ) -> Response:
+ """
+ 说明:
+ Post
+ 参数:
+ url: url
+ data: data
+ content: content
+ files: files
+ use_proxy: 是否默认代理
+ proxy: 指定代理
+ json: json
+ params: params
+ headers: 请求头
+ cookies: cookies
+ timeout: 超时时间
+ """
+ if not headers:
+ headers = get_user_agent()
+ _proxy = proxy if proxy else cls.proxy if use_proxy else None
+ async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore
+ return await client.post(
+ url,
+ content=content,
+ data=data,
+ files=files,
+ json=json,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ timeout=timeout,
+ **kwargs,
+ )
+
+ @classmethod
+ async def download_file(
+ cls,
+ url: str,
+ path: str | Path,
+ *,
+ params: Dict[str, str] | None = None,
+ verify: bool = True,
+ use_proxy: bool = True,
+ proxy: Dict[str, str] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
+ timeout: int = 30,
+ stream: bool = False,
+ **kwargs,
+ ) -> bool:
+ """下载文件
+
+ 参数:
+ url: url
+ path: 存储路径
+ params: params
+ verify: verify
+ use_proxy: 使用代理
+ proxy: 指定代理
+ headers: 请求头
+ cookies: cookies
+ timeout: 超时时间
+ stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件)
+ """
+ if isinstance(path, str):
+ path = Path(path)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ try:
+ for _ in range(3):
+ if not stream:
+ try:
+ content = (
+ await cls.get(
+ url,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ use_proxy=use_proxy,
+ proxy=proxy,
+ timeout=timeout,
+ **kwargs,
+ )
+ ).content
+ async with aiofiles.open(path, "wb") as wf:
+ await wf.write(content)
+ logger.info(f"下载 {url} 成功.. Path:{path.absolute()}")
+ return True
+ except (TimeoutError, ConnectTimeout):
+ pass
+ else:
+ if not headers:
+ headers = get_user_agent()
+ _proxy = proxy if proxy else cls.proxy if use_proxy else None
+ try:
+ async with httpx.AsyncClient(
+ proxies=_proxy, verify=verify # type: ignore
+ ) as client:
+ async with client.stream(
+ "GET",
+ url,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ timeout=timeout,
+ **kwargs,
+ ) as response:
+ logger.info(
+ f"开始下载 {path.name}.. Path: {path.absolute()}"
+ )
+ async with aiofiles.open(path, "wb") as wf:
+ total = int(response.headers["Content-Length"])
+ with rich.progress.Progress( # type: ignore
+ rich.progress.TextColumn(path.name), # type: ignore
+ "[progress.percentage]{task.percentage:>3.0f}%", # type: ignore
+ rich.progress.BarColumn(bar_width=None), # type: ignore
+ rich.progress.DownloadColumn(), # type: ignore
+ rich.progress.TransferSpeedColumn(), # type: ignore
+ ) as progress:
+ download_task = progress.add_task(
+ "Download", total=total
+ )
+ async for chunk in response.aiter_bytes():
+ await wf.write(chunk)
+ await wf.flush()
+ progress.update(
+ download_task,
+ completed=response.num_bytes_downloaded,
+ )
+ logger.info(
+ f"下载 {url} 成功.. Path:{path.absolute()}"
+ )
+ return True
+ except (TimeoutError, ConnectTimeout):
+ pass
+ else:
+ logger.error(f"下载 {url} 下载超时.. Path:{path.absolute()}")
+ except Exception as e:
+ logger.error(f"下载 {url} 错误 Path:{path.absolute()}", e=e)
+ return False
+
+ @classmethod
+ async def gather_download_file(
+ cls,
+ url_list: list[str],
+ path_list: list[str | Path],
+ *,
+ limit_async_number: int | None = None,
+ params: Dict[str, str] | None = None,
+ use_proxy: bool = True,
+ proxy: Dict[str, str] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
+ timeout: int = 30,
+ **kwargs,
+ ) -> list[bool]:
+ """分组同时下载文件
+
+ 参数:
+ url_list: url列表
+ path_list: 存储路径列表
+ limit_async_number: 限制同时请求数量
+ params: params
+ use_proxy: 使用代理
+ proxy: 指定代理
+ headers: 请求头
+ cookies: cookies
+ timeout: 超时时间
+ """
+ if n := len(url_list) != len(path_list):
+ raise UrlPathNumberNotEqual(
+ f"Url数量与Path数量不对等,Url:{len(url_list)},Path:{len(path_list)}"
+ )
+ if limit_async_number and n > limit_async_number:
+ m = float(n) / limit_async_number
+ x = 0
+ j = limit_async_number
+ _split_url_list = []
+ _split_path_list = []
+ for _ in range(int(m)):
+ _split_url_list.append(url_list[x:j])
+ _split_path_list.append(path_list[x:j])
+ x += limit_async_number
+ j += limit_async_number
+ if int(m) < m:
+ _split_url_list.append(url_list[j:])
+ _split_path_list.append(path_list[j:])
+ else:
+ _split_url_list = [url_list]
+ _split_path_list = [path_list]
+ tasks = []
+ result_ = []
+ for x, y in zip(_split_url_list, _split_path_list):
+ for url, path in zip(x, y):
+ tasks.append(
+ asyncio.create_task(
+ cls.download_file(
+ url,
+ path,
+ params=params,
+ headers=headers,
+ cookies=cookies,
+ use_proxy=use_proxy,
+ timeout=timeout,
+ proxy=proxy,
+ **kwargs,
+ )
+ )
+ )
+ _x = await asyncio.gather(*tasks)
+ result_ = result_ + list(_x)
+ tasks.clear()
+ return result_
+
+
+class AsyncPlaywright:
+ @classmethod
+ @asynccontextmanager
+ async def new_page(cls, **kwargs) -> AsyncGenerator[Page, None]:
+ """获取一个新页面
+
+ 参数:
+ user_agent: 请求头
+ """
+ browser = get_browser()
+ ctx = await browser.new_context(**kwargs)
+ page = await ctx.new_page()
+ try:
+ yield page
+ finally:
+ await page.close()
+ await ctx.close()
+
+ @classmethod
+ async def screenshot(
+ cls,
+ url: str,
+ path: Path | str,
+ element: str | list[str],
+ *,
+ wait_time: int | None = None,
+ viewport_size: Dict[str, int] | None = None,
+ wait_until: (
+ Literal["domcontentloaded", "load", "networkidle"] | None
+ ) = "networkidle",
+ timeout: float | None = None,
+ type_: Literal["jpeg", "png"] | None = None,
+ user_agent: str | None = None,
+ **kwargs,
+ ) -> Image | None:
+ """截图,该方法仅用于简单快捷截图,复杂截图请操作 page
+
+ 参数:
+ url: 网址
+ path: 存储路径
+ element: 元素选择
+ wait_time: 等待截取超时时间
+ viewport_size: 窗口大小
+ wait_until: 等待类型
+ timeout: 超时限制
+ type_: 保存类型
+ """
+ if viewport_size is None:
+ viewport_size = dict(width=2560, height=1080)
+ if isinstance(path, str):
+ path = Path(path)
+ wait_time = wait_time * 1000 if wait_time else None
+ if isinstance(element, str):
+ element_list = [element]
+ else:
+ element_list = element
+ async with cls.new_page(
+ viewport=viewport_size,
+ user_agent=user_agent,
+ **kwargs,
+ ) as page:
+ await page.goto(url, timeout=timeout, wait_until=wait_until)
+ card = page
+ for e in element_list:
+ if not card:
+ return None
+ card = await card.wait_for_selector(e, timeout=wait_time)
+ if card:
+ await card.screenshot(path=path, timeout=timeout, type=type_)
+ return Image(path)
+ return None
+
+
+class UrlPathNumberNotEqual(Exception):
+ pass
+
+
+class BrowserIsNone(Exception):
+ pass
diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py
index fe09e627..b09e9d92 100644
--- a/zhenxun/utils/image_utils.py
+++ b/zhenxun/utils/image_utils.py
@@ -1,2 +1,339 @@
-from ._build_image import BuildImage
+import os
+import random
+import re
+from pathlib import Path
+from typing import Awaitable, Callable
+
+from nonebot.utils import is_coroutine_callable
+
+from ._build_image import BuildImage, ColorAlias
from ._build_mat import BuildMat
+from ._image_template import ImageTemplate, RowStyle
+
+# TODO: text2image 长度错误
+
+
+async def text2image(
+ text: str,
+ auto_parse: bool = True,
+ font_size: int = 20,
+ color: str | tuple[int, int, int] = (255, 255, 255),
+ font: str = "HYWenHei-85W.ttf",
+ font_color: str | tuple[int, int, int] = (0, 0, 0),
+ padding: int | tuple[int, int, int, int] = 0,
+ _add_height: float = 0,
+) -> BuildImage:
+ """解析文本并转为图片
+ 使用标签
+
+ 可选配置项
+ font: str -> 特殊文本字体
+ fs / font_size: int -> 特殊文本大小
+ fc / font_color: Union[str, Tuple[int, int, int]] -> 特殊文本颜色
+ 示例
+ 在不在,HibiKi小姐,
+ 你最近还好吗,我非常想你,这段时间我非常不好过,
+ 抽卡抽不到金色,这让我很痛苦
+ 参数:
+ text: 文本
+ auto_parse: 是否自动解析,否则原样发送
+ font_size: 普通字体大小
+ color: 背景颜色
+ font: 普通字体
+ font_color: 普通字体颜色
+ padding: 文本外边距,元组类型时为 (上,左,下,右)
+ _add_height: 由于get_size无法返回正确的高度,采用手动方式额外添加高度
+ """
+ if not text:
+ raise ValueError("文本转图片 text 不能为空...")
+ pw = ph = top_padding = left_padding = 0
+ if padding:
+ if isinstance(padding, int):
+ pw = padding * 2
+ ph = padding * 2
+ top_padding = left_padding = padding
+ elif isinstance(padding, tuple):
+ pw = padding[0] + padding[2]
+ ph = padding[1] + padding[3]
+ top_padding = padding[0]
+ left_padding = padding[1]
+ _font = BuildImage.load_font(font, font_size)
+ if auto_parse and re.search(r"(.*)", text):
+ _data = []
+ new_text = ""
+ placeholder_index = 0
+ for s in text.split(""):
+ r = re.search(r"(.*)", s)
+ if r:
+ start, end = r.span()
+ if start != 0 and (t := s[:start]):
+ new_text += t
+ _data.append(
+ [
+ (start, end),
+ f"[placeholder_{placeholder_index}]",
+ r.group(1).strip(),
+ r.group(2),
+ ]
+ )
+ new_text += f"[placeholder_{placeholder_index}]"
+ placeholder_index += 1
+ new_text += text.split("")[-1]
+ image_list = []
+ current_placeholder_index = 0
+ # 切分换行,每行为单张图片
+ for s in new_text.split("\n"):
+ _tmp_text = s
+ img_width = 0
+ img_height = BuildImage.get_text_size("正", _font)[1]
+ _tmp_index = current_placeholder_index
+ for _ in range(s.count("[placeholder_")):
+ placeholder = _data[_tmp_index]
+ if "font_size" in placeholder[2]:
+ r = re.search(r"font_size=['\"]?(\d+)", placeholder[2])
+ if r:
+ w, h = BuildImage.get_text_size(
+ placeholder[3], font, int(r.group(1))
+ )
+ img_height = img_height if img_height > h else h
+ img_width += w
+ else:
+ img_width += BuildImage.get_text_size(placeholder[3], _font)[0]
+ _tmp_text = _tmp_text.replace(f"[placeholder_{_tmp_index}]", "")
+ _tmp_index += 1
+ img_width += BuildImage.get_text_size(_tmp_text, _font)[0]
+ # 开始画图
+ A = BuildImage(
+ img_width, img_height, color=color, font=font, font_size=font_size
+ )
+ basic_font_h = A.getsize("正")[1]
+ current_width = 0
+ # 遍历占位符
+ for _ in range(s.count("[placeholder_")):
+ if not s.startswith(f"[placeholder_{current_placeholder_index}]"):
+ slice_ = s.split(f"[placeholder_{current_placeholder_index}]")
+ await A.text(
+ (current_width, A.height - basic_font_h - 1),
+ slice_[0],
+ font_color,
+ )
+ current_width += A.getsize(slice_[0])[0]
+ placeholder = _data[current_placeholder_index]
+ # 解析配置
+ _font = font
+ _font_size = font_size
+ _font_color = font_color
+ for e in placeholder[2].split():
+ if e.startswith("font="):
+ _font = e.split("=")[-1]
+ if e.startswith("font_size=") or e.startswith("fs="):
+ _font_size = int(e.split("=")[-1])
+ if _font_size > 1000:
+ _font_size = 1000
+ if _font_size < 1:
+ _font_size = 1
+ if e.startswith("font_color") or e.startswith("fc="):
+ _font_color = e.split("=")[-1]
+ text_img = await BuildImage.build_text_image(
+ placeholder[3], font=_font, size=_font_size, font_color=_font_color
+ )
+ _img_h = (
+ int(A.height / 2 - text_img.height / 2)
+ if new_text == "[placeholder_0]"
+ else A.height - text_img.height
+ )
+ await A.paste(text_img, (current_width, _img_h - 1))
+ current_width += text_img.width
+ s = s[
+ s.index(f"[placeholder_{current_placeholder_index}]")
+ + len(f"[placeholder_{current_placeholder_index}]") :
+ ]
+ current_placeholder_index += 1
+ if s:
+ slice_ = s.split(f"[placeholder_{current_placeholder_index}]")
+ await A.text((current_width, A.height - basic_font_h), slice_[0])
+ current_width += A.getsize(slice_[0])[0]
+ await A.crop((0, 0, current_width, A.height))
+ # A.show()
+ image_list.append(A)
+ height = 0
+ width = 0
+ for img in image_list:
+ height += img.h
+ width = width if width > img.w else img.w
+ width += pw
+ height += ph
+ A = BuildImage(width + left_padding, height + top_padding, color=color)
+ current_height = top_padding
+ for img in image_list:
+ await A.paste(img, (left_padding, current_height))
+ current_height += img.h
+ else:
+ width = 0
+ height = 0
+ _, h = BuildImage.get_text_size("正", _font)
+ line_height = int(font_size / 3)
+ image_list = []
+ for s in text.split("\n"):
+ w, _ = BuildImage.get_text_size(s.strip() or "正", _font)
+ height += h + line_height
+ width = width if width > w else w
+ image_list.append(
+ await BuildImage.build_text_image(
+ s.strip(), font, font_size, font_color
+ )
+ )
+ width += pw
+ height += ph
+ A = BuildImage(
+ width + left_padding,
+ height + top_padding + 2,
+ color=color,
+ )
+ cur_h = ph
+ for img in image_list:
+ await A.paste(img, (pw, cur_h))
+ cur_h += img.height + line_height
+ return A
+
+
+def group_image(image_list: list[BuildImage]) -> tuple[list[list[BuildImage]], int]:
+ """
+ 说明:
+ 根据图片大小进行分组
+ 参数:
+ image_list: 排序图片列表
+ """
+ image_list.sort(key=lambda x: x.height, reverse=True)
+ max_image = max(image_list, key=lambda x: x.height)
+
+ image_list.remove(max_image)
+ max_h = max_image.height
+ total_w = 0
+
+ # 图片分组
+ image_group = [[max_image]]
+ is_use = []
+ surplus_list = image_list[:]
+
+ for image in image_list:
+ if image.uid not in is_use:
+ group = [image]
+ is_use.append(image.uid)
+ curr_h = image.height
+ while True:
+ surplus_list = [x for x in surplus_list if x.uid not in is_use]
+ for tmp in surplus_list:
+ temp_h = curr_h + tmp.height + 10
+ if temp_h < max_h or abs(max_h - temp_h) < 100:
+ curr_h += tmp.height + 15
+ is_use.append(tmp.uid)
+ group.append(tmp)
+ break
+ else:
+ break
+ total_w += max([x.width for x in group]) + 15
+ image_group.append(group)
+ while surplus_list:
+ surplus_list = [x for x in surplus_list if x.uid not in is_use]
+ if not surplus_list:
+ break
+ surplus_list.sort(key=lambda x: x.height, reverse=True)
+ for img in surplus_list:
+ if img.uid not in is_use:
+ _w = 0
+ index = -1
+ for i, ig in enumerate(image_group):
+ if s := sum([x.height for x in ig]) > _w:
+ _w = s
+ index = i
+ if index != -1:
+ image_group[index].append(img)
+ is_use.append(img.uid)
+
+ max_h = 0
+ max_w = 0
+ for ig in image_group:
+ if (_h := sum([x.height + 15 for x in ig])) > max_h:
+ max_h = _h
+ max_w += max([x.width for x in ig]) + 30
+ is_use.clear()
+ while abs(max_h - max_w) > 200 and len(image_group) - 1 >= len(image_group[-1]):
+ for img in image_group[-1]:
+ _min_h = 999999
+ _min_index = -1
+ for i, ig in enumerate(image_group):
+ # if i not in is_use and (_h := sum([x.h for x in ig]) + img.h) > _min_h:
+ if (_h := sum([x.height for x in ig]) + img.height) < _min_h:
+ _min_h = _h
+ _min_index = i
+ is_use.append(_min_index)
+ image_group[_min_index].append(img)
+ max_w -= max([x.width for x in image_group[-1]]) - 30
+ image_group.pop(-1)
+ max_h = max([sum([x.height + 15 for x in ig]) for ig in image_group])
+ return image_group, max(max_h + 250, max_w + 70)
+
+
+async def build_sort_image(
+ image_group: list[list[BuildImage]],
+ h: int | None = None,
+ padding_top: int = 200,
+ color: ColorAlias = (
+ 255,
+ 255,
+ 255,
+ ),
+ background_path: Path | None = None,
+ background_handle: Callable[[BuildImage], Awaitable] | None = None,
+) -> BuildImage:
+ """
+ 说明:
+ 对group_image的图片进行组装
+ 参数:
+ image_group: 分组图片列表
+ h: max(宽,高),一般为group_image的返回值,有值时,图片必定为正方形
+ padding_top: 图像列表与最顶层间距
+ color: 背景颜色
+ background_path: 背景图片文件夹路径(随机)
+ background_handle: 背景图额外操作
+ """
+ bk_file = None
+ if background_path:
+ random_bk = os.listdir(background_path)
+ if random_bk:
+ bk_file = random.choice(random_bk)
+ image_w = 0
+ image_h = 0
+ if not h:
+ for ig in image_group:
+ _w = max([x.width + 30 for x in ig])
+ image_w += _w + 30
+ _h = sum([x.height + 10 for x in ig])
+ if _h > image_h:
+ image_h = _h
+ image_h += padding_top
+ else:
+ image_w = h
+ image_h = h
+ A = BuildImage(
+ image_w,
+ image_h,
+ font_size=24,
+ font="CJGaoDeGuo.otf",
+ color=color,
+ background=(background_path / bk_file) if background_path and bk_file else None,
+ )
+ if background_handle:
+ if is_coroutine_callable(background_handle):
+ await background_handle(A)
+ else:
+ background_handle(A)
+ curr_w = 50
+ for ig in image_group:
+ curr_h = padding_top - 20
+ for img in ig:
+ await A.paste(img, (curr_w, curr_h))
+ curr_h += img.height + 10
+ curr_w += max([x.width for x in ig]) + 30
+ return A
diff --git a/zhenxun/utils/user_agent.py b/zhenxun/utils/user_agent.py
new file mode 100644
index 00000000..3047da33
--- /dev/null
+++ b/zhenxun/utils/user_agent.py
@@ -0,0 +1,50 @@
+import random
+
+user_agent = [
+ "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
+ "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
+ "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
+ "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko",
+ "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
+ "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
+ "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
+ "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
+ "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
+ "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)",
+ "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
+ "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
+ "Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
+ "Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
+ "Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
+ "MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
+ "Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10",
+ "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13",
+ "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+",
+ "Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0",
+ "Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124",
+ "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)",
+ "UCWEB7.0.2.37/28/999",
+ "NOKIA5700/ UCWEB7.0.2.37/28/999",
+ "Openwave/ UCWEB7.0.2.37/28/999",
+ "Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999",
+ # iPhone 6:
+ "Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25",
+]
+
+
+def get_user_agent():
+ return {"User-Agent": random.choice(user_agent)}
+
+
+def get_user_agent_str():
+ return random.choice(user_agent)
diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py
index 660b0327..86e8c4e4 100644
--- a/zhenxun/utils/utils.py
+++ b/zhenxun/utils/utils.py
@@ -1,11 +1,42 @@
import os
+import time
+from collections import defaultdict
from pathlib import Path
+from typing import Any
import httpx
from zhenxun.services.log import logger
+class WithdrawManager:
+ """
+ 消息撤回
+ """
+
+ _data = {}
+
+ @classmethod
+ def append(cls, message_id: str, second: int):
+ """添加一个撤回消息id和时间
+
+ 参数:
+ message_id: 撤回消息id
+ time: 延迟时间
+ """
+ cls._data[message_id] = second
+
+ @classmethod
+ def remove(cls, message_id: str):
+ """删除一个数据
+
+ 参数:
+ message_id: 撤回消息id
+ """
+ if message_id in cls._data:
+ del cls._data[message_id]
+
+
class ResourceDirManager:
"""
临时文件管理器
@@ -45,6 +76,69 @@ class ResourceDirManager:
cls.__tree_append(path)
+class CountLimiter:
+ """
+ 次数检测工具,检测调用次数是否超过设定值
+ """
+
+ def __init__(self, max_count: int):
+ self.count = defaultdict(int)
+ self.max_count = max_count
+
+ def add(self, key: Any):
+ self.count[key] += 1
+
+ def check(self, key: Any) -> bool:
+ if self.count[key] >= self.max_count:
+ self.count[key] = 0
+ return True
+ return False
+
+
+class UserBlockLimiter:
+ """
+ 检测用户是否正在调用命令
+ """
+
+ def __init__(self):
+ self.flag_data = defaultdict(bool)
+ self.time = time.time()
+
+ def set_true(self, key: Any):
+ self.time = time.time()
+ self.flag_data[key] = True
+
+ def set_false(self, key: Any):
+ self.flag_data[key] = False
+
+ def check(self, key: Any) -> bool:
+ if time.time() - self.time > 30:
+ self.set_false(key)
+ return False
+ return self.flag_data[key]
+
+
+class FreqLimiter:
+ """
+ 命令冷却,检测用户是否处于冷却状态
+ """
+
+ def __init__(self, default_cd_seconds: int):
+ self.next_time = defaultdict(float)
+ self.default_cd = default_cd_seconds
+
+ def check(self, key: Any) -> bool:
+ return time.time() >= self.next_time[key]
+
+ def start_cd(self, key: Any, cd_time: int = 0):
+ self.next_time[key] = time.time() + (
+ cd_time if cd_time > 0 else self.default_cd
+ )
+
+ def left_time(self, key: Any) -> float:
+ return self.next_time[key] - time.time()
+
+
async def get_user_avatar(uid: int | str) -> bytes | None:
"""快捷获取用户头像