nonebot-plugin-onebot-luckperms 0.1.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.
Files changed (42) hide show
  1. nonebot_plugin_onebot_luckperms-0.1.0/LICENSE +21 -0
  2. nonebot_plugin_onebot_luckperms-0.1.0/PKG-INFO +23 -0
  3. nonebot_plugin_onebot_luckperms-0.1.0/README.md +483 -0
  4. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/__init__.py +127 -0
  5. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/adapter/__init__.py +17 -0
  6. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/adapter/context.py +31 -0
  7. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/adapter/identity.py +64 -0
  8. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/adapter/permission.py +157 -0
  9. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/commands/__init__.py +3 -0
  10. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/commands/admin.py +857 -0
  11. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/config.py +20 -0
  12. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/core/__init__.py +20 -0
  13. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/core/cache.py +44 -0
  14. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/core/context_provider.py +74 -0
  15. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/core/engine.py +162 -0
  16. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/core/exceptions.py +6 -0
  17. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/core/models.py +123 -0
  18. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/core/registry.py +72 -0
  19. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/message.py +126 -0
  20. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/py.typed +0 -0
  21. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/storage/__init__.py +29 -0
  22. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/storage/memory.py +46 -0
  23. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/storage/protocol.py +21 -0
  24. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/storage/redis.py +162 -0
  25. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/storage/sqlite.py +189 -0
  26. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/webeditor/__init__.py +9 -0
  27. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/webeditor/bytebin.py +71 -0
  28. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/webeditor/manager.py +228 -0
  29. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/webeditor/session.py +109 -0
  30. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms/webeditor/websocket.py +180 -0
  31. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms.egg-info/PKG-INFO +23 -0
  32. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms.egg-info/SOURCES.txt +40 -0
  33. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms.egg-info/dependency_links.txt +1 -0
  34. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms.egg-info/requires.txt +17 -0
  35. nonebot_plugin_onebot_luckperms-0.1.0/nonebot_plugin_onebot_luckperms.egg-info/top_level.txt +1 -0
  36. nonebot_plugin_onebot_luckperms-0.1.0/pyproject.toml +39 -0
  37. nonebot_plugin_onebot_luckperms-0.1.0/setup.cfg +4 -0
  38. nonebot_plugin_onebot_luckperms-0.1.0/tests/test_borderline.py +125 -0
  39. nonebot_plugin_onebot_luckperms-0.1.0/tests/test_context.py +189 -0
  40. nonebot_plugin_onebot_luckperms-0.1.0/tests/test_models.py +205 -0
  41. nonebot_plugin_onebot_luckperms-0.1.0/tests/test_storage.py +99 -0
  42. nonebot_plugin_onebot_luckperms-0.1.0/tests/test_webeditor.py +180 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 WisTex TechSero Ltd. Co.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-onebot-luckperms
3
+ Version: 0.1.0
4
+ Summary: LuckPerms-style permission system for NoneBot2 + OneBot
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/XChen446/nonebot-plugin-onebot-luckperms
7
+ Requires-Python: >=3.10
8
+ License-File: LICENSE
9
+ Requires-Dist: nonebot2>=2.5.0
10
+ Requires-Dist: nonebot-adapter-onebot>=2.4.6
11
+ Requires-Dist: pydantic>=2.13.4
12
+ Requires-Dist: aiohttp>=3.14.1
13
+ Requires-Dist: pyyaml>=6.0
14
+ Requires-Dist: aiosqlite>=0.22.1
15
+ Provides-Extra: redis
16
+ Requires-Dist: redis>=8.0; extra == "redis"
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=9.0; extra == "test"
19
+ Requires-Dist: pytest-asyncio>=1.4; extra == "test"
20
+ Provides-Extra: all
21
+ Requires-Dist: nonebot-plugin-onebot-luckperms[redis]; extra == "all"
22
+ Requires-Dist: nonebot-plugin-onebot-luckperms[test]; extra == "all"
23
+ Dynamic: license-file
@@ -0,0 +1,483 @@
1
+ # nonebot-plugin-onebot-luckperms(OBLP)
2
+
3
+ LuckPerms 风格的权限节点管理系统,专为 NoneBot2 + OneBot 适配器设计。
4
+
5
+ > English: [README_EN.md](./README_EN.md)
6
+
7
+ ---
8
+
9
+ ## 目录
10
+
11
+ - [安装](#安装)
12
+ - [快速开始](#快速开始)
13
+ - [配置](#配置)
14
+ - [命令列表](#命令列表)
15
+ - [内建 Context 键](#内建-context-键)
16
+ - [权限节点注册(面向开发者)](#权限节点注册面向开发者)
17
+ - [ContextProvider(面向开发者)](#contextprovider面向开发者)
18
+ - [API 参考](#api-参考)
19
+
20
+ ---
21
+
22
+ ## 安装
23
+
24
+ ### 通过 NB-CLI(推荐)
25
+
26
+ ```bash
27
+ nb plugin install nonebot-plugin-onebot-luckperms
28
+ ```
29
+
30
+ ### 通过 pip
31
+
32
+ ```bash
33
+ pip install nonebot-plugin-onebot-luckperms
34
+ ```
35
+
36
+ 如需 Redis 后端:
37
+
38
+ ```bash
39
+ pip install nonebot-plugin-onebot-luckperms[redis]
40
+ ```
41
+
42
+ 全部安装:
43
+
44
+ ```bash
45
+ pip install nonebot-plugin-onebot-luckperms[all]
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 快速开始
51
+
52
+ 在 NoneBot2 的 `pyproject.toml` 或 `.env` 中添加插件:
53
+
54
+ ```toml
55
+ [tool.nonebot]
56
+ plugins = ["nonebot_plugin_onebot_luckperms"]
57
+ ```
58
+
59
+ 插件加载后,所有 `/lp` 命令默认仅 **SUPERUSER**(在 `env` 中配置 `SUPERUSER=123456`)可执行。
60
+
61
+ ### 给用户一个权限
62
+
63
+ ```text
64
+ /lp user 123456 permission set myplugin.ban true
65
+ ```
66
+
67
+ ### 创建一个组并分配权限
68
+
69
+ ```text
70
+ /lp creategroup admin 100
71
+ /lp group admin permission set myplugin.kick true
72
+ /lp user 123456 parent add admin
73
+ ```
74
+
75
+ ### 设置仅在特定群生效的权限
76
+
77
+ ```text
78
+ /lp user 123456 permission set myplugin.mute true group_id=987654
79
+ ```
80
+
81
+ ### 临时权限(30 分钟后自动过期)
82
+
83
+ ```text
84
+ /lp user 123456 permission settemp myplugin.ban true 30m
85
+ ```
86
+
87
+ ### 打开 Web Editor
88
+
89
+ ```text
90
+ /lp editor
91
+ ```
92
+
93
+ 在群聊中打开时,editor 会自动附带该群所有成员 QQ 到用户列表。
94
+
95
+ ---
96
+
97
+ ## 配置
98
+
99
+ 在 `.env` 或 `pyproject.toml` 中(前缀 `OBLP_`):
100
+
101
+ | 环境变量 | 默认值 | 说明 |
102
+ |---------|--------|------|
103
+ | `OBLP_STORE_TYPE` | `sqlite` | 存储后端:`memory` / `sqlite` / `redis` |
104
+ | `OBLP_SQLITE_PATH` | `./data/oblp/permissions.db` | SQLite 文件路径 |
105
+ | `OBLP_REDIS_URL` | `redis://localhost:6379/0` | Redis 连接 URL |
106
+ | `OBLP_DEFAULT_GROUP_OWNER` | `["luckperms.help"]` | 群主自动继承的权限节点 |
107
+ | `OBLP_DEFAULT_GROUP_ADMIN` | `[]` | 群管理自动继承的权限节点 |
108
+ | `OBLP_DEFAULT_GROUP_MEMBER` | `[]` | 普通成员自动继承的权限节点 |
109
+ | `OBLP_SUPERUSER_INHERIT` | `["luckperms.*"]` | SUPERUSER 自动继承的权限节点 |
110
+ | `OBLP_CACHE_TTL` | `300` | 权限缓存时间(秒),`0` 禁用缓存 |
111
+ | `OBLP_MESSAGE_FILE` | `./data/oblp/messages.yml` | 自定义回复消息的 YAML 文件路径 |
112
+ | `OBLP_DEBUG_MODE` | `false` | 开启后打印每次权限判定日志 |
113
+
114
+ ### 配置示例
115
+
116
+ ```ini
117
+ SUPERUSER=123456
118
+ OBLP_STORE_TYPE=sqlite
119
+ OBLP_SQLITE_PATH=./data/oblp/permissions.db
120
+ OBLP_DEFAULT_GROUP_ADMIN=["myplugin.mute","myplugin.kick"]
121
+ OBLP_DEFAULT_GROUP_MEMBER=[]
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 命令列表
127
+
128
+ 所有命令均以 `/lp` 开头。每个命令都受对应的 `luckperms.*` 权限节点保护。
129
+
130
+ SUPERUSER 默认拥有 `luckperms.*`(全部权限)。群主默认拥有 `luckperms.help`(仅能看帮助)。
131
+
132
+ ### 通用命令
133
+
134
+ | 命令 | 所需权限节点 | 说明 |
135
+ |------|-------------|------|
136
+ | `/lp help` | `luckperms.help` | 显示帮助 |
137
+ | `/lp sync` | `luckperms.sync` | 重新加载数据 |
138
+ | `/lp info` | `luckperms.info` | 显示统计信息 |
139
+ | `/lp editor` | `luckperms.editor` | 打开 LuckPerms Web Editor |
140
+ | `/lp applyedits <code>` | `luckperms.applyedits` | 应用 Web Editor 的 edits |
141
+
142
+ ### 用户管理
143
+
144
+ | 命令 | 所需权限节点 | 说明 |
145
+ |------|-------------|------|
146
+ | `/lp user <id> info` | `luckperms.user.info` | 查看用户详情 |
147
+ | `/lp user <id> permission ...` | `luckperms.user.permission` | 管理用户权限节点 |
148
+ | `/lp user <id> parent ...` | `luckperms.user.parent` | 管理用户组 |
149
+ | `/lp user <id> promote <track>` | `luckperms.user.promote` | 晋升 |
150
+ | `/lp user <id> demote <track>` | `luckperms.user.demote` | 降级 |
151
+ | `/lp user <id> clear [ctx...]` | `luckperms.user.clear` | 清空所有权限节点 |
152
+ | `/lp user <id> clone <target>` | `luckperms.user.clone` | 克隆用户 |
153
+ | `/lp user <id> editor` | `luckperms.editor` | 在 editor 中编辑此用户 |
154
+ | `/lp user create <id>` | `luckperms.user.create` | 创建用户 |
155
+ | `/lp user delete <id>` | `luckperms.user.delete` | 删除用户 |
156
+ | `/lp user list` | `luckperms.user.list` | 列出所有用户 |
157
+
158
+ ### 组管理
159
+
160
+ | 命令 | 所需权限节点 | 说明 |
161
+ |------|-------------|------|
162
+ | `/lp group <name> info` | `luckperms.group.info` | 查看组详情 |
163
+ | `/lp group <name> permission ...` | `luckperms.group.permission` | 管理组权限 |
164
+ | `/lp group <name> parent ...` | `luckperms.group.parent` | 继承管理 |
165
+ | `/lp group <name> setweight <weight>` | `luckperms.group.setweight` | 设置权重 |
166
+ | `/lp group <name> setdisplayname <name>` | `luckperms.group.setdisplayname` | 设置显示名 |
167
+ | `/lp group <name> clear [ctx...]` | `luckperms.group.clear` | 清空权限 |
168
+ | `/lp group <name> rename <new>` | `luckperms.group.rename` | 重命名组 |
169
+ | `/lp group <name> clone <new>` | `luckperms.group.clone` | 克隆组 |
170
+ | `/lp group <name> listmembers` | `luckperms.group.listmembers` | 列出组成员 |
171
+ | `/lp group <name> editor` | `luckperms.editor` | 在 editor 中编辑此组 |
172
+ | `/lp creategroup <name> [weight]` | `luckperms.group.create` | 创建组 |
173
+ | `/lp deletegroup <name>` | `luckperms.group.delete` | 删除组 |
174
+ | `/lp listgroups` | `luckperms.group.list` | 列出所有组 |
175
+
176
+ ### 轨道管理(预留)
177
+
178
+ | 命令 | 所需权限节点 | 说明 |
179
+ |------|-------------|------|
180
+ | `/lp track <name> ...` | `luckperms.*` | 轨道操作 |
181
+
182
+ ### 系统命令
183
+
184
+ | 命令 | 所需权限节点 | 说明 |
185
+ |------|-------------|------|
186
+ | `/lp check <user> <node> [ctx...]` | `luckperms.check` | 模拟权限检查 |
187
+ | `/lp tree [scope]` | `luckperms.tree` | 权限继承树可视化 |
188
+
189
+ ### 命令执行反馈
190
+
191
+ 无权限时(用户没有所需的 `luckperms.xxx` 节点),bot 回复 `deny` 消息。
192
+
193
+ ### 自定义回复消息
194
+
195
+ 所有用户可见的回复文本均存储在 `OBLP_MESSAGE_FILE` 指向的 YAML 文件中(默认 `./data/oblp/messages.yml`)。
196
+ 插件首次运行时会自动生成此文件。你可以编辑它来自定义所有回复,支持占位符:
197
+
198
+ ```yaml
199
+ # data/oblp/messages.yml
200
+ deny: "你没有权限执行此命令。"
201
+ deny_hint: "请联系管理员开通对应权限节点。"
202
+ perm_set: "{entity}: {action} {node}"
203
+ perm_temp: "{entity}: 临时{action} {node} ({duration})"
204
+ editor_opened: "Web 编辑器已开启: {url}"
205
+ ```
206
+
207
+ 可用占位符:
208
+ | 占位符 | 说明 |
209
+ |--------|------|
210
+ | `{user_id}` | 用户 ID |
211
+ | `{node}` | 权限节点名 |
212
+ | `{entity}` | 用户或组 ID |
213
+ | `{action}` | grant 或 deny |
214
+ | `{duration}` | 持续时间 |
215
+ | `{context}` | 上下文键值对 |
216
+ | `{url}` | Editor URL |
217
+ | `{code}` | Edit code |
218
+ | `{error}` | 错误描述 |
219
+ | `{name}` | 组名 |
220
+ | `{weight}` | 组权重 |
221
+
222
+ 有权限时的执行反馈示例:
223
+
224
+ ```
225
+ /lp sync
226
+ → Data reloaded
227
+
228
+ /lp user 123456 permission set myplugin.ban true
229
+ → 123456: grant myplugin.ban
230
+
231
+ /lp user 123456 permission settemp myplugin.kick false 30m
232
+ → 123456: temp deny myplugin.kick (30m)
233
+
234
+ /lp group admin permission set myplugin.mute true group_id=987654
235
+ → admin: grant myplugin.mute (ctx: {'group_id': '987654'})
236
+
237
+ /lp check 123456 myplugin.ban
238
+ → Check: user=123456, node=myplugin.ban
239
+ Context: (empty)
240
+ Result: ALLOW
241
+ Resolved nodes (2):
242
+ myplugin.ban=grant
243
+ luckperms.*=grant
244
+ ```
245
+
246
+ ### 默认角色权限
247
+
248
+ | 角色 | 默认拥有的节点 | 说明 |
249
+ |------|--------------|------|
250
+ | SUPERUSER | `luckperms.*` | 全部命令 |
251
+ | 群主 (owner) | `luckperms.help` | 仅可看帮助(可配置) |
252
+ | 管理 (admin) | *(空)* | 无内建命令权限(可配置) |
253
+ | 成员 (member) | *(空)* | 无内建命令权限(可配置) |
254
+
255
+ 可通过 `OBLP_DEFAULT_GROUP_OWNER`、`OBLP_DEFAULT_GROUP_ADMIN`、`OBLP_DEFAULT_GROUP_MEMBER` 配置。
256
+
257
+ ### 临时权限 duration 格式
258
+
259
+ | 写法 | 含义 |
260
+ |------|------|
261
+ | `30s` | 30 秒 |
262
+ | `5m` | 5 分钟 |
263
+ | `2h` | 2 小时 |
264
+ | `1d` | 1 天 |
265
+
266
+ ---
267
+
268
+ ## 内建 Context 键
269
+
270
+ 权限节点可以通过 `key=value` 附加上下文约束,使节点仅在特定环境中生效。由系统自动注入:
271
+
272
+ | Context 键 | 类型 | 来源 | 示例值 |
273
+ |-----------|------|---------|--------|
274
+ | `platform` | `str` | 适配器类型 | `onebot_v11`, `onebot_v12` |
275
+ | `user_id` | `str` | 发送者 ID | `123456` |
276
+ | `group_id` | `str` | 群号(群聊时) | `987654` |
277
+ | `role` | `str` | 群内角色 | `owner`, `admin`, `member`, `superuser` |
278
+
279
+ 第三方插件可通过 ContextProvider(见下文)注册自定义 Context 键。
280
+
281
+ ### Context 使用示例
282
+
283
+ 仅群 123456 中有效:
284
+
285
+ ```text
286
+ /lp user 123456 permission set myplugin.ban true group_id=123456
287
+ ```
288
+
289
+ 仅群管理时有效:
290
+
291
+ ```text
292
+ /lp user 123456 permission set myplugin.kick true role=admin
293
+ ```
294
+
295
+ 仅群 123456 且为群主时有效:
296
+
297
+ ```text
298
+ /lp group admin permission set myplugin.ban true group_id=123456 role=owner
299
+ ```
300
+
301
+ ---
302
+
303
+ ## 权限节点注册(面向开发者)
304
+
305
+ ### 基本用法
306
+
307
+ ```python
308
+ from nonebot import on_command
309
+ from nonebot_plugin_onebot_luckperms import register_node, require
310
+
311
+ # 在插件加载时注册节点
312
+ register_node("myplugin.ban", "禁言用户", default=False)
313
+
314
+ # 绑定到命令
315
+ ban = on_command("ban", permission=require("myplugin.ban"))
316
+
317
+ @ban.handle()
318
+ async def _():
319
+ await ban.send("已执行")
320
+ ```
321
+
322
+ ### 带默认上下文的注册
323
+
324
+ ```python
325
+ from nonebot_plugin_onebot_luckperms import register_node, ContextSet
326
+
327
+ # 仅群主默认拥有此权限
328
+ register_node("myplugin.admincmd", "管理命令", default=True,
329
+ contexts=ContextSet({"role": "owner"}))
330
+
331
+ # 仅在特定群 + admin 时默认拥有
332
+ register_node("myplugin.vipcmd", "VIP 命令", default=True,
333
+ contexts=ContextSet({"group_id": "123456", "role": "admin"}))
334
+ ```
335
+
336
+ ### require / require_any / require_all
337
+
338
+ ```python
339
+ from nonebot_plugin_onebot_luckperms import require, require_any, require_all
340
+ from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN
341
+
342
+ # 单节点
343
+ cmd = on_command("cmd", permission=require("myplugin.cmd"))
344
+
345
+ # 多节点 OR
346
+ cmd = on_command("cmd", permission=require_any("node.a", "node.b"))
347
+
348
+ # 多节点 AND
349
+ cmd = on_command("cmd", permission=require_all("node.a", "node.b"))
350
+
351
+ # 与原生权限组合
352
+ cmd = on_command("cmd", permission=require("myplugin.cmd") | GROUP_ADMIN)
353
+ ```
354
+
355
+ ### 获取权限上下文
356
+
357
+ ```python
358
+ from nonebot_plugin_onebot_luckperms import get_context
359
+
360
+ @cmd.handle()
361
+ async def _():
362
+ ctx = get_context()
363
+ if not ctx:
364
+ return
365
+ print(ctx.user.user_id) # "123456"
366
+ print(ctx.identity.role) # "admin"
367
+ print(ctx.resolved_nodes) # {"myplugin.ban": True, ...}
368
+ ```
369
+
370
+ ---
371
+
372
+ ## ContextProvider(面向开发者)
373
+
374
+ ContextProvider 允许第三方插件向全局权限判定环境注入自定义 Context 键值对。
375
+
376
+ ### 定义并注册
377
+
378
+ ```python
379
+ from nonebot_plugin_onebot_luckperms import register_context_provider
380
+
381
+ class BindProvider:
382
+ context_keys = {"bind"} # 声明此 provider 会注入的键
383
+
384
+ async def __call__(self, bot, event, current_ctx):
385
+ # 从数据库或 API 查询用户的绑定状态
386
+ return {"bind": "true"} # 键值对会合并到判定上下文中
387
+
388
+ register_context_provider(BindProvider())
389
+ ```
390
+
391
+ 注册后,其他所有插件都可以利用这个 Context 来控制权限生效范围:
392
+
393
+ ```python
394
+ register_node("someplugin.premium", default=True,
395
+ contexts=ContextSet({"bind": "true"}))
396
+ ```
397
+
398
+ ### 冲突检测
399
+
400
+ 如果两个不同的 Provider 声明了相同的 `context_keys`,后者注册时会抛出 `DuplicateProviderError`:
401
+
402
+ ```python
403
+ ProviderA: context_keys = {"bind"} # 注册成功
404
+ ProviderB: context_keys = {"bind"} # → DuplicateProviderError!
405
+ ```
406
+
407
+ ---
408
+
409
+ ## API 参考
410
+
411
+ ### 用户端函数
412
+
413
+ | 函数 | 说明 |
414
+ |------|------|
415
+ | `require(node_key, contexts=None) -> Permission` | 单节点权限检查器 |
416
+ | `require_any(*node_keys, contexts=None) -> Permission` | OR 权限检查器 |
417
+ | `require_all(*node_keys, contexts=None) -> Permission` | AND 权限检查器 |
418
+ | `get_context() -> Optional[LPContext]` | 在 Handler 中获取权限上下文 |
419
+ | `get_store() -> Optional[PermissionStore]` | 获取当前存储后端 |
420
+
421
+ ### 开发者端函数
422
+
423
+ | 函数 | 说明 |
424
+ |------|------|
425
+ | `register_node(key, description="", default=False, contexts=None)` | 注册权限节点 |
426
+ | `register_context_provider(provider)` | 注册全局 ContextProvider |
427
+ | `set_resolver(resolver)` | 覆盖 IdentityResolver |
428
+ | `ContextSet(data={})` | 上下文集合 |
429
+ | `PermissionNode(key, value=True, expiry=None, contexts=ContextSet())` | 权限节点 |
430
+
431
+ ### 模型类
432
+
433
+ ```python
434
+ @dataclass(frozen=True)
435
+ class ContextSet:
436
+ data: Dict[str, str]
437
+ def with_context(self, key, value) -> ContextSet
438
+ def matches(self, requirement) -> bool
439
+ def is_empty(self) -> bool
440
+
441
+ @dataclass
442
+ class PermissionNode:
443
+ key: str
444
+ value: bool
445
+ expiry: Optional[int]
446
+ contexts: ContextSet
447
+ def is_expired(self) -> bool
448
+ def applies_in(self, ctx) -> bool
449
+
450
+ @dataclass(frozen=True)
451
+ class LPContext:
452
+ user: User
453
+ identity: Identity
454
+ query_options: QueryOptions
455
+ resolved_nodes: Dict[str, bool]
456
+ matched_node: Optional[str]
457
+ timestamp: float
458
+ ```
459
+
460
+ ---
461
+
462
+ ## 测试
463
+
464
+ ```bash
465
+ pytest tests/ -v
466
+ ```
467
+
468
+ 当前 91 项测试全部通过。
469
+
470
+ ---
471
+
472
+ ## 架构
473
+
474
+ ```
475
+ Layer 4: NoneBot Integration (adapter/) — require(), IdentityResolver, LPContext
476
+ Layer 3: Core Engine (core/) — 纯 Python,不依赖 NoneBot
477
+ Layer 2: Storage (storage/) — Memory / SQLite / Redis
478
+ Layer 1: Config & Bootstrap (config/) — Pydantic 配置
479
+ ```
480
+
481
+ ## License
482
+
483
+ MIT
@@ -0,0 +1,127 @@
1
+ import logging
2
+
3
+ from nonebot.plugin import PluginMetadata
4
+
5
+ from .core.registry import register_node
6
+ from .core.models import ContextSet, PermissionNode
7
+ from .core.context_provider import register_context_provider, ContextProvider
8
+ from .storage import get_store
9
+ from .adapter import set_resolver, require, require_any, require_all, get_context
10
+ from .adapter.context import LPContext
11
+ from .config import OBLPConfig, oblp_config
12
+
13
+ logger = logging.getLogger("oblp")
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __plugin_meta__ = PluginMetadata(
18
+ name="nonebot-plugin-onebot-luckperms",
19
+ description="LuckPerms-style permission system for NoneBot2 + OneBot",
20
+ usage="Register nodes with register_node(), use require() in command matchers",
21
+ type="library",
22
+ homepage="https://github.com/XChen446/nonebot-plugin-onebot-luckperms",
23
+ config=OBLPConfig,
24
+ supported_adapters={"~onebot.v11"},
25
+ )
26
+
27
+ __all__ = [
28
+ "register_node",
29
+ "require",
30
+ "require_any",
31
+ "require_all",
32
+ "get_context",
33
+ "set_resolver",
34
+ "ContextSet",
35
+ "PermissionNode",
36
+ "oblp_config",
37
+ "get_store",
38
+ "LPContext",
39
+ "register_context_provider",
40
+ "ContextProvider",
41
+ ]
42
+
43
+ _store_initialized = False
44
+
45
+
46
+ def _register_builtin_nodes():
47
+ register_node("luckperms.*", "Access to all LuckPerms commands", default=True)
48
+ register_node("luckperms.help", "View LuckPerms help", default=False)
49
+ register_node("luckperms.info", "View LuckPerms info", default=False)
50
+ register_node("luckperms.sync", "Reload LuckPerms data", default=False)
51
+ register_node("luckperms.editor", "Open LuckPerms Web Editor", default=False)
52
+ register_node("luckperms.applyedits", "Apply Web Editor edits", default=False)
53
+ register_node("luckperms.check", "Check user permissions", default=False)
54
+ register_node("luckperms.tree", "View permission inheritance tree", default=False)
55
+ register_node("luckperms.user.info", "View user details", default=False)
56
+ register_node("luckperms.user.permission", "Manage user permissions", default=False)
57
+ register_node("luckperms.user.parent", "Manage user group membership", default=False)
58
+ register_node("luckperms.user.promote", "Promote user", default=False)
59
+ register_node("luckperms.user.demote", "Demote user", default=False)
60
+ register_node("luckperms.user.clear", "Clear user permissions", default=False)
61
+ register_node("luckperms.user.clone", "Clone user", default=False)
62
+ register_node("luckperms.user.create", "Create user", default=False)
63
+ register_node("luckperms.user.delete", "Delete user", default=False)
64
+ register_node("luckperms.user.list", "List all users", default=False)
65
+ register_node("luckperms.group.info", "View group details", default=False)
66
+ register_node("luckperms.group.permission", "Manage group permissions", default=False)
67
+ register_node("luckperms.group.parent", "Manage group inheritance", default=False)
68
+ register_node("luckperms.group.setweight", "Set group weight", default=False)
69
+ register_node("luckperms.group.setdisplayname", "Set group display name", default=False)
70
+ register_node("luckperms.group.clear", "Clear group permissions", default=False)
71
+ register_node("luckperms.group.rename", "Rename group", default=False)
72
+ register_node("luckperms.group.clone", "Clone group", default=False)
73
+ register_node("luckperms.group.listmembers", "List group members", default=False)
74
+ register_node("luckperms.group.create", "Create group", default=False)
75
+ register_node("luckperms.group.delete", "Delete group", default=False)
76
+ register_node("luckperms.group.list", "List all groups", default=False)
77
+
78
+
79
+ _register_builtin_nodes()
80
+
81
+
82
+ async def _init():
83
+ global _store_initialized
84
+
85
+ if _store_initialized:
86
+ return
87
+
88
+ from nonebot import get_plugin_config
89
+ from .config import OBLPConfig
90
+ from .storage import init_store
91
+ from .adapter.identity import set_resolver as _set_resolver, OneBotV11Resolver
92
+ from .commands import register_admin_commands
93
+ from .message import load_messages, _export_defaults
94
+
95
+ raw_cfg = get_plugin_config(OBLPConfig)
96
+
97
+ for field in raw_cfg.model_fields:
98
+ setattr(oblp_config, field, getattr(raw_cfg, field))
99
+
100
+ store_type = oblp_config.store_type
101
+ kwargs = {}
102
+ if store_type == "sqlite":
103
+ kwargs["db_path"] = oblp_config.sqlite_path
104
+ elif store_type == "redis":
105
+ kwargs["redis_url"] = oblp_config.redis_url
106
+
107
+ store = init_store(store_type, **kwargs)
108
+ await store.load_all()
109
+ _set_resolver(OneBotV11Resolver())
110
+ register_admin_commands()
111
+
112
+ await load_messages(oblp_config.message_file)
113
+ try:
114
+ await _export_defaults(oblp_config.message_file)
115
+ except Exception:
116
+ pass
117
+
118
+ _store_initialized = True
119
+ logger.info(f"OBLP initialized (store: {store_type})")
120
+
121
+
122
+ try:
123
+ from nonebot import get_driver
124
+ driver = get_driver()
125
+ driver.on_startup(_init)
126
+ except (ValueError, RuntimeError, ImportError):
127
+ logger.debug("NoneBot not initialized, OBLP will init on first use")
@@ -0,0 +1,17 @@
1
+ from typing import Optional
2
+
3
+ from .identity import Identity, IdentityResolver, OneBotV11Resolver, set_resolver
4
+ from .permission import require, require_any, require_all
5
+ from .context import get_context, LPContext
6
+
7
+ __all__ = [
8
+ "Identity",
9
+ "IdentityResolver",
10
+ "OneBotV11Resolver",
11
+ "set_resolver",
12
+ "require",
13
+ "require_any",
14
+ "require_all",
15
+ "get_context",
16
+ "LPContext",
17
+ ]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import contextvars
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from typing import Dict, Optional, Any
7
+
8
+ from ..core.models import User, QueryOptions, ContextSet
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class LPContext:
13
+ user: User
14
+ identity: "Identity"
15
+ query_options: QueryOptions
16
+ resolved_nodes: Dict[str, bool]
17
+ matched_node: Optional[str] = None
18
+ timestamp: float = field(default_factory=time.time)
19
+
20
+
21
+ _lp_context: contextvars.ContextVar[Optional[LPContext]] = contextvars.ContextVar(
22
+ "oblp_context", default=None
23
+ )
24
+
25
+
26
+ def get_context() -> Optional[LPContext]:
27
+ return _lp_context.get()
28
+
29
+
30
+ def set_context(ctx: Optional[LPContext]):
31
+ _lp_context.set(ctx)