nonebot-plugin-onebot-luckperms 0.1.0__py3-none-any.whl

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 (32) hide show
  1. nonebot_plugin_onebot_luckperms/__init__.py +127 -0
  2. nonebot_plugin_onebot_luckperms/adapter/__init__.py +17 -0
  3. nonebot_plugin_onebot_luckperms/adapter/context.py +31 -0
  4. nonebot_plugin_onebot_luckperms/adapter/identity.py +64 -0
  5. nonebot_plugin_onebot_luckperms/adapter/permission.py +157 -0
  6. nonebot_plugin_onebot_luckperms/commands/__init__.py +3 -0
  7. nonebot_plugin_onebot_luckperms/commands/admin.py +857 -0
  8. nonebot_plugin_onebot_luckperms/config.py +20 -0
  9. nonebot_plugin_onebot_luckperms/core/__init__.py +20 -0
  10. nonebot_plugin_onebot_luckperms/core/cache.py +44 -0
  11. nonebot_plugin_onebot_luckperms/core/context_provider.py +74 -0
  12. nonebot_plugin_onebot_luckperms/core/engine.py +162 -0
  13. nonebot_plugin_onebot_luckperms/core/exceptions.py +6 -0
  14. nonebot_plugin_onebot_luckperms/core/models.py +123 -0
  15. nonebot_plugin_onebot_luckperms/core/registry.py +72 -0
  16. nonebot_plugin_onebot_luckperms/message.py +126 -0
  17. nonebot_plugin_onebot_luckperms/py.typed +0 -0
  18. nonebot_plugin_onebot_luckperms/storage/__init__.py +29 -0
  19. nonebot_plugin_onebot_luckperms/storage/memory.py +46 -0
  20. nonebot_plugin_onebot_luckperms/storage/protocol.py +21 -0
  21. nonebot_plugin_onebot_luckperms/storage/redis.py +162 -0
  22. nonebot_plugin_onebot_luckperms/storage/sqlite.py +189 -0
  23. nonebot_plugin_onebot_luckperms/webeditor/__init__.py +9 -0
  24. nonebot_plugin_onebot_luckperms/webeditor/bytebin.py +71 -0
  25. nonebot_plugin_onebot_luckperms/webeditor/manager.py +228 -0
  26. nonebot_plugin_onebot_luckperms/webeditor/session.py +109 -0
  27. nonebot_plugin_onebot_luckperms/webeditor/websocket.py +180 -0
  28. nonebot_plugin_onebot_luckperms-0.1.0.dist-info/METADATA +23 -0
  29. nonebot_plugin_onebot_luckperms-0.1.0.dist-info/RECORD +32 -0
  30. nonebot_plugin_onebot_luckperms-0.1.0.dist-info/WHEEL +5 -0
  31. nonebot_plugin_onebot_luckperms-0.1.0.dist-info/licenses/LICENSE +21 -0
  32. nonebot_plugin_onebot_luckperms-0.1.0.dist-info/top_level.txt +1 -0
@@ -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)
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Dict, Optional, Protocol
6
+
7
+ from nonebot.adapters import Bot, Event
8
+
9
+ logger = logging.getLogger("oblp")
10
+
11
+
12
+ @dataclass
13
+ class Identity:
14
+ user_id: str
15
+ platform: str
16
+ group_id: Optional[str] = None
17
+ role: str = "member"
18
+ extra: Dict[str, Any] = field(default_factory=dict)
19
+
20
+
21
+ class IdentityResolver(Protocol):
22
+ async def resolve(self, bot: Bot, event: Event) -> Identity:
23
+ ...
24
+
25
+
26
+ _resolver: Optional[IdentityResolver] = None
27
+
28
+
29
+ def set_resolver(resolver: IdentityResolver):
30
+ global _resolver
31
+ _resolver = resolver
32
+ logger.info(f"IdentityResolver set to {type(resolver).__name__}")
33
+
34
+
35
+ def get_resolver() -> Optional[IdentityResolver]:
36
+ return _resolver
37
+
38
+
39
+ class OneBotV11Resolver:
40
+ async def resolve(self, bot: Bot, event: Event) -> Identity:
41
+ from nonebot.adapters.onebot.v11 import GroupMessageEvent, PrivateMessageEvent
42
+
43
+ user_id = event.get_user_id()
44
+ group_id = None
45
+ role = "member"
46
+ platform = "onebot_v11"
47
+
48
+ if isinstance(event, GroupMessageEvent):
49
+ group_id = str(event.group_id)
50
+ if event.sender and event.sender.role:
51
+ role = event.sender.role
52
+ elif isinstance(event, PrivateMessageEvent):
53
+ platform = "onebot_v11_private"
54
+
55
+ if user_id in getattr(bot.config, "superusers", set()):
56
+ role = "superuser"
57
+
58
+ return Identity(
59
+ user_id=user_id,
60
+ platform=platform,
61
+ group_id=group_id,
62
+ role=role,
63
+ extra={"raw_event_name": event.get_event_name()},
64
+ )
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Optional, Set
5
+
6
+ from nonebot.permission import Permission
7
+ from nonebot.adapters import Bot, Event
8
+
9
+ from ..core.models import ContextSet, QueryOptions, User, PermissionNode
10
+ from ..core.registry import NodeRegistry
11
+ from ..core.engine import PermissionEngine
12
+ from ..core.cache import PermissionCache
13
+ from ..core.context_provider import get_context_providers
14
+ from ..config import oblp_config
15
+ from ..storage import get_store
16
+ from .identity import get_resolver, Identity
17
+ from .context import LPContext, set_context
18
+
19
+ logger = logging.getLogger("oblp")
20
+
21
+
22
+ def _apply_default_nodes(user: User, identity: Identity) -> list:
23
+ """Apply default permission nodes based on the user's role from config."""
24
+ nodes = []
25
+ role_map = {
26
+ "owner": oblp_config.default_group_owner,
27
+ "admin": oblp_config.default_group_admin,
28
+ "member": oblp_config.default_group_member,
29
+ "superuser": oblp_config.superuser_inherit,
30
+ }
31
+ patterns = role_map.get(identity.role, [])
32
+ for pattern in patterns:
33
+ if pattern == "*":
34
+ nodes.append(PermissionNode(key="*", value=True))
35
+ elif pattern:
36
+ nodes.append(PermissionNode(key=pattern, value=True))
37
+ return nodes
38
+
39
+
40
+ def _checker_factory(
41
+ node_keys: Set[str],
42
+ mode: str,
43
+ base_contexts: Optional[ContextSet] = None,
44
+ ):
45
+ async def checker(bot: Bot, event: Event) -> bool:
46
+ resolver = get_resolver()
47
+ store = get_store()
48
+ if resolver is None or store is None:
49
+ logger.warning("OBLP not initialized: no resolver or store")
50
+ return False
51
+
52
+ try:
53
+ identity: Identity = await resolver.resolve(bot, event)
54
+ except Exception as e:
55
+ logger.error(f"Failed to resolve identity: {e}")
56
+ return False
57
+
58
+ ctx = ContextSet.from_dict({
59
+ "platform": identity.platform,
60
+ "user_id": identity.user_id,
61
+ "role": identity.role,
62
+ })
63
+ if identity.group_id:
64
+ ctx = ctx.with_context("group_id", identity.group_id)
65
+
66
+ if base_contexts:
67
+ for k, v in base_contexts.data.items():
68
+ ctx = ctx.with_context(k, v)
69
+
70
+ # Call all registered third-party context providers
71
+ for provider in get_context_providers():
72
+ try:
73
+ extra = await provider(bot, event, ctx)
74
+ if extra:
75
+ for k, v in extra.items():
76
+ ctx = ctx.with_context(k, v)
77
+ except Exception:
78
+ logger.exception(f"ContextProvider {type(provider).__name__} error")
79
+
80
+ query_opts = QueryOptions(
81
+ mode="contextual",
82
+ contexts=ctx,
83
+ flags={"include_inherited", "resolve_inheritance"},
84
+ )
85
+
86
+ cache_key = PermissionCache.make_key(identity.user_id, ctx)
87
+ cached = PermissionCache.get(cache_key)
88
+ if cached is not None:
89
+ resolved = cached
90
+ else:
91
+ user = await store.get_user(identity.user_id)
92
+ if user is None:
93
+ user = User(user_id=identity.user_id)
94
+
95
+ default_nodes = _apply_default_nodes(user, identity)
96
+ resolved = await user.get_effective_nodes(store, query_opts, ctx)
97
+
98
+ for dn in default_nodes:
99
+ if dn.key not in resolved:
100
+ resolved[dn.key] = dn.value
101
+
102
+ PermissionCache.set(cache_key, resolved)
103
+
104
+ matched_node = None
105
+ if mode == "any":
106
+ result = any(
107
+ PermissionEngine.check(resolved, nk)
108
+ for nk in node_keys
109
+ )
110
+ if result:
111
+ for nk in node_keys:
112
+ if PermissionEngine.check(resolved, nk):
113
+ matched_node = nk
114
+ break
115
+ elif mode == "all":
116
+ result = all(
117
+ PermissionEngine.check(resolved, nk)
118
+ for nk in node_keys
119
+ )
120
+ if result:
121
+ matched_node = ",".join(node_keys)
122
+ else:
123
+ target = next(iter(node_keys))
124
+ result = PermissionEngine.check(resolved, target)
125
+ if result:
126
+ matched_node = target
127
+
128
+ lp_ctx = LPContext(
129
+ user=user if 'user' in dir() else User(user_id=identity.user_id),
130
+ identity=identity,
131
+ query_options=query_opts,
132
+ resolved_nodes=resolved,
133
+ matched_node=matched_node,
134
+ )
135
+ set_context(lp_ctx)
136
+
137
+ if logger.isEnabledFor(logging.DEBUG):
138
+ logger.debug(
139
+ f"[oblp] User {identity.user_id} check {node_keys} "
140
+ f"in {ctx.to_dict()} -> {result}"
141
+ )
142
+
143
+ return result
144
+
145
+ return checker
146
+
147
+
148
+ def require(node_key: str, contexts: Optional[ContextSet] = None) -> Permission:
149
+ return Permission(_checker_factory({node_key}, "single", contexts))
150
+
151
+
152
+ def require_any(*node_keys: str, contexts: Optional[ContextSet] = None) -> Permission:
153
+ return Permission(_checker_factory(set(node_keys), "any", contexts))
154
+
155
+
156
+ def require_all(*node_keys: str, contexts: Optional[ContextSet] = None) -> Permission:
157
+ return Permission(_checker_factory(set(node_keys), "all", contexts))
@@ -0,0 +1,3 @@
1
+ from .admin import register_admin_commands
2
+
3
+ __all__ = ["register_admin_commands"]