nonebot-plugin-fakemsg-next 0.2.0__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.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.3
2
+ Name: nonebot-plugin-fakemsg-next
3
+ Version: 0.2.0
4
+ Summary: 使用显式命令触发的合并转发伪造消息插件
5
+ Author: Misty02600
6
+ Author-email: Misty02600 <Misty02600@gmail.com>
7
+ Requires-Dist: nonebot-adapter-onebot>=2.4.6,<3.0.0
8
+ Requires-Dist: nonebot2>=2.4.2,<3.0.0
9
+ Requires-Python: >=3.11
10
+ Project-URL: Homepage, https://github.com/Misty02600/nonebot-plugin-fakemsg-next
11
+ Project-URL: Issues, https://github.com/Misty02600/nonebot-plugin-fakemsg-next/issues
12
+ Project-URL: Repository, https://github.com/Misty02600/nonebot-plugin-fakemsg-next.git
13
+ Description-Content-Type: text/markdown
14
+
15
+ <div align="center">
16
+ <img src="https://github.com/Misty02600/nonebot-plugin-template/releases/download/assets/NoneBotPlugin.png" width="310" alt="logo">
17
+
18
+ ## ✨ nonebot-plugin-fakemsg-next ✨
19
+
20
+ NoneBot 合并转发伪造消息插件。
21
+ </div>
22
+
23
+ ## 介绍
24
+
25
+ [Cvandia/nonebot-plugin-fakemsg](https://github.com/Cvandia/nonebot-plugin-fakemsg) 支持图片和 QQ 表情的实现版本
26
+
27
+
28
+ ## 安装
29
+
30
+ ### 使用 uv
31
+
32
+ ```bash
33
+ uv add nonebot-plugin-fakemsg-next
34
+ ```
35
+
36
+ ### 使用 nb-cli
37
+
38
+ ```bash
39
+ nb plugin install nonebot-plugin-fakemsg-next --upgrade
40
+ ```
41
+
42
+ 在 `pyproject.toml` 中启用插件:
43
+
44
+ ```toml
45
+ [tool.nonebot.plugins]
46
+ nonebot-plugin-fakemsg-next = ["nonebot_plugin_fakemsg_next"]
47
+ ```
48
+
49
+ ## 使用
50
+
51
+ 每个分隔符分隔一条完整的伪造节点,每段均需显式写目标:
52
+
53
+ ```text
54
+ /伪造消息 @user1说ping
55
+ /伪造消息 @user1说ping|@user1说pong
56
+ /消息伪造 @user1说ping|@user2说pong
57
+ /fakemsg @user1说ping|@user2说pong
58
+ ```
59
+
60
+ - 每条消息结构必须为 `@用户说<内容>`
61
+ - 仅支持群聊中通过 `@用户` 指定伪造目标
62
+ - 如果正文需要字面量 `|`,可写成 `\|`
63
+
64
+ ## 配置
65
+
66
+
67
+ ```env
68
+ FAKEMSG_NEXT_QUICK_SEPARATOR=|
69
+ ```
70
+
71
+ - `fakemsg_next_quick_separator`
72
+ - 单字符分隔符,默认 `|`
@@ -0,0 +1,58 @@
1
+ <div align="center">
2
+ <img src="https://github.com/Misty02600/nonebot-plugin-template/releases/download/assets/NoneBotPlugin.png" width="310" alt="logo">
3
+
4
+ ## ✨ nonebot-plugin-fakemsg-next ✨
5
+
6
+ NoneBot 合并转发伪造消息插件。
7
+ </div>
8
+
9
+ ## 介绍
10
+
11
+ [Cvandia/nonebot-plugin-fakemsg](https://github.com/Cvandia/nonebot-plugin-fakemsg) 支持图片和 QQ 表情的实现版本
12
+
13
+
14
+ ## 安装
15
+
16
+ ### 使用 uv
17
+
18
+ ```bash
19
+ uv add nonebot-plugin-fakemsg-next
20
+ ```
21
+
22
+ ### 使用 nb-cli
23
+
24
+ ```bash
25
+ nb plugin install nonebot-plugin-fakemsg-next --upgrade
26
+ ```
27
+
28
+ 在 `pyproject.toml` 中启用插件:
29
+
30
+ ```toml
31
+ [tool.nonebot.plugins]
32
+ nonebot-plugin-fakemsg-next = ["nonebot_plugin_fakemsg_next"]
33
+ ```
34
+
35
+ ## 使用
36
+
37
+ 每个分隔符分隔一条完整的伪造节点,每段均需显式写目标:
38
+
39
+ ```text
40
+ /伪造消息 @user1说ping
41
+ /伪造消息 @user1说ping|@user1说pong
42
+ /消息伪造 @user1说ping|@user2说pong
43
+ /fakemsg @user1说ping|@user2说pong
44
+ ```
45
+
46
+ - 每条消息结构必须为 `@用户说<内容>`
47
+ - 仅支持群聊中通过 `@用户` 指定伪造目标
48
+ - 如果正文需要字面量 `|`,可写成 `\|`
49
+
50
+ ## 配置
51
+
52
+
53
+ ```env
54
+ FAKEMSG_NEXT_QUICK_SEPARATOR=|
55
+ ```
56
+
57
+ - `fakemsg_next_quick_separator`
58
+ - 单字符分隔符,默认 `|`
@@ -0,0 +1,125 @@
1
+ [project]
2
+ name = "nonebot-plugin-fakemsg-next"
3
+ version = "0.2.0"
4
+ description = "使用显式命令触发的合并转发伪造消息插件"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ authors = [{ name = "Misty02600", email = "Misty02600@gmail.com" }]
8
+ dependencies = [
9
+ "nonebot-adapter-onebot>=2.4.6,<3.0.0",
10
+ "nonebot2>=2.4.2,<3.0.0",
11
+ ]
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/Misty02600/nonebot-plugin-fakemsg-next"
15
+ Issues = "https://github.com/Misty02600/nonebot-plugin-fakemsg-next/issues"
16
+ Repository = "https://github.com/Misty02600/nonebot-plugin-fakemsg-next.git"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "commitizen>=4.1.0",
21
+ "git-cliff>=2.11.0,<3.0.0",
22
+ "prek>=0.2.0",
23
+ { include-group = "test" },
24
+ ]
25
+ test = [
26
+ "basedpyright>=1.16.0",
27
+ "ruff>=0.14.13,<1.0.0",
28
+ "nonebot2[fastapi]>=2.4.4,<3.0.0",
29
+ "nonebug>=0.4.3,<1.0.0",
30
+ "pytest-asyncio>=1.3.0,<1.4.0",
31
+ "pytest-cov>=7.0.0",
32
+ ]
33
+
34
+ [tool.nonebot.plugins]
35
+ nonebot-plugin-fakemsg-next = ["nonebot_plugin_fakemsg_next"]
36
+
37
+ [tool.nonebot.adapters]
38
+ nonebot-adapter-onebot = [
39
+ { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
40
+ ]
41
+
42
+ [build-system]
43
+ requires = ["uv_build>=0.10.0,<0.11.0"]
44
+ build-backend = "uv_build"
45
+
46
+ [tool.commitizen]
47
+ name = "cz_conventional_commits"
48
+ version = "0.1.0"
49
+ version_provider = "uv"
50
+ tag_format = "v$version"
51
+ annotated_tag = true
52
+ major_version_zero = true # 允许 0.x.x 版本号
53
+
54
+ [tool.coverage.report]
55
+ exclude_lines = [
56
+ "raise NotImplementedError",
57
+ "if TYPE_CHECKING:",
58
+ "@overload",
59
+ "except ImportError:",
60
+ ]
61
+
62
+ [tool.pyright]
63
+ include = ["src"] # 默认仅检查源码目录;测试通过 pytest / ruff 保证质量
64
+ pythonVersion = "3.11" # 目标 Python 版本(影响可用的类型特性)
65
+ pythonPlatform = "All" # 目标平台:All | Linux | Windows | Darwin
66
+ # typeCheckingMode 类型检查严格程度,可选值:
67
+ # - "off": 关闭类型检查
68
+ # - "basic": 基础检查(仅报告明确的类型错误)
69
+ # - "standard": 标准检查(推荐,平衡严格性与实用性)
70
+ # - "strict": 严格检查(要求完整的类型注解,适合新项目)
71
+ # - "all": 最严格(启用所有检查,可能产生大量警告)
72
+ typeCheckingMode = "standard"
73
+
74
+ [tool.pytest.ini_options]
75
+ addopts = [
76
+ "--import-mode=importlib", # 使用 importlib 导入模式(推荐,避免 sys.path 污染)
77
+ "--strict-markers", # 严格标记模式(未注册的 marker 会报错)
78
+ "--tb=short", # 简短的错误回溯(减少输出噪音)
79
+ "-ra", # 显示所有非通过测试的摘要(a=all except passed)
80
+ ]
81
+ pythonpath = ["src", "tests"] # 添加到 Python 路径,确保模块可导入
82
+ asyncio_mode = "auto" # 自动检测异步测试函数,无需手动标记 @pytest.mark.asyncio
83
+
84
+ [tool.ruff]
85
+ line-length = 88 # 每行最大字符数(与 Black 保持一致)
86
+ src = ["src", "tests"] # 源代码目录(用于判断导入是本地模块还是第三方模块)
87
+ extend-exclude = ["scripts"] # 脚本目录不做 lint 检查
88
+
89
+ [tool.ruff.format]
90
+ line-ending = "lf" # 行尾符:lf(Unix)| crlf(Windows)| auto | native
91
+
92
+ [tool.ruff.lint]
93
+ # 启用的规则集,每个字母代码对应一组规则
94
+ select = [
95
+ "F", # Pyflakes:基础语法和逻辑错误检查
96
+ "W", # pycodestyle warnings:PEP 8 风格警告
97
+ "E", # pycodestyle errors:PEP 8 风格错误
98
+ "I", # isort:导入语句排序
99
+ "B", # flake8-bugbear:发现潜在 bug 和设计问题
100
+ "UP", # pyupgrade:升级到新版 Python 语法
101
+ "ASYNC", # flake8-async:异步代码最佳实践
102
+ "C4", # flake8-comprehensions:推导式优化建议
103
+ "T10", # flake8-debugger:检测遗留的调试器语句(如 pdb)
104
+ "T20", # flake8-print:检测 print 语句(生产代码应使用 logging)
105
+ "PYI", # flake8-pyi:类型存根文件(.pyi)规范检查
106
+ "PT", # flake8-pytest-style:pytest 代码风格检查
107
+ "Q", # flake8-quotes:引号使用规范
108
+ "TID", # flake8-tidy-imports:导入语句整理
109
+ "RUF", # Ruff-specific:Ruff 特有的规则
110
+ ]
111
+ # 忽略的规则(根据项目需求放宽)
112
+ ignore = [
113
+ "E501", # 行长度由 formatter 控制,lint 不重复检查
114
+ "E402", # 允许模块导入不在文件顶部(某些情况下需要先执行代码)
115
+ "UP037", # 允许使用引号包裹的类型注解(前向引用)
116
+ "RUF001", # 允许字符串中的中文等 Unicode 字符
117
+ "RUF002", # 允许文档字符串中的中文等 Unicode 字符
118
+ "RUF003", # 允许注释中的中文等 Unicode 字符
119
+ "W191", # 允许使用制表符缩进
120
+ "TID252", # 允许使用相对导入
121
+ "B008", # 允许在函数参数默认值中使用函数调用
122
+ ]
123
+
124
+ [tool.ruff.lint.isort]
125
+ extra-standard-library = ["typing_extensions"] # 将 typing_extensions 视为标准库
@@ -0,0 +1,16 @@
1
+ from nonebot.plugin import PluginMetadata
2
+
3
+ from .config import Config
4
+
5
+ __plugin_meta__ = PluginMetadata(
6
+ name="伪造消息 Next",
7
+ description="使用显式命令触发的合并转发伪造消息插件。",
8
+ usage="伪造消息 @用户说ping|@用户说pong",
9
+ type="application",
10
+ homepage="https://github.com/Misty02600/nonebot-plugin-fakemsg-next",
11
+ config=Config,
12
+ supported_adapters={"~onebot.v11"},
13
+ extra={"author": "Misty02600"},
14
+ )
15
+
16
+ from . import handlers as handlers
@@ -0,0 +1,14 @@
1
+ from nonebot.plugin import get_plugin_config
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class Config(BaseModel):
6
+ fakemsg_next_quick_separator: str = Field(
7
+ default="|",
8
+ min_length=1,
9
+ max_length=1,
10
+ description="多条伪造消息的分隔符,必须是单个字符。",
11
+ )
12
+
13
+
14
+ plugin_config = get_plugin_config(Config)
@@ -0,0 +1,77 @@
1
+ from contextlib import suppress
2
+
3
+ from nonebot import on_command
4
+ from nonebot.adapters.onebot.v11 import (
5
+ Bot,
6
+ GroupMessageEvent,
7
+ Message,
8
+ MessageSegment,
9
+ )
10
+ from nonebot.params import CommandArg
11
+
12
+ from .config import plugin_config
13
+ from .parser import parse_quick_message
14
+
15
+
16
+ async def resolve_display_name(
17
+ bot: Bot,
18
+ event: GroupMessageEvent,
19
+ user_id: int,
20
+ ) -> str:
21
+ """解析伪造节点的显示名。
22
+
23
+ 优先尝试群昵称,失败时直接使用 QQ 号字符串。
24
+
25
+ Args:
26
+ bot: 当前 OneBot v11 bot 实例。
27
+ event: 触发命令的群消息事件。
28
+ user_id: 目标用户 QQ 号。
29
+
30
+ Returns:
31
+ 适合写入合并转发节点的显示名。
32
+ """
33
+
34
+ with suppress(Exception):
35
+ info = await bot.get_group_member_info(
36
+ group_id=event.group_id,
37
+ user_id=user_id,
38
+ )
39
+ display_name = info.get("nickname")
40
+ if display_name:
41
+ return display_name
42
+
43
+ return str(user_id)
44
+
45
+
46
+ fakemsg_command = on_command(
47
+ "伪造消息",
48
+ aliases={"fakemsg", "消息伪造"},
49
+ priority=10,
50
+ block=True,
51
+ )
52
+
53
+
54
+ @fakemsg_command.handle()
55
+ async def handle_fakemsg(
56
+ bot: Bot,
57
+ event: GroupMessageEvent,
58
+ arg: Message = CommandArg(),
59
+ ) -> None:
60
+ try:
61
+ parsed = parse_quick_message(
62
+ arg,
63
+ separator=plugin_config.fakemsg_next_quick_separator,
64
+ )
65
+ except ValueError as exc:
66
+ await fakemsg_command.finish(str(exc))
67
+
68
+ messages = [
69
+ MessageSegment.node_custom(
70
+ user_id=node.target_id,
71
+ nickname=await resolve_display_name(bot, event, node.target_id),
72
+ content=node.content,
73
+ )
74
+ for node in parsed
75
+ ]
76
+ await bot.send_group_forward_msg(group_id=event.group_id, messages=messages)
77
+ await fakemsg_command.finish()
@@ -0,0 +1,168 @@
1
+ from typing import NamedTuple
2
+
3
+ from nonebot.adapters.onebot.v11 import Message, MessageSegment
4
+
5
+
6
+ class ParsedNode(NamedTuple):
7
+ target_id: int
8
+ content: Message
9
+
10
+
11
+ def _trim_message_edges(message: Message) -> Message:
12
+ segments = list(message)
13
+ while segments and segments[0].type == "text":
14
+ stripped = segments[0].data.get("text", "").lstrip()
15
+ if not stripped:
16
+ segments.pop(0)
17
+ continue
18
+ if stripped != segments[0].data.get("text", ""):
19
+ segments[0] = MessageSegment.text(stripped)
20
+ break
21
+
22
+ while segments and segments[-1].type == "text":
23
+ stripped = segments[-1].data.get("text", "").rstrip()
24
+ if not stripped:
25
+ segments.pop()
26
+ continue
27
+ if stripped != segments[-1].data.get("text", ""):
28
+ segments[-1] = MessageSegment.text(stripped)
29
+ break
30
+
31
+ return Message(segments)
32
+
33
+
34
+ def _format_error(index: int, reason: str) -> ValueError:
35
+ return ValueError(f"格式不正确:第{index}条消息{reason}")
36
+
37
+
38
+ def split_message_chunks(message: Message, *, separator: str) -> list[Message]:
39
+ """把一条命令参数拆成多条伪造消息。
40
+
41
+ 只有文本段里的分隔符会触发拆分,图片、表情、at 等非文本段会留在
42
+ 当前消息里。正文里需要保留分隔符本身时,可以在它前面加反斜杠转义。
43
+ 连续分隔符、开头分隔符和结尾分隔符会产生空消息片段,交给后续格式
44
+ 校验返回精确错误。
45
+
46
+ Args:
47
+ message: 待拆分的原始消息。
48
+ separator: 已从配置读取并校验过的单字符分隔符。
49
+
50
+ Returns:
51
+ 按顺序拆出的消息片段列表。
52
+ """
53
+
54
+ chunks: list[Message] = []
55
+ current = Message()
56
+
57
+ def append_text(text: str) -> None:
58
+ nonlocal current
59
+ if text:
60
+ current += MessageSegment.text(text)
61
+
62
+ def flush_chunk() -> None:
63
+ nonlocal current
64
+ chunk = _trim_message_edges(current)
65
+ current = Message()
66
+ chunks.append(chunk)
67
+
68
+ for segment in _trim_message_edges(message):
69
+ if segment.type != "text":
70
+ current += segment
71
+ continue
72
+
73
+ text = segment.data.get("text", "")
74
+ buffer: list[str] = []
75
+ index = 0
76
+ while index < len(text):
77
+ char = text[index]
78
+ if char == "\\" and index + 1 < len(text) and text[index + 1] == separator:
79
+ buffer.append(separator)
80
+ index += 2
81
+ continue
82
+
83
+ if char == separator:
84
+ append_text("".join(buffer))
85
+ buffer.clear()
86
+ flush_chunk()
87
+ index += 1
88
+ continue
89
+
90
+ buffer.append(char)
91
+ index += 1
92
+
93
+ append_text("".join(buffer))
94
+
95
+ flush_chunk()
96
+ return chunks
97
+
98
+
99
+ def parse_targeted_message(message: Message, *, index: int = 1) -> ParsedNode:
100
+ """解析单条“@用户说<内容>”结构。
101
+
102
+ Args:
103
+ message: 单条伪造消息片段。
104
+ index: 当前片段序号,用于错误提示。
105
+
106
+ Returns:
107
+ 解析后的目标用户和消息内容。
108
+
109
+ Raises:
110
+ ValueError: 当缺少用户、缺少“说”或缺少内容时抛出。
111
+ """
112
+
113
+ remaining = _trim_message_edges(message)
114
+ if not remaining:
115
+ raise _format_error(index, "缺少内容")
116
+
117
+ first = remaining[0]
118
+ if first.type != "at":
119
+ raise _format_error(index, "缺少用户")
120
+
121
+ qq = first.data.get("qq")
122
+ if not qq or qq == "all":
123
+ raise _format_error(index, "缺少用户")
124
+ target_id = int(qq)
125
+ payload = _trim_message_edges(remaining[1:])
126
+ if not payload or payload[0].type != "text":
127
+ raise _format_error(index, "缺少“说”")
128
+
129
+ first_text = payload[0].data.get("text", "")
130
+ if not first_text.startswith("说"):
131
+ raise _format_error(index, "缺少“说”")
132
+
133
+ rest = first_text[1:]
134
+ if rest:
135
+ payload = MessageSegment.text(rest) + payload[1:]
136
+ else:
137
+ payload = payload[1:]
138
+
139
+ content = _trim_message_edges(payload)
140
+ if not content:
141
+ raise _format_error(index, "缺少内容")
142
+
143
+ return ParsedNode(target_id=target_id, content=content)
144
+
145
+
146
+ def parse_quick_message(
147
+ message: Message,
148
+ *,
149
+ separator: str,
150
+ ) -> list[ParsedNode]:
151
+ """解析整条快捷命令参数。
152
+
153
+ Args:
154
+ message: 命令参数部分的原始消息。
155
+ separator: 多条伪造消息的顶层分隔符。
156
+
157
+ Returns:
158
+ 每个分段对应的一组目标用户和消息内容。
159
+
160
+ Raises:
161
+ ValueError: 当任意分段不符合“@用户说<内容>”结构时抛出。
162
+ """
163
+
164
+ chunks = split_message_chunks(message, separator=separator)
165
+ return [
166
+ parse_targeted_message(chunk, index=index)
167
+ for index, chunk in enumerate(chunks, start=1)
168
+ ]