From f06895139e5e2f031a4c99b42dcf086e3543400a Mon Sep 17 00:00:00 2001 From: ssttkkl Date: Fri, 17 Nov 2023 00:36:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E6=8E=A5=E5=8F=A3=E4=B8=8E=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=88=86=E7=A6=BB=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add context.py * 重构 * 重构 * 重构 * 重构 * 重构 * 版本号v1.1.0 --- README.MD | 194 +----------- poetry.lock | 132 +++++---- pyproject.toml | 7 +- src/nonebot_plugin_ac_demo/__init__.py | 4 + src/nonebot_plugin_ac_demo/event_demo.py | 9 +- src/nonebot_plugin_ac_demo/plugin_service.py | 6 +- src/nonebot_plugin_access_control/__init__.py | 9 +- src/nonebot_plugin_access_control/errors.py | 27 -- .../event_bus.py | 74 ----- .../handler/__init__.py | 6 +- .../handler/limit_handler.py | 13 +- .../handler/permission_handler.py | 13 +- .../handler/service_handler.py | 5 +- .../handler/subject_handler.py | 5 +- .../handler/utils/permission.py | 5 +- .../models/__init__.py | 4 - src/nonebot_plugin_access_control/patcher.py | 3 +- .../{service/impl => repository}/__init__.py | 0 .../repository/orm/__init__.py | 0 .../{models => repository/orm}/permission.py | 0 .../{models => repository/orm}/rate_limit.py | 0 .../repository/permission/__init__.py | 4 + .../repository/permission/impl.py | 78 +++++ .../repository/permission/interface.py | 23 ++ .../repository/rate_limit/__init__.py | 4 + .../repository/rate_limit/impl.py | 105 +++++++ .../repository/rate_limit/interface.py | 29 ++ .../repository/rate_limit_token/__init__.py | 20 ++ .../rate_limit_token}/datastore.py | 40 ++- .../rate_limit_token}/inmemory.py | 39 +-- .../rate_limit_token}/interface.py | 10 +- .../{utils/session.py => repository/utils.py} | 0 .../service/__init__.py | 8 +- .../service/_impl/__init__.py | 1 + .../service/_impl/factory.py | 37 +++ .../service/_impl/patcher.py | 93 ++++++ .../service/{impl => _impl}/permission.py | 113 +++---- .../__init__.py => _impl/rate_limit.py} | 181 +++-------- .../service/base.py | 280 ------------------ .../service/impl/rate_limit/token/__init__.py | 22 -- .../service/interface/__init__.py | 3 - .../service/interface/base.py | 73 ----- .../service/interface/permission.py | 57 ---- .../service/interface/rate_limit.py | 95 ------ .../service/interface/service.py | 22 -- .../service/interface/subservice_owner.py | 10 - .../service/methods.py | 41 --- .../service/nonebot.py | 80 ----- .../service/permission/__init__.py | 3 - .../service/permission/permission.py | 10 - .../service/plugin.py | 34 --- .../service/rate_limit/__init__.py | 4 - .../service/rate_limit/rule.py | 14 - .../service/rate_limit/token.py | 10 - .../service/subservice.py | 28 -- .../service/subservice_owner.py | 53 ---- .../subject/__init__.py | 2 +- .../subject/extractor/__init__.py | 34 +-- .../subject/extractor/base.py | 40 --- .../subject/extractor/builtin/__init__.py | 0 .../subject/extractor/builtin/kaiheila.py | 4 +- .../subject/extractor/builtin/onebot_v11.py | 4 +- .../subject/extractor/builtin/qqguild.py | 4 +- .../subject/extractor/builtin/session.py | 6 +- .../subject/manager.py | 54 ---- .../subject/model.py | 13 - .../utils/call_with_params.py | 11 - .../utils/superuser.py | 10 - 68 files changed, 679 insertions(+), 1643 deletions(-) delete mode 100644 src/nonebot_plugin_access_control/errors.py delete mode 100644 src/nonebot_plugin_access_control/event_bus.py delete mode 100644 src/nonebot_plugin_access_control/models/__init__.py rename src/nonebot_plugin_access_control/{service/impl => repository}/__init__.py (100%) create mode 100644 src/nonebot_plugin_access_control/repository/orm/__init__.py rename src/nonebot_plugin_access_control/{models => repository/orm}/permission.py (100%) rename src/nonebot_plugin_access_control/{models => repository/orm}/rate_limit.py (100%) create mode 100644 src/nonebot_plugin_access_control/repository/permission/__init__.py create mode 100644 src/nonebot_plugin_access_control/repository/permission/impl.py create mode 100644 src/nonebot_plugin_access_control/repository/permission/interface.py create mode 100644 src/nonebot_plugin_access_control/repository/rate_limit/__init__.py create mode 100644 src/nonebot_plugin_access_control/repository/rate_limit/impl.py create mode 100644 src/nonebot_plugin_access_control/repository/rate_limit/interface.py create mode 100644 src/nonebot_plugin_access_control/repository/rate_limit_token/__init__.py rename src/nonebot_plugin_access_control/{service/impl/rate_limit/token => repository/rate_limit_token}/datastore.py (79%) rename src/nonebot_plugin_access_control/{service/impl/rate_limit/token => repository/rate_limit_token}/inmemory.py (75%) rename src/nonebot_plugin_access_control/{service/impl/rate_limit/token => repository/rate_limit_token}/interface.py (65%) rename src/nonebot_plugin_access_control/{utils/session.py => repository/utils.py} (100%) create mode 100644 src/nonebot_plugin_access_control/service/_impl/__init__.py create mode 100644 src/nonebot_plugin_access_control/service/_impl/factory.py create mode 100644 src/nonebot_plugin_access_control/service/_impl/patcher.py rename src/nonebot_plugin_access_control/service/{impl => _impl}/permission.py (55%) rename src/nonebot_plugin_access_control/service/{impl/rate_limit/__init__.py => _impl/rate_limit.py} (53%) delete mode 100644 src/nonebot_plugin_access_control/service/base.py delete mode 100644 src/nonebot_plugin_access_control/service/impl/rate_limit/token/__init__.py delete mode 100644 src/nonebot_plugin_access_control/service/interface/__init__.py delete mode 100644 src/nonebot_plugin_access_control/service/interface/base.py delete mode 100644 src/nonebot_plugin_access_control/service/interface/permission.py delete mode 100644 src/nonebot_plugin_access_control/service/interface/rate_limit.py delete mode 100644 src/nonebot_plugin_access_control/service/interface/service.py delete mode 100644 src/nonebot_plugin_access_control/service/interface/subservice_owner.py delete mode 100644 src/nonebot_plugin_access_control/service/methods.py delete mode 100644 src/nonebot_plugin_access_control/service/nonebot.py delete mode 100644 src/nonebot_plugin_access_control/service/permission/__init__.py delete mode 100644 src/nonebot_plugin_access_control/service/permission/permission.py delete mode 100644 src/nonebot_plugin_access_control/service/plugin.py delete mode 100644 src/nonebot_plugin_access_control/service/rate_limit/__init__.py delete mode 100644 src/nonebot_plugin_access_control/service/rate_limit/rule.py delete mode 100644 src/nonebot_plugin_access_control/service/rate_limit/token.py delete mode 100644 src/nonebot_plugin_access_control/service/subservice.py delete mode 100644 src/nonebot_plugin_access_control/service/subservice_owner.py delete mode 100644 src/nonebot_plugin_access_control/subject/extractor/base.py create mode 100644 src/nonebot_plugin_access_control/subject/extractor/builtin/__init__.py delete mode 100644 src/nonebot_plugin_access_control/subject/manager.py delete mode 100644 src/nonebot_plugin_access_control/subject/model.py delete mode 100644 src/nonebot_plugin_access_control/utils/call_with_params.py delete mode 100644 src/nonebot_plugin_access_control/utils/superuser.py diff --git a/README.MD b/README.MD index 2ecf921..0fa458a 100644 --- a/README.MD +++ b/README.MD @@ -233,199 +233,7 @@ nonebot.load_builtin_plugins("echo") ## 插件适配 -完整代码:[src/nonebot_plugin_ac_demo/matcher_demo.py](src/nonebot_plugin_ac_demo/matcher_demo.py) - -1. 创建一个名为nonebot_plugin_ac_demo的插件 - -2. 通过create_plugin_service函数创建一个PluginService实例(注意参数必须为插件包名) - -```python -from nonebot import require - -require("nonebot_plugin_access_control") - -from nonebot_plugin_access_control.service import create_plugin_service - -plugin_service = create_plugin_service("nonebot_plugin_ac_demo") -``` - -3. 通过PluginService.create_subservice创建SubService实例。调用`Service.patch_matcher()` - 应用至Matcher,或在事件处理函数上应用装饰器`Service.patch_handle()`。(二选一) - -```python -group1 = plugin_service.create_subservice("group1") - -a_matcher = on_command('a') -a_service = group1.create_subservice('a') -a_service.patch_matcher(a_matcher) - - -@a_matcher.handle() -async def _(matcher: Matcher): - await matcher.send("a") - - -b_matcher = on_command('b') -b_service = group1.create_subservice('b') -b_service.patch_matcher(b_matcher) - - -@b_matcher.handle() -async def _(matcher: Matcher): - await matcher.send("b") - - -c_matcher = on_command('c') -c_service = plugin_service.create_subservice('c') - - -@c_matcher.handle() -@c_service.patch_handler() # 必须在 @c_matcher.handle() 之下 -async def _(matcher: Matcher): - await matcher.send("c") - -``` - -插件服务的结构如下所示: - -![](docs/img/2.svg) - -4. 通过指令配置服务权限 - -执行下面的指令后,所有用户将无法调用指令`/a`与`/b` - -``` -/ac permission deny --sbj all --srv nonebot_plugin_ac_demo.group1 -``` - -执行下面的指令后,QQ用户12345678将无法调用指令`/a` - -``` -/ac permission deny --sbj qq:12345678 --srv nonebot_plugin_ac_demo.group1.a -``` - -执行下面的指令后,QQ群组87654321的所有用户将无法调用除`/c`以外的任何指令 - -``` -/ac permission deny --sbj qq:g87654321 --srv nonebot_plugin_ac_demo -/ac permission allow --sbj qq:g87654321 --srv nonebot_plugin_ac_demo.c -``` - -5. 手动鉴权 - -对于非Matcher的功能入口(如APScheduler的定时任务等),需要开发者手动进行鉴权。 - -- 方法一:调用`service.check(Bot, Event)`方法,传入Bot及Event实例,返回bool值表示该用户是否具有权限 -- 方法二:调用`service.check_by_subject(*str)`方法,传入主体字符串,返回bool值表示该用户是否具有权限 - -APScheduler示例:[src/nonebot_plugin_ac_demo/apscheduler_demo.py](src/nonebot_plugin_ac_demo/apscheduler_demo.py) - -对于一些功能,如果希望在执行失败时不消耗限流次数,则需要开发者手动进行鉴权与限流。 - -```python -d_matcher = on_command('d') -d_service = plugin_service.create_subservice('d') - - -@d_matcher.handle() -async def _(bot: Bot, event: Event, matcher: Matcher): - if not await d_service.check(bot, event, acquire_rate_limit_token=False): - # 没有权限 - await matcher.finish() - - token = await d_service.acquire_token_for_rate_limit(bot, event) - if token is None: - # 已达到限流次数上线 - await matcher.finish() - - ok = do_your_logic() - - if not ok: - # 功能执行失败时,收回限流消耗的次数 - await token.retire() - - await matcher.send("c") -``` - -6. 事件订阅 - -通过`service.on_set_permission`、`service.on_remove_permission`、`service.on_change_permission`方法可以订阅事件,具体如下表: - -| 装饰器 | 事件类型 | 事件接收函数的参数 | -|-------------------------------------|---------------------------------------|-----------------------------| -| `service.on_set_permission` | 服务设置主体权限 | service:服务
permission:权限 | -| `service.on_remove_permission` | 服务删除主体权限 | service:服务
subject:主体 | -| `service.on_change_permission` | 服务变更主体权限(包括该服务及其所有祖先服务设置、删除权限导致的权限变更) | service:服务
permission:权限 | -| `service.on_add_rate_limit_rule` | 服务添加限流规则(该服务及其所有祖先服务添加限流规则时都会触发) | service:服务
rule:限流规则 | -| `service.on_remove_rate_limit_rule` | 服务删除限流规则(该服务及其所有祖先服务删除限流规则时都会触发) | service:服务
rule:限流规则 | - -**调用事件接收函数时通过具名参数传参,因此事件接收函数的参数必须严格遵循参数名。** - -事件订阅示例:[src/nonebot_plugin_ac_demo/event_demo.py](src/nonebot_plugin_ac_demo/event_demo.py) - -## 拓展定义新的主体 - -主体提取器(Subject Extractor)用于提取事件发出用户所具有的主体。开发者可以在自己的插件中注册自定义的主体提取器,从而实现拓展定义新的主体。 - -主体提取器为一个函数(或可调用对象),其应该具有如下的函数签名,并通过`add_subject_extractor`装饰器注入到插件中: - -```python -@add_subject_extractor -def your_custom_subject_extractor(bot: Bot, event: Event, manager: SubjectManager): - ... -``` - -传入参数`bot`与`event`其义自明,而`manager`参数提供方法用于添加主体。 - -主体的模型定义如下: - -```python -class SubjectModel(NamedTuple): - content: str - offer_by: str - tag: Optional[str] -``` - -其中content为主体字符串,offer_by用于标识提取出该主体的主体提取器(通常为插件名),tag用于标识主体的种类。通常我们约定,拥有同一个tag的主体包含的信息量完全一致。 - -在主体提取流程,插件会**按顺序依次**调用已注册的主体提取器,请注意维护主体提取器注册的顺序。(tips:如果你的主体提取器依赖其他插件中实现的主体提取器,可以依赖`nonebot` -提供的`require`机制控制主体提取器注册的顺序) - -例如,下面这个主体提取器用于为OneBot V11协议中群管理/群主提取相应的主体: - -```python -OFFER_BY = "nonebot_plugin_access_control" - - -def extract_onebot_v11_group_role(bot: Bot, event: Event, manager: SubjectManager): - if bot.type != "OneBot V11": - return - - group_id = getattr(event, "group_id", None) - sender: Optional["Sender"] = getattr(event, "sender", None) - - if group_id is not None and sender is not None: - li = [] - - if sender.role == "owner": - li.append( - SubjectModel( - f"qq:g{group_id}.group_owner", OFFER_BY, "qq:group.group_owner" - ) - ) - li.append(SubjectModel("qq:group_owner", OFFER_BY, "qq:group_owner")) - - if sender.role == "owner" or sender.role == "admin": - li.append( - SubjectModel( - f"qq:g{group_id}.group_admin", OFFER_BY, "qq:group.group_admin" - ) - ) - li.append(SubjectModel("qq:group_admin", OFFER_BY, "qq:group_admin")) - - # 添加在platform:group之前 - manager.insert_before("platform:group", *li) -``` +参考 [https://github.com/ssttkkl/nonebot-plugin-access-control-api/blob/v1.1.0/README.MD] ## CLI支持 diff --git a/poetry.lock b/poetry.lock index 6a8a3ee..91082df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -89,14 +89,14 @@ zookeeper = ["kazoo"] [[package]] name = "arclet-alconna" -version = "1.7.33" +version = "1.7.34" description = "A High-performance, Generality, Humane Command Line Arguments Parser Library." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "arclet_alconna-1.7.33-py3-none-any.whl", hash = "sha256:9f540a0885354bca207099ab4b3daabc67ca312c6cc54db8c452815e9b0193a7"}, - {file = "arclet_alconna-1.7.33.tar.gz", hash = "sha256:f4dc5a82f66caa9579093a5bed1d22f2c53015300f43246f099af4120c92dfbc"}, + {file = "arclet_alconna-1.7.34-py3-none-any.whl", hash = "sha256:2c9999fc1ae6c690e4ff79c8ba1134eb782b61bd9c5fe7b7a4a3b92ec4be2f36"}, + {file = "arclet_alconna-1.7.34.tar.gz", hash = "sha256:ff409c34fdf934cbb10e8ff9ab9fad0ff05530cccab697050ecada091b2bdf93"}, ] [package.dependencies] @@ -159,30 +159,30 @@ chardet = ">=3.0.2" [[package]] name = "black" -version = "23.10.1" +version = "23.11.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, - {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, - {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, - {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, - {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, - {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, - {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, - {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, - {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, - {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, - {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, - {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, ] [package.dependencies] @@ -594,14 +594,14 @@ files = [ [[package]] name = "httpcore" -version = "1.0.1" +version = "1.0.2" description = "A minimal low-level HTTP client." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.1-py3-none-any.whl", hash = "sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0"}, - {file = "httpcore-1.0.1.tar.gz", hash = "sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92"}, + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, ] [package.dependencies] @@ -750,14 +750,14 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs [[package]] name = "importlib-resources" -version = "6.1.0" +version = "6.1.1" description = "Read resources from Python packages" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.1.0-py3-none-any.whl", hash = "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"}, - {file = "importlib_resources-6.1.0.tar.gz", hash = "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9"}, + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, ] [package.dependencies] @@ -845,14 +845,14 @@ dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptio [[package]] name = "mako" -version = "1.2.4" +version = "1.3.0" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, - {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, + {file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"}, + {file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"}, ] [package.dependencies] @@ -1302,6 +1302,23 @@ files = [ anyio = ">=3.6.2,<4.0.0" nonebot2 = ">=2.0.0b1,<3.0.0" +[[package]] +name = "nonebot-plugin-access-control-api" +version = "1.1.0" +description = "" +category = "main" +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "nonebot_plugin_access_control_api-1.1.0-py3-none-any.whl", hash = "sha256:d5f2a0ae600a750a2eef5b2a59558146d4a8cb0485ba726226fcfe1c406d2f53"}, + {file = "nonebot_plugin_access_control_api-1.1.0.tar.gz", hash = "sha256:40f542ab8db4e70bcfc9dba977232d8b7f689016a6eb064a0c68fe6ac7e8e405"}, +] + +[package.dependencies] +nonebot-plugin-session = ">=0.2.0,<0.3.0" +nonebot2 = ">=2.1.0,<3.0.0" +ssttkkl-nonebot-utils = ">=0.1.17" + [[package]] name = "nonebot-plugin-apscheduler" version = "0.3.0" @@ -1360,14 +1377,14 @@ typing-extensions = ">=4.0.0" [[package]] name = "nonebot-plugin-orm" -version = "0.3.0" +version = "0.5.1" description = "SQLAlchemy ORM support for nonebot" category = "main" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "nonebot_plugin_orm-0.3.0-py3-none-any.whl", hash = "sha256:1778cbc0080bbbd5b7201b946ae9b6037f55402c6f4f806d4aed559b369becf5"}, - {file = "nonebot_plugin_orm-0.3.0.tar.gz", hash = "sha256:27dbbfa72d8a27fea9253c533ad3e5cc5d50a67698605900acafcc54c7e4c1fd"}, + {file = "nonebot_plugin_orm-0.5.1-py3-none-any.whl", hash = "sha256:5f1f74aed93a56ea3743b40b9a6b4ffb1d53e6359928f51ef7ffad1ae3d83181"}, + {file = "nonebot_plugin_orm-0.5.1.tar.gz", hash = "sha256:7e3f84fcc932f28f4c235bf2f1bd26da11988f4245bc499dfb781d379816b2d9"}, ] [package.dependencies] @@ -1376,7 +1393,7 @@ alembic = ">=1.12,<2.0" click = ">=8.1,<9.0" importlib-metadata = {version = ">=6.8,<7.0", markers = "python_version < \"3.10\""} importlib-resources = {version = ">=6.1,<7.0", markers = "python_version < \"3.12\""} -nonebot-plugin-localstore = {version = ">=0.5,<1.0", optional = true, markers = "extra == \"default\""} +nonebot-plugin-localstore = ">=0.5,<1.0" nonebot2 = ">=2.1,<3.0" sqlalchemy = ">=2.0,<3.0" typing-extensions = {version = ">=4.8,<5.0", markers = "python_version < \"3.12\""} @@ -1386,7 +1403,7 @@ aiomysql = ["aiomysql (>=0.2,<1.0)"] aiosqlite = ["aiosqlite (>=0.19,<1.0)"] asyncmy = ["asyncmy (>=0.2,<1.0)"] asyncpg = ["asyncpg (>=0.28,<1.0)"] -default = ["aiosqlite (>=0.19,<1.0)", "nonebot-plugin-localstore (>=0.5,<1.0)"] +default = ["aiosqlite (>=0.19,<1.0)"] mysql = ["aiomysql (>=0.2,<1.0)"] postgresql = ["psycopg[binary] (>=3.1,<4.0)"] psycopg = ["psycopg[binary] (>=3.1,<4.0)"] @@ -1548,14 +1565,14 @@ virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" -version = "3.0.39" +version = "3.0.41" description = "Library for building powerful interactive command lines in Python" category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, + {file = "prompt_toolkit-3.0.41-py3-none-any.whl", hash = "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"}, + {file = "prompt_toolkit-3.0.41.tar.gz", hash = "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0"}, ] [package.dependencies] @@ -1842,14 +1859,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.6.0" +version = "13.7.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, - {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, ] [package.dependencies] @@ -2181,14 +2198,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.2" +version = "0.12.3" description = "Style preserving TOML library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.2-py3-none-any.whl", hash = "sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"}, - {file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] [[package]] @@ -2262,32 +2279,31 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "urllib3" -version = "2.0.7" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.23.2" +version = "0.24.0.post1" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, - {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, + {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, + {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, ] [package.dependencies] @@ -2461,14 +2477,14 @@ anyio = ">=3.0.0" [[package]] name = "wcwidth" -version = "0.2.9" +version = "0.2.10" description = "Measures the displayed width of unicode strings in a terminal" category = "main" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.9-py2.py3-none-any.whl", hash = "sha256:9a929bd8380f6cd9571a968a9c8f4353ca58d7cd812a4822bba831f8d685b223"}, - {file = "wcwidth-0.2.9.tar.gz", hash = "sha256:a675d1a4a2d24ef67096a04b85b02deeecd8e226f57b5e3a72dbb9ed99d27da8"}, + {file = "wcwidth-0.2.10-py2.py3-none-any.whl", hash = "sha256:aec5179002dd0f0d40c456026e74a729661c9d468e1ed64405e3a6c2176ca36f"}, + {file = "wcwidth-0.2.10.tar.gz", hash = "sha256:390c7454101092a6a5e43baad8f83de615463af459201709556b6e4b1c861f97"}, ] [[package]] @@ -2679,4 +2695,4 @@ migrate = ["nonebot-plugin-datastore"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "f5ed34d1c031e6424625d7faff6b7160e6f1e98325cbea9f06194cefad10587f" +content-hash = "aa2290d86a27d0578c209bd68abd640d0a4a6d0a4679259cbe8f5ae3a31ffcb1" diff --git a/pyproject.toml b/pyproject.toml index 6c1840d..c9c94c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot-plugin-access-control" -version = "1.0.2" +version = "1.1.0" description = "" authors = ["ssttkkl "] license = "MIT" @@ -13,9 +13,10 @@ packages = [ [tool.poetry.dependencies] python = "^3.9" nonebot2 = "^2.1.0" +nonebot-plugin-access-control-api = "^1.1.0" nonebot-plugin-apscheduler = ">=0.3.0" nonebot-plugin-session = "^0.2.0" -nonebot-plugin-orm = "^0.3.0" +nonebot-plugin-orm = "^0.5.0" arclet-alconna = "^1.7.24" shortuuid = "^1.0.11" pytimeparser = "^0.2.0" @@ -33,7 +34,7 @@ pre-commit = "^3.1.0" setuptools = "^68.1.2" nb-cli = "^1.2.5" -nonebot-plugin-orm = {extras = ["default"], version = "^0.3.0"} +nonebot-plugin-orm = {extras = ["default"], version = "^0.5.0"} nonebot-adapter-onebot = "*" nonebot-adapter-kaiheila = "*" diff --git a/src/nonebot_plugin_ac_demo/__init__.py b/src/nonebot_plugin_ac_demo/__init__.py index f661392..31ac1b0 100644 --- a/src/nonebot_plugin_ac_demo/__init__.py +++ b/src/nonebot_plugin_ac_demo/__init__.py @@ -1,3 +1,7 @@ +from nonebot import require + +require("nonebot_plugin_access_control_api") + from . import event_demo, matcher_demo, apscheduler_demo __all__ = ("apscheduler_demo", "event_demo", "matcher_demo") diff --git a/src/nonebot_plugin_ac_demo/event_demo.py b/src/nonebot_plugin_ac_demo/event_demo.py index 1aeef99..76edc62 100644 --- a/src/nonebot_plugin_ac_demo/event_demo.py +++ b/src/nonebot_plugin_ac_demo/event_demo.py @@ -1,11 +1,10 @@ from nonebot import logger +from nonebot_plugin_access_control_api.models.permission import Permission +from nonebot_plugin_access_control_api.models.rate_limit import RateLimitRule +from nonebot_plugin_access_control_api.service import Service -from nonebot_plugin_access_control.service import Service -from nonebot_plugin_access_control.service.permission import Permission -from nonebot_plugin_access_control.service.rate_limit import RateLimitRule - -from .plugin_service import plugin_service from .matcher_demo import a_service, b_service, c_service +from .plugin_service import plugin_service @plugin_service.on_set_permission diff --git a/src/nonebot_plugin_ac_demo/plugin_service.py b/src/nonebot_plugin_ac_demo/plugin_service.py index 5964ff0..da5b8e3 100644 --- a/src/nonebot_plugin_ac_demo/plugin_service.py +++ b/src/nonebot_plugin_ac_demo/plugin_service.py @@ -1,7 +1,3 @@ -from nonebot import require - -require("nonebot_plugin_access_control") - -from nonebot_plugin_access_control.service import create_plugin_service +from nonebot_plugin_access_control_api.service import create_plugin_service plugin_service = create_plugin_service("nonebot_plugin_ac_demo") diff --git a/src/nonebot_plugin_access_control/__init__.py b/src/nonebot_plugin_access_control/__init__.py index 21417a8..2afdf5f 100644 --- a/src/nonebot_plugin_access_control/__init__.py +++ b/src/nonebot_plugin_access_control/__init__.py @@ -3,14 +3,14 @@ @Author : ssttkkl @License : MIT -@GitHub : https://github.com/ssttkkl/nonebot-access-control +@GitHub : https://github.com/bot-ssttkkl/nonebot-plugin-access-control """ from nonebot import require +require("nonebot_plugin_access_control_api") require("nonebot_plugin_apscheduler") -require("nonebot_plugin_orm") require("nonebot_plugin_session") -require("ssttkkl_nonebot_utils") +require("nonebot_plugin_orm") from nonebot.plugin import PluginMetadata, inherit_supported_adapters @@ -23,12 +23,13 @@ description="对功能进行权限控制以及调用次数限制", usage=help_ac(), type="application", - homepage="https://github.com/bot-ssttkkl/nonebot-access-control", + homepage="https://github.com/bot-ssttkkl/nonebot-plugin-access-control", config=Config, supported_adapters=inherit_supported_adapters("nonebot_plugin_session"), extra={"orm_version_location": orm_migrations}, ) +from . import service # noqa from . import matcher # noqa from . import patcher # noqa from . import datastore # noqa diff --git a/src/nonebot_plugin_access_control/errors.py b/src/nonebot_plugin_access_control/errors.py deleted file mode 100644 index ecac508..0000000 --- a/src/nonebot_plugin_access_control/errors.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import TYPE_CHECKING - -from ssttkkl_nonebot_utils.errors.errors import QueryError, BadRequestError - -if TYPE_CHECKING: - from .service.interface.rate_limit import AcquireTokenResult - - -class AccessControlError(RuntimeError): - ... - - -class PermissionDeniedError(AccessControlError): - ... - - -class RateLimitedError(AccessControlError): - def __init__(self, result: "AcquireTokenResult"): - self.result = result - - -class AccessControlBadRequestError(BadRequestError, AccessControlError): - ... - - -class AccessControlQueryError(QueryError, AccessControlError): - ... diff --git a/src/nonebot_plugin_access_control/event_bus.py b/src/nonebot_plugin_access_control/event_bus.py deleted file mode 100644 index 7dd67e4..0000000 --- a/src/nonebot_plugin_access_control/event_bus.py +++ /dev/null @@ -1,74 +0,0 @@ -from enum import Enum -from asyncio import gather -from inspect import isawaitable -from collections import defaultdict -from collections.abc import Awaitable -from typing import Any, TypeVar, Callable, Optional - -from nonebot import logger - -from nonebot_plugin_access_control.utils.call_with_params import call_with_params - - -class EventType(str, Enum): - service_set_permission = "service_set_permission" - """ - 当某个服务设置权限成功时触发 - """ - - service_remove_permission = "service_remove_permission" - """ - 当某个服务删除权限成功时触发 - """ - - service_change_permission = "service_change_permission" - """ - 当某个服务权限变更时触发(包括该服务及其所有祖先服务设置、删除权限导致的权限变更) - """ - - service_add_rate_limit_rule = "service_add_rate_limit_rule" - """ - 当某个服务添加限流规则时触发(该服务及其所有祖先服务添加限流规则时都会触发) - """ - service_remove_rate_limit_rule = "service_remove_rate_limit_rule" - """ - 当某个服务删除限流规则时触发(该服务及其所有祖先服务删除限流规则时都会触发) - """ - - -T = TypeVar("T") -T_Kwargs = dict[str, Any] -T_Filter = Callable[..., bool] -T_Listener = Callable[..., Awaitable[None]] - -_listeners: dict[EventType, list[tuple[T_Filter, T_Listener]]] = defaultdict(list) - - -async def fire_event(event_type: EventType, kwargs: T_Kwargs): - logger.trace( - f"on event {event_type} " - f"(kwargs: {', '.join(f'{k}={kwargs[k]}' for k in kwargs)})" - ) - - coros = [] - - for filter_func, func in _listeners[event_type]: - if call_with_params(filter_func, kwargs): - coro = call_with_params(func, kwargs) - if isawaitable(coro): - coros.append(coro) - - await gather(*coros) - - -def on_event( - event_type: EventType, filter_func: T_Filter, func: Optional[T_Listener] = None -): - def decorator(func): - _listeners[event_type].append((filter_func, func)) - return func - - if func is None: - return decorator - else: - return decorator(func) diff --git a/src/nonebot_plugin_access_control/handler/__init__.py b/src/nonebot_plugin_access_control/handler/__init__.py index 3985347..6c8e7c9 100644 --- a/src/nonebot_plugin_access_control/handler/__init__.py +++ b/src/nonebot_plugin_access_control/handler/__init__.py @@ -6,9 +6,13 @@ from arclet.alconna.typing import DataCollection from ssttkkl_nonebot_utils.errors.error_handler import ErrorHandlers +from nonebot_plugin_access_control_api.errors import ( + PermissionDeniedError, + AccessControlBadRequestError, +) + from ..alc import alc_ac from ..config import conf -from ..errors import PermissionDeniedError, AccessControlBadRequestError from . import ( help_handler, limit_handler, diff --git a/src/nonebot_plugin_access_control/handler/limit_handler.py b/src/nonebot_plugin_access_control/handler/limit_handler.py index 0b7ed1b..ee24ced 100644 --- a/src/nonebot_plugin_access_control/handler/limit_handler.py +++ b/src/nonebot_plugin_access_control/handler/limit_handler.py @@ -2,10 +2,17 @@ import pytimeparser -from ..service.rate_limit import RateLimitRule +from nonebot_plugin_access_control_api.models.rate_limit import RateLimitRule +from nonebot_plugin_access_control_api.service import Service +from nonebot_plugin_access_control_api.service.methods import ( + get_service_by_qualified_name, +) +from nonebot_plugin_access_control_api.errors import ( + AccessControlQueryError, + AccessControlBadRequestError, +) + from .utils.permission import require_superuser_or_script -from ..service import Service, get_service_by_qualified_name -from ..errors import AccessControlQueryError, AccessControlBadRequestError def _map_rule(f: TextIO, rule: RateLimitRule, service_name: Optional[str]): diff --git a/src/nonebot_plugin_access_control/handler/permission_handler.py b/src/nonebot_plugin_access_control/handler/permission_handler.py index 1789aac..82409e3 100644 --- a/src/nonebot_plugin_access_control/handler/permission_handler.py +++ b/src/nonebot_plugin_access_control/handler/permission_handler.py @@ -1,9 +1,16 @@ from typing import TextIO, Optional -from ..service.permission import Permission +from nonebot_plugin_access_control_api.errors import ( + AccessControlQueryError, + AccessControlBadRequestError, +) +from nonebot_plugin_access_control_api.models.permission import Permission +from nonebot_plugin_access_control_api.service import ( + get_service_by_qualified_name, + Service, +) + from .utils.permission import require_superuser_or_script -from ..service import Service, get_service_by_qualified_name -from ..errors import AccessControlQueryError, AccessControlBadRequestError def _map_permission(p: Permission, query_service_name: Optional[str] = None) -> str: diff --git a/src/nonebot_plugin_access_control/handler/service_handler.py b/src/nonebot_plugin_access_control/handler/service_handler.py index 50434b1..9eba562 100644 --- a/src/nonebot_plugin_access_control/handler/service_handler.py +++ b/src/nonebot_plugin_access_control/handler/service_handler.py @@ -1,7 +1,10 @@ from typing import TextIO, Optional +from nonebot_plugin_access_control_api.service.methods import ( + get_service_by_qualified_name, +) + from ..utils.tree import get_tree_summary -from ..service import get_service_by_qualified_name from .utils.permission import require_superuser_or_script diff --git a/src/nonebot_plugin_access_control/handler/subject_handler.py b/src/nonebot_plugin_access_control/handler/subject_handler.py index ae14c67..44f96c6 100644 --- a/src/nonebot_plugin_access_control/handler/subject_handler.py +++ b/src/nonebot_plugin_access_control/handler/subject_handler.py @@ -2,9 +2,10 @@ from nonebot.internal.matcher import current_bot, current_event +from nonebot_plugin_access_control_api.subject import extract_subjects +from nonebot_plugin_access_control_api.errors import AccessControlBadRequestError + from .utils.env import ac_get_env -from ..subject import extract_subjects -from ..errors import AccessControlBadRequestError async def subject(f: TextIO): diff --git a/src/nonebot_plugin_access_control/handler/utils/permission.py b/src/nonebot_plugin_access_control/handler/utils/permission.py index b7ad80c..277af17 100644 --- a/src/nonebot_plugin_access_control/handler/utils/permission.py +++ b/src/nonebot_plugin_access_control/handler/utils/permission.py @@ -3,8 +3,9 @@ from nonebot.permission import SUPERUSER from nonebot.internal.matcher import current_bot, current_event -from nonebot_plugin_access_control.errors import PermissionDeniedError -from nonebot_plugin_access_control.handler.utils.env import ac_get_env +from nonebot_plugin_access_control_api.errors import PermissionDeniedError + +from .env import ac_get_env def require_superuser_or_script(f): diff --git a/src/nonebot_plugin_access_control/models/__init__.py b/src/nonebot_plugin_access_control/models/__init__.py deleted file mode 100644 index 57fe21b..0000000 --- a/src/nonebot_plugin_access_control/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .permission import PermissionOrm -from .rate_limit import RateLimitRuleOrm, RateLimitTokenOrm - -__all__ = ("PermissionOrm", "RateLimitRuleOrm", "RateLimitTokenOrm") diff --git a/src/nonebot_plugin_access_control/patcher.py b/src/nonebot_plugin_access_control/patcher.py index d71e9e5..0c86efe 100644 --- a/src/nonebot_plugin_access_control/patcher.py +++ b/src/nonebot_plugin_access_control/patcher.py @@ -1,7 +1,8 @@ from nonebot import logger, get_driver, get_loaded_plugins +from nonebot_plugin_access_control_api.service.methods import get_nonebot_service + from .config import conf -from .service import get_nonebot_service if conf().access_control_auto_patch_enabled: diff --git a/src/nonebot_plugin_access_control/service/impl/__init__.py b/src/nonebot_plugin_access_control/repository/__init__.py similarity index 100% rename from src/nonebot_plugin_access_control/service/impl/__init__.py rename to src/nonebot_plugin_access_control/repository/__init__.py diff --git a/src/nonebot_plugin_access_control/repository/orm/__init__.py b/src/nonebot_plugin_access_control/repository/orm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/nonebot_plugin_access_control/models/permission.py b/src/nonebot_plugin_access_control/repository/orm/permission.py similarity index 100% rename from src/nonebot_plugin_access_control/models/permission.py rename to src/nonebot_plugin_access_control/repository/orm/permission.py diff --git a/src/nonebot_plugin_access_control/models/rate_limit.py b/src/nonebot_plugin_access_control/repository/orm/rate_limit.py similarity index 100% rename from src/nonebot_plugin_access_control/models/rate_limit.py rename to src/nonebot_plugin_access_control/repository/orm/rate_limit.py diff --git a/src/nonebot_plugin_access_control/repository/permission/__init__.py b/src/nonebot_plugin_access_control/repository/permission/__init__.py new file mode 100644 index 0000000..4d32d11 --- /dev/null +++ b/src/nonebot_plugin_access_control/repository/permission/__init__.py @@ -0,0 +1,4 @@ +from . import impl # noqa +from .interface import IPermissionRepository + +__all__ = ("IPermissionRepository",) diff --git a/src/nonebot_plugin_access_control/repository/permission/impl.py b/src/nonebot_plugin_access_control/repository/permission/impl.py new file mode 100644 index 0000000..6ef7638 --- /dev/null +++ b/src/nonebot_plugin_access_control/repository/permission/impl.py @@ -0,0 +1,78 @@ +from typing import Optional +from collections.abc import AsyncGenerator + +from sqlalchemy import select + +from nonebot_plugin_access_control_api.context import context +from nonebot_plugin_access_control_api.service.interface import IService +from nonebot_plugin_access_control_api.models.permission import Permission +from nonebot_plugin_access_control_api.service.interface.nonebot_service import ( + INoneBotService, +) + +from ..utils import use_ac_session +from ..orm.permission import PermissionOrm +from .interface import IPermissionRepository + + +@context.bind_singleton_to(IPermissionRepository) +class PermissionRepository(IPermissionRepository): + async def get_permissions( + self, service: Optional[IService], subject: Optional[str] + ) -> AsyncGenerator[Permission, None]: + async with use_ac_session() as session: + stmt = select(PermissionOrm) + if service is not None: + stmt = stmt.where(PermissionOrm.service == service.qualified_name) + if subject is not None: + stmt = stmt.where(PermissionOrm.subject == subject) + + async for x in await session.stream_scalars(stmt): + s = service + if s is None: + s = context.require(INoneBotService).get_service_by_qualified_name( + x.service + ) + if s is not None: + yield Permission(s, x.subject, x.allow) + + async def set_permission( + self, service: Optional[IService], subject: str, allow: bool + ) -> bool: + async with use_ac_session() as sess: + stmt = select(PermissionOrm).where( + PermissionOrm.service == service.qualified_name, + PermissionOrm.subject == subject, + ) + p = (await sess.execute(stmt)).scalar_one_or_none() + if p is None: + p = PermissionOrm( + service=service.qualified_name, subject=subject, allow=allow + ) + sess.add(p) + old_allow = None + else: + old_allow = p.allow + p.allow = allow + + if old_allow != allow: + await sess.commit() + return True + else: + return False + + async def remove_permission( + self, service: Optional[IService], subject: str + ) -> bool: + async with use_ac_session() as sess: + stmt = select(PermissionOrm).where( + PermissionOrm.service == service.qualified_name, + PermissionOrm.subject == subject, + ) + p = (await sess.execute(stmt)).scalar_one_or_none() + if p is None: + return False + + await sess.delete(p) + await sess.commit() + return True diff --git a/src/nonebot_plugin_access_control/repository/permission/interface.py b/src/nonebot_plugin_access_control/repository/permission/interface.py new file mode 100644 index 0000000..ea5d84f --- /dev/null +++ b/src/nonebot_plugin_access_control/repository/permission/interface.py @@ -0,0 +1,23 @@ +from typing import Optional, Protocol +from collections.abc import AsyncGenerator + +from nonebot_plugin_access_control_api.service.interface import IService +from nonebot_plugin_access_control_api.models.permission import Permission + + +class IPermissionRepository(Protocol): + async def get_permissions( + self, service: Optional[IService], subject: Optional[str] + ) -> AsyncGenerator[Permission, None]: + raise NotImplementedError() + yield Permission() # noqa + + async def set_permission( + self, service: Optional[IService], subject: str, allow: bool + ) -> bool: + raise NotImplementedError() + + async def remove_permission( + self, service: Optional[IService], subject: str + ) -> bool: + raise NotImplementedError() diff --git a/src/nonebot_plugin_access_control/repository/rate_limit/__init__.py b/src/nonebot_plugin_access_control/repository/rate_limit/__init__.py new file mode 100644 index 0000000..3b32554 --- /dev/null +++ b/src/nonebot_plugin_access_control/repository/rate_limit/__init__.py @@ -0,0 +1,4 @@ +from . import impl # noqa +from .interface import IRateLimitRepository + +__all__ = ("IRateLimitRepository",) diff --git a/src/nonebot_plugin_access_control/repository/rate_limit/impl.py b/src/nonebot_plugin_access_control/repository/rate_limit/impl.py new file mode 100644 index 0000000..6956d9a --- /dev/null +++ b/src/nonebot_plugin_access_control/repository/rate_limit/impl.py @@ -0,0 +1,105 @@ +from typing import Optional +from datetime import timedelta +from collections.abc import AsyncGenerator + +from sqlalchemy import func, select + +from nonebot_plugin_access_control_api.context import context +from nonebot_plugin_access_control_api.service.interface import IService +from nonebot_plugin_access_control_api.errors import AccessControlQueryError +from nonebot_plugin_access_control_api.models.rate_limit import RateLimitRule +from nonebot_plugin_access_control_api.service.interface.nonebot_service import ( + INoneBotService, +) + +from ..utils import use_ac_session +from .interface import IRateLimitRepository +from ..orm.rate_limit import RateLimitRuleOrm + + +@context.bind_singleton_to(IRateLimitRepository) +class RateLimitRepository(IRateLimitRepository): + async def get_rules_by_subject( + self, service: Optional[IService], subject: Optional[str] + ) -> AsyncGenerator[RateLimitRuleOrm, None]: + async with use_ac_session() as session: + stmt = select(RateLimitRuleOrm) + if service is not None: + stmt = stmt.where(RateLimitRuleOrm.service == service.qualified_name) + if subject is not None: + stmt = stmt.where(RateLimitRuleOrm.subject == subject) + + async for x in await session.stream_scalars(stmt): + s = service + if s is None: + s = context.require(INoneBotService).get_service_by_qualified_name( + x.service + ) + if s is not None: + yield RateLimitRule( + x.id, + s, + x.subject, + timedelta(seconds=x.time_span), + x.limit, + x.overwrite, + ) + + async def add_rate_limit_rule( + self, + service: IService, + subject: str, + time_span: timedelta, + limit: int, + overwrite: bool = False, + ) -> RateLimitRule: + async with use_ac_session() as sess: + if overwrite: + stmt = select(func.count()).where( + RateLimitRuleOrm.subject == subject, + RateLimitRuleOrm.service == service.qualified_name, + ) + cnt = (await sess.execute(stmt)).scalar_one() + + if cnt > 0: + raise AccessControlQueryError("已存在对该实体与服务的限流规则,不允许再添加覆写规则") + + orm = RateLimitRuleOrm( + subject=subject, + service=service.qualified_name, + time_span=int(time_span.total_seconds()), + limit=limit, + overwrite=overwrite, + ) + sess.add(orm) + await sess.commit() + + await sess.refresh(orm) + + rule = RateLimitRule(orm.id, service, subject, time_span, limit, overwrite) + + return rule + + async def remove_rate_limit_rule(self, rule_id: str) -> Optional[RateLimitRule]: + async with use_ac_session() as sess: + orm = await sess.get(RateLimitRuleOrm, rule_id) + if orm is None: + return None + + await sess.delete(orm) + await sess.commit() + + service = context.require(INoneBotService).get_service_by_qualified_name( + orm.service + ) + + rule = RateLimitRule( + orm.id, + service, + orm.subject, + timedelta(seconds=orm.time_span), + orm.limit, + orm.overwrite, + ) + + return rule diff --git a/src/nonebot_plugin_access_control/repository/rate_limit/interface.py b/src/nonebot_plugin_access_control/repository/rate_limit/interface.py new file mode 100644 index 0000000..4e9a994 --- /dev/null +++ b/src/nonebot_plugin_access_control/repository/rate_limit/interface.py @@ -0,0 +1,29 @@ +from datetime import timedelta +from typing import Optional, Protocol +from collections.abc import AsyncGenerator + +from nonebot_plugin_access_control_api.service.interface import IService +from nonebot_plugin_access_control_api.models.rate_limit import RateLimitRule + +from ..orm.rate_limit import RateLimitRuleOrm + + +class IRateLimitRepository(Protocol): + async def get_rules_by_subject( + self, service: Optional[IService], subject: Optional[str] + ) -> AsyncGenerator[RateLimitRuleOrm, None]: + raise NotImplementedError() + yield RateLimitRuleOrm() # noqa + + async def add_rate_limit_rule( + self, + service: IService, + subject: str, + time_span: timedelta, + limit: int, + overwrite: bool = False, + ) -> RateLimitRule: + raise NotImplementedError() + + async def remove_rate_limit_rule(self, rule_id: str) -> Optional[RateLimitRule]: + raise NotImplementedError() diff --git a/src/nonebot_plugin_access_control/repository/rate_limit_token/__init__.py b/src/nonebot_plugin_access_control/repository/rate_limit_token/__init__.py new file mode 100644 index 0000000..e8b16a6 --- /dev/null +++ b/src/nonebot_plugin_access_control/repository/rate_limit_token/__init__.py @@ -0,0 +1,20 @@ +from nonebot import logger + +from ...config import conf +from .interface import IRateLimitTokenRepository + +if conf().access_control_rate_limit_token_storage == "datastore": + from . import datastore # noqa + + logger.opt(colors=True).info("use datastore rate_limit_token storage") +elif conf().access_control_rate_limit_token_storage == "inmemory": + from . import inmemory # noqa + + logger.opt(colors=True).info("use inmemory rate_limit_token storage") +else: + raise RuntimeError( + f"invalid access_control_rate_limit_token_storage: " + f"{conf().access_control_rate_limit_token_storage}" + ) + +__all__ = ("IRateLimitTokenRepository",) diff --git a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/datastore.py b/src/nonebot_plugin_access_control/repository/rate_limit_token/datastore.py similarity index 79% rename from src/nonebot_plugin_access_control/service/impl/rate_limit/token/datastore.py rename to src/nonebot_plugin_access_control/repository/rate_limit_token/datastore.py index e454a37..9069266 100644 --- a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/datastore.py +++ b/src/nonebot_plugin_access_control/repository/rate_limit_token/datastore.py @@ -1,5 +1,7 @@ from nonebot import require +from nonebot_plugin_access_control_api.context import context + require("nonebot_plugin_apscheduler") from typing import Optional @@ -10,13 +12,25 @@ from nonebot_plugin_apscheduler import scheduler from apscheduler.triggers.interval import IntervalTrigger -from .interface import TokenStorage -from .....utils.session import use_ac_session -from .....models import RateLimitRuleOrm, RateLimitTokenOrm -from ....rate_limit import RateLimitRule, RateLimitSingleToken +from nonebot_plugin_access_control_api.models.rate_limit import ( + RateLimitRule, + RateLimitSingleToken, +) + +from ..utils import use_ac_session +from .interface import IRateLimitTokenRepository +from ..orm.rate_limit import RateLimitRuleOrm, RateLimitTokenOrm + +@context.bind_singleton_to(IRateLimitTokenRepository) +class DataStoreTokenRepository(IRateLimitTokenRepository): + def __init__(self): + scheduler.add_job( + self.delete_outdated_tokens, + IntervalTrigger(minutes=1), + id="delete_outdated_tokens_inmemory", + ) -class DataStoreTokenStorage(TokenStorage): async def get_first_expire_token( self, rule: RateLimitRule, user: str ) -> Optional[RateLimitSingleToken]: @@ -111,13 +125,9 @@ async def delete_outdated_tokens(self): logger.debug(f"deleted {rowcount} outdated rate limit token(s)") - -datastore_storage = DataStoreTokenStorage() - -scheduler.scheduled_job( - IntervalTrigger(minutes=10), id="delete_outdated_tokens_datastore" -)(datastore_storage.delete_outdated_tokens) - - -def get_datastore_token_storage() -> TokenStorage: - return datastore_storage + async def clear_token(self): + async with use_ac_session() as sess: + stmt = delete(RateLimitTokenOrm) + result = await sess.execute(stmt) + await sess.commit() + logger.debug(f"deleted {result.rowcount} rate limit token(s)") diff --git a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/inmemory.py b/src/nonebot_plugin_access_control/repository/rate_limit_token/inmemory.py similarity index 75% rename from src/nonebot_plugin_access_control/service/impl/rate_limit/token/inmemory.py rename to src/nonebot_plugin_access_control/repository/rate_limit_token/inmemory.py index dbe3203..48e7c22 100644 --- a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/inmemory.py +++ b/src/nonebot_plugin_access_control/repository/rate_limit_token/inmemory.py @@ -1,5 +1,7 @@ from nonebot import require +from nonebot_plugin_access_control_api.context import context + require("nonebot_plugin_apscheduler") from datetime import datetime @@ -8,8 +10,12 @@ from nonebot_plugin_apscheduler import scheduler from apscheduler.triggers.interval import IntervalTrigger -from .interface import TokenStorage -from ....rate_limit import RateLimitRule, RateLimitSingleToken +from nonebot_plugin_access_control_api.models.rate_limit import ( + RateLimitRule, + RateLimitSingleToken, +) + +from .interface import IRateLimitTokenRepository class StorageKey(NamedTuple): @@ -24,11 +30,18 @@ def _handle_expired( return tuple(filter(lambda x: x.expire_time > now, tokens)) -class InmemoryTokenStorage(TokenStorage): +@context.bind_singleton_to(IRateLimitTokenRepository) +class InmemoryTokenRepository(IRateLimitTokenRepository): def __init__(self): self.id_cnt = 0 self.data: dict[StorageKey, tuple[RateLimitSingleToken, ...]] = {} + scheduler.add_job( + self.delete_outdated_tokens, + IntervalTrigger(minutes=1), + id="delete_outdated_tokens_inmemory", + ) + def next_id(self) -> int: self.id_cnt += 1 return self.id_cnt @@ -76,21 +89,13 @@ async def retire_token(self, token: RateLimitSingleToken): async def delete_outdated_tokens(self): del_keys = set() - for k in inmemory_storage.data: - inmemory_storage.data[k] = _handle_expired(inmemory_storage.data[k]) - if len(inmemory_storage.data[k]) == 0: + for k in self.data: + self.data[k] = _handle_expired(self.data[k]) + if len(self.data[k]) == 0: del_keys.add(k) for k in del_keys: - del inmemory_storage.data[k] - - -inmemory_storage = InmemoryTokenStorage() - -scheduler.scheduled_job( - IntervalTrigger(minutes=1), id="delete_outdated_tokens_inmemory" -)(inmemory_storage.delete_outdated_tokens) - + del self.data[k] -def get_inmemory_token_storage(**kwargs) -> TokenStorage: - return inmemory_storage + async def clear_token(self): + self.data = {} diff --git a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/interface.py b/src/nonebot_plugin_access_control/repository/rate_limit_token/interface.py similarity index 65% rename from src/nonebot_plugin_access_control/service/impl/rate_limit/token/interface.py rename to src/nonebot_plugin_access_control/repository/rate_limit_token/interface.py index 56ed284..cf1c1ac 100644 --- a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/interface.py +++ b/src/nonebot_plugin_access_control/repository/rate_limit_token/interface.py @@ -1,9 +1,12 @@ from typing import Optional, Protocol -from ....rate_limit import RateLimitRule, RateLimitSingleToken +from nonebot_plugin_access_control_api.models.rate_limit import ( + RateLimitRule, + RateLimitSingleToken, +) -class TokenStorage(Protocol): +class IRateLimitTokenRepository(Protocol): async def get_first_expire_token( self, rule: RateLimitRule, user: str ) -> Optional[RateLimitSingleToken]: @@ -16,3 +19,6 @@ async def acquire_token( async def retire_token(self, token: RateLimitSingleToken): ... + + async def clear_token(self): + ... diff --git a/src/nonebot_plugin_access_control/utils/session.py b/src/nonebot_plugin_access_control/repository/utils.py similarity index 100% rename from src/nonebot_plugin_access_control/utils/session.py rename to src/nonebot_plugin_access_control/repository/utils.py diff --git a/src/nonebot_plugin_access_control/service/__init__.py b/src/nonebot_plugin_access_control/service/__init__.py index 42cbe67..d73a7dd 100644 --- a/src/nonebot_plugin_access_control/service/__init__.py +++ b/src/nonebot_plugin_access_control/service/__init__.py @@ -1,7 +1 @@ -from .methods import * -from .base import Service -from .plugin import PluginService -from .subservice import SubService -from .nonebot import NoneBotService - -__all__ = ("Service", "NoneBotService", "PluginService", "SubService") +from . import _impl # noqa diff --git a/src/nonebot_plugin_access_control/service/_impl/__init__.py b/src/nonebot_plugin_access_control/service/_impl/__init__.py new file mode 100644 index 0000000..4b06f83 --- /dev/null +++ b/src/nonebot_plugin_access_control/service/_impl/__init__.py @@ -0,0 +1 @@ +from . import factory # noqa diff --git a/src/nonebot_plugin_access_control/service/_impl/factory.py b/src/nonebot_plugin_access_control/service/_impl/factory.py new file mode 100644 index 0000000..8317955 --- /dev/null +++ b/src/nonebot_plugin_access_control/service/_impl/factory.py @@ -0,0 +1,37 @@ +from nonebot_plugin_access_control_api.context import context +from nonebot_plugin_access_control_api.service.interface import IService +from nonebot_plugin_access_control_api.service.interface.factory import ( + IServiceComponentFactory, +) +from nonebot_plugin_access_control_api.service.interface.patcher import IServicePatcher +from nonebot_plugin_access_control_api.service.interface.permission import ( + IServicePermission, +) +from nonebot_plugin_access_control_api.service.interface.rate_limit import ( + IServiceRateLimit, +) + +from .patcher import ServicePatcherImpl +from .permission import ServicePermissionImpl +from .rate_limit import ServiceRateLimitImpl + + +@context.bind_singleton_to(IServiceComponentFactory) +class ServiceComponentFactory(IServiceComponentFactory): + def create_patcher_impl(self, service: IService) -> IServicePatcher: + return ServicePatcherImpl(service) + + def typeof_patcher_impl(self) -> type[IServicePatcher]: + return ServicePatcherImpl + + def create_permission_impl(self, service: IService) -> IServicePermission: + return ServicePermissionImpl(service) + + def typeof_permission_impl(self) -> type[IServicePermission]: + return ServicePermissionImpl + + def create_rate_limit_impl(self, service: IService) -> IServiceRateLimit: + return ServiceRateLimitImpl(service) + + def typeof_rate_limit_impl(self) -> type[IServiceRateLimit]: + return ServiceRateLimitImpl diff --git a/src/nonebot_plugin_access_control/service/_impl/patcher.py b/src/nonebot_plugin_access_control/service/_impl/patcher.py new file mode 100644 index 0000000..28cad43 --- /dev/null +++ b/src/nonebot_plugin_access_control/service/_impl/patcher.py @@ -0,0 +1,93 @@ +from datetime import datetime +from functools import wraps + +from nonebot import logger, Bot +from nonebot.exception import IgnoredException +from nonebot.internal.adapter import Event +from nonebot.internal.matcher import ( + Matcher, + current_bot, + current_event, + current_matcher, +) +from nonebot.message import run_preprocessor +from nonebot_plugin_access_control_api.errors import ( + PermissionDeniedError, + RateLimitedError, +) +from nonebot_plugin_access_control_api.service.interface import IService +from nonebot_plugin_access_control_api.service.interface.patcher import IServicePatcher + +from ...config import conf + + +class ServicePatcherImpl(IServicePatcher): + _matcher_service_mapping: dict[type[Matcher], IService] = {} + + def __init__(self, service: IService): + self.service = service + + def patch_matcher(self, matcher: type[Matcher]) -> type[Matcher]: + self._matcher_service_mapping[matcher] = self.service + logger.debug(f"patched {matcher} (with service {self.service.qualified_name})") + return matcher + + def patch_handler(self, retire_on_throw: bool = False): + def decorator(func): + @wraps(func) + async def wrapped_func(*args, **kwargs): + bot = current_bot.get() + event = current_event.get() + + if not await self.service.check( + bot, event, acquire_rate_limit_token=False + ): + raise PermissionDeniedError() + + result = ( + await self.service.acquire_token_for_rate_limit_receiving_result( + bot, event + ) + ) + if not result.success: + raise RateLimitedError(result) + + matcher = current_matcher.get() + matcher.state["ac_token"] = result.token + + try: + return await func(*args, **kwargs) + except BaseException as e: + if retire_on_throw: + await result.token.retire() + raise e + + return wrapped_func + + return decorator + + +@run_preprocessor +async def check(matcher: Matcher, bot: Bot, event: Event): + service = ServicePatcherImpl._matcher_service_mapping.get(type(matcher), None) + if service is None: + return + + try: + await service.check(bot, event, throw_on_fail=True) + except PermissionDeniedError: + if conf().access_control_reply_on_permission_denied_enabled: + await matcher.send(conf().access_control_reply_on_permission_denied) + raise IgnoredException("permission denied (by nonebot_plugin_access_control)") + except RateLimitedError as e: + if conf().access_control_reply_on_rate_limited_enabled: + msg = conf().access_control_reply_on_rate_limited + if msg is None: + now = datetime.utcnow() + available_time = e.result.available_time + msg = ( + "使用太频繁了,请稍后再试。" + f"下次可用时间:{available_time.timestamp() - now.timestamp():.0f}秒后" + ) + await matcher.send(msg) + raise IgnoredException("rate limited (by nonebot_plugin_access_control)") diff --git a/src/nonebot_plugin_access_control/service/impl/permission.py b/src/nonebot_plugin_access_control/service/_impl/permission.py similarity index 55% rename from src/nonebot_plugin_access_control/service/impl/permission.py rename to src/nonebot_plugin_access_control/service/_impl/permission.py index e6a83ee..e7038aa 100644 --- a/src/nonebot_plugin_access_control/service/impl/permission.py +++ b/src/nonebot_plugin_access_control/service/_impl/permission.py @@ -1,23 +1,28 @@ from collections.abc import AsyncGenerator -from typing import Generic, TypeVar, Optional +from typing import Optional from nonebot import logger -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession +from nonebot_plugin_access_control_api.context import context +from nonebot_plugin_access_control_api.event_bus import ( + EventType, + T_Listener, + on_event, + fire_event, +) +from nonebot_plugin_access_control_api.models.permission import Permission +from nonebot_plugin_access_control_api.service.interface.permission import ( + IServicePermission, +) +from nonebot_plugin_access_control_api.service.interface.service import IService from ...config import conf -from ...models import PermissionOrm -from ..permission import Permission -from ..interface.service import IService -from ...utils.session import use_ac_session -from ..interface.permission import IServicePermission -from ...event_bus import EventType, T_Listener, on_event, fire_event +from ...repository.permission import IPermissionRepository -T_Service = TypeVar("T_Service", bound=IService) +class ServicePermissionImpl(IServicePermission): + repo = context.require(IPermissionRepository) -class ServicePermissionImpl(Generic[T_Service], IServicePermission): - def __init__(self, service: T_Service): + def __init__(self, service: IService): self.service = service def on_set_permission(self, func: Optional[T_Listener] = None): @@ -41,36 +46,16 @@ def on_remove_permission(self, func: Optional[T_Listener] = None): func, ) - @staticmethod - async def _get_permissions( - service: Optional[T_Service], subject: Optional[str] - ) -> AsyncGenerator[Permission, None]: - async with use_ac_session() as session: - stmt = select(PermissionOrm) - if service is not None: - stmt = stmt.where(PermissionOrm.service == service.qualified_name) - if subject is not None: - stmt = stmt.where(PermissionOrm.subject == subject) - - async for x in await session.stream_scalars(stmt): - s = service - if s is None: - from ..methods import get_service_by_qualified_name - - s = get_service_by_qualified_name(x.service) - if s is not None: - yield Permission(s, x.subject, x.allow) - async def get_permission_by_subject( self, *subject: str, trace: bool = True ) -> Optional[Permission]: for sub in subject: if trace: for node in self.service.trace(): - async for p in self._get_permissions(node, sub): + async for p in self.repo.get_permissions(node, sub): return p else: - async for p in self._get_permissions(self.service, sub): + async for p in self.repo.get_permissions(self.service, sub): return p return None @@ -80,10 +65,10 @@ async def get_permissions( ) -> AsyncGenerator[Permission, None]: if trace: for node in self.service.trace(): - async for p in self._get_permissions(node, None): + async for p in self.repo.get_permissions(node, None): yield p else: - async for p in self._get_permissions(self.service, None): + async for p in self.repo.get_permissions(self.service, None): yield p @classmethod @@ -92,14 +77,14 @@ async def get_all_permissions_by_subject( ) -> AsyncGenerator[Permission, None]: overridden_services = set() for sub in subject: - async for x in cls._get_permissions(None, sub): + async for x in cls.repo.get_permissions(None, sub): if x.service not in overridden_services: yield x overridden_services.add(x.service) @classmethod async def get_all_permissions(cls) -> AsyncGenerator[Permission, None]: - async for x in cls._get_permissions(None, None): + async for x in cls.repo.get_permissions(None, None): yield x async def _fire_service_set_permission(self, subject: str, allow: bool): @@ -117,9 +102,7 @@ async def _fire_service_remove_permission(self, subject: str): {"service": self.service, "subject": subject}, ) - async def _fire_service_change_permission( - self, subject: str, allow: bool, session: AsyncSession - ): + async def _fire_service_change_permission(self, subject: str, allow: bool): await fire_event( EventType.service_change_permission, { @@ -133,7 +116,7 @@ async def _fire_service_change_permission( continue cnt = 0 - async for x in self._get_permissions(node, subject): + async for x in self.repo.get_permissions(node, subject): cnt += 1 if cnt == 0: @@ -146,43 +129,17 @@ async def _fire_service_change_permission( ) async def set_permission(self, subject: str, allow: bool) -> bool: - async with use_ac_session() as sess: - stmt = select(PermissionOrm).where( - PermissionOrm.service == self.service.qualified_name, - PermissionOrm.subject == subject, - ) - p = (await sess.execute(stmt)).scalar_one_or_none() - if p is None: - p = PermissionOrm( - service=self.service.qualified_name, subject=subject, allow=allow - ) - sess.add(p) - old_allow = None - else: - old_allow = p.allow - p.allow = allow - - if old_allow != allow: - await sess.commit() - await self._fire_service_set_permission(subject, allow) - await self._fire_service_change_permission(subject, allow, sess) - return True - else: - return False + ok = await self.repo.set_permission(self.service, subject, allow) - async def remove_permission(self, subject: str) -> bool: - async with use_ac_session() as sess: - stmt = select(PermissionOrm).where( - PermissionOrm.service == self.service.qualified_name, - PermissionOrm.subject == subject, - ) - p = (await sess.execute(stmt)).scalar_one_or_none() - if p is None: - return False + if ok: + await self._fire_service_set_permission(subject, allow) + await self._fire_service_change_permission(subject, allow) - await sess.delete(p) - await sess.commit() + return ok + async def remove_permission(self, subject: str) -> bool: + ok = await self.repo.remove_permission(self.service, subject) + if ok: await self._fire_service_remove_permission(subject) p = await self.get_permission_by_subject(subject) @@ -190,9 +147,9 @@ async def remove_permission(self, subject: str) -> bool: allow = p.allow else: allow = conf().access_control_default_permission == "allow" - await self._fire_service_change_permission(subject, allow, sess) + await self._fire_service_change_permission(subject, allow) - return True + return ok async def check_permission(self, *subject: str) -> bool: p = await self.get_permission_by_subject(*subject) diff --git a/src/nonebot_plugin_access_control/service/impl/rate_limit/__init__.py b/src/nonebot_plugin_access_control/service/_impl/rate_limit.py similarity index 53% rename from src/nonebot_plugin_access_control/service/impl/rate_limit/__init__.py rename to src/nonebot_plugin_access_control/service/_impl/rate_limit.py index 9e5d3df..bc1f575 100644 --- a/src/nonebot_plugin_access_control/service/impl/rate_limit/__init__.py +++ b/src/nonebot_plugin_access_control/service/_impl/rate_limit.py @@ -1,27 +1,28 @@ -from datetime import timedelta -from typing import Generic, TypeVar, Optional from collections.abc import Collection, AsyncGenerator - -from nonebot import Bot, logger -from nonebot.internal.adapter import Event -from sqlalchemy import func, delete, select -from sqlalchemy.ext.asyncio import AsyncSession - -from ...interface import IService -from .token import get_token_storage -from ....subject import extract_subjects -from ....utils.session import use_ac_session -from ....errors import AccessControlQueryError -from ....models import RateLimitRuleOrm, RateLimitTokenOrm -from ...rate_limit import RateLimitRule, RateLimitSingleToken -from ....event_bus import EventType, T_Listener, on_event, fire_event -from ...interface.rate_limit import ( +from datetime import timedelta +from typing import Optional + +from nonebot import logger +from nonebot_plugin_access_control_api.context import context +from nonebot_plugin_access_control_api.event_bus import ( + EventType, + T_Listener, + on_event, + fire_event, +) +from nonebot_plugin_access_control_api.models.rate_limit import ( + RateLimitRule, IRateLimitToken, - IServiceRateLimit, AcquireTokenResult, + RateLimitSingleToken, +) +from nonebot_plugin_access_control_api.service.interface import IService +from nonebot_plugin_access_control_api.service.interface.rate_limit import ( + IServiceRateLimit, ) -T_Service = TypeVar("T_Service", bound=IService) +from ...repository.rate_limit import IRateLimitRepository +from ...repository.rate_limit_token import IRateLimitTokenRepository class RateLimitTokenImpl(IRateLimitToken): @@ -36,8 +37,11 @@ async def retire(self): await self.service._retire_token(t) -class ServiceRateLimitImpl(Generic[T_Service], IServiceRateLimit): - def __init__(self, service: T_Service): +class ServiceRateLimitImpl(IServiceRateLimit): + repo = context.require(IRateLimitRepository) + token_repo = context.require(IRateLimitTokenRepository) + + def __init__(self, service: IService): self.service = service def on_add_rate_limit_rule(self, func: Optional[T_Listener] = None): @@ -54,32 +58,12 @@ def on_remove_rate_limit_rule(self, func: Optional[T_Listener] = None): func, ) - @staticmethod + @classmethod async def _get_rules_by_subject( - service: Optional[T_Service], subject: Optional[str] + cls, service: Optional[IService], subject: Optional[str] ) -> AsyncGenerator[RateLimitRule, None]: - async with use_ac_session() as session: - stmt = select(RateLimitRuleOrm) - if service is not None: - stmt = stmt.where(RateLimitRuleOrm.service == service.qualified_name) - if subject is not None: - stmt = stmt.where(RateLimitRuleOrm.subject == subject) - - async for x in await session.stream_scalars(stmt): - s = service - if s is None: - from ...methods import get_service_by_qualified_name - - s = get_service_by_qualified_name(x.service) - if s is not None: - yield RateLimitRule( - x.id, - s, - x.subject, - timedelta(seconds=x.time_span), - x.limit, - x.overwrite, - ) + async for x in cls.repo.get_rules_by_subject(service, subject): + yield x async def get_rate_limit_rules_by_subject( self, *subject: str, trace: bool = True @@ -139,73 +123,32 @@ async def _fire_service_remove_rate_limit_rule(rule: RateLimitRule): async def add_rate_limit_rule( self, subject: str, time_span: timedelta, limit: int, overwrite: bool = False ) -> RateLimitRule: - async with use_ac_session() as sess: - if overwrite: - stmt = select(func.count()).where( - RateLimitRuleOrm.subject == subject, - RateLimitRuleOrm.service == self.service.qualified_name, - ) - cnt = (await sess.execute(stmt)).scalar_one() - - if cnt > 0: - raise AccessControlQueryError("已存在对该实体与服务的限流规则,不允许再添加覆写规则") - - orm = RateLimitRuleOrm( - subject=subject, - service=self.service.qualified_name, - time_span=int(time_span.total_seconds()), - limit=limit, - overwrite=overwrite, - ) - sess.add(orm) - await sess.commit() - - await sess.refresh(orm) - - rule = RateLimitRule( - orm.id, self.service, subject, time_span, limit, overwrite - ) - await self._fire_service_add_rate_limit_rule(rule) - - return rule + rule = await self.repo.add_rate_limit_rule( + self.service, subject, time_span, limit, overwrite + ) + await self._fire_service_add_rate_limit_rule(rule) + return rule @classmethod async def remove_rate_limit_rule(cls, rule_id: str) -> bool: - async with use_ac_session() as sess: - orm = await sess.get(RateLimitRuleOrm, rule_id) - if orm is None: - return False - - await sess.delete(orm) - await sess.commit() - - from ...methods import get_service_by_qualified_name - - service = get_service_by_qualified_name(orm.service) - - rule = RateLimitRule( - orm.id, - service, - orm.subject, - timedelta(seconds=orm.time_span), - orm.limit, - orm.overwrite, - ) + rule = await cls.repo.remove_rate_limit_rule(rule_id) + if rule is not None: await cls._fire_service_remove_rate_limit_rule(rule) - return True + else: + return False - @staticmethod + @classmethod async def _get_first_expire_token( - rule: RateLimitRule, user: str + cls, rule: RateLimitRule, user: str ) -> Optional[RateLimitSingleToken]: - return await get_token_storage().get_first_expire_token(rule, user) + return await cls.token_repo.get_first_expire_token(rule, user) - @staticmethod + @classmethod async def _acquire_token( - rule: RateLimitRule, user: str + cls, rule: RateLimitRule, user: str ) -> Optional[RateLimitSingleToken]: - x = await get_token_storage().acquire_token(rule, user) + x = await cls.token_repo.acquire_token(rule, user) if x is not None: logger.trace( f"[rate limit] token {x.id} acquired " @@ -214,37 +157,15 @@ async def _acquire_token( ) return x - @staticmethod - async def _retire_token(token: RateLimitSingleToken): - await get_token_storage().retire_token(token) + @classmethod + async def _retire_token(cls, token: RateLimitSingleToken): + repo = cls.token_repo.require(IRateLimitTokenRepository) + await repo.retire_token(token) logger.trace( f"[rate limit] token {token.id} retired for " f"rule {token.rule_id} by user {token.user}" ) - async def acquire_token_for_rate_limit( - self, bot: Bot, event: Event, *, session: Optional[AsyncSession] = None - ) -> Optional[RateLimitTokenImpl]: - result = await self.acquire_token_for_rate_limit_receiving_result( - bot, event, session=session - ) - return result.token - - async def acquire_token_for_rate_limit_receiving_result( - self, bot: Bot, event: Event, *, session: Optional[AsyncSession] = None - ) -> AcquireTokenResult: - return await self.acquire_token_for_rate_limit_by_subjects_receiving_result( - *extract_subjects(bot, event) - ) - - async def acquire_token_for_rate_limit_by_subjects( - self, *subject: str, session: Optional[AsyncSession] = None - ) -> Optional[RateLimitTokenImpl]: - result = await self.acquire_token_for_rate_limit_by_subjects_receiving_result( - *subject - ) - return result.token - async def acquire_token_for_rate_limit_by_subjects_receiving_result( self, *subject: str ) -> AcquireTokenResult: @@ -296,8 +217,4 @@ async def acquire_token_for_rate_limit_by_subjects_receiving_result( @classmethod async def clear_rate_limit_tokens(cls): - async with use_ac_session() as sess: - stmt = delete(RateLimitTokenOrm) - result = await sess.execute(stmt) - await sess.commit() - logger.debug(f"deleted {result.rowcount} rate limit token(s)") + await cls.token_repo.clear_token() diff --git a/src/nonebot_plugin_access_control/service/base.py b/src/nonebot_plugin_access_control/service/base.py deleted file mode 100644 index cae6006..0000000 --- a/src/nonebot_plugin_access_control/service/base.py +++ /dev/null @@ -1,280 +0,0 @@ -from abc import ABC -from functools import wraps -from datetime import datetime, timedelta -from collections.abc import AsyncGenerator -from typing import Generic, TypeVar, Optional - -from nonebot import Bot, logger -from nonebot.internal.adapter import Event -from nonebot.message import run_preprocessor -from nonebot.exception import IgnoredException -from nonebot.internal.matcher import ( - Matcher, - current_bot, - current_event, - current_matcher, -) - -from ..config import conf -from .interface import IService -from ..event_bus import T_Listener -from .permission import Permission -from .rate_limit import RateLimitRule -from ..subject import extract_subjects -from .impl.rate_limit import ServiceRateLimitImpl -from .impl.permission import ServicePermissionImpl -from .interface.rate_limit import IRateLimitToken, AcquireTokenResult -from ..errors import RateLimitedError, AccessControlError, PermissionDeniedError - -T_ParentService = TypeVar("T_ParentService", bound=Optional["Service"], covariant=True) -T_ChildService = TypeVar("T_ChildService", bound="Service", covariant=True) - - -class Service( - Generic[T_ParentService, T_ChildService], - IService["Service", T_ParentService, T_ChildService], - ABC, -): - _matcher_service_mapping: dict[type[Matcher], "Service"] = {} - - def __init__(self): - self._permission_impl = ServicePermissionImpl[Service](self) - self._rate_limit_impl = ServiceRateLimitImpl[Service](self) - - def __repr__(self): - return self.qualified_name - - def travel(self): - sta = [self] - while len(sta) != 0: - top, sta = sta[-1], sta[:-1] - yield top - sta.extend(top.children) - - def trace(self): - node = self - while node is not None: - yield node - node = node.parent - - def get_child(self, name: str) -> Optional["Service"]: - for s in self.children: - if s.name == name: - return s - return None - - def patch_matcher(self, matcher: type[Matcher]) -> type[Matcher]: - self._matcher_service_mapping[matcher] = self - logger.debug(f"patched {matcher} (with service {self.qualified_name})") - return matcher - - def patch_handler(self, retire_on_throw: bool = False): - def decorator(func): - @wraps(func) - async def wrapped_func(*args, **kwargs): - bot = current_bot.get() - event = current_event.get() - - if not await self.check(bot, event, acquire_rate_limit_token=False): - raise PermissionDeniedError() - - result = await self.acquire_token_for_rate_limit_receiving_result( - bot, event - ) - if not result.success: - raise RateLimitedError(result) - - matcher = current_matcher.get() - matcher.state["ac_token"] = result.token - - try: - return await func(*args, **kwargs) - except BaseException as e: - if retire_on_throw: - await result.token.retire() - raise e - - return wrapped_func - - return decorator - - async def check( - self, - bot: Bot, - event: Event, - *, - acquire_rate_limit_token: bool = True, - throw_on_fail: bool = False, - ) -> bool: - subjects = extract_subjects(bot, event) - return await self.check_by_subject( - *subjects, - acquire_rate_limit_token=acquire_rate_limit_token, - throw_on_fail=throw_on_fail, - ) - - async def check_by_subject( - self, - *subjects: str, - acquire_rate_limit_token: bool = True, - throw_on_fail: bool = False, - ) -> bool: - if not throw_on_fail: - try: - await self.check_by_subject( - *subjects, - acquire_rate_limit_token=acquire_rate_limit_token, - throw_on_fail=True, - ) - return True - except AccessControlError: - return False - - allow = await self.check_permission(*subjects) - if not allow: - raise PermissionDeniedError() - - if acquire_rate_limit_token: - result = ( - await self.acquire_token_for_rate_limit_by_subjects_receiving_result( - *subjects - ) - ) - if not result.success: - raise RateLimitedError(result) - - def on_set_permission(self, func: Optional[T_Listener] = None): - return self._permission_impl.on_set_permission(func) - - def on_change_permission(self, func: Optional[T_Listener] = None): - return self._permission_impl.on_change_permission(func) - - def on_remove_permission(self, func: Optional[T_Listener] = None): - return self._permission_impl.on_remove_permission(func) - - async def get_permission_by_subject( - self, *subject: str, trace: bool = True - ) -> Optional[Permission]: - return await self._permission_impl.get_permission_by_subject( - *subject, trace=trace - ) - - def get_permissions( - self, *, trace: bool = True - ) -> AsyncGenerator[Permission, None]: - return self._permission_impl.get_permissions(trace=trace) - - @classmethod - def get_all_permissions_by_subject( - cls, *subject: str - ) -> AsyncGenerator[Permission, None]: - return ServicePermissionImpl.get_all_permissions_by_subject(*subject) - - @classmethod - def get_all_permissions(cls) -> AsyncGenerator[Permission, None]: - return ServicePermissionImpl.get_all_permissions() - - async def set_permission(self, subject: str, allow: bool) -> bool: - return await self._permission_impl.set_permission(subject, allow) - - async def remove_permission(self, subject: str) -> bool: - return await self._permission_impl.remove_permission(subject) - - async def check_permission(self, *subject: str) -> bool: - return await self._permission_impl.check_permission(*subject) - - def on_add_rate_limit_rule(self, func: Optional[T_Listener] = None): - return self._rate_limit_impl.on_add_rate_limit_rule(func) - - def on_remove_rate_limit_rule(self, func: Optional[T_Listener] = None): - return self._rate_limit_impl.on_remove_rate_limit_rule(func) - - def get_rate_limit_rules_by_subject( - self, *subject: str, trace: bool = True - ) -> AsyncGenerator[RateLimitRule, None]: - return self._rate_limit_impl.get_rate_limit_rules_by_subject( - *subject, trace=trace - ) - - def get_rate_limit_rules( - self, *, trace: bool = True - ) -> AsyncGenerator[RateLimitRule, None]: - return self._rate_limit_impl.get_rate_limit_rules(trace=trace) - - @classmethod - def get_all_rate_limit_rules_by_subject( - cls, *subject: str - ) -> AsyncGenerator[RateLimitRule, None]: - return ServiceRateLimitImpl.get_all_rate_limit_rules_by_subject(*subject) - - @classmethod - def get_all_rate_limit_rules(cls) -> AsyncGenerator[RateLimitRule, None]: - return ServiceRateLimitImpl.get_all_rate_limit_rules() - - async def add_rate_limit_rule( - self, subject: str, time_span: timedelta, limit: int, overwrite: bool = False - ) -> RateLimitRule: - return await self._rate_limit_impl.add_rate_limit_rule( - subject, time_span, limit, overwrite - ) - - @classmethod - async def remove_rate_limit_rule(cls, rule_id: str) -> bool: - return await ServiceRateLimitImpl.remove_rate_limit_rule(rule_id) - - async def acquire_token_for_rate_limit( - self, bot: Bot, event: Event - ) -> Optional[IRateLimitToken]: - return await self._rate_limit_impl.acquire_token_for_rate_limit(bot, event) - - async def acquire_token_for_rate_limit_receiving_result( - self, bot: Bot, event: Event - ) -> AcquireTokenResult: - return ( - await self._rate_limit_impl.acquire_token_for_rate_limit_receiving_result( - bot, event - ) - ) - - async def acquire_token_for_rate_limit_by_subjects( - self, *subject: str - ) -> Optional[IRateLimitToken]: - return await self._rate_limit_impl.acquire_token_for_rate_limit_by_subjects( - *subject - ) - - async def acquire_token_for_rate_limit_by_subjects_receiving_result( - self, *subject: str - ) -> AcquireTokenResult: - return await self._rate_limit_impl.acquire_token_for_rate_limit_by_subjects_receiving_result( - *subject - ) - - @classmethod - async def clear_rate_limit_tokens(cls): - return await ServiceRateLimitImpl.clear_rate_limit_tokens() - - -@run_preprocessor -async def check(matcher: Matcher, bot: Bot, event: Event): - service = Service._matcher_service_mapping.get(type(matcher), None) - if service is None: - return - - try: - await service.check(bot, event, throw_on_fail=True) - except PermissionDeniedError: - if conf().access_control_reply_on_permission_denied_enabled: - await matcher.send(conf().access_control_reply_on_permission_denied) - raise IgnoredException("permission denied (by nonebot_plugin_access_control)") - except RateLimitedError as e: - if conf().access_control_reply_on_rate_limited_enabled: - msg = conf().access_control_reply_on_rate_limited - if msg is None: - now = datetime.utcnow() - available_time = e.result.available_time - msg = "使用太频繁了,请稍后再试。" "下次可用时间:{:.0f}秒后".format( - available_time.timestamp() - now.timestamp() - ) - await matcher.send(msg) - raise IgnoredException("rate limited (by nonebot_plugin_access_control)") diff --git a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/__init__.py b/src/nonebot_plugin_access_control/service/impl/rate_limit/token/__init__.py deleted file mode 100644 index 4af63ad..0000000 --- a/src/nonebot_plugin_access_control/service/impl/rate_limit/token/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from nonebot import logger - -from .....config import conf -from .interface import TokenStorage - -if conf().access_control_rate_limit_token_storage == "datastore": - from .datastore import get_datastore_token_storage - - get_token_storage = get_datastore_token_storage - logger.opt(colors=True).info("use datastore token storage") -elif conf().access_control_rate_limit_token_storage == "inmemory": - from .inmemory import get_inmemory_token_storage - - get_token_storage = get_inmemory_token_storage - logger.opt(colors=True).info("use inmemory token storage") -else: - raise RuntimeError( - f"invalid access_control_rate_limit_token_storage: " - f"{conf().access_control_rate_limit_token_storage}" - ) - -__all__ = ("get_token_storage", "TokenStorage") diff --git a/src/nonebot_plugin_access_control/service/interface/__init__.py b/src/nonebot_plugin_access_control/service/interface/__init__.py deleted file mode 100644 index b5568cb..0000000 --- a/src/nonebot_plugin_access_control/service/interface/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import IService - -__all__ = ("IService",) diff --git a/src/nonebot_plugin_access_control/service/interface/base.py b/src/nonebot_plugin_access_control/service/interface/base.py deleted file mode 100644 index 0312b08..0000000 --- a/src/nonebot_plugin_access_control/service/interface/base.py +++ /dev/null @@ -1,73 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Generic, TypeVar, Optional -from collections.abc import Generator, Collection - -from nonebot import Bot -from nonebot.internal.adapter import Event -from nonebot.internal.matcher import Matcher - -T_Service = TypeVar("T_Service", bound="IServiceBase", covariant=True) -T_ParentService = TypeVar( - "T_ParentService", bound=Optional["IServiceBase"], covariant=True -) -T_ChildService = TypeVar("T_ChildService", bound="IServiceBase", covariant=True) - - -class IServiceBase(Generic[T_Service, T_ParentService, T_ChildService], ABC): - @property - @abstractmethod - def name(self) -> str: - raise NotImplementedError() - - @property - def qualified_name(self) -> str: - raise NotImplementedError() - - @property - @abstractmethod - def parent(self) -> Optional[T_ParentService]: - raise NotImplementedError() - - @property - def children(self) -> Collection[T_ChildService]: - raise NotImplementedError() - - @abstractmethod - def travel(self) -> Generator[T_Service, None, None]: - raise NotImplementedError() - - @abstractmethod - def trace(self) -> Generator[T_Service, None, None]: - raise NotImplementedError() - - @abstractmethod - def get_child(self, name: str) -> Optional[T_Service]: - raise NotImplementedError() - - @abstractmethod - def patch_matcher(self, matcher: type[Matcher]) -> type[Matcher]: - raise NotImplementedError() - - @abstractmethod - def patch_handler(self, retire_on_throw: bool = False): - raise NotImplementedError() - - @abstractmethod - async def check( - self, - bot: Bot, - event: Event, - *, - acquire_rate_limit_token: bool = True, - throw_on_fail: bool = False, - ) -> bool: - raise NotImplementedError() - - @abstractmethod - async def check_by_subject( - self, - *subjects: str, - acquire_rate_limit_token: bool = True, - throw_on_fail: bool = False, - ) -> bool: - raise NotImplementedError() diff --git a/src/nonebot_plugin_access_control/service/interface/permission.py b/src/nonebot_plugin_access_control/service/interface/permission.py deleted file mode 100644 index 5adff7f..0000000 --- a/src/nonebot_plugin_access_control/service/interface/permission.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Optional -from abc import ABC, abstractmethod -from collections.abc import AsyncGenerator - -from nonebot_plugin_access_control.event_bus import T_Listener - -from ..permission import Permission - - -class IServicePermission(ABC): - @abstractmethod - def on_set_permission(self, func: Optional[T_Listener] = None): - raise NotImplementedError() - - @abstractmethod - def on_change_permission(self, func: Optional[T_Listener] = None): - raise NotImplementedError() - - @abstractmethod - def on_remove_permission(self, func: Optional[T_Listener] = None): - raise NotImplementedError() - - @abstractmethod - async def get_permission_by_subject( - self, *subject: str, trace: bool = True - ) -> Optional[Permission]: - raise NotImplementedError() - - @abstractmethod - def get_permissions( - self, *, trace: bool = True - ) -> AsyncGenerator[Permission, None]: - raise NotImplementedError() - - @classmethod - @abstractmethod - def get_all_permissions_by_subject( - cls, *subject: str - ) -> AsyncGenerator[Permission, None]: - raise NotImplementedError() - - @classmethod - @abstractmethod - def get_all_permissions(cls) -> AsyncGenerator[Permission, None]: - raise NotImplementedError() - - @abstractmethod - async def set_permission(self, subject: str, allow: bool) -> bool: - raise NotImplementedError() - - @abstractmethod - async def remove_permission(self, subject: str) -> bool: - raise NotImplementedError() - - @abstractmethod - async def check_permission(self, *subject: str) -> bool: - raise NotImplementedError() diff --git a/src/nonebot_plugin_access_control/service/interface/rate_limit.py b/src/nonebot_plugin_access_control/service/interface/rate_limit.py deleted file mode 100644 index dd2abfd..0000000 --- a/src/nonebot_plugin_access_control/service/interface/rate_limit.py +++ /dev/null @@ -1,95 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Optional, NamedTuple -from datetime import datetime, timedelta -from collections.abc import Sequence, AsyncGenerator - -from nonebot import Bot -from nonebot.internal.adapter import Event - -from ...event_bus import T_Listener -from ..rate_limit import RateLimitRule - - -class IRateLimitToken: - async def retire(self): - ... - - -class AcquireTokenResult(NamedTuple): - success: bool - token: Optional[IRateLimitToken] = None - violating: Optional[Sequence[RateLimitRule]] = None - available_time: Optional[datetime] = None - - -class IServiceRateLimit(ABC): - @abstractmethod - def on_add_rate_limit_rule(self, func: Optional[T_Listener] = None): - raise NotImplementedError() - - @abstractmethod - def on_remove_rate_limit_rule(self, func: Optional[T_Listener] = None): - raise NotImplementedError() - - @abstractmethod - def get_rate_limit_rules_by_subject( - self, *subject: str, trace: bool = True - ) -> AsyncGenerator[RateLimitRule, None]: - ... - - @abstractmethod - def get_rate_limit_rules( - self, *, trace: bool = True - ) -> AsyncGenerator[RateLimitRule, None]: - ... - - @classmethod - @abstractmethod - def get_all_rate_limit_rules_by_subject( - cls, *subject: str - ) -> AsyncGenerator[RateLimitRule, None]: - ... - - @classmethod - @abstractmethod - def get_all_rate_limit_rules(cls) -> AsyncGenerator[RateLimitRule, None]: - ... - - @abstractmethod - async def add_rate_limit_rule( - self, subject: str, time_span: timedelta, limit: int, overwrite: bool = False - ) -> RateLimitRule: - ... - - @classmethod - async def remove_rate_limit_rule(cls, rule_id: str) -> bool: - ... - - @abstractmethod - async def acquire_token_for_rate_limit( - self, bot: Bot, event: Event - ) -> Optional[IRateLimitToken]: - ... - - @abstractmethod - async def acquire_token_for_rate_limit_receiving_result( - self, bot: Bot, event: Event - ) -> AcquireTokenResult: - ... - - @abstractmethod - async def acquire_token_for_rate_limit_by_subjects( - self, *subject: str - ) -> Optional[IRateLimitToken]: - ... - - @abstractmethod - async def acquire_token_for_rate_limit_by_subjects_receiving_result( - self, bot: Bot, event: Event - ) -> AcquireTokenResult: - ... - - @classmethod - @abstractmethod - async def clear_rate_limit_tokens(cls): - ... diff --git a/src/nonebot_plugin_access_control/service/interface/service.py b/src/nonebot_plugin_access_control/service/interface/service.py deleted file mode 100644 index 0b7ee55..0000000 --- a/src/nonebot_plugin_access_control/service/interface/service.py +++ /dev/null @@ -1,22 +0,0 @@ -from abc import ABC -from typing import Generic, TypeVar, Optional - -from .base import IServiceBase -from .rate_limit import IServiceRateLimit -from .permission import IServicePermission - -T_Service = TypeVar("T_Service", bound="IService", covariant=True) -T_ParentService = TypeVar( - "T_ParentService", bound=Optional["IServiceBase"], covariant=True -) -T_ChildService = TypeVar("T_ChildService", bound="IService", covariant=True) - - -class IService( - Generic[T_Service, T_ParentService, T_ChildService], - IServiceBase[T_Service, T_ParentService, T_ChildService], - IServicePermission, - IServiceRateLimit, - ABC, -): - ... diff --git a/src/nonebot_plugin_access_control/service/interface/subservice_owner.py b/src/nonebot_plugin_access_control/service/interface/subservice_owner.py deleted file mode 100644 index 329e95f..0000000 --- a/src/nonebot_plugin_access_control/service/interface/subservice_owner.py +++ /dev/null @@ -1,10 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Generic, TypeVar - -T_SubService = TypeVar("T_SubService", bound="ISubServiceOwner", covariant=True) - - -class ISubServiceOwner(Generic[T_SubService], ABC): - @abstractmethod - def create_subservice(self, name: str) -> T_SubService: - raise NotImplementedError() diff --git a/src/nonebot_plugin_access_control/service/methods.py b/src/nonebot_plugin_access_control/service/methods.py deleted file mode 100644 index 2d6c002..0000000 --- a/src/nonebot_plugin_access_control/service/methods.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -from .nonebot import NoneBotService - -if TYPE_CHECKING: - from .base import Service - from .plugin import PluginService - -_nonebot_service = NoneBotService() - - -def get_nonebot_service() -> "NoneBotService": - return _nonebot_service - - -def create_plugin_service(plugin_name: str) -> "PluginService": - return get_nonebot_service().create_plugin_service(plugin_name) - - -def get_plugin_service( - plugin_name: str, *, raise_on_not_exists: bool = False -) -> Optional["PluginService"]: - return get_nonebot_service().get_plugin_service( - plugin_name, raise_on_not_exists=raise_on_not_exists - ) - - -def get_service_by_qualified_name( - qualified_name: str, *, raise_on_not_exists: bool = False -) -> Optional["Service"]: - return get_nonebot_service().get_service_by_qualified_name( - qualified_name, raise_on_not_exists=raise_on_not_exists - ) - - -__all__ = ( - "get_nonebot_service", - "create_plugin_service", - "get_plugin_service", - "get_service_by_qualified_name", -) diff --git a/src/nonebot_plugin_access_control/service/nonebot.py b/src/nonebot_plugin_access_control/service/nonebot.py deleted file mode 100644 index 047dd7f..0000000 --- a/src/nonebot_plugin_access_control/service/nonebot.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Optional -from collections.abc import Collection - -import nonebot -from nonebot import logger - -from .base import Service -from .plugin import PluginService -from ..errors import AccessControlError, AccessControlQueryError - - -class NoneBotService(Service[None, PluginService]): - def __init__(self): - super().__init__() - self._plugin_services: dict[str, PluginService] = {} - - @property - def name(self) -> str: - return "nonebot" - - @property - def qualified_name(self) -> str: - return "nonebot" - - @property - def parent(self) -> None: - return None - - @property - def children(self) -> Collection[PluginService]: - return self._plugin_services.values() - - def _create_plugin_service( - self, plugin_name: str, auto_create: bool - ) -> PluginService: - if plugin_name in self._plugin_services: - raise ValueError(f"{plugin_name} already created") - - service = PluginService(plugin_name, auto_create, self) - self._plugin_services[plugin_name] = service - logger.trace(f"created plugin service {service.qualified_name}") - return service - - def create_plugin_service(self, plugin_name: str) -> PluginService: - return self._create_plugin_service(plugin_name, auto_create=False) - - def get_plugin_service( - self, plugin_name: str, *, raise_on_not_exists: bool = False - ) -> Optional[PluginService]: - if plugin_name in self._plugin_services: - return self._plugin_services[plugin_name] - if raise_on_not_exists: - raise AccessControlQueryError(f"找不到服务 {plugin_name}") - return None - - def get_or_create_plugin_service(self, plugin_name: str) -> PluginService: - if plugin_name in self._plugin_services: - return self._plugin_services[plugin_name] - else: - plugin = nonebot.get_plugin(plugin_name) - if plugin is not None: - return self._create_plugin_service(plugin_name, auto_create=True) - else: - raise AccessControlError("No such plugin") - - def get_service_by_qualified_name( - self, qualified_name: str, *, raise_on_not_exists: bool = False - ) -> Optional[Service]: - if qualified_name == "nonebot": - return self - - seg = qualified_name.split(".") - service: Optional[Service] = self.get_plugin_service(seg[0]) - for i in range(1, len(seg)): - if service is None: - if raise_on_not_exists: - raise AccessControlQueryError(f"找不到服务 {qualified_name}") - return None - service = service.get_child(seg[i]) - return service diff --git a/src/nonebot_plugin_access_control/service/permission/__init__.py b/src/nonebot_plugin_access_control/service/permission/__init__.py deleted file mode 100644 index f4d7846..0000000 --- a/src/nonebot_plugin_access_control/service/permission/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .permission import Permission - -__all__ = ("Permission",) diff --git a/src/nonebot_plugin_access_control/service/permission/permission.py b/src/nonebot_plugin_access_control/service/permission/permission.py deleted file mode 100644 index 3517c7e..0000000 --- a/src/nonebot_plugin_access_control/service/permission/permission.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import TYPE_CHECKING, NamedTuple - -if TYPE_CHECKING: - from nonebot_plugin_access_control.service import Service - - -class Permission(NamedTuple): - service: "Service" - subject: str - allow: bool diff --git a/src/nonebot_plugin_access_control/service/plugin.py b/src/nonebot_plugin_access_control/service/plugin.py deleted file mode 100644 index ec55f72..0000000 --- a/src/nonebot_plugin_access_control/service/plugin.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING - -from .subservice import SubService -from .subservice_owner import SubServiceOwner - -if TYPE_CHECKING: - from .nonebot import NoneBotService - - -class PluginService(SubServiceOwner["NoneBotService", SubService]): - def __init__(self, name: str, auto_created: bool, parent: "NoneBotService"): - super().__init__() - self._name = name - self._auto_created = auto_created - self._parent = parent - - @property - def name(self) -> str: - return self._name - - @property - def qualified_name(self) -> str: - return self._name - - @property - def parent(self) -> "NoneBotService": - return self._parent - - @property - def auto_created(self) -> bool: - return self._auto_created - - def _make_subservice(self, name: str) -> SubService: - return SubService(name, self) diff --git a/src/nonebot_plugin_access_control/service/rate_limit/__init__.py b/src/nonebot_plugin_access_control/service/rate_limit/__init__.py deleted file mode 100644 index 330ce02..0000000 --- a/src/nonebot_plugin_access_control/service/rate_limit/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .rule import RateLimitRule -from .token import RateLimitSingleToken - -__all__ = ("RateLimitRule", "RateLimitSingleToken") diff --git a/src/nonebot_plugin_access_control/service/rate_limit/rule.py b/src/nonebot_plugin_access_control/service/rate_limit/rule.py deleted file mode 100644 index be9a17a..0000000 --- a/src/nonebot_plugin_access_control/service/rate_limit/rule.py +++ /dev/null @@ -1,14 +0,0 @@ -from datetime import timedelta -from typing import TYPE_CHECKING, NamedTuple - -if TYPE_CHECKING: - from nonebot_plugin_access_control.service import Service - - -class RateLimitRule(NamedTuple): - id: str - service: "Service" - subject: str - time_span: timedelta - limit: int - overwrite: bool diff --git a/src/nonebot_plugin_access_control/service/rate_limit/token.py b/src/nonebot_plugin_access_control/service/rate_limit/token.py deleted file mode 100644 index 2b8584c..0000000 --- a/src/nonebot_plugin_access_control/service/rate_limit/token.py +++ /dev/null @@ -1,10 +0,0 @@ -from datetime import datetime -from typing import NamedTuple - - -class RateLimitSingleToken(NamedTuple): - id: int - rule_id: str - user: str - acquire_time: datetime - expire_time: datetime diff --git a/src/nonebot_plugin_access_control/service/subservice.py b/src/nonebot_plugin_access_control/service/subservice.py deleted file mode 100644 index 9caf692..0000000 --- a/src/nonebot_plugin_access_control/service/subservice.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import TYPE_CHECKING, Union - -from .subservice_owner import SubServiceOwner - -if TYPE_CHECKING: - from .plugin import PluginService - - -class SubService(SubServiceOwner[Union["PluginService", "SubService"], "SubService"]): - def __init__(self, name: str, parent: Union["PluginService", "SubService"]): - super().__init__() - self._name = name - self._parent = parent - - @property - def name(self) -> str: - return self._name - - @property - def qualified_name(self) -> str: - return self.parent.qualified_name + "." + self.name - - @property - def parent(self) -> Union["PluginService", "SubService"]: - return self._parent - - def _make_subservice(self, name: str) -> "SubService": - return SubService(name, self) diff --git a/src/nonebot_plugin_access_control/service/subservice_owner.py b/src/nonebot_plugin_access_control/service/subservice_owner.py deleted file mode 100644 index 1232b77..0000000 --- a/src/nonebot_plugin_access_control/service/subservice_owner.py +++ /dev/null @@ -1,53 +0,0 @@ -import re -from abc import ABC, abstractmethod -from typing import TypeVar, Optional -from collections.abc import Collection - -from nonebot import logger - -from nonebot_plugin_access_control.errors import AccessControlError - -from .base import Service -from .interface.subservice_owner import ISubServiceOwner - - -def _validate_name(name: str) -> bool: - match_result = re.match(r"[_a-zA-Z]\w*", name) - return match_result is not None - - -T_ParentService = TypeVar("T_ParentService", bound=Optional[Service], covariant=True) -T_ChildService = TypeVar("T_ChildService", bound="SubServiceOwner", covariant=True) - - -class SubServiceOwner( - Service[T_ParentService, T_ChildService], ISubServiceOwner[T_ChildService], ABC -): - def __init__(self): - super().__init__() - self._subservices: dict[str, T_ChildService] = {} - - @abstractmethod - def _make_subservice(self, name: str) -> T_ChildService: - raise NotImplementedError() - - @property - def children(self) -> Collection[T_ChildService]: - return self._subservices.values() - - def create_subservice(self, name: str) -> T_ChildService: - if not _validate_name(name): - raise AccessControlError(f"invalid name: {name}") - - if name in self._subservices: - raise AccessControlError( - f"subservice already exists: {self.qualified_name}.{name}" - ) - - service = self._make_subservice(name) - self._subservices[name] = service - logger.trace( - f"created subservice {service.qualified_name}" - f" (parent: {self.qualified_name})" - ) - return self._subservices[name] diff --git a/src/nonebot_plugin_access_control/subject/__init__.py b/src/nonebot_plugin_access_control/subject/__init__.py index 71fc199..8f9661b 100644 --- a/src/nonebot_plugin_access_control/subject/__init__.py +++ b/src/nonebot_plugin_access_control/subject/__init__.py @@ -1 +1 @@ -from .extractor import * +from . import extractor # noqa diff --git a/src/nonebot_plugin_access_control/subject/extractor/__init__.py b/src/nonebot_plugin_access_control/subject/extractor/__init__.py index 140b15c..b869d9b 100644 --- a/src/nonebot_plugin_access_control/subject/extractor/__init__.py +++ b/src/nonebot_plugin_access_control/subject/extractor/__init__.py @@ -1,21 +1,15 @@ -from nonebot import require - -require("nonebot_plugin_session") - from collections.abc import Sequence -from nonebot import Bot, logger -from nonebot.internal.adapter import Event +from nonebot import logger +from nonebot_plugin_access_control_api.subject.extractor import extractor_chain from nonebot_plugin_session import Session -from ..manager import SubjectManager -from .builtin.qqguild import extract_qqguild_role from .builtin.kaiheila import extract_kaiheila_role -from .base import T_SubjectExtractor, SubjectExtractorChain from .builtin.onebot_v11 import extract_onebot_v11_group_role +from .builtin.qqguild import extract_qqguild_role from .builtin.session import extract_by_session, extract_from_session -extractor_chain = SubjectExtractorChain( +extractor_chain.add_first( extract_by_session, extract_onebot_v11_group_role, extract_qqguild_role, @@ -23,28 +17,10 @@ ) -def add_subject_extractor(extractor: T_SubjectExtractor) -> T_SubjectExtractor: - extractor_chain.add(extractor) - return extractor - - -def extract_subjects(bot: Bot, event: Event) -> Sequence[str]: - manager = SubjectManager() - extractor_chain(bot, event, manager) - sbj = [x.content for x in manager.subjects] - logger.debug("subjects: " + ", ".join(sbj)) - return sbj - - def extract_subjects_from_session(session: Session) -> Sequence[str]: sbj = [x.content for x in extract_from_session(session)] logger.debug("subjects: " + ", ".join(sbj)) return sbj -__all__ = ( - "T_SubjectExtractor", - "add_subject_extractor", - "extract_subjects", - "extract_subjects_from_session", -) +__all__ = ("extract_subjects_from_session",) diff --git a/src/nonebot_plugin_access_control/subject/extractor/base.py b/src/nonebot_plugin_access_control/subject/extractor/base.py deleted file mode 100644 index 83760f7..0000000 --- a/src/nonebot_plugin_access_control/subject/extractor/base.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Union, Callable -from collections.abc import Sequence - -from nonebot import Bot, logger -from nonebot.internal.adapter import Event - -from ..model import SubjectModel -from ..manager import SubjectManager -from ...utils.call_with_params import call_with_params - -T_SubjectExtractor = Callable[[...], Union[Sequence[SubjectModel], None]] - - -class SubjectExtractorChain: - def __init__(self, *extractors: T_SubjectExtractor): - self.extractors = list(extractors) - - def __call__(self, bot: Bot, event: Event, manager: SubjectManager): - for ext in self.extractors: - result = call_with_params( - ext, - { - "bot": bot, - "event": event, - "current": manager.subjects, - "manager": manager, - }, - ) - # 如果返回值是Sequence[SubjectModel] - if ( - result is not None - and isinstance(result, (list, tuple)) - and all(isinstance(x, SubjectModel) for x in result) - ): - manager.replace(*result) - - logger.trace("current subjects: " + ", ".join(map(str, manager.subjects))) - - def add(self, extractor: T_SubjectExtractor): - self.extractors.append(extractor) diff --git a/src/nonebot_plugin_access_control/subject/extractor/builtin/__init__.py b/src/nonebot_plugin_access_control/subject/extractor/builtin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/nonebot_plugin_access_control/subject/extractor/builtin/kaiheila.py b/src/nonebot_plugin_access_control/subject/extractor/builtin/kaiheila.py index f47e077..b0a4c8b 100644 --- a/src/nonebot_plugin_access_control/subject/extractor/builtin/kaiheila.py +++ b/src/nonebot_plugin_access_control/subject/extractor/builtin/kaiheila.py @@ -3,8 +3,8 @@ from nonebot import Bot from nonebot.internal.adapter import Event -from ...model import SubjectModel -from ...manager import SubjectManager +from nonebot_plugin_access_control_api.subject.model import SubjectModel +from nonebot_plugin_access_control_api.subject.manager import SubjectManager if TYPE_CHECKING: from nonebot.adapters.kaiheila.event import User diff --git a/src/nonebot_plugin_access_control/subject/extractor/builtin/onebot_v11.py b/src/nonebot_plugin_access_control/subject/extractor/builtin/onebot_v11.py index 1aaaec8..9b944c5 100644 --- a/src/nonebot_plugin_access_control/subject/extractor/builtin/onebot_v11.py +++ b/src/nonebot_plugin_access_control/subject/extractor/builtin/onebot_v11.py @@ -3,8 +3,8 @@ from nonebot import Bot from nonebot.internal.adapter import Event -from ...model import SubjectModel -from ...manager import SubjectManager +from nonebot_plugin_access_control_api.subject.model import SubjectModel +from nonebot_plugin_access_control_api.subject.manager import SubjectManager if TYPE_CHECKING: from nonebot.adapters.onebot.v11.event import Sender diff --git a/src/nonebot_plugin_access_control/subject/extractor/builtin/qqguild.py b/src/nonebot_plugin_access_control/subject/extractor/builtin/qqguild.py index 0b4bea6..0b8a040 100644 --- a/src/nonebot_plugin_access_control/subject/extractor/builtin/qqguild.py +++ b/src/nonebot_plugin_access_control/subject/extractor/builtin/qqguild.py @@ -3,8 +3,8 @@ from nonebot import Bot from nonebot.internal.adapter import Event -from ...model import SubjectModel -from ...manager import SubjectManager +from nonebot_plugin_access_control_api.subject.model import SubjectModel +from nonebot_plugin_access_control_api.subject.manager import SubjectManager if TYPE_CHECKING: from nonebot.adapters.qqguild.event import Member diff --git a/src/nonebot_plugin_access_control/subject/extractor/builtin/session.py b/src/nonebot_plugin_access_control/subject/extractor/builtin/session.py index bfc7b6d..c5b9ca2 100644 --- a/src/nonebot_plugin_access_control/subject/extractor/builtin/session.py +++ b/src/nonebot_plugin_access_control/subject/extractor/builtin/session.py @@ -4,9 +4,9 @@ from nonebot.internal.adapter import Event from nonebot_plugin_session import Session, SessionLevel, extract_session -from ...model import SubjectModel -from ...manager import SubjectManager -from ....utils.superuser import is_superuser +from nonebot_plugin_access_control_api.subject.model import SubjectModel +from nonebot_plugin_access_control_api.subject.manager import SubjectManager +from nonebot_plugin_access_control_api.utils.superuser import is_superuser OFFER_BY = "nonebot_plugin_access_control" diff --git a/src/nonebot_plugin_access_control/subject/manager.py b/src/nonebot_plugin_access_control/subject/manager.py deleted file mode 100644 index 0ed7589..0000000 --- a/src/nonebot_plugin_access_control/subject/manager.py +++ /dev/null @@ -1,54 +0,0 @@ -from copy import copy -from collections.abc import Sequence - -from .model import SubjectModel - - -class SubjectManager: - def __init__(self): - self.subjects: Sequence[SubjectModel] = [] - - def index_of(self, tag: str) -> int: - for i in range(len(self.subjects)): - if self.subjects[i].tag == tag: - return i - return -1 - - def append(self, *subject: SubjectModel): - self.subjects = [*self.subjects, *subject] - - def insert_after(self, tag: str, *subject: SubjectModel): - idx = self.index_of(tag) - - if idx != -1: - if idx != len(self.subjects) - 1: - self.subjects = [ - *self.subjects[: idx + 1], - *subject, - *self.subjects[idx + 1 :], - ] - else: - self.subjects = [*self.subjects, *subject] - else: - self.subjects = [*self.subjects, *subject] - - def insert_before(self, tag: str, *subject: SubjectModel): - idx = self.index_of(tag) - - if idx != -1: - self.subjects = [ - *self.subjects[:idx], - *subject, - *self.subjects[idx:], - ] - else: - self.subjects = [*subject, *self.subjects] - - def replace(self, *subject: SubjectModel): - self.subjects = copy(subject) - - def remove(self, tag: str): - idx = self.index_of(tag) - if idx != -1: - self.subjects = [*self.subjects] - self.subjects.pop(idx) diff --git a/src/nonebot_plugin_access_control/subject/model.py b/src/nonebot_plugin_access_control/subject/model.py deleted file mode 100644 index 298dd81..0000000 --- a/src/nonebot_plugin_access_control/subject/model.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Optional, NamedTuple - - -class SubjectModel(NamedTuple): - content: str - offer_by: str - tag: Optional[str] - - def __str__(self): - return self.content - - def __repr__(self): - return f"{self.content} (tag: {self.tag}, offer by: {self.offer_by})" diff --git a/src/nonebot_plugin_access_control/utils/call_with_params.py b/src/nonebot_plugin_access_control/utils/call_with_params.py deleted file mode 100644 index 4b10d11..0000000 --- a/src/nonebot_plugin_access_control/utils/call_with_params.py +++ /dev/null @@ -1,11 +0,0 @@ -from inspect import signature - - -def call_with_params(func, kwargs): - filtered_kwargs = {} - - sig = signature(func) - for p in sig.parameters: - filtered_kwargs[p] = kwargs[p] - - return func(**filtered_kwargs) diff --git a/src/nonebot_plugin_access_control/utils/superuser.py b/src/nonebot_plugin_access_control/utils/superuser.py deleted file mode 100644 index 3707c4c..0000000 --- a/src/nonebot_plugin_access_control/utils/superuser.py +++ /dev/null @@ -1,10 +0,0 @@ -from nonebot import get_driver - -superusers = get_driver().config.superusers - - -def is_superuser(user_id: str, bot_type: str) -> bool: - return ( - f"{bot_type.split(maxsplit=1)[0].lower()}:{user_id}" in superusers - or user_id in superusers # 兼容旧配置 - )