From 7f1035ff43e8429bdad30f61727f91f77ed47256 Mon Sep 17 00:00:00 2001 From: passthem Date: Sat, 7 Mar 2026 15:19:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E8=8E=B7=E5=8F=96=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=9A=84=E5=9F=BA=E7=A1=80=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/common/permsys/__init__.py | 62 ++++++ konabot/common/permsys/entity.py | 61 ++++++ .../migrates/mu1_create_permsys_table.sql | 9 +- konabot/common/permsys/repo.py | 180 ++++++++++++++++++ konabot/common/permsys/sql/create_entity.sql | 11 ++ konabot/common/permsys/sql/get_entity_id.sql | 8 + konabot/common/permsys/sql/get_perm_info.sql | 7 + .../common/permsys/sql/update_perm_info.sql | 4 + 8 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 konabot/common/permsys/__init__.py create mode 100644 konabot/common/permsys/entity.py create mode 100644 konabot/common/permsys/repo.py create mode 100644 konabot/common/permsys/sql/create_entity.sql create mode 100644 konabot/common/permsys/sql/get_entity_id.sql create mode 100644 konabot/common/permsys/sql/get_perm_info.sql create mode 100644 konabot/common/permsys/sql/update_perm_info.sql diff --git a/konabot/common/permsys/__init__.py b/konabot/common/permsys/__init__.py new file mode 100644 index 0000000..3a03c54 --- /dev/null +++ b/konabot/common/permsys/__init__.py @@ -0,0 +1,62 @@ +from typing import Iterable +import nonebot +from nonebot.adapters import Event + +from konabot.common.database import DatabaseManager +from konabot.common.path import DATA_PATH +from konabot.common.permsys.entity import PermEntity, get_entity_chain +from konabot.common.permsys.migrates import execute_migration +from konabot.common.permsys.repo import PermRepo + + +driver = nonebot.get_driver() + +db = DatabaseManager(DATA_PATH / "perm.sqlite3") + + +class PermManager: + def __init__(self, db: DatabaseManager) -> None: + self.db = db + + async def check_has_permission( + self, entities: Event | PermEntity | list[PermEntity], key: str + ) -> bool: + if isinstance(entities, Event): + entities = await get_entity_chain(entities) + if isinstance(entities, PermEntity): + entities = [entities] + + key_split = key.split(".") + keys = [".".join(key_split[: i + 1]) for i in range(len(key_split))][::-1] + + async with self.db.get_conn() as conn: + repo = PermRepo(conn) + # for entity in entities: + # for k in keys: + # perm = await repo.get_perm_info(entity, k) + # if perm is not None: + # return perm + data = await repo.get_perm_info_batch(entities, keys) + for entity in entities: + for k in keys: + p = data.get((entity, k)) + if p is not None: + return p + return False + + +def perm_manager(_db: DatabaseManager | None = None) -> PermManager: + if _db is None: + _db = db + return PermManager(_db) + + +@driver.on_startup +async def _(): + async with db.get_conn() as conn: + await execute_migration(conn) + + +@driver.on_shutdown +async def _(): + await db.close_all_connections() diff --git a/konabot/common/permsys/entity.py b/konabot/common/permsys/entity.py new file mode 100644 index 0000000..6eb2dd7 --- /dev/null +++ b/konabot/common/permsys/entity.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from nonebot.internal.adapter import Event + +from nonebot.adapters.onebot.v11 import Event as OB11Event +from nonebot.adapters.onebot.v11.event import GroupMessageEvent as OB11GroupEvent +from nonebot.adapters.onebot.v11.event import PrivateMessageEvent as OB11PrivateEvent + +from nonebot.adapters.discord.event import Event as DiscordEvent +from nonebot.adapters.discord.event import GuildMessageCreateEvent as DiscordGMEvent +from nonebot.adapters.discord.event import DirectMessageCreateEvent as DiscordDMEvent + +from nonebot.adapters.minecraft.event import MessageEvent as MinecraftMessageEvent + +from nonebot.adapters.console.event import MessageEvent as ConsoleEvent + + +@dataclass +class PermEntity: + platform: str + entity_type: str + external_id: str + + +async def get_entity_chain(event: Event) -> list[PermEntity]: + entities = [PermEntity("sys", "global", "global")] + + if isinstance(event, OB11Event): + entities.append(PermEntity("ob11", "global", "global")) + + if isinstance(event, OB11GroupEvent): + entities.append(PermEntity("ob11", "group", str(event.group_id))) + entities.append(PermEntity("ob11", "user", str(event.user_id))) + + if isinstance(event, OB11PrivateEvent): + entities.append(PermEntity("ob11", "user", str(event.user_id))) + + if isinstance(event, DiscordEvent): + entities.append(PermEntity("discord", "global", "global")) + + if isinstance(event, DiscordGMEvent): + entities.append(PermEntity("discord", "guilt", str(event.guild_id))) + entities.append(PermEntity("discord", "channel", str(event.channel_id))) + entities.append(PermEntity("discord", "user", str(event.user_id))) + + if isinstance(event, DiscordDMEvent): + entities.append(PermEntity("discord", "channel", str(event.channel_id))) + entities.append(PermEntity("discord", "user", str(event.user_id))) + + if isinstance(event, MinecraftMessageEvent): + entities.append(PermEntity("minecraft", "global", "global")) + entities.append(PermEntity("minecraft", "server", event.server_name)) + player_uuid = event.player.uuid + if player_uuid is not None: + entities.append(PermEntity("minecraft", "player", player_uuid.hex)) + + if isinstance(event, ConsoleEvent): + entities.append(PermEntity("console", "global", "global")) + entities.append(PermEntity("console", "channel", event.channel.id)) + entities.append(PermEntity("console", "user", event.user.id)) + + return entities[::-1] diff --git a/konabot/common/permsys/migrates/mu1_create_permsys_table.sql b/konabot/common/permsys/migrates/mu1_create_permsys_table.sql index 98793b8..cb8994f 100644 --- a/konabot/common/permsys/migrates/mu1_create_permsys_table.sql +++ b/konabot/common/permsys/migrates/mu1_create_permsys_table.sql @@ -7,11 +7,16 @@ CREATE TABLE perm_entity( updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); +CREATE UNIQUE INDEX idx_perm_entity_lookup +ON perm_entity(platform, entity_type, external_id); + CREATE TABLE perm_info( entity_id INTEGER NOT NULL, config_key TEXT NOT NULL, - value BOOLEAN NOT NULL, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + value BOOLEAN, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + -- 联合主键 + PRIMARY KEY (entity_id, config_key) ); CREATE TRIGGER perm_entity_update AFTER UPDATE diff --git a/konabot/common/permsys/repo.py b/konabot/common/permsys/repo.py new file mode 100644 index 0000000..2485bdf --- /dev/null +++ b/konabot/common/permsys/repo.py @@ -0,0 +1,180 @@ +from dataclasses import dataclass +from pathlib import Path + +import aiosqlite + +from .entity import PermEntity + + +def s(p: str): + """读取 SQL 文件内容。 + + Args: + p: SQL 文件名(相对于当前文件所在目录的 sql/ 子目录)。 + + Returns: + SQL 文件的内容字符串。 + """ + return (Path(__file__).parent / "./sql/" / p).read_text() + + +@dataclass +class PermRepo: + """权限实体存储库,负责与数据库交互管理权限实体。 + + Attributes: + conn: aiosqlite 数据库连接对象。 + """ + + conn: aiosqlite.Connection + + async def create_entity(self, entity: PermEntity) -> int: + """创建新的权限实体并返回其 ID。 + + Args: + entity: 要创建的权限实体对象。 + + Returns: + 新创建实体的数据库 ID。 + + Raises: + AssertionError: 如果创建后无法获取实体 ID。 + """ + await self.conn.execute( + s("create_entity.sql"), + (entity.platform, entity.entity_type, entity.external_id), + ) + await self.conn.commit() + eid = await self._get_entity_id_or_none(entity) + assert eid is not None + return eid + + async def _get_entity_id_or_none(self, entity: PermEntity) -> int | None: + """查询实体 ID,如果不存在则返回 None。 + + Args: + entity: 要查询的权限实体对象。 + + Returns: + 实体 ID,如果不存在则返回 None。 + """ + res = await self.conn.execute( + s("get_entity_id.sql"), + (entity.platform, entity.entity_type, entity.external_id), + ) + row = await res.fetchone() + if row is None: + return None + return row[0] + + async def get_entity_id(self, entity: PermEntity) -> int: + """获取实体 ID,如果不存在则自动创建。 + + Args: + entity: 权限实体对象。 + + Returns: + 实体的数据库 ID。 + """ + eid = await self._get_entity_id_or_none(entity) + if eid is None: + return await self.create_entity(entity) + return eid + + async def get_perm_info(self, entity: PermEntity, config_key: str) -> bool | None: + """获取实体的权限配置信息。 + + Args: + entity: 权限实体对象。 + config_key: 配置项的键名。 + + Returns: + 配置值(True/False),如果不存在则返回 None。 + """ + eid = await self.get_entity_id(entity) + res = await self.conn.execute( + s("get_perm_info.sql"), + (eid, config_key), + ) + row = await res.fetchone() + if row is None: + return None + return row[0] + + async def update_perm_info( + self, entity: PermEntity, config_key: str, value: bool | None + ): + """更新实体的权限配置信息。 + + Args: + entity: 权限实体对象。 + config_key: 配置项的键名。 + value: 要设置的配置值(True/False/None)。 + """ + eid = await self.get_entity_id(entity) + await self.conn.execute(s("update_perm_info.sql"), (eid, config_key, value)) + await self.conn.commit() + + async def get_entity_id_batch( + self, entities: list[PermEntity] + ) -> dict[PermEntity, int]: + """批量获取 Entity 的 eneity_id + + Args: + entities: PermEntity 列表 + + Returns: + 字典,键为 PermEntity,值为对应的 ID + """ + + for entity in entities: + await self.conn.execute( + s("create_entity.sql"), + (entity.platform, entity.entity_type, entity.external_id), + ) + await self.conn.commit() + val_placeholders = ", ".join(["(?, ?, ?)"] * len(entities)) + params = [] + for e in entities: + params.extend([e.platform, e.entity_type, e.external_id]) + cursor = await self.conn.execute( + f""" + SELECT id, platform, entity_type, external_id + FROM perm_entity + WHERE (platform, entity_type, external_id) IN (VALUES {val_placeholders}); + """, + params, + ) + rows = await cursor.fetchall() + return {PermEntity(row[1], row[2], row[3]): row[0] for row in rows} + + async def get_perm_info_batch( + self, entities: list[PermEntity], config_keys: list[str] + ) -> dict[tuple[PermEntity, str], bool]: + """批量获取权限信息 + + Args: + entities: PermEntity 列表 + config_keys: 查询的键列表 + + Returns: + 字典,键是 PermEntity 和 config_key 的元组,值是布尔,过滤掉所有空值 + """ + entity_ids = { + v: k for k, v in (await self.get_entity_id_batch(entities)).items() + } + placeholders1 = ", ".join("?" * len(entity_ids)) + placeholders2 = ", ".join("?" * len(config_keys)) + sql = f""" + SELECT entity_id, config_key, value + FROM perm_info + WHERE entity_id IN ({placeholders1}) + AND config_key IN ({placeholders2}) + AND value IS NOT NULL; + """ + + params = tuple(entity_ids.keys()) + tuple(config_keys) + cursor = await self.conn.execute(sql, params) + rows = await cursor.fetchall() + + return {(entity_ids[row[0]], row[1]): row[2] for row in rows} diff --git a/konabot/common/permsys/sql/create_entity.sql b/konabot/common/permsys/sql/create_entity.sql new file mode 100644 index 0000000..0c803ed --- /dev/null +++ b/konabot/common/permsys/sql/create_entity.sql @@ -0,0 +1,11 @@ +INSERT +OR IGNORE INTO perm_entity( + platform, + entity_type, + external_id +) +VALUES( + ?, + ?, + ? +); diff --git a/konabot/common/permsys/sql/get_entity_id.sql b/konabot/common/permsys/sql/get_entity_id.sql new file mode 100644 index 0000000..b497aa3 --- /dev/null +++ b/konabot/common/permsys/sql/get_entity_id.sql @@ -0,0 +1,8 @@ +SELECT + id +FROM + perm_entity +WHERE + perm_entity.platform = ? + AND perm_entity.entity_type = ? + AND perm_entity.external_id = ?; diff --git a/konabot/common/permsys/sql/get_perm_info.sql b/konabot/common/permsys/sql/get_perm_info.sql new file mode 100644 index 0000000..229c5ed --- /dev/null +++ b/konabot/common/permsys/sql/get_perm_info.sql @@ -0,0 +1,7 @@ +SELECT + VALUE +FROM + perm_info +WHERE + entity_id = ? + AND config_key = ?; diff --git a/konabot/common/permsys/sql/update_perm_info.sql b/konabot/common/permsys/sql/update_perm_info.sql new file mode 100644 index 0000000..1b23621 --- /dev/null +++ b/konabot/common/permsys/sql/update_perm_info.sql @@ -0,0 +1,4 @@ +INSERT INTO perm_info (entity_id, config_key, value) +VALUES (?, ?, ?) +ON CONFLICT(entity_id, config_key) +DO UPDATE SET value=excluded.value;