Skip to content

Commit 856f319

Browse files
committed
New feature: Persistence
1 parent ad67a5b commit 856f319

File tree

7 files changed

+138
-11
lines changed

7 files changed

+138
-11
lines changed

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,23 @@ from nonebot_plugin_limiter import (
6363
GlobalScope, UserScope, SceneScope, UserSceneScope, PrivateScope, PublicScope
6464
)
6565
```
66+
配置项
67+
```bash
68+
COOLDOWN_ENABLE_PERSISTENCE = false # 开启持久化
69+
COOLDOWN_SAVE_INTERVAL = 60 # 开启持久化后保存时间,单位为秒
70+
```
71+
修改持久化本地存储目录请参考 localstore [插件配置方法](https://github.com/nonebot/plugin-localstore?tab=readme-ov-file#%E9%85%8D%E7%BD%AE%E9%A1%B9) 更改 `LOCALSTORE_PLUGIN_DATA_DIR`
6672

6773
## 快速上手
6874

6975
基本使用方式
7076
```python
77+
from nonebot import require
78+
from nonebot_plugin_uninfo import Uninfo
7179
from nonebot.permission import SUPERUSER
72-
from nonebot_plugin_limiter import UserScope
80+
81+
require("nonebot_plugin_limiter")
82+
from nonebot_plugin_limiter import UserScope, Cooldown
7383

7484
matcher = on()
7585
@matcher.handle(parameterless=[
@@ -93,7 +103,7 @@ from datetime import timedelta # 支持传入 timedelta
93103

94104
# 同步样例。获取限制对象的唯一 ID
95105
def get_entity_id(bot: Bot, event: Event): # 可依赖注入
96-
if any_condition:
106+
if <any_condition>:
97107
return "__bypass" # 返回 `__bypass` 限制器将不会约束该对象的使用量
98108
return event.get_user_id()
99109

@@ -139,16 +149,16 @@ dailysign = on_startswith("签到")
139149
),
140150
])
141151
async def _():
142-
await dailysign.finish("签到成功!")
152+
await dailysign.finish("签到成功")
143153
```
144154

145155
### Feature
146156
- [x] 固定窗口
147157
- [x] 滑动窗口
148158
- [ ] 漏桶
149159
- [ ] 令牌桶
150-
- [ ] reject handler 依赖注入
151-
- [ ] 本地持久化状态
160+
- [ ] reject 依赖注入
161+
- [x] 本地持久化状态
152162

153163
## 鸣谢
154164
本插件部分代码参考了 [nonebot/adapter-onebot](https://github.com/nonebot/adapter-onebot)`Cooldown` [实现](https://github.com/nonebot/adapter-onebot/blob/51294404cc8bf0b3d03008e09f34d3dd1a6acfd8/nonebot/adapters/onebot/v11/helpers.py#L224) ,在此表示感谢

nonebot_plugin_limiter/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
require("nonebot_plugin_uninfo")
55
require("nonebot_plugin_alconna")
6+
require("nonebot_plugin_localstore")
67
require("nonebot_plugin_apscheduler")
78
from .config import Config
89

@@ -11,7 +12,7 @@
1112
description="通用命令冷却限制器",
1213
usage="",
1314
type="library",
14-
homepage="https://github.com/MiddleRed/nonebot-plugin-cooldown",
15+
homepage="https://github.com/MiddleRed/nonebot-plugin-limiter",
1516
config=Config,
1617
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna", "nonebot_plugin_uninfo"),
1718
extra={"author": "MiddleRed <[email protected]>"},
@@ -26,3 +27,5 @@
2627
from .entity import SceneScope as SceneScope
2728
from .entity import UserSceneScope as UserSceneScope
2829
from .entity import UserScope as UserScope
30+
from .persist import load_usage_data as load_usage_data
31+
from .persist import save_usage_data as save_usage_data

nonebot_plugin_limiter/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44

55
class Config(BaseModel):
6-
cooldown_remove_expired_interval: int = 60
7-
# TODO: cooldown_enable_persistence: bool | None = False
6+
cooldown_enable_persistence: bool | None = False
7+
cooldown_save_interval: int | None = 60
88

99

1010
plugin_config: Config = get_plugin_config(Config)

nonebot_plugin_limiter/entity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import abstractmethod
22
from typing import Literal
33

4-
from nonebot.internal.adapter import Bot, Event
4+
from nonebot.adapters import Bot, Event
55
from nonebot.permission import Permission
66
from nonebot_plugin_uninfo import get_session
77

nonebot_plugin_limiter/persist.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from collections import deque
2+
from datetime import datetime
3+
import json
4+
from pathlib import Path
5+
6+
from nonebot import get_driver
7+
from nonebot.adapters import Bot
8+
from nonebot.log import logger
9+
from nonebot_plugin_apscheduler import scheduler
10+
import nonebot_plugin_localstore as store
11+
from pydantic import BaseModel, ValidationError
12+
13+
from .config import plugin_config
14+
from .cooldown import FixWindowUsage, SlidingWindowUsage, _FixWindowCooldownDict, _SlidingWindowCooldownDict, _tz
15+
16+
driver = get_driver()
17+
plugin_data_file: Path = store.get_plugin_data_file("limiter_data.json")
18+
19+
20+
class PersistData(BaseModel):
21+
class FixWindowSet(BaseModel):
22+
start_time: int
23+
available: int
24+
25+
fix_window: dict[str, dict[str, FixWindowSet]] | None = None
26+
27+
class SlidingWindowSet(BaseModel):
28+
timestamps: list[int]
29+
30+
sliding_window: dict[str, dict[str, SlidingWindowSet]] | None = None
31+
32+
33+
def load_usage_data() -> None:
34+
"""加载本地存储的用量数据"""
35+
36+
if not plugin_data_file.exists():
37+
return
38+
try:
39+
data = PersistData.model_validate_json(plugin_data_file.read_text())
40+
except ValidationError:
41+
logger.warning("Failed to load previous usage data (ValidationError), will ignore it.")
42+
return
43+
44+
if data.fix_window is not None:
45+
for name, usage_set in data.fix_window.items():
46+
if name not in _FixWindowCooldownDict:
47+
_FixWindowCooldownDict[name] = {}
48+
bucket = _FixWindowCooldownDict[name]
49+
for _id, usage in usage_set.items():
50+
bucket[_id] = FixWindowUsage(
51+
start_time=datetime.fromtimestamp(usage.start_time, tz=_tz),
52+
available=usage.available,
53+
)
54+
55+
if data.sliding_window is not None:
56+
for name, usage_set in data.sliding_window.items():
57+
if name not in _SlidingWindowCooldownDict:
58+
_SlidingWindowCooldownDict[name] = {}
59+
bucket = _SlidingWindowCooldownDict[name]
60+
for _id, usage in usage_set.items():
61+
bucket[_id] = SlidingWindowUsage(timestamps=deque(datetime.fromtimestamp(t, tz=_tz) for t in usage.timestamps))
62+
63+
logger.info("Loaded previous usage data.")
64+
65+
66+
def save_usage_data() -> None:
67+
"""保存用量数据到本地"""
68+
69+
j = {"fix_window": {}, "sliding_window": {}}
70+
71+
for name, usage_set in _FixWindowCooldownDict.items():
72+
j["fix_window"][name] = {
73+
_id: {
74+
"start_time": int(usage.start_time.timestamp()),
75+
"available": usage.available,
76+
}
77+
for _id, usage in usage_set.items()
78+
}
79+
80+
for name, usage_set in _SlidingWindowCooldownDict.items():
81+
j["sliding_window"][name] = {
82+
_id: {
83+
"timestamps": [int(t.timestamp()) for t in usage.timestamps],
84+
}
85+
for _id, usage in usage_set.items()
86+
}
87+
88+
plugin_data_file.write_text(json.dumps(j))
89+
90+
@driver.on_startup
91+
def _startup() -> None:
92+
if not plugin_config.cooldown_enable_persistence:
93+
return
94+
95+
load_usage_data()
96+
scheduler.add_job(save_usage_data, "interval", seconds=plugin_config.cooldown_save_interval)
97+
if not scheduler.running:
98+
scheduler.start()
99+
100+
101+
@driver.on_bot_disconnect
102+
def _disconnect_save(bot: Bot) -> None:
103+
if not plugin_config.cooldown_enable_persistence:
104+
return
105+
106+
save_usage_data()
107+
108+
109+
@driver.on_shutdown
110+
def _shutdown_save() -> None:
111+
if not plugin_config.cooldown_enable_persistence:
112+
return
113+
114+
save_usage_data()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ requires-python = ">=3.10,<3.13"
99
dependencies = [
1010
"nonebot2>=2.4.2,<3.0.0",
1111

12-
# "nonebot-plugin-localstore>=0.7.4,<1.0.0", # TODO: 本地持久化
12+
"nonebot-plugin-localstore>=0.7.4,<1.0.0",
1313
"nonebot-plugin-apscheduler>=0.5.0,<1.0.0",
1414
"nonebot-plugin-uninfo>=0.7.3,<1.0.0",
1515
"nonebot-plugin-alconna>=0.57.2,<1.0.0",

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)