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,46 @@
1
+ from typing import Optional, List, Dict
2
+
3
+ from ..core.models import User, Group, PermissionNode
4
+
5
+
6
+ class MemoryStore:
7
+ def __init__(self):
8
+ self._users: Dict[str, User] = {}
9
+ self._groups: Dict[str, Group] = {}
10
+ self._registered_nodes: Dict[str, PermissionNode] = {}
11
+
12
+ async def get_user(self, user_id: str) -> Optional[User]:
13
+ return self._users.get(user_id)
14
+
15
+ async def save_user(self, user: User) -> None:
16
+ self._users[user.user_id] = user
17
+
18
+ async def delete_user(self, user_id: str) -> None:
19
+ self._users.pop(user_id, None)
20
+
21
+ async def list_users(self) -> List[str]:
22
+ return list(self._users.keys())
23
+
24
+ async def get_group(self, name: str) -> Optional[Group]:
25
+ return self._groups.get(name)
26
+
27
+ async def save_group(self, group: Group) -> None:
28
+ self._groups[group.name] = group
29
+
30
+ async def delete_group(self, name: str) -> None:
31
+ self._groups.pop(name, None)
32
+
33
+ async def list_groups(self) -> List[str]:
34
+ return list(self._groups.keys())
35
+
36
+ async def get_registered_nodes(self) -> List[PermissionNode]:
37
+ return list(self._registered_nodes.values())
38
+
39
+ async def save_registered_node(self, node: PermissionNode) -> None:
40
+ self._registered_nodes[node.key] = node
41
+
42
+ async def load_all(self) -> None:
43
+ pass
44
+
45
+ async def save_all(self) -> None:
46
+ pass
@@ -0,0 +1,21 @@
1
+ from typing import Protocol, Optional, List, Dict, Any
2
+
3
+ from ..core.models import User, Group, PermissionNode
4
+
5
+
6
+ class PermissionStore(Protocol):
7
+ async def get_user(self, user_id: str) -> Optional[User]: ...
8
+ async def save_user(self, user: User) -> None: ...
9
+ async def delete_user(self, user_id: str) -> None: ...
10
+ async def list_users(self) -> List[str]: ...
11
+
12
+ async def get_group(self, name: str) -> Optional[Group]: ...
13
+ async def save_group(self, group: Group) -> None: ...
14
+ async def delete_group(self, name: str) -> None: ...
15
+ async def list_groups(self) -> List[str]: ...
16
+
17
+ async def get_registered_nodes(self) -> List[PermissionNode]: ...
18
+ async def save_registered_node(self, node: PermissionNode) -> None: ...
19
+
20
+ async def load_all(self) -> None: ...
21
+ async def save_all(self) -> None: ...
@@ -0,0 +1,162 @@
1
+ import json
2
+ import logging
3
+ import time
4
+ from typing import Optional, List
5
+
6
+ import redis.asyncio as aioredis
7
+
8
+ from ..core.models import User, Group, PermissionNode, ContextSet
9
+
10
+ logger = logging.getLogger("oblp")
11
+
12
+
13
+ class RedisStore:
14
+ def __init__(self, url: str = "redis://localhost:6379/0"):
15
+ self.url = url
16
+ self._redis: Optional[aioredis.Redis] = None
17
+ self._prefix = "oblp"
18
+
19
+ async def _get_redis(self) -> aioredis.Redis:
20
+ if self._redis is None:
21
+ self._redis = aioredis.from_url(self.url, decode_responses=True)
22
+ return self._redis
23
+
24
+ async def _set_ttl(self, key: str, node: PermissionNode):
25
+ if node.expiry is not None:
26
+ ttl = int(node.expiry - time.time())
27
+ if ttl > 0:
28
+ r = await self._get_redis()
29
+ await r.expire(key, ttl)
30
+
31
+ async def get_user(self, user_id: str) -> Optional[User]:
32
+ r = await self._get_redis()
33
+ data = await r.hgetall(f"{self._prefix}:user:{user_id}")
34
+ if not data:
35
+ return None
36
+ return User(
37
+ user_id=data.get("user_id", user_id),
38
+ username=data.get("username"),
39
+ primary_group=data.get("primary_group"),
40
+ groups=json.loads(data.get("groups", "[]")),
41
+ nodes=[self._node_from_dict(n) for n in json.loads(data.get("nodes", "[]"))],
42
+ )
43
+
44
+ async def save_user(self, user: User) -> None:
45
+ r = await self._get_redis()
46
+ key = f"{self._prefix}:user:{user.user_id}"
47
+ await r.hset(
48
+ key,
49
+ mapping={
50
+ "user_id": user.user_id,
51
+ "username": user.username or "",
52
+ "primary_group": user.primary_group or "",
53
+ "groups": json.dumps(user.groups),
54
+ "nodes": json.dumps([self._node_to_dict(n) for n in user.nodes]),
55
+ },
56
+ )
57
+ max_expiry = min((n.expiry for n in user.nodes if n.expiry), default=None)
58
+ if max_expiry:
59
+ await r.expire(key, int(max_expiry - time.time()))
60
+
61
+ async def delete_user(self, user_id: str) -> None:
62
+ r = await self._get_redis()
63
+ await r.delete(f"{self._prefix}:user:{user_id}")
64
+
65
+ async def list_users(self) -> List[str]:
66
+ r = await self._get_redis()
67
+ cursor = "0"
68
+ keys = []
69
+ while cursor != 0:
70
+ cursor, batch = await r.scan(cursor=cursor, match=f"{self._prefix}:user:*")
71
+ keys.extend([k.split(":", 2)[2] for k in batch])
72
+ cursor = int(cursor)
73
+ return keys
74
+
75
+ async def get_group(self, name: str) -> Optional[Group]:
76
+ r = await self._get_redis()
77
+ data = await r.hgetall(f"{self._prefix}:group:{name}")
78
+ if not data:
79
+ return None
80
+ return Group(
81
+ name=name,
82
+ display_name=data.get("display_name"),
83
+ weight=int(data.get("weight", 0)),
84
+ parents=json.loads(data.get("parents", "[]")),
85
+ nodes=[self._node_from_dict(n) for n in json.loads(data.get("nodes", "[]"))],
86
+ )
87
+
88
+ async def save_group(self, group: Group) -> None:
89
+ r = await self._get_redis()
90
+ key = f"{self._prefix}:group:{group.name}"
91
+ await r.hset(
92
+ key,
93
+ mapping={
94
+ "display_name": group.display_name or "",
95
+ "weight": str(group.weight),
96
+ "parents": json.dumps(group.parents),
97
+ "nodes": json.dumps([self._node_to_dict(n) for n in group.nodes]),
98
+ },
99
+ )
100
+ max_expiry = min((n.expiry for n in group.nodes if n.expiry), default=None)
101
+ if max_expiry:
102
+ await r.expire(key, int(max_expiry - time.time()))
103
+
104
+ async def delete_group(self, name: str) -> None:
105
+ r = await self._get_redis()
106
+ await r.delete(f"{self._prefix}:group:{name}")
107
+
108
+ async def list_groups(self) -> List[str]:
109
+ r = await self._get_redis()
110
+ cursor = "0"
111
+ keys = []
112
+ while cursor != 0:
113
+ cursor, batch = await r.scan(cursor=cursor, match=f"{self._prefix}:group:*")
114
+ keys.extend([k.split(":", 2)[2] for k in batch])
115
+ cursor = int(cursor)
116
+ return keys
117
+
118
+ async def get_registered_nodes(self) -> List[PermissionNode]:
119
+ r = await self._get_redis()
120
+ raw = await r.get(f"{self._prefix}:registry")
121
+ if not raw:
122
+ return []
123
+ return [self._node_from_dict(n) for n in json.loads(raw)]
124
+
125
+ async def save_registered_node(self, node: PermissionNode) -> None:
126
+ existing = await self.get_registered_nodes()
127
+ existing_dict = {n.key: n for n in existing}
128
+ existing_dict[node.key] = node
129
+ r = await self._get_redis()
130
+ await r.set(
131
+ f"{self._prefix}:registry",
132
+ json.dumps([self._node_to_dict(n) for n in existing_dict.values()]),
133
+ )
134
+
135
+ async def load_all(self) -> None:
136
+ pass
137
+
138
+ async def save_all(self) -> None:
139
+ pass
140
+
141
+ async def close(self):
142
+ if self._redis is not None:
143
+ await self._redis.close()
144
+ self._redis = None
145
+
146
+ @staticmethod
147
+ def _node_to_dict(node: PermissionNode) -> dict:
148
+ return {
149
+ "key": node.key,
150
+ "value": node.value,
151
+ "expiry": node.expiry,
152
+ "contexts": node.contexts.to_dict(),
153
+ }
154
+
155
+ @staticmethod
156
+ def _node_from_dict(d: dict) -> PermissionNode:
157
+ return PermissionNode(
158
+ key=d["key"],
159
+ value=d.get("value", True),
160
+ expiry=d.get("expiry"),
161
+ contexts=ContextSet.from_dict(d.get("contexts", {})),
162
+ )
@@ -0,0 +1,189 @@
1
+ import json
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Optional, List, Dict
5
+
6
+ import aiosqlite
7
+
8
+ from ..core.models import User, Group, PermissionNode, ContextSet
9
+
10
+ logger = logging.getLogger("oblp")
11
+
12
+
13
+ class SQLiteStore:
14
+ def __init__(self, db_path: str = "./data/oblp/permissions.db"):
15
+ self.db_path = db_path
16
+ self._conn: Optional[aiosqlite.Connection] = None
17
+
18
+ async def _ensure_dir(self):
19
+ p = Path(self.db_path)
20
+ p.parent.mkdir(parents=True, exist_ok=True)
21
+
22
+ async def _get_conn(self) -> aiosqlite.Connection:
23
+ if self._conn is None:
24
+ await self._ensure_dir()
25
+ self._conn = await aiosqlite.connect(self.db_path)
26
+ self._conn.row_factory = aiosqlite.Row
27
+ return self._conn
28
+
29
+ async def _init_tables(self):
30
+ conn = await self._get_conn()
31
+ await conn.executescript("""
32
+ CREATE TABLE IF NOT EXISTS users (
33
+ user_id TEXT PRIMARY KEY,
34
+ username TEXT,
35
+ primary_group TEXT,
36
+ groups_json TEXT NOT NULL DEFAULT '[]',
37
+ nodes_json TEXT NOT NULL DEFAULT '[]'
38
+ );
39
+ CREATE TABLE IF NOT EXISTS groups (
40
+ name TEXT PRIMARY KEY,
41
+ display_name TEXT,
42
+ weight INTEGER NOT NULL DEFAULT 0,
43
+ parents_json TEXT NOT NULL DEFAULT '[]',
44
+ nodes_json TEXT NOT NULL DEFAULT '[]'
45
+ );
46
+ CREATE TABLE IF NOT EXISTS meta (
47
+ key TEXT PRIMARY KEY,
48
+ value TEXT NOT NULL
49
+ );
50
+ """)
51
+ await conn.commit()
52
+
53
+ async def get_user(self, user_id: str) -> Optional[User]:
54
+ await self._init_tables()
55
+ conn = await self._get_conn()
56
+ cursor = await conn.execute("SELECT * FROM users WHERE user_id = ?", (user_id,))
57
+ row = await cursor.fetchone()
58
+ if row is None:
59
+ return None
60
+ return User(
61
+ user_id=row["user_id"],
62
+ username=row["username"],
63
+ primary_group=row["primary_group"],
64
+ groups=json.loads(row["groups_json"]),
65
+ nodes=[self._node_from_dict(n) for n in json.loads(row["nodes_json"])],
66
+ )
67
+
68
+ async def save_user(self, user: User) -> None:
69
+ await self._init_tables()
70
+ conn = await self._get_conn()
71
+ await conn.execute(
72
+ """INSERT OR REPLACE INTO users (user_id, username, primary_group, groups_json, nodes_json)
73
+ VALUES (?, ?, ?, ?, ?)""",
74
+ (
75
+ user.user_id,
76
+ user.username,
77
+ user.primary_group,
78
+ json.dumps(user.groups),
79
+ json.dumps([self._node_to_dict(n) for n in user.nodes]),
80
+ ),
81
+ )
82
+ await conn.commit()
83
+
84
+ async def delete_user(self, user_id: str) -> None:
85
+ await self._init_tables()
86
+ conn = await self._get_conn()
87
+ await conn.execute("DELETE FROM users WHERE user_id = ?", (user_id,))
88
+ await conn.commit()
89
+
90
+ async def list_users(self) -> List[str]:
91
+ await self._init_tables()
92
+ conn = await self._get_conn()
93
+ cursor = await conn.execute("SELECT user_id FROM users")
94
+ rows = await cursor.fetchall()
95
+ return [row["user_id"] for row in rows]
96
+
97
+ async def get_group(self, name: str) -> Optional[Group]:
98
+ await self._init_tables()
99
+ conn = await self._get_conn()
100
+ cursor = await conn.execute("SELECT * FROM groups WHERE name = ?", (name,))
101
+ row = await cursor.fetchone()
102
+ if row is None:
103
+ return None
104
+ return Group(
105
+ name=row["name"],
106
+ display_name=row["display_name"],
107
+ weight=row["weight"],
108
+ parents=json.loads(row["parents_json"]),
109
+ nodes=[self._node_from_dict(n) for n in json.loads(row["nodes_json"])],
110
+ )
111
+
112
+ async def save_group(self, group: Group) -> None:
113
+ await self._init_tables()
114
+ conn = await self._get_conn()
115
+ await conn.execute(
116
+ """INSERT OR REPLACE INTO groups (name, display_name, weight, parents_json, nodes_json)
117
+ VALUES (?, ?, ?, ?, ?)""",
118
+ (
119
+ group.name,
120
+ group.display_name,
121
+ group.weight,
122
+ json.dumps(group.parents),
123
+ json.dumps([self._node_to_dict(n) for n in group.nodes]),
124
+ ),
125
+ )
126
+ await conn.commit()
127
+
128
+ async def delete_group(self, name: str) -> None:
129
+ await self._init_tables()
130
+ conn = await self._get_conn()
131
+ await conn.execute("DELETE FROM groups WHERE name = ?", (name,))
132
+ await conn.commit()
133
+
134
+ async def list_groups(self) -> List[str]:
135
+ await self._init_tables()
136
+ conn = await self._get_conn()
137
+ cursor = await conn.execute("SELECT name FROM groups")
138
+ rows = await cursor.fetchall()
139
+ return [row["name"] for row in rows]
140
+
141
+ async def get_registered_nodes(self) -> List[PermissionNode]:
142
+ await self._init_tables()
143
+ conn = await self._get_conn()
144
+ cursor = await conn.execute("SELECT value FROM meta WHERE key = 'registered_nodes'")
145
+ row = await cursor.fetchone()
146
+ if row is None:
147
+ return []
148
+ return [self._node_from_dict(n) for n in json.loads(row["value"])]
149
+
150
+ async def save_registered_node(self, node: PermissionNode) -> None:
151
+ await self._init_tables()
152
+ existing = await self.get_registered_nodes()
153
+ existing_dict = {n.key: n for n in existing}
154
+ existing_dict[node.key] = node
155
+ conn = await self._get_conn()
156
+ await conn.execute(
157
+ "INSERT OR REPLACE INTO meta (key, value) VALUES ('registered_nodes', ?)",
158
+ (json.dumps([self._node_to_dict(n) for n in existing_dict.values()]),),
159
+ )
160
+ await conn.commit()
161
+
162
+ async def load_all(self) -> None:
163
+ await self._init_tables()
164
+
165
+ async def save_all(self) -> None:
166
+ await self._init_tables()
167
+
168
+ async def close(self):
169
+ if self._conn is not None:
170
+ await self._conn.close()
171
+ self._conn = None
172
+
173
+ @staticmethod
174
+ def _node_to_dict(node: PermissionNode) -> dict:
175
+ return {
176
+ "key": node.key,
177
+ "value": node.value,
178
+ "expiry": node.expiry,
179
+ "contexts": node.contexts.to_dict(),
180
+ }
181
+
182
+ @staticmethod
183
+ def _node_from_dict(d: dict) -> PermissionNode:
184
+ return PermissionNode(
185
+ key=d["key"],
186
+ value=d.get("value", True),
187
+ expiry=d.get("expiry"),
188
+ contexts=ContextSet.from_dict(d.get("contexts", {})),
189
+ )
@@ -0,0 +1,9 @@
1
+ from .bytebin import BytebinClient
2
+ from .websocket import BytesocksClient
3
+ from .session import WebEditorSession
4
+
5
+ __all__ = [
6
+ "BytebinClient",
7
+ "BytesocksClient",
8
+ "WebEditorSession",
9
+ ]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ import json
5
+ import logging
6
+ from typing import Any
7
+
8
+ import aiohttp
9
+
10
+ log = logging.getLogger("oblp.webeditor")
11
+
12
+ DEFAULT_BYTEBIN_URL = "https://usercontent.luckperms.net"
13
+
14
+
15
+ class BytebinClient:
16
+ def __init__(self, base_url: str = DEFAULT_BYTEBIN_URL):
17
+ self.base_url = base_url.rstrip("/")
18
+
19
+ async def upload(self, payload: dict[str, Any]) -> str:
20
+ json_bytes = json.dumps(payload, ensure_ascii=False).encode("utf-8")
21
+ compressed = gzip.compress(json_bytes)
22
+
23
+ headers = {
24
+ "Content-Type": "application/json",
25
+ "Content-Encoding": "gzip",
26
+ "User-Agent": "LuckPerms/5.4.0/editor",
27
+ }
28
+
29
+ async with aiohttp.ClientSession() as session:
30
+ async with session.post(
31
+ f"{self.base_url}/post",
32
+ data=compressed,
33
+ headers=headers,
34
+ ) as resp:
35
+ if resp.status not in (200, 201):
36
+ text = await resp.text()
37
+ raise RuntimeError(
38
+ f"Bytebin upload failed: HTTP {resp.status} - {text}"
39
+ )
40
+
41
+ key: str | None = None
42
+ location = resp.headers.get("Location")
43
+ if location:
44
+ key = location.rstrip("/").split("/")[-1]
45
+
46
+ if not key:
47
+ result = await resp.json()
48
+ key = result.get("key") or result.get("code") or result.get("id")
49
+
50
+ if not key:
51
+ raise RuntimeError(f"Bytebin invalid response: {await resp.text()}")
52
+ log.info("Bytebin upload OK, key=%s", key)
53
+ return key
54
+
55
+ async def download(self, code: str) -> dict[str, Any]:
56
+ async with aiohttp.ClientSession() as session:
57
+ async with session.get(
58
+ f"{self.base_url}/{code}",
59
+ headers={"User-Agent": "LuckPerms/5.4.0/editor"},
60
+ ) as resp:
61
+ if resp.status != 200:
62
+ text = await resp.text()
63
+ raise RuntimeError(
64
+ f"Bytebin download failed: HTTP {resp.status} - {text}"
65
+ )
66
+ raw = await resp.read()
67
+ try:
68
+ decompressed = gzip.decompress(raw)
69
+ except Exception:
70
+ decompressed = raw
71
+ return json.loads(decompressed.decode("utf-8"))