nonebot-plugin-parser 2.6.5__tar.gz → 2.6.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/PKG-INFO +6 -3
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/README.md +4 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/pyproject.toml +4 -5
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/config.py +7 -0
- nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/matchers/filter.py +82 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +2 -2
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/task.py +6 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py +1 -1
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/__init__.py +1 -6
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/base.py +2 -2
- nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/renders/common/__init__.py +4 -0
- nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/renders/common/assets.py +103 -0
- nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/renders/common/font.py +86 -0
- nonebot_plugin_parser-2.6.5/src/nonebot_plugin_parser/renders/common.py → nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/renders/common/renderer.py +64 -169
- nonebot_plugin_parser-2.6.5/src/nonebot_plugin_parser/matchers/filter.py +0 -72
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/constants.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/download/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/download/task.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/exception.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/helper.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/matchers/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/matchers/rule.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/acfun/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/acfun/video.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/base.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/data.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/kuaishou/states.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/twitter.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/utils.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/weibo/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/weibo/article.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/weibo/common.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/weibo/show.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/xiaohongshu/common.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/xiaohongshu/discovery.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/xiaohongshu/explore.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/youtube/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/youtube/meta.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/default.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/htmlrender.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/__init__.py +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/avatar.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/1.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/2.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/3.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/4.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/5.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/6.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/7.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/8.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/fallback_pic/9.jpg +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/play.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/renders/templates/card.html.jinja2 +0 -0
- {nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/utils.py +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: nonebot-plugin-parser
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.7
|
|
4
4
|
Summary: NoneBot2 链接分享解析 Alconna 版, 现支持B站|抖音|快手|微博|小红书|YouTube|TikTok|Twitter|AcFun|NGA
|
|
5
5
|
Keywords: acfun,bilibili,douyin,kuaishou,nga,nonebot,nonebot2,tiktok,twitter,video,weibo,xiaohongshu,youtube
|
|
6
6
|
Author: fllesser
|
|
7
7
|
Author-email: fllesser <fllessive@gmail.com>
|
|
8
8
|
Classifier: Development Status :: 5 - Production/Stable
|
|
9
9
|
Classifier: Framework :: AsyncIO
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
10
|
Classifier: Operating System :: OS Independent
|
|
12
11
|
Classifier: Programming Language :: Python :: 3
|
|
13
12
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -28,7 +27,7 @@ Requires-Dist: nonebot2>=2.5.0,<3.0.0
|
|
|
28
27
|
Requires-Dist: apilmoji[rich]>=0.3.1,<1.0.0
|
|
29
28
|
Requires-Dist: beautifulsoup4>=4.12.0,<5.0.0
|
|
30
29
|
Requires-Dist: curl-cffi>=0.13.0,!=0.14.0,<1.0.0
|
|
31
|
-
Requires-Dist: bilibili-api-python>=17.4.
|
|
30
|
+
Requires-Dist: bilibili-api-python>=17.4.2,<18.0.0
|
|
32
31
|
Requires-Dist: nonebot-plugin-uninfo>=0.10.1,<1.0.0
|
|
33
32
|
Requires-Dist: nonebot-plugin-alconna>=0.60.4,<1.0.0
|
|
34
33
|
Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
|
|
@@ -316,6 +315,10 @@ parser_emoji_cdn="https://emojicdn.elk.sh"
|
|
|
316
315
|
|
|
317
316
|
# [可选] emoji 渲染样式 "apple", "google", "twitter", "facebook"(默认)
|
|
318
317
|
parser_emoji_style="facebook"
|
|
318
|
+
|
|
319
|
+
# [可选] 是否启用群组黑名单模式(默认启用,即所有群聊的解析都是开启的)
|
|
320
|
+
parser_group_blacklist_enabled=True
|
|
321
|
+
|
|
319
322
|
```
|
|
320
323
|
|
|
321
324
|
</details>
|
|
@@ -262,6 +262,10 @@ parser_emoji_cdn="https://emojicdn.elk.sh"
|
|
|
262
262
|
|
|
263
263
|
# [可选] emoji 渲染样式 "apple", "google", "twitter", "facebook"(默认)
|
|
264
264
|
parser_emoji_style="facebook"
|
|
265
|
+
|
|
266
|
+
# [可选] 是否启用群组黑名单模式(默认启用,即所有群聊的解析都是开启的)
|
|
267
|
+
parser_group_blacklist_enabled=True
|
|
268
|
+
|
|
265
269
|
```
|
|
266
270
|
|
|
267
271
|
</details>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nonebot-plugin-parser"
|
|
3
|
-
version = "2.6.
|
|
3
|
+
version = "2.6.7"
|
|
4
4
|
description = "NoneBot2 链接分享解析 Alconna 版, 现支持B站|抖音|快手|微博|小红书|YouTube|TikTok|Twitter|AcFun|NGA"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -24,7 +24,6 @@ keywords = [
|
|
|
24
24
|
classifiers = [
|
|
25
25
|
"Development Status :: 5 - Production/Stable",
|
|
26
26
|
"Framework :: AsyncIO",
|
|
27
|
-
"License :: OSI Approved :: MIT License",
|
|
28
27
|
"Operating System :: OS Independent",
|
|
29
28
|
"Programming Language :: Python :: 3",
|
|
30
29
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -48,7 +47,7 @@ dependencies = [
|
|
|
48
47
|
"apilmoji[rich]>=0.3.1,<1.0.0",
|
|
49
48
|
"beautifulsoup4>=4.12.0,<5.0.0",
|
|
50
49
|
"curl_cffi>=0.13.0,<1.0.0,!=0.14.0",
|
|
51
|
-
"bilibili-api-python>=17.4.
|
|
50
|
+
"bilibili-api-python>=17.4.2,<18.0.0",
|
|
52
51
|
"nonebot-plugin-uninfo>=0.10.1,<1.0.0",
|
|
53
52
|
"nonebot-plugin-alconna>=0.60.4,<1.0.0",
|
|
54
53
|
"nonebot-plugin-localstore>=0.7.4,<1.0.0",
|
|
@@ -100,7 +99,7 @@ pydantic-v2 = ["pydantic>=2.0.0"]
|
|
|
100
99
|
telegram = ["nonebot-adapter-telegram>=0.1.0b20"]
|
|
101
100
|
|
|
102
101
|
[build-system]
|
|
103
|
-
requires = ["uv_build>=0.
|
|
102
|
+
requires = ["uv_build>=0.11.0,<0.12.0"]
|
|
104
103
|
build-backend = "uv_build"
|
|
105
104
|
|
|
106
105
|
[tool.uv]
|
|
@@ -116,7 +115,7 @@ conflicts = [
|
|
|
116
115
|
[tool.bumpversion]
|
|
117
116
|
tag = true
|
|
118
117
|
commit = true
|
|
119
|
-
current_version = "2.6.
|
|
118
|
+
current_version = "2.6.7"
|
|
120
119
|
message = "release: bump vesion from {current_version} to {new_version}"
|
|
121
120
|
|
|
122
121
|
[[tool.bumpversion.files]]
|
{nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/config.py
RENAMED
|
@@ -56,6 +56,8 @@ class Config(BaseModel):
|
|
|
56
56
|
"""Pilmoji 表情 CDN"""
|
|
57
57
|
parser_emoji_style: EmojiStyle = EmojiStyle.FACEBOOK
|
|
58
58
|
"""Pilmoji 表情样式"""
|
|
59
|
+
parser_group_blacklist_enabled: bool = True
|
|
60
|
+
"""是否启用群组黑名单模式(默认启用,即所有群聊的解析都是开启的)"""
|
|
59
61
|
|
|
60
62
|
@property
|
|
61
63
|
def nickname(self) -> str:
|
|
@@ -182,6 +184,11 @@ class Config(BaseModel):
|
|
|
182
184
|
"""Pilmoji 表情样式"""
|
|
183
185
|
return self.parser_emoji_style
|
|
184
186
|
|
|
187
|
+
@property
|
|
188
|
+
def group_blacklist_enabled(self) -> bool:
|
|
189
|
+
"""是否启用群组黑名单模式"""
|
|
190
|
+
return self.parser_group_blacklist_enabled
|
|
191
|
+
|
|
185
192
|
|
|
186
193
|
pconfig: Config = get_plugin_config(Config)
|
|
187
194
|
"""插件配置"""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from nonebot import on_command
|
|
5
|
+
from nonebot.rule import to_me
|
|
6
|
+
from nonebot.matcher import Matcher
|
|
7
|
+
from nonebot.permission import SUPERUSER
|
|
8
|
+
from nonebot_plugin_uninfo import ADMIN, Session, UniSession
|
|
9
|
+
|
|
10
|
+
from ..config import pconfig
|
|
11
|
+
|
|
12
|
+
_DISABLED_GROUPS_PATH: Path = pconfig.data_dir / "disabled_groups.json"
|
|
13
|
+
_GROUP_SET_PATH: Path = pconfig.data_dir / "group_set.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_or_initialize_set() -> set[str]:
|
|
17
|
+
"""加载群配置"""
|
|
18
|
+
# 判断是否存在
|
|
19
|
+
if not _GROUP_SET_PATH.exists():
|
|
20
|
+
if _DISABLED_GROUPS_PATH.exists():
|
|
21
|
+
# 迁移旧的关闭解析名单
|
|
22
|
+
_DISABLED_GROUPS_PATH.rename(_GROUP_SET_PATH)
|
|
23
|
+
else:
|
|
24
|
+
_GROUP_SET_PATH.write_text(json.dumps([]))
|
|
25
|
+
|
|
26
|
+
return set(json.loads(_GROUP_SET_PATH.read_text()))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_GROUP_SET: set[str] = _load_or_initialize_set()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _save_group_set():
|
|
33
|
+
_GROUP_SET_PATH.write_text(json.dumps(list(_GROUP_SET)))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _add_group(group_key: str):
|
|
37
|
+
_GROUP_SET.add(group_key)
|
|
38
|
+
_save_group_set()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _remove_group(group_key: str):
|
|
42
|
+
_GROUP_SET.discard(group_key)
|
|
43
|
+
_save_group_set()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_group_key(session: Session) -> str:
|
|
47
|
+
"""获取群组的唯一标识符 由平台名称和会话场景 ID 组成,例如 `QQClient_123456789`"""
|
|
48
|
+
return f"{session.scope}_{session.scene_path}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_enabled(session: Session = UniSession()) -> bool:
|
|
52
|
+
"""判断当前会话是否在关闭解析的名单中"""
|
|
53
|
+
if session.scene.is_private:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
group_key = _get_group_key(session)
|
|
57
|
+
if pconfig.group_blacklist_enabled:
|
|
58
|
+
return group_key not in _GROUP_SET
|
|
59
|
+
else:
|
|
60
|
+
return group_key in _GROUP_SET
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@on_command("开启解析", rule=to_me(), permission=SUPERUSER | ADMIN(), block=True).handle()
|
|
64
|
+
async def _(matcher: Matcher, session: Session = UniSession()):
|
|
65
|
+
"""开启解析"""
|
|
66
|
+
group_key = _get_group_key(session)
|
|
67
|
+
if pconfig.group_blacklist_enabled:
|
|
68
|
+
_remove_group(group_key)
|
|
69
|
+
else:
|
|
70
|
+
_add_group(group_key)
|
|
71
|
+
await matcher.finish("解析已开启")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@on_command("关闭解析", rule=to_me(), permission=SUPERUSER | ADMIN(), block=True).handle()
|
|
75
|
+
async def _(matcher: Matcher, session: Session = UniSession()):
|
|
76
|
+
"""关闭解析"""
|
|
77
|
+
group_key = _get_group_key(session)
|
|
78
|
+
if pconfig.group_blacklist_enabled:
|
|
79
|
+
_add_group(group_key)
|
|
80
|
+
else:
|
|
81
|
+
_remove_group(group_key)
|
|
82
|
+
await matcher.finish("解析已关闭")
|
|
@@ -11,7 +11,7 @@ class Author(Struct):
|
|
|
11
11
|
face: str
|
|
12
12
|
mid: int
|
|
13
13
|
pub_time: str
|
|
14
|
-
pub_ts:
|
|
14
|
+
pub_ts: str
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Image(Struct):
|
|
@@ -115,7 +115,7 @@ class OpusItem(Struct):
|
|
|
115
115
|
"""获取发布时间戳"""
|
|
116
116
|
for module in self.item.modules:
|
|
117
117
|
if module.module_type == "MODULE_TYPE_AUTHOR" and module.module_author:
|
|
118
|
-
return module.module_author.pub_ts
|
|
118
|
+
return int(module.module_author.pub_ts)
|
|
119
119
|
return None
|
|
120
120
|
|
|
121
121
|
def extract_nodes(self):
|
|
@@ -3,6 +3,10 @@ from asyncio import Task, create_task
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from collections.abc import Callable, Coroutine
|
|
5
5
|
|
|
6
|
+
from nonebot import logger
|
|
7
|
+
|
|
8
|
+
from ..exception import ParseException
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
class PathTask:
|
|
8
12
|
__slots__ = ("_path", "_task")
|
|
@@ -31,6 +35,8 @@ class PathTask:
|
|
|
31
35
|
try:
|
|
32
36
|
return await self.get()
|
|
33
37
|
except Exception as e:
|
|
38
|
+
if not isinstance(e, ParseException):
|
|
39
|
+
logger.opt(exception=e).error(f"task({self._task.get_name()}) failed")
|
|
34
40
|
if on_error is not None:
|
|
35
41
|
on_error(e)
|
|
36
42
|
return None
|
|
@@ -33,7 +33,7 @@ class XiaoHongShuParser(BaseParser):
|
|
|
33
33
|
self.headers["cookie"] = pconfig.xhs_ck
|
|
34
34
|
self.ios_headers["cookie"] = pconfig.xhs_ck
|
|
35
35
|
|
|
36
|
-
@handle("xhslink
|
|
36
|
+
@handle("xhslink", r"xhslink\.(com|cn)/[A-Za-z0-9._?%&+=/#@-]+")
|
|
37
37
|
async def _parse_short_link(self, searched: re.Match[str]):
|
|
38
38
|
url = f"https://{searched.group(0)}"
|
|
39
39
|
return await self.parse_with_redirect(url, self.ios_headers)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
|
|
3
|
-
from nonebot import logger
|
|
3
|
+
from nonebot import logger
|
|
4
4
|
|
|
5
5
|
from .base import BaseRenderer
|
|
6
6
|
from .common import CommonRenderer
|
|
@@ -37,8 +37,3 @@ def get_renderer(platform: str) -> type[BaseRenderer]:
|
|
|
37
37
|
|
|
38
38
|
module = importlib.import_module("." + platform, package=__name__)
|
|
39
39
|
return getattr(module, "Renderer")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@get_driver().on_startup
|
|
43
|
-
async def load_resources():
|
|
44
|
-
CommonRenderer.load_resources()
|
|
@@ -11,7 +11,7 @@ import aiofiles
|
|
|
11
11
|
from ..config import pconfig
|
|
12
12
|
from ..helper import UniHelper, UniMessage, ForwardNodeInner
|
|
13
13
|
from ..parsers import ParseResult, AudioContent, ImageContent, VideoContent
|
|
14
|
-
from ..exception import DownloadException
|
|
14
|
+
from ..exception import IgnoreException, DownloadException
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class BaseRenderer(ABC):
|
|
@@ -39,7 +39,7 @@ class BaseRenderer(ABC):
|
|
|
39
39
|
other_segs: list[ForwardNodeInner] = []
|
|
40
40
|
|
|
41
41
|
def on_error(e: Exception):
|
|
42
|
-
if isinstance(e,
|
|
42
|
+
if not isinstance(e, IgnoreException):
|
|
43
43
|
nonlocal failed_count
|
|
44
44
|
failed_count += 1
|
|
45
45
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
from nonebot import logger, get_driver
|
|
5
|
+
from apilmoji import EmojiCDNSource
|
|
6
|
+
|
|
7
|
+
from .. import resources
|
|
8
|
+
from .font import CardFonts, CardTheme
|
|
9
|
+
from ...config import pconfig
|
|
10
|
+
|
|
11
|
+
PILImage = Image.Image
|
|
12
|
+
|
|
13
|
+
# apilmoji emoji 源
|
|
14
|
+
EMOJI_SOURCE = EmojiCDNSource(
|
|
15
|
+
base_url=pconfig.emoji_cdn,
|
|
16
|
+
style=pconfig.emoji_style,
|
|
17
|
+
cache_dir=pconfig.cache_dir / "emojis",
|
|
18
|
+
show_progress=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
DEFAULT_THEME = CardTheme(
|
|
22
|
+
name=(0, 122, 255),
|
|
23
|
+
title=(102, 51, 153),
|
|
24
|
+
body=(51, 51, 51),
|
|
25
|
+
muted=(136, 136, 136),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
AVATAR_SIZE = 80
|
|
29
|
+
FONTS: CardFonts
|
|
30
|
+
PLATFORM_LOGOS: dict[str, PILImage]
|
|
31
|
+
AVATAR_IMAGE: PILImage
|
|
32
|
+
VIDEO_BUTTON_IMAGE: PILImage
|
|
33
|
+
|
|
34
|
+
_resources_loaded = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@get_driver().on_startup
|
|
38
|
+
async def load_common_renderer_resources():
|
|
39
|
+
load_resources()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ensure_resources() -> None:
|
|
43
|
+
if _resources_loaded:
|
|
44
|
+
return
|
|
45
|
+
load_resources()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_resources() -> None:
|
|
49
|
+
"""加载渲染资源(幂等)"""
|
|
50
|
+
global _resources_loaded, FONTS, PLATFORM_LOGOS, AVATAR_IMAGE, VIDEO_BUTTON_IMAGE
|
|
51
|
+
|
|
52
|
+
if _resources_loaded:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
FONTS = _load_fonts()
|
|
56
|
+
PLATFORM_LOGOS = _load_platform_logos()
|
|
57
|
+
AVATAR_IMAGE = _load_default_avatar()
|
|
58
|
+
VIDEO_BUTTON_IMAGE = _load_video_button()
|
|
59
|
+
|
|
60
|
+
_resources_loaded = True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_fonts() -> CardFonts:
|
|
64
|
+
"""字体(昵称 / 标题 / 正文 / 辅助文案)"""
|
|
65
|
+
font_path = pconfig.custom_font or resources.DEFAULT_FONT_PATH
|
|
66
|
+
loaded = CardFonts.load(font_path, DEFAULT_THEME)
|
|
67
|
+
logger.success(f"加载字体「{font_path.name}」成功")
|
|
68
|
+
return loaded
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _load_platform_logos() -> dict[str, PILImage]:
|
|
72
|
+
"""平台 Logo"""
|
|
73
|
+
from ...constants import PlatformEnum
|
|
74
|
+
|
|
75
|
+
logos: dict[str, PILImage] = {}
|
|
76
|
+
loaded_platforms = []
|
|
77
|
+
for platform_name in PlatformEnum:
|
|
78
|
+
logo_path = resources.RESOURCES_DIR / f"{platform_name}.png"
|
|
79
|
+
if logo_path.exists():
|
|
80
|
+
with Image.open(logo_path) as img:
|
|
81
|
+
logos[str(platform_name)] = img.convert("RGBA")
|
|
82
|
+
loaded_platforms.append(platform_name)
|
|
83
|
+
logger.debug(f"加载 Logo「{', '.join(loaded_platforms)}」成功")
|
|
84
|
+
return logos
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_default_avatar() -> PILImage:
|
|
88
|
+
"""默认头像(作者无头像或加载失败时回退)"""
|
|
89
|
+
with Image.open(resources.DEFAULT_AVATAR_PATH) as img:
|
|
90
|
+
loaded = img.convert("RGBA").resize((AVATAR_SIZE, AVATAR_SIZE))
|
|
91
|
+
logger.debug(f"加载头像「{resources.DEFAULT_AVATAR_PATH.name}」成功")
|
|
92
|
+
return loaded
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _load_video_button() -> PILImage:
|
|
96
|
+
"""视频播放按钮(封面居中叠加,半透明)"""
|
|
97
|
+
with Image.open(resources.DEFAULT_VIDEO_BUTTON_PATH) as img:
|
|
98
|
+
button = img.convert("RGBA").resize((100, 100))
|
|
99
|
+
alpha = button.split()[-1]
|
|
100
|
+
alpha = alpha.point(lambda x: int(x * 0.6))
|
|
101
|
+
button.putalpha(alpha)
|
|
102
|
+
logger.debug(f"加载视频播放按钮「{resources.DEFAULT_VIDEO_BUTTON_PATH.name}」成功")
|
|
103
|
+
return button
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from PIL import ImageFont
|
|
8
|
+
from apilmoji.core import get_font_height
|
|
9
|
+
|
|
10
|
+
Color = tuple[int, int, int]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(eq=False, frozen=True, slots=True)
|
|
14
|
+
class FontMetrics:
|
|
15
|
+
"""字体度量(换行、估高)"""
|
|
16
|
+
|
|
17
|
+
font: ImageFont.FreeTypeFont
|
|
18
|
+
line_height: int
|
|
19
|
+
cjk_width: int
|
|
20
|
+
|
|
21
|
+
def __hash__(self) -> int:
|
|
22
|
+
return hash((id(self.font), self.line_height, self.cjk_width))
|
|
23
|
+
|
|
24
|
+
@lru_cache(maxsize=500)
|
|
25
|
+
def get_char_width(self, char: str) -> int:
|
|
26
|
+
bbox = self.font.getbbox(char)
|
|
27
|
+
return int(bbox[2] - bbox[0])
|
|
28
|
+
|
|
29
|
+
def get_char_width_fast(self, char: str) -> int:
|
|
30
|
+
if "\u4e00" <= char <= "\u9fff":
|
|
31
|
+
return self.cjk_width
|
|
32
|
+
return self.get_char_width(char)
|
|
33
|
+
|
|
34
|
+
def get_text_width(self, text: str) -> int:
|
|
35
|
+
if not text:
|
|
36
|
+
return 0
|
|
37
|
+
return sum(self.get_char_width_fast(char) for char in text)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class CardTheme:
|
|
42
|
+
"""卡片颜色"""
|
|
43
|
+
|
|
44
|
+
name: Color
|
|
45
|
+
title: Color
|
|
46
|
+
body: Color
|
|
47
|
+
muted: Color
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class StyledFont:
|
|
52
|
+
"""度量 + 颜色"""
|
|
53
|
+
|
|
54
|
+
metrics: FontMetrics
|
|
55
|
+
fill: Color
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_styled(font_path: Path, size: int, fill: Color) -> StyledFont:
|
|
59
|
+
font = ImageFont.truetype(font_path, size)
|
|
60
|
+
return StyledFont(
|
|
61
|
+
metrics=FontMetrics(
|
|
62
|
+
font=font,
|
|
63
|
+
line_height=get_font_height(font),
|
|
64
|
+
cjk_width=size,
|
|
65
|
+
),
|
|
66
|
+
fill=fill,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class CardFonts:
|
|
72
|
+
"""卡片各区块字体(加载时组合 theme)"""
|
|
73
|
+
|
|
74
|
+
name: StyledFont
|
|
75
|
+
title: StyledFont
|
|
76
|
+
body: StyledFont
|
|
77
|
+
muted: StyledFont
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def load(cls, font_path: Path, theme: CardTheme) -> CardFonts:
|
|
81
|
+
return cls(
|
|
82
|
+
name=_load_styled(font_path, 28, theme.name),
|
|
83
|
+
title=_load_styled(font_path, 30, theme.title),
|
|
84
|
+
body=_load_styled(font_path, 24, theme.body),
|
|
85
|
+
muted=_load_styled(font_path, 24, theme.muted),
|
|
86
|
+
)
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
from io import BytesIO
|
|
2
2
|
from typing import ClassVar
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from functools import lru_cache
|
|
5
|
-
from dataclasses import dataclass
|
|
6
4
|
from typing_extensions import override
|
|
7
5
|
|
|
8
6
|
import emoji
|
|
9
7
|
from PIL import Image, ImageDraw, ImageFont
|
|
10
8
|
from nonebot import logger
|
|
11
|
-
from apilmoji import Apilmoji
|
|
12
|
-
from apilmoji.core import get_font_height
|
|
9
|
+
from apilmoji import Apilmoji
|
|
13
10
|
|
|
14
|
-
from . import
|
|
15
|
-
from
|
|
16
|
-
from
|
|
11
|
+
from . import assets
|
|
12
|
+
from .. import resources
|
|
13
|
+
from .font import StyledFont, FontMetrics
|
|
14
|
+
from ..base import ParseResult, ImageContent, ImageRenderer
|
|
17
15
|
|
|
18
16
|
Color = tuple[int, int, int]
|
|
19
17
|
PILImage = Image.Image
|
|
@@ -28,71 +26,11 @@ except OSError:
|
|
|
28
26
|
emosvg = None
|
|
29
27
|
|
|
30
28
|
|
|
31
|
-
@dataclass(eq=False, frozen=True, slots=True)
|
|
32
|
-
class FontInfo:
|
|
33
|
-
"""字体信息数据类"""
|
|
34
|
-
|
|
35
|
-
font: ImageFont.FreeTypeFont
|
|
36
|
-
fill: Color
|
|
37
|
-
line_height: int
|
|
38
|
-
cjk_width: int
|
|
39
|
-
|
|
40
|
-
def __hash__(self) -> int:
|
|
41
|
-
return hash((id(self.font), self.line_height, self.cjk_width, self.fill))
|
|
42
|
-
|
|
43
|
-
@lru_cache(maxsize=500)
|
|
44
|
-
def get_char_width(self, char: str) -> int:
|
|
45
|
-
bbox = self.font.getbbox(char)
|
|
46
|
-
return int(bbox[2] - bbox[0])
|
|
47
|
-
|
|
48
|
-
def get_char_width_fast(self, char: str) -> int:
|
|
49
|
-
if "\u4e00" <= char <= "\u9fff":
|
|
50
|
-
return self.cjk_width
|
|
51
|
-
return self.get_char_width(char)
|
|
52
|
-
|
|
53
|
-
def get_text_width(self, text: str) -> int:
|
|
54
|
-
if not text:
|
|
55
|
-
return 0
|
|
56
|
-
return sum(self.get_char_width_fast(char) for char in text)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@dataclass(eq=False, frozen=True, slots=True)
|
|
60
|
-
class FontSet:
|
|
61
|
-
"""字体集数据类"""
|
|
62
|
-
|
|
63
|
-
_FONT_INFOS = (
|
|
64
|
-
("name", 28, (0, 122, 255)),
|
|
65
|
-
("title", 30, (102, 51, 153)),
|
|
66
|
-
("text", 24, (51, 51, 51)),
|
|
67
|
-
("extra", 24, (136, 136, 136)),
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
name: FontInfo
|
|
71
|
-
title: FontInfo
|
|
72
|
-
text: FontInfo
|
|
73
|
-
extra: FontInfo
|
|
74
|
-
|
|
75
|
-
@classmethod
|
|
76
|
-
def new(cls, font_path: Path):
|
|
77
|
-
font_infos: dict[str, FontInfo] = {}
|
|
78
|
-
for name, size, fill in cls._FONT_INFOS:
|
|
79
|
-
font = ImageFont.truetype(font_path, size)
|
|
80
|
-
height = get_font_height(font)
|
|
81
|
-
font_infos[name] = FontInfo(
|
|
82
|
-
font=font,
|
|
83
|
-
fill=fill,
|
|
84
|
-
line_height=height,
|
|
85
|
-
cjk_width=size,
|
|
86
|
-
)
|
|
87
|
-
return FontSet(**font_infos)
|
|
88
|
-
|
|
89
|
-
|
|
90
29
|
class CommonRenderer(ImageRenderer):
|
|
91
30
|
"""统一渲染器"""
|
|
92
31
|
|
|
93
32
|
# 布局常量
|
|
94
33
|
PADDING = 25
|
|
95
|
-
AVATAR_SIZE = 80
|
|
96
34
|
AVATAR_TEXT_GAP = 15
|
|
97
35
|
SECTION_SPACING = 15
|
|
98
36
|
NAME_TIME_GAP = 5
|
|
@@ -116,16 +54,9 @@ class CommonRenderer(ImageRenderer):
|
|
|
116
54
|
REPOST_BG_COLOR: ClassVar[Color] = (247, 247, 247)
|
|
117
55
|
REPOST_BORDER_COLOR: ClassVar[Color] = (230, 230, 230)
|
|
118
56
|
|
|
119
|
-
# apilmoji emoji 源
|
|
120
|
-
EMOJI_SOURCE: ClassVar[EmojiCDNSource] = EmojiCDNSource(
|
|
121
|
-
base_url=pconfig.emoji_cdn,
|
|
122
|
-
style=pconfig.emoji_style,
|
|
123
|
-
cache_dir=pconfig.cache_dir / "emojis",
|
|
124
|
-
show_progress=True,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
57
|
def __init__(self, result: ParseResult, not_repost: bool = True):
|
|
128
58
|
super().__init__(result, not_repost)
|
|
59
|
+
assets.ensure_resources()
|
|
129
60
|
|
|
130
61
|
self.card_width: int = self.DEFAULT_CARD_WIDTH
|
|
131
62
|
self.content_width: int = self.card_width - 2 * self.PADDING
|
|
@@ -140,49 +71,6 @@ class CommonRenderer(ImageRenderer):
|
|
|
140
71
|
False,
|
|
141
72
|
)
|
|
142
73
|
|
|
143
|
-
@classmethod
|
|
144
|
-
def load_resources(cls):
|
|
145
|
-
"""加载资源"""
|
|
146
|
-
cls._load_fonts()
|
|
147
|
-
cls._load_platform_logos()
|
|
148
|
-
cls._load_other_resources()
|
|
149
|
-
|
|
150
|
-
@classmethod
|
|
151
|
-
def _load_fonts(cls):
|
|
152
|
-
font_path = pconfig.custom_font or resources.DEFAULT_FONT_PATH
|
|
153
|
-
cls.fontset = FontSet.new(font_path)
|
|
154
|
-
logger.success(f"加载字体「{font_path.name}」成功")
|
|
155
|
-
|
|
156
|
-
@classmethod
|
|
157
|
-
def _load_platform_logos(cls):
|
|
158
|
-
from ..constants import PlatformEnum
|
|
159
|
-
|
|
160
|
-
cls.platform_logos: dict[str, PILImage] = {}
|
|
161
|
-
loaded_platforms = []
|
|
162
|
-
for platform_name in PlatformEnum:
|
|
163
|
-
logo_path = resources.RESOURCES_DIR / f"{platform_name}.png"
|
|
164
|
-
|
|
165
|
-
if logo_path.exists():
|
|
166
|
-
with Image.open(logo_path) as img:
|
|
167
|
-
cls.platform_logos[str(platform_name)] = img.convert("RGBA")
|
|
168
|
-
loaded_platforms.append(platform_name)
|
|
169
|
-
logger.debug(f"加载 Logo「{', '.join(loaded_platforms)}」成功")
|
|
170
|
-
|
|
171
|
-
@classmethod
|
|
172
|
-
def _load_other_resources(cls):
|
|
173
|
-
# avatar
|
|
174
|
-
with Image.open(resources.DEFAULT_AVATAR_PATH) as img:
|
|
175
|
-
cls.avatar_image: PILImage = img.convert("RGBA").resize((cls.AVATAR_SIZE, cls.AVATAR_SIZE))
|
|
176
|
-
logger.debug(f"加载头像「{resources.DEFAULT_AVATAR_PATH.name}」成功")
|
|
177
|
-
|
|
178
|
-
# video button
|
|
179
|
-
with Image.open(resources.DEFAULT_VIDEO_BUTTON_PATH) as img:
|
|
180
|
-
cls.video_button_image: PILImage = img.convert("RGBA").resize((100, 100))
|
|
181
|
-
alpha = cls.video_button_image.split()[-1]
|
|
182
|
-
alpha = alpha.point(lambda x: int(x * 0.6))
|
|
183
|
-
cls.video_button_image.putalpha(alpha)
|
|
184
|
-
logger.debug(f"加载视频播放按钮「{resources.DEFAULT_VIDEO_BUTTON_PATH.name}」成功")
|
|
185
|
-
|
|
186
74
|
@override
|
|
187
75
|
async def render_image(self) -> bytes:
|
|
188
76
|
image = await self._render_image()
|
|
@@ -214,11 +102,11 @@ class CommonRenderer(ImageRenderer):
|
|
|
214
102
|
def _estimate_text_height(
|
|
215
103
|
self,
|
|
216
104
|
text: str,
|
|
217
|
-
|
|
105
|
+
metrics: FontMetrics,
|
|
218
106
|
content_width: int,
|
|
219
107
|
) -> int:
|
|
220
108
|
"""估算文本高度(考虑换行符)"""
|
|
221
|
-
return (text.count("\n") + 1 + len(text) *
|
|
109
|
+
return (text.count("\n") + 1 + len(text) * metrics.cjk_width // content_width) * metrics.line_height
|
|
222
110
|
|
|
223
111
|
def _estimate_height(self) -> int:
|
|
224
112
|
"""估算画布高度"""
|
|
@@ -227,13 +115,13 @@ class CommonRenderer(ImageRenderer):
|
|
|
227
115
|
|
|
228
116
|
# 头部(头像 + 名称 + 时间)
|
|
229
117
|
if self.result.author:
|
|
230
|
-
height +=
|
|
118
|
+
height += assets.AVATAR_SIZE + self.SECTION_SPACING
|
|
231
119
|
|
|
232
120
|
# 标题
|
|
233
121
|
if self.result.title:
|
|
234
122
|
height += self._estimate_text_height(
|
|
235
123
|
self.result.title,
|
|
236
|
-
|
|
124
|
+
assets.FONTS.title.metrics,
|
|
237
125
|
self.content_width,
|
|
238
126
|
)
|
|
239
127
|
height += self.SECTION_SPACING
|
|
@@ -244,7 +132,7 @@ class CommonRenderer(ImageRenderer):
|
|
|
244
132
|
if isinstance(item, str):
|
|
245
133
|
height += self._estimate_text_height(
|
|
246
134
|
item,
|
|
247
|
-
|
|
135
|
+
assets.FONTS.body.metrics,
|
|
248
136
|
self.content_width,
|
|
249
137
|
)
|
|
250
138
|
else:
|
|
@@ -258,14 +146,19 @@ class CommonRenderer(ImageRenderer):
|
|
|
258
146
|
if self.result.text:
|
|
259
147
|
height += self._estimate_text_height(
|
|
260
148
|
self.result.text,
|
|
261
|
-
|
|
149
|
+
assets.FONTS.body.metrics,
|
|
262
150
|
self.content_width,
|
|
263
151
|
)
|
|
264
152
|
height += self.SECTION_SPACING
|
|
265
153
|
|
|
266
154
|
# 额外信息
|
|
267
155
|
if self.result.extra_info:
|
|
268
|
-
height += self.
|
|
156
|
+
height += self._estimate_text_height(
|
|
157
|
+
self.result.extra_info,
|
|
158
|
+
assets.FONTS.muted.metrics,
|
|
159
|
+
self.content_width,
|
|
160
|
+
)
|
|
161
|
+
height += self.SECTION_SPACING
|
|
269
162
|
|
|
270
163
|
# 转发内容
|
|
271
164
|
if self.result.repost:
|
|
@@ -288,21 +181,21 @@ class CommonRenderer(ImageRenderer):
|
|
|
288
181
|
self._image.paste(avatar, (x_pos, self.y_pos), avatar)
|
|
289
182
|
|
|
290
183
|
# 文字区域
|
|
291
|
-
text_x = self.PADDING +
|
|
292
|
-
name_height =
|
|
184
|
+
text_x = self.PADDING + assets.AVATAR_SIZE + self.AVATAR_TEXT_GAP
|
|
185
|
+
name_height = assets.FONTS.name.metrics.line_height
|
|
293
186
|
time_str = self.result.formartted_datetime
|
|
294
|
-
time_height = (self.NAME_TIME_GAP +
|
|
187
|
+
time_height = (self.NAME_TIME_GAP + assets.FONTS.muted.metrics.line_height) if time_str else 0
|
|
295
188
|
text_height = name_height + time_height
|
|
296
189
|
|
|
297
190
|
# 垂直居中
|
|
298
|
-
text_y = self.y_pos + (
|
|
191
|
+
text_y = self.y_pos + (assets.AVATAR_SIZE - text_height) // 2
|
|
299
192
|
|
|
300
193
|
# 名称
|
|
301
194
|
self._draw.text(
|
|
302
195
|
(text_x, text_y),
|
|
303
196
|
self.result.author.name,
|
|
304
|
-
font=
|
|
305
|
-
fill=
|
|
197
|
+
font=assets.FONTS.name.metrics.font,
|
|
198
|
+
fill=assets.FONTS.name.fill,
|
|
306
199
|
)
|
|
307
200
|
text_y += name_height
|
|
308
201
|
|
|
@@ -312,39 +205,39 @@ class CommonRenderer(ImageRenderer):
|
|
|
312
205
|
self._draw.text(
|
|
313
206
|
(text_x, text_y),
|
|
314
207
|
time_str,
|
|
315
|
-
font=
|
|
316
|
-
fill=
|
|
208
|
+
font=assets.FONTS.muted.metrics.font,
|
|
209
|
+
fill=assets.FONTS.muted.fill,
|
|
317
210
|
)
|
|
318
211
|
|
|
319
212
|
# 平台 Logo
|
|
320
213
|
if self.not_repost:
|
|
321
214
|
platform_name = self.result.platform.name
|
|
322
|
-
if platform_name in
|
|
323
|
-
logo =
|
|
215
|
+
if platform_name in assets.PLATFORM_LOGOS:
|
|
216
|
+
logo = assets.PLATFORM_LOGOS[platform_name]
|
|
324
217
|
logo_x = self._image.width - self.PADDING - logo.width
|
|
325
|
-
logo_y = self.y_pos + (
|
|
218
|
+
logo_y = self.y_pos + (assets.AVATAR_SIZE - logo.height) // 2
|
|
326
219
|
self._image.paste(logo, (logo_x, logo_y), logo)
|
|
327
220
|
|
|
328
|
-
self.y_pos +=
|
|
221
|
+
self.y_pos += assets.AVATAR_SIZE + self.SECTION_SPACING
|
|
329
222
|
|
|
330
223
|
def _load_avatar(self, avatar_path: Path | None) -> PILImage:
|
|
331
224
|
"""加载头像(带圆形裁剪)"""
|
|
332
225
|
if avatar_path is None or not avatar_path.exists():
|
|
333
|
-
return
|
|
226
|
+
return assets.AVATAR_IMAGE
|
|
334
227
|
|
|
335
228
|
try:
|
|
336
229
|
with Image.open(avatar_path) as img:
|
|
337
230
|
avatar = img.convert("RGBA")
|
|
338
231
|
avatar = avatar.resize(
|
|
339
|
-
(
|
|
232
|
+
(assets.AVATAR_SIZE, assets.AVATAR_SIZE),
|
|
340
233
|
Image.Resampling.LANCZOS,
|
|
341
234
|
)
|
|
342
235
|
except Exception:
|
|
343
|
-
return
|
|
236
|
+
return assets.AVATAR_IMAGE
|
|
344
237
|
|
|
345
238
|
# 圆形遮罩
|
|
346
|
-
mask = Image.new("L", (
|
|
347
|
-
ImageDraw.Draw(mask).ellipse((0, 0,
|
|
239
|
+
mask = Image.new("L", (assets.AVATAR_SIZE, assets.AVATAR_SIZE), 0)
|
|
240
|
+
ImageDraw.Draw(mask).ellipse((0, 0, assets.AVATAR_SIZE - 1, assets.AVATAR_SIZE - 1), fill=255)
|
|
348
241
|
avatar.putalpha(mask)
|
|
349
242
|
return avatar
|
|
350
243
|
|
|
@@ -356,9 +249,9 @@ class CommonRenderer(ImageRenderer):
|
|
|
356
249
|
lines = self._wrap_text(
|
|
357
250
|
self.result.title,
|
|
358
251
|
self.content_width,
|
|
359
|
-
|
|
252
|
+
assets.FONTS.title.metrics,
|
|
360
253
|
)
|
|
361
|
-
self.y_pos += await self._draw_text(lines,
|
|
254
|
+
self.y_pos += await self._draw_text(lines, assets.FONTS.title)
|
|
362
255
|
self.y_pos += self.SECTION_SPACING
|
|
363
256
|
|
|
364
257
|
async def _render_main_content(self) -> None:
|
|
@@ -416,15 +309,15 @@ class CommonRenderer(ImageRenderer):
|
|
|
416
309
|
btn_size = 100
|
|
417
310
|
btn_x, btn_y = (img.width - btn_size) // 2, (img.height - btn_size) // 2
|
|
418
311
|
img.paste(
|
|
419
|
-
|
|
312
|
+
assets.VIDEO_BUTTON_IMAGE,
|
|
420
313
|
(btn_x, btn_y),
|
|
421
|
-
|
|
314
|
+
assets.VIDEO_BUTTON_IMAGE,
|
|
422
315
|
)
|
|
423
316
|
|
|
424
317
|
# 视频时长
|
|
425
318
|
# display_duration = video_content.display_duration
|
|
426
319
|
|
|
427
|
-
#
|
|
320
|
+
# paint = assets.FONTS.muted
|
|
428
321
|
# text_width = font.get_text_width(display_duration)
|
|
429
322
|
# # 计算文本绘制位置
|
|
430
323
|
# text_x = img.width - text_width - 20
|
|
@@ -450,8 +343,8 @@ class CommonRenderer(ImageRenderer):
|
|
|
450
343
|
# ImageDraw.Draw(img).text(
|
|
451
344
|
# (text_x, text_y),
|
|
452
345
|
# display_duration,
|
|
453
|
-
# font=
|
|
454
|
-
# fill=
|
|
346
|
+
# font=paint.metrics.font,
|
|
347
|
+
# fill=paint.fill,
|
|
455
348
|
# )
|
|
456
349
|
|
|
457
350
|
return img.copy()
|
|
@@ -594,15 +487,16 @@ class CommonRenderer(ImageRenderer):
|
|
|
594
487
|
# Alt 文本
|
|
595
488
|
if image_content.alt:
|
|
596
489
|
self.y_pos += self.SECTION_SPACING
|
|
597
|
-
|
|
490
|
+
paint = assets.FONTS.muted
|
|
491
|
+
text_w = paint.metrics.get_text_width(image_content.alt)
|
|
598
492
|
text_x = self.PADDING + (self.content_width - text_w) // 2
|
|
599
493
|
self._draw.text(
|
|
600
494
|
(text_x, self.y_pos),
|
|
601
495
|
image_content.alt,
|
|
602
|
-
font=
|
|
603
|
-
fill=
|
|
496
|
+
font=paint.metrics.font,
|
|
497
|
+
fill=paint.fill,
|
|
604
498
|
)
|
|
605
|
-
self.y_pos +=
|
|
499
|
+
self.y_pos += paint.metrics.line_height
|
|
606
500
|
|
|
607
501
|
self.y_pos += self.SECTION_SPACING
|
|
608
502
|
|
|
@@ -615,9 +509,9 @@ class CommonRenderer(ImageRenderer):
|
|
|
615
509
|
lines = self._wrap_text(
|
|
616
510
|
text,
|
|
617
511
|
self.content_width,
|
|
618
|
-
|
|
512
|
+
assets.FONTS.body.metrics,
|
|
619
513
|
)
|
|
620
|
-
self.y_pos += await self._draw_text(lines,
|
|
514
|
+
self.y_pos += await self._draw_text(lines, assets.FONTS.body)
|
|
621
515
|
self.y_pos += self.SECTION_SPACING
|
|
622
516
|
|
|
623
517
|
async def _render_extra(self) -> None:
|
|
@@ -628,9 +522,9 @@ class CommonRenderer(ImageRenderer):
|
|
|
628
522
|
lines = self._wrap_text(
|
|
629
523
|
self.result.extra_info,
|
|
630
524
|
self.content_width,
|
|
631
|
-
|
|
525
|
+
assets.FONTS.muted.metrics,
|
|
632
526
|
)
|
|
633
|
-
self.y_pos += await self._draw_text(lines,
|
|
527
|
+
self.y_pos += await self._draw_text(lines, assets.FONTS.muted)
|
|
634
528
|
|
|
635
529
|
async def _render_repost(self) -> None:
|
|
636
530
|
"""渲染转发内容"""
|
|
@@ -664,34 +558,35 @@ class CommonRenderer(ImageRenderer):
|
|
|
664
558
|
self._image.paste(repost_img, (card_x, card_y))
|
|
665
559
|
self.y_pos += container_h + self.SECTION_SPACING
|
|
666
560
|
|
|
667
|
-
async def _draw_text(self, lines: list[str],
|
|
561
|
+
async def _draw_text(self, lines: list[str], styled: StyledFont) -> int:
|
|
668
562
|
"""绘制多行文本"""
|
|
669
563
|
if not lines:
|
|
670
564
|
return 0
|
|
671
565
|
|
|
566
|
+
metrics = styled.metrics
|
|
672
567
|
xy = (self.PADDING, self.y_pos)
|
|
673
568
|
if emosvg is not None:
|
|
674
569
|
emosvg.text(
|
|
675
570
|
self._image,
|
|
676
571
|
xy,
|
|
677
572
|
lines,
|
|
678
|
-
|
|
679
|
-
fill=
|
|
680
|
-
line_height=
|
|
573
|
+
metrics.font,
|
|
574
|
+
fill=styled.fill,
|
|
575
|
+
line_height=metrics.line_height,
|
|
681
576
|
)
|
|
682
577
|
else:
|
|
683
578
|
await Apilmoji.text(
|
|
684
579
|
self._image,
|
|
685
580
|
xy,
|
|
686
581
|
lines,
|
|
687
|
-
|
|
688
|
-
fill=
|
|
689
|
-
line_height=
|
|
690
|
-
source=
|
|
582
|
+
metrics.font,
|
|
583
|
+
fill=styled.fill,
|
|
584
|
+
line_height=metrics.line_height,
|
|
585
|
+
source=assets.EMOJI_SOURCE,
|
|
691
586
|
)
|
|
692
|
-
return
|
|
587
|
+
return metrics.line_height * len(lines)
|
|
693
588
|
|
|
694
|
-
def _wrap_text(self, text: str, max_width: int,
|
|
589
|
+
def _wrap_text(self, text: str, max_width: int, metrics: FontMetrics) -> list[str]:
|
|
695
590
|
"""文本自动换行"""
|
|
696
591
|
if not text:
|
|
697
592
|
return []
|
|
@@ -718,12 +613,12 @@ class CommonRenderer(ImageRenderer):
|
|
|
718
613
|
if ed["match_start"] == idx:
|
|
719
614
|
char = ed["emoji"]
|
|
720
615
|
idx = ed["match_end"]
|
|
721
|
-
char_width =
|
|
616
|
+
char_width = metrics.font.size
|
|
722
617
|
break
|
|
723
618
|
else:
|
|
724
619
|
char = paragraph[idx]
|
|
725
620
|
idx += 1
|
|
726
|
-
char_width =
|
|
621
|
+
char_width = metrics.get_char_width_fast(char)
|
|
727
622
|
|
|
728
623
|
if not current_line:
|
|
729
624
|
current_line = char
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from nonebot import on_command
|
|
5
|
-
from nonebot.rule import to_me
|
|
6
|
-
from nonebot.matcher import Matcher
|
|
7
|
-
from nonebot.permission import SUPERUSER
|
|
8
|
-
from nonebot_plugin_uninfo import ADMIN, Session, UniSession
|
|
9
|
-
|
|
10
|
-
from ..config import pconfig
|
|
11
|
-
|
|
12
|
-
_DISABLED_GROUPS_PATH: Path = pconfig.data_dir / "disabled_groups.json"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def load_or_initialize_set() -> set[str]:
|
|
16
|
-
"""加载或初始化关闭解析的名单"""
|
|
17
|
-
# 判断是否存在
|
|
18
|
-
if not _DISABLED_GROUPS_PATH.exists():
|
|
19
|
-
_DISABLED_GROUPS_PATH.write_text(json.dumps([]))
|
|
20
|
-
return set(json.loads(_DISABLED_GROUPS_PATH.read_text()))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def save_disabled_groups():
|
|
24
|
-
"""保存关闭解析的名单"""
|
|
25
|
-
_DISABLED_GROUPS_PATH.write_text(json.dumps(list(_DISABLED_GROUPS_SET)))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# 内存中关闭解析的名单,第一次先进行初始化
|
|
29
|
-
_DISABLED_GROUPS_SET: set[str] = load_or_initialize_set()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def get_group_key(session: Session) -> str:
|
|
33
|
-
"""获取群组的唯一标识符
|
|
34
|
-
|
|
35
|
-
由平台名称和会话场景 ID 组成,例如 `QQClient_123456789`。
|
|
36
|
-
"""
|
|
37
|
-
return f"{session.scope}_{session.scene_path}"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def is_enabled(session: Session = UniSession()) -> bool:
|
|
41
|
-
"""判断当前会话是否在关闭解析的名单中"""
|
|
42
|
-
if session.scene.is_private:
|
|
43
|
-
return True
|
|
44
|
-
|
|
45
|
-
group_key = get_group_key(session)
|
|
46
|
-
if group_key in _DISABLED_GROUPS_SET:
|
|
47
|
-
return False
|
|
48
|
-
return True
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@on_command("开启解析", rule=to_me(), permission=SUPERUSER | ADMIN(), block=True).handle()
|
|
52
|
-
async def _(matcher: Matcher, session: Session = UniSession()):
|
|
53
|
-
"""开启解析"""
|
|
54
|
-
group_key = get_group_key(session)
|
|
55
|
-
if group_key in _DISABLED_GROUPS_SET:
|
|
56
|
-
_DISABLED_GROUPS_SET.remove(group_key)
|
|
57
|
-
save_disabled_groups()
|
|
58
|
-
await matcher.finish("解析已开启")
|
|
59
|
-
else:
|
|
60
|
-
await matcher.finish("解析已开启,无需重复开启")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@on_command("关闭解析", rule=to_me(), permission=SUPERUSER | ADMIN(), block=True).handle()
|
|
64
|
-
async def _(matcher: Matcher, session: Session = UniSession()):
|
|
65
|
-
"""关闭解析"""
|
|
66
|
-
group_key = get_group_key(session)
|
|
67
|
-
if group_key not in _DISABLED_GROUPS_SET:
|
|
68
|
-
_DISABLED_GROUPS_SET.add(group_key)
|
|
69
|
-
save_disabled_groups()
|
|
70
|
-
await matcher.finish("解析已关闭")
|
|
71
|
-
else:
|
|
72
|
-
await matcher.finish("解析已关闭,无需重复关闭")
|
{nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/exception.py
RENAMED
|
File without changes
|
{nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/parsers/nga.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_parser-2.6.5 → nonebot_plugin_parser-2.6.7}/src/nonebot_plugin_parser/utils.py
RENAMED
|
File without changes
|