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.
- nonebot_plugin_onebot_luckperms/__init__.py +127 -0
- nonebot_plugin_onebot_luckperms/adapter/__init__.py +17 -0
- nonebot_plugin_onebot_luckperms/adapter/context.py +31 -0
- nonebot_plugin_onebot_luckperms/adapter/identity.py +64 -0
- nonebot_plugin_onebot_luckperms/adapter/permission.py +157 -0
- nonebot_plugin_onebot_luckperms/commands/__init__.py +3 -0
- nonebot_plugin_onebot_luckperms/commands/admin.py +857 -0
- nonebot_plugin_onebot_luckperms/config.py +20 -0
- nonebot_plugin_onebot_luckperms/core/__init__.py +20 -0
- nonebot_plugin_onebot_luckperms/core/cache.py +44 -0
- nonebot_plugin_onebot_luckperms/core/context_provider.py +74 -0
- nonebot_plugin_onebot_luckperms/core/engine.py +162 -0
- nonebot_plugin_onebot_luckperms/core/exceptions.py +6 -0
- nonebot_plugin_onebot_luckperms/core/models.py +123 -0
- nonebot_plugin_onebot_luckperms/core/registry.py +72 -0
- nonebot_plugin_onebot_luckperms/message.py +126 -0
- nonebot_plugin_onebot_luckperms/py.typed +0 -0
- nonebot_plugin_onebot_luckperms/storage/__init__.py +29 -0
- nonebot_plugin_onebot_luckperms/storage/memory.py +46 -0
- nonebot_plugin_onebot_luckperms/storage/protocol.py +21 -0
- nonebot_plugin_onebot_luckperms/storage/redis.py +162 -0
- nonebot_plugin_onebot_luckperms/storage/sqlite.py +189 -0
- nonebot_plugin_onebot_luckperms/webeditor/__init__.py +9 -0
- nonebot_plugin_onebot_luckperms/webeditor/bytebin.py +71 -0
- nonebot_plugin_onebot_luckperms/webeditor/manager.py +228 -0
- nonebot_plugin_onebot_luckperms/webeditor/session.py +109 -0
- nonebot_plugin_onebot_luckperms/webeditor/websocket.py +180 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/METADATA +23 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/RECORD +32 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/WHEEL +5 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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,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"))
|