nonebot-plugin-bililive 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. bililive/__init__.py +35 -0
  2. bililive/__main__.py +4 -0
  3. bililive/cli/__init__.py +19 -0
  4. bililive/cli/bot.py +38 -0
  5. bililive/cli/utils.py +52 -0
  6. bililive/compat.py +42 -0
  7. bililive/config.py +123 -0
  8. bililive/database/__init__.py +1 -0
  9. bililive/database/db.py +282 -0
  10. bililive/database/models.py +74 -0
  11. bililive/libs/__init__.py +1 -0
  12. bililive/libs/dynamic/__init__.py +49 -0
  13. bililive/libs/dynamic/card.py +53 -0
  14. bililive/libs/dynamic/desc.py +31 -0
  15. bililive/libs/dynamic/display.py +50 -0
  16. bililive/libs/dynamic/user_profile.py +52 -0
  17. bililive/plugins/__init__.py +11 -0
  18. bililive/plugins/at/__init__.py +0 -0
  19. bililive/plugins/at/at_off.py +41 -0
  20. bililive/plugins/at/at_on.py +41 -0
  21. bililive/plugins/auto_agree.py +21 -0
  22. bililive/plugins/auto_delete.py +14 -0
  23. bililive/plugins/dynamic/__init__.py +0 -0
  24. bililive/plugins/dynamic/dynamic_off.py +36 -0
  25. bililive/plugins/dynamic/dynamic_on.py +36 -0
  26. bililive/plugins/help.py +24 -0
  27. bililive/plugins/live/__init__.py +0 -0
  28. bililive/plugins/live/live_now.py +22 -0
  29. bililive/plugins/live/live_off.py +36 -0
  30. bililive/plugins/live/live_on.py +36 -0
  31. bililive/plugins/permission/__init__.py +0 -0
  32. bililive/plugins/permission/permission_off.py +24 -0
  33. bililive/plugins/permission/permission_on.py +24 -0
  34. bililive/plugins/pusher/__init__.py +1 -0
  35. bililive/plugins/pusher/dynamic_pusher.py +151 -0
  36. bililive/plugins/pusher/live_pusher.py +76 -0
  37. bililive/plugins/sub/__init__.py +0 -0
  38. bililive/plugins/sub/add_sub.py +70 -0
  39. bililive/plugins/sub/delete_sub.py +37 -0
  40. bililive/plugins/sub/sub_list.py +43 -0
  41. bililive/utils/__init__.py +307 -0
  42. bililive/utils/browser.py +243 -0
  43. bililive/utils/captcha_solver.py +130 -0
  44. bililive/utils/fonts_provider.py +28 -0
  45. bililive/utils/mobile.js +239 -0
  46. bililive/version.py +4 -0
  47. nonebot_plugin_bililive/__init__.py +25 -0
  48. nonebot_plugin_bililive-2.0.1.dist-info/METADATA +199 -0
  49. nonebot_plugin_bililive-2.0.1.dist-info/RECORD +52 -0
  50. nonebot_plugin_bililive-2.0.1.dist-info/WHEEL +4 -0
  51. nonebot_plugin_bililive-2.0.1.dist-info/entry_points.txt +5 -0
  52. nonebot_plugin_bililive-2.0.1.dist-info/licenses/LICENSE +661 -0
bililive/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ from nonebot.plugin import PluginMetadata
2
+ from nonebot.plugin.manager import PluginLoader
3
+
4
+ from .compat import patch_httpx_compat
5
+ from .config import Config
6
+
7
+ patch_httpx_compat()
8
+
9
+ def bootstrap_plugin(force: bool = False):
10
+ if force or isinstance(globals()["__loader__"], PluginLoader):
11
+ from .utils import on_startup
12
+
13
+ on_startup()
14
+
15
+ from . import plugins # noqa: F401
16
+
17
+
18
+ bootstrap_plugin()
19
+
20
+ from .version import VERSION, __version__ # noqa: F401
21
+
22
+ __plugin_meta__ = PluginMetadata(
23
+ name="BiliLive",
24
+ description="将B站UP主的动态和直播信息推送至QQ",
25
+ usage="https://github.com/Akiyy-dev/nonebot-plugin-bililive#readme",
26
+ homepage="https://github.com/Akiyy-dev/nonebot-plugin-bililive",
27
+ type="application",
28
+ config=Config,
29
+ supported_adapters={"~onebot.v11"},
30
+ extra={
31
+ "author": "SK-415",
32
+ "version": __version__,
33
+ "priority": 1,
34
+ },
35
+ )
bililive/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,19 @@
1
+ import click
2
+
3
+ from .utils import create_env
4
+
5
+
6
+ @click.group()
7
+ def main():
8
+ pass
9
+
10
+
11
+ @click.command()
12
+ def run():
13
+ create_env()
14
+ from .bot import run
15
+
16
+ run()
17
+
18
+
19
+ main.add_command(run)
bililive/cli/bot.py ADDED
@@ -0,0 +1,38 @@
1
+ import sys
2
+ from os import path
3
+
4
+ import nonebot
5
+ from nonebot.adapters.onebot.v11 import Adapter
6
+ from nonebot.log import default_format, logger
7
+
8
+ nonebot.init()
9
+ driver = nonebot.get_driver()
10
+ driver.register_adapter(Adapter)
11
+ app = nonebot.get_asgi()
12
+
13
+ # 删除插件缓存导入,否则 nonebot 导入时会忽略
14
+ for module_name in (
15
+ "nonebot_plugin_bililive",
16
+ "bililive",
17
+ ):
18
+ sys.modules.pop(module_name, None)
19
+ nonebot.load_plugin("nonebot_plugin_bililive")
20
+
21
+ # Modify some config / config depends on loaded configs
22
+ #
23
+ # config = nonebot.get_driver().config
24
+ # do something...
25
+
26
+ logger.add(
27
+ path.join("log", "error.log"),
28
+ rotation="00:00",
29
+ retention="1 week",
30
+ diagnose=False,
31
+ level="ERROR",
32
+ format=default_format,
33
+ encoding="utf-8",
34
+ )
35
+
36
+
37
+ def run():
38
+ nonebot.run(app="bililive.cli.bot:app")
bililive/cli/utils.py ADDED
@@ -0,0 +1,52 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ import dotenv
5
+
6
+ env = {
7
+ "HOST": "127.0.0.1",
8
+ "PORT": "8080",
9
+ "SECRET": "",
10
+ "ACCESS_TOKEN": "",
11
+ "SUPERUSERS": [],
12
+ "COMMAND_START": [""],
13
+ "BILILIVE_DIR": "./data/",
14
+ }
15
+
16
+
17
+ def create_env():
18
+ file_path = Path("./.env.prod").resolve()
19
+ if file_path.exists():
20
+ return
21
+
22
+ if any(Path.cwd().iterdir()):
23
+ print("文件夹不为空,请更换空文件夹后重试")
24
+ import sys
25
+
26
+ sys.exit()
27
+
28
+ while True:
29
+ try:
30
+ superusers = click.prompt(
31
+ "主人QQ号 (多个使用空格分开)", default="", show_default=False
32
+ )
33
+ superusers = [int(superuser) for superuser in superusers.split()]
34
+ env["SUPERUSERS"] = superusers
35
+ break
36
+ except ValueError:
37
+ print("输入格式有误,请重新输入")
38
+
39
+ with open(".env.prod", "w"):
40
+ pass
41
+
42
+ for key, value in env.items():
43
+ dotenv.set_key(
44
+ file_path,
45
+ key,
46
+ str(value).replace("'", '"').replace(" ", ""),
47
+ quote_mode="never",
48
+ )
49
+
50
+
51
+ if __name__ == "__main__":
52
+ create_env()
bililive/compat.py ADDED
@@ -0,0 +1,42 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
+
4
+ import httpx
5
+ import httpx._types as httpx_types
6
+
7
+
8
+ def _normalize_proxy_value(proxies: Any):
9
+ if proxies is None:
10
+ return None
11
+ if isinstance(proxies, Mapping):
12
+ if "all://" in proxies:
13
+ return proxies["all://"]
14
+ for value in proxies.values():
15
+ if value is not None:
16
+ return value
17
+ return None
18
+ return proxies
19
+
20
+
21
+ def patch_httpx_compat():
22
+ if not hasattr(httpx_types, "ProxiesTypes") and hasattr(httpx_types, "ProxyTypes"):
23
+ httpx_types.ProxiesTypes = httpx_types.ProxyTypes
24
+
25
+ if getattr(httpx.AsyncClient, "__bililive_compat_patch__", False):
26
+ return
27
+
28
+ original_async_client = httpx.AsyncClient
29
+
30
+ class CompatAsyncClient(original_async_client):
31
+ __bililive_compat_patch__ = True
32
+
33
+ def __init__(self, *args, proxies=Ellipsis, **kwargs):
34
+ if proxies is not Ellipsis and "proxy" not in kwargs:
35
+ kwargs["proxy"] = _normalize_proxy_value(proxies)
36
+ super().__init__(*args, **kwargs)
37
+
38
+ CompatAsyncClient.__name__ = original_async_client.__name__
39
+ CompatAsyncClient.__qualname__ = original_async_client.__qualname__
40
+ CompatAsyncClient.__module__ = original_async_client.__module__
41
+
42
+ httpx.AsyncClient = CompatAsyncClient
bililive/config.py ADDED
@@ -0,0 +1,123 @@
1
+
2
+ from loguru import logger
3
+ from nonebot import get_driver
4
+ from pydantic import (
5
+ AliasChoices,
6
+ BaseModel,
7
+ ConfigDict,
8
+ Field,
9
+ ValidationInfo,
10
+ field_validator,
11
+ )
12
+
13
+
14
+ def compat_field(default, new_name: str, legacy_name: str):
15
+ return Field(
16
+ default=default,
17
+ validation_alias=AliasChoices(new_name, legacy_name),
18
+ )
19
+
20
+
21
+ # 其他地方出现的类似 from .. import config,均是从 __init__.py 导入的 Config 实例
22
+ class Config(BaseModel):
23
+ model_config = ConfigDict(extra="ignore")
24
+
25
+ fastapi_reload: bool = False
26
+ bililive_dir: str | None = compat_field(None, "bililive_dir", "haruka_dir")
27
+ bililive_to_me: bool = compat_field(True, "bililive_to_me", "haruka_to_me")
28
+ bililive_live_off_notify: bool = compat_field(
29
+ False,
30
+ "bililive_live_off_notify",
31
+ "haruka_live_off_notify",
32
+ )
33
+ bililive_proxy: str | None = compat_field(
34
+ None,
35
+ "bililive_proxy",
36
+ "haruka_proxy",
37
+ )
38
+ bililive_interval: int = compat_field(10, "bililive_interval", "haruka_interval")
39
+ bililive_live_interval: int = compat_field(
40
+ 10,
41
+ "bililive_live_interval",
42
+ "haruka_live_interval",
43
+ )
44
+ bililive_dynamic_interval: int = compat_field(
45
+ 0,
46
+ "bililive_dynamic_interval",
47
+ "haruka_dynamic_interval",
48
+ )
49
+ bililive_dynamic_at: bool = compat_field(
50
+ False,
51
+ "bililive_dynamic_at",
52
+ "haruka_dynamic_at",
53
+ )
54
+ bililive_screenshot_style: str = compat_field(
55
+ "mobile",
56
+ "bililive_screenshot_style",
57
+ "haruka_screenshot_style",
58
+ )
59
+ bililive_captcha_address: str = compat_field(
60
+ "https://captcha-cd.ngworks.cn",
61
+ "bililive_captcha_address",
62
+ "haruka_captcha_address",
63
+ )
64
+ bililive_captcha_token: str = compat_field(
65
+ "bililive",
66
+ "bililive_captcha_token",
67
+ "haruka_captcha_token",
68
+ )
69
+ bililive_browser_ua: str | None = compat_field(
70
+ None,
71
+ "bililive_browser_ua",
72
+ "haruka_browser_ua",
73
+ )
74
+ bililive_dynamic_timeout: int = compat_field(
75
+ 30,
76
+ "bililive_dynamic_timeout",
77
+ "haruka_dynamic_timeout",
78
+ )
79
+ bililive_dynamic_font_source: str = compat_field(
80
+ "system",
81
+ "bililive_dynamic_font_source",
82
+ "haruka_dynamic_font_source",
83
+ )
84
+ bililive_dynamic_font: str | None = compat_field(
85
+ "Noto Sans CJK SC",
86
+ "bililive_dynamic_font",
87
+ "haruka_dynamic_font",
88
+ )
89
+ bililive_dynamic_big_image: bool = compat_field(
90
+ False,
91
+ "bililive_dynamic_big_image",
92
+ "haruka_dynamic_big_image",
93
+ )
94
+ bililive_command_prefix: str = compat_field(
95
+ "",
96
+ "bililive_command_prefix",
97
+ "haruka_command_prefix",
98
+ )
99
+
100
+ @field_validator(
101
+ "bililive_interval", "bililive_live_interval", "bililive_dynamic_interval"
102
+ )
103
+ @classmethod
104
+ def non_negative(cls, v: int, info: ValidationInfo):
105
+ """定时器为负返回默认值"""
106
+ default = cls.model_fields[info.field_name].default
107
+ return default if v < 1 else v
108
+
109
+ @field_validator("bililive_screenshot_style")
110
+ @classmethod
111
+ def screenshot_style(cls, v: str):
112
+ if v != "mobile":
113
+ logger.warning("截图样式目前只支持 mobile,pc 样式现已被弃用")
114
+ return "mobile"
115
+
116
+
117
+ global_config = get_driver().config
118
+ if hasattr(global_config, "model_dump"):
119
+ plugin_config = Config.model_validate(global_config.model_dump())
120
+ elif hasattr(global_config, "dict"):
121
+ plugin_config = Config.model_validate(global_config.dict())
122
+ else:
123
+ plugin_config = Config.model_validate(global_config)
@@ -0,0 +1 @@
1
+ from .db import DB, dynamic_offset # noqa: F401
@@ -0,0 +1,282 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from nonebot import get_driver
5
+ from nonebot.log import logger
6
+ from packaging.version import Version as version_parser
7
+ from tortoise import Tortoise
8
+ from tortoise.connection import connections
9
+
10
+ from ..utils import get_path
11
+ from ..version import VERSION as APP_VERSION
12
+ from .models import Group, Sub, User, Version
13
+
14
+ uid_list = {"live": {"list": [], "index": 0}, "dynamic": {"list": [], "index": 0}}
15
+ dynamic_offset = {}
16
+
17
+
18
+ class DB:
19
+ """数据库交互类,与增删改查无关的部分不应该在这里面实现"""
20
+
21
+ @classmethod
22
+ async def init(cls):
23
+ """初始化数据库"""
24
+ config = {
25
+ "connections": {
26
+ # "bililive": {
27
+ # "engine": "tortoise.backends.sqlite",
28
+ # "credentials": {"file_path": get_path("data.sqlite3")},
29
+ # },
30
+ "bililive": f"sqlite://{get_path('data.sqlite3')}"
31
+ },
32
+ "apps": {
33
+ "bililive_app": {
34
+ "models": ["bililive.database.models"],
35
+ "default_connection": "bililive",
36
+ }
37
+ },
38
+ }
39
+
40
+ await Tortoise.init(config)
41
+
42
+ await Tortoise.generate_schemas()
43
+ await cls.migrate()
44
+ await cls.update_uid_list()
45
+
46
+ @classmethod
47
+ async def close(cls):
48
+ await connections.close_all()
49
+
50
+ @classmethod
51
+ async def get_user(cls, **kwargs):
52
+ """获取 UP 主信息"""
53
+ return await User.get(**kwargs).first()
54
+
55
+ @classmethod
56
+ async def get_name(cls, uid) -> str | None:
57
+ """获取 UP 主昵称"""
58
+ user = await cls.get_user(uid=uid)
59
+ if user:
60
+ return user.name
61
+ return None
62
+
63
+ @classmethod
64
+ async def add_user(cls, **kwargs):
65
+ """添加 UP 主信息"""
66
+ return await User.add(**kwargs)
67
+
68
+ @classmethod
69
+ async def delete_user(cls, uid) -> bool:
70
+ """删除 UP 主信息"""
71
+ if await cls.get_sub(uid=uid):
72
+ # 还存在该 UP 主订阅,不能删除
73
+ return False
74
+ await User.delete(uid=uid)
75
+ return True
76
+
77
+ @classmethod
78
+ async def update_user(cls, uid: int, name: str) -> bool:
79
+ """更新 UP 主信息"""
80
+ if await cls.get_user(uid=uid):
81
+ await User.update({"uid": uid}, name=name)
82
+ return True
83
+ return False
84
+
85
+ @classmethod
86
+ async def get_group(cls, **kwargs):
87
+ """获取群设置"""
88
+ return await Group.get(**kwargs).first()
89
+
90
+ @classmethod
91
+ async def get_group_admin(cls, group_id) -> bool:
92
+ """获取指定群权限状态"""
93
+ group = await cls.get_group(id=group_id)
94
+ if not group:
95
+ # TODO 自定义默认状态
96
+ return True
97
+ return bool(group.admin)
98
+
99
+ @classmethod
100
+ async def add_group(cls, **kwargs):
101
+ """创建群设置"""
102
+ return await Group.add(**kwargs)
103
+
104
+ @classmethod
105
+ async def delete_group(cls, id) -> bool:
106
+ """删除群设置"""
107
+ if await cls.get_sub(type="group", type_id=id):
108
+ # 当前群还有订阅,不能删除
109
+ return False
110
+ await Group.delete(id=id)
111
+ return True
112
+
113
+ @classmethod
114
+ async def set_permission(cls, id, switch) -> bool:
115
+ """设置指定群组权限"""
116
+ group = await cls.get_group(id=id)
117
+ if not group:
118
+ await cls.add_group(id=id, admin=switch)
119
+ return True
120
+ if bool(group.admin) == switch:
121
+ return False
122
+ await Group.update({"id": id}, admin=switch)
123
+ return True
124
+
125
+ @classmethod
126
+ async def get_sub(cls, **kwargs):
127
+ """获取指定位置的订阅信息"""
128
+ return await Sub.get(**kwargs).first()
129
+
130
+ @classmethod
131
+ async def get_subs(cls, **kwargs):
132
+ return await Sub.get(**kwargs)
133
+
134
+ @classmethod
135
+ async def get_push_list(cls, uid, func) -> list[Sub]:
136
+ """根据类型和 UID 获取需要推送的 QQ 列表"""
137
+ return await cls.get_subs(uid=uid, **{func: True})
138
+
139
+ @classmethod
140
+ async def get_sub_list(cls, type, type_id) -> list[Sub]:
141
+ """获取指定位置的推送列表"""
142
+ return await cls.get_subs(type=type, type_id=type_id)
143
+
144
+ @classmethod
145
+ async def add_sub(cls, *, name, **kwargs) -> bool:
146
+ """添加订阅"""
147
+ if not await Sub.add(**kwargs):
148
+ return False
149
+ await cls.add_user(uid=kwargs["uid"], name=name)
150
+ if kwargs["type"] == "group":
151
+ await cls.add_group(id=kwargs["type_id"], admin=True)
152
+ await cls.update_uid_list()
153
+ return True
154
+
155
+ @classmethod
156
+ async def delete_sub(cls, uid, type, type_id) -> bool:
157
+ """删除指定订阅"""
158
+ if await Sub.delete(uid=uid, type=type, type_id=type_id):
159
+ await cls.delete_user(uid=uid)
160
+ await cls.update_uid_list()
161
+ return True
162
+ # 订阅不存在
163
+ return False
164
+
165
+ @classmethod
166
+ async def delete_sub_list(cls, type, type_id):
167
+ """删除指定位置的推送列表"""
168
+ async for sub in Sub.get(type=type, type_id=type_id):
169
+ await cls.delete_sub(uid=sub.uid, type=sub.type, type_id=sub.type_id)
170
+ await cls.update_uid_list()
171
+
172
+ @classmethod
173
+ async def set_sub(cls, conf, switch, **kwargs):
174
+ """开关订阅设置"""
175
+ return await Sub.update(kwargs, **{conf: switch})
176
+
177
+ @classmethod
178
+ async def get_version(cls):
179
+ """获取数据库版本"""
180
+ version = await Version.first()
181
+ return version_parser(version.version) if version else None
182
+
183
+ @classmethod
184
+ async def migrate(cls):
185
+ """迁移数据库"""
186
+ DBVERSION = await cls.get_version()
187
+ # 新数据库
188
+ if not DBVERSION:
189
+ # 检查是否有旧的 json 数据库需要迁移
190
+ await cls.migrate_from_json()
191
+ await Version.add(version=str(APP_VERSION))
192
+ return
193
+ if DBVERSION != APP_VERSION:
194
+ # await cls._migrate()
195
+ await Version.update({}, version=APP_VERSION)
196
+ return
197
+
198
+ @classmethod
199
+ async def migrate_from_json(cls):
200
+ """从 TinyDB 的 config.json 迁移数据"""
201
+ json_path = Path(get_path("config.json"))
202
+ if not json_path.exists():
203
+ return
204
+
205
+ logger.info("正在从 config.json 迁移数据库")
206
+ with json_path.open("r", encoding="utf-8") as f:
207
+ old_db = json.loads(f.read())
208
+ subs: dict[int, dict] = old_db["_default"]
209
+ groups: dict[int, dict] = old_db["groups"]
210
+ for sub in subs.values():
211
+ await cls.add_sub(
212
+ uid=sub["uid"],
213
+ type=sub["type"],
214
+ type_id=sub["type_id"],
215
+ bot_id=sub["bot_id"],
216
+ name=sub["name"],
217
+ live=sub["live"],
218
+ dynamic=sub["dynamic"],
219
+ at=sub["at"],
220
+ )
221
+ for group in groups.values():
222
+ await cls.set_permission(group["group_id"], group["admin"])
223
+
224
+ json_path.rename(get_path("config.json.bak"))
225
+ logger.info("数据库迁移完成")
226
+
227
+ @classmethod
228
+ async def get_uid_list(cls, func) -> list:
229
+ """根据类型获取需要爬取的 UID 列表"""
230
+ return uid_list[func]["list"]
231
+
232
+ @classmethod
233
+ async def next_uid(cls, func):
234
+ """获取下一个要爬取的 UID"""
235
+ func = uid_list[func]
236
+ if func["list"] == []:
237
+ return None
238
+
239
+ if func["index"] >= len(func["list"]):
240
+ func["index"] = 1
241
+ return func["list"][0]
242
+ else:
243
+ index = func["index"]
244
+ func["index"] += 1
245
+ return func["list"][index]
246
+
247
+ @classmethod
248
+ async def update_uid_list(cls):
249
+ """更新需要推送的 UP 主列表"""
250
+ subs = Sub.all()
251
+ uid_list["live"]["list"] = list(
252
+ set([sub.uid async for sub in subs if sub.live])
253
+ )
254
+ uid_list["dynamic"]["list"] = list(
255
+ set([sub.uid async for sub in subs if sub.dynamic])
256
+ )
257
+
258
+ # 清除没有订阅的 offset
259
+ dynamic_offset_keys = set(dynamic_offset)
260
+ dynamic_uids = set(uid_list["dynamic"]["list"])
261
+ for uid in dynamic_offset_keys - dynamic_uids:
262
+ del dynamic_offset[uid]
263
+ for uid in dynamic_uids - dynamic_offset_keys:
264
+ dynamic_offset[uid] = -1
265
+
266
+ async def backup(self):
267
+ """备份数据库"""
268
+ pass
269
+
270
+ @classmethod
271
+ async def get_login(cls):
272
+ """获取登录信息"""
273
+ pass
274
+
275
+ @classmethod
276
+ async def update_login(cls, tokens):
277
+ """更新登录信息"""
278
+ pass
279
+
280
+
281
+ get_driver().on_startup(DB.init)
282
+ get_driver().on_shutdown(DB.close)
@@ -0,0 +1,74 @@
1
+ from tortoise.fields.data import BooleanField, CharField, IntField
2
+ from tortoise.models import Model
3
+
4
+
5
+ class BaseModel(Model):
6
+ @classmethod
7
+ def get_(cls, *args, **kwargs):
8
+ super().get(*args, **kwargs)
9
+
10
+ @classmethod
11
+ def get(cls, **kwargs):
12
+ return cls.filter(**kwargs)
13
+
14
+ @classmethod
15
+ async def add(cls, **kwargs):
16
+ pk_name = cls.describe()["pk_field"]["name"]
17
+ if pk_name == "id" and pk_name not in kwargs:
18
+ filters = kwargs
19
+ else:
20
+ filters = {pk_name: kwargs[pk_name]}
21
+ if await cls.get(**filters).exists():
22
+ return False
23
+ await cls.create(**kwargs)
24
+ return True
25
+
26
+ @classmethod
27
+ async def delete(cls, **kwargs):
28
+ query = cls.get(**kwargs)
29
+ if await query.exists():
30
+ await query.delete()
31
+ return True
32
+ return False
33
+
34
+ @classmethod
35
+ async def update(cls, q, **kwargs):
36
+ query = cls.get(**q)
37
+ if await query.exists():
38
+ await query.update(**kwargs)
39
+ return True
40
+ return False
41
+
42
+ class Meta:
43
+ abstract = True
44
+
45
+
46
+ # TODO 自定义默认权限
47
+ class Sub(BaseModel):
48
+ type = CharField(max_length=10)
49
+ type_id = IntField()
50
+ uid = IntField()
51
+ live = BooleanField() # default=True
52
+ dynamic = BooleanField() # default=True
53
+ at = BooleanField() # default=False
54
+ bot_id = IntField()
55
+
56
+
57
+ class User(BaseModel):
58
+ uid = IntField(pk=True)
59
+ name = CharField(max_length=20)
60
+
61
+
62
+ class Group(BaseModel):
63
+ id = IntField(pk=True)
64
+ admin = BooleanField() # default=True
65
+
66
+
67
+ class Version(BaseModel):
68
+ version = CharField(max_length=30)
69
+
70
+
71
+ # class Login(BaseModel):
72
+ # uid = IntField(pk=True)
73
+ # data = JSONField()
74
+ # expireed = IntField()
@@ -0,0 +1 @@
1
+ from . import dynamic # noqa