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.
- bililive/__init__.py +35 -0
- bililive/__main__.py +4 -0
- bililive/cli/__init__.py +19 -0
- bililive/cli/bot.py +38 -0
- bililive/cli/utils.py +52 -0
- bililive/compat.py +42 -0
- bililive/config.py +123 -0
- bililive/database/__init__.py +1 -0
- bililive/database/db.py +282 -0
- bililive/database/models.py +74 -0
- bililive/libs/__init__.py +1 -0
- bililive/libs/dynamic/__init__.py +49 -0
- bililive/libs/dynamic/card.py +53 -0
- bililive/libs/dynamic/desc.py +31 -0
- bililive/libs/dynamic/display.py +50 -0
- bililive/libs/dynamic/user_profile.py +52 -0
- bililive/plugins/__init__.py +11 -0
- bililive/plugins/at/__init__.py +0 -0
- bililive/plugins/at/at_off.py +41 -0
- bililive/plugins/at/at_on.py +41 -0
- bililive/plugins/auto_agree.py +21 -0
- bililive/plugins/auto_delete.py +14 -0
- bililive/plugins/dynamic/__init__.py +0 -0
- bililive/plugins/dynamic/dynamic_off.py +36 -0
- bililive/plugins/dynamic/dynamic_on.py +36 -0
- bililive/plugins/help.py +24 -0
- bililive/plugins/live/__init__.py +0 -0
- bililive/plugins/live/live_now.py +22 -0
- bililive/plugins/live/live_off.py +36 -0
- bililive/plugins/live/live_on.py +36 -0
- bililive/plugins/permission/__init__.py +0 -0
- bililive/plugins/permission/permission_off.py +24 -0
- bililive/plugins/permission/permission_on.py +24 -0
- bililive/plugins/pusher/__init__.py +1 -0
- bililive/plugins/pusher/dynamic_pusher.py +151 -0
- bililive/plugins/pusher/live_pusher.py +76 -0
- bililive/plugins/sub/__init__.py +0 -0
- bililive/plugins/sub/add_sub.py +70 -0
- bililive/plugins/sub/delete_sub.py +37 -0
- bililive/plugins/sub/sub_list.py +43 -0
- bililive/utils/__init__.py +307 -0
- bililive/utils/browser.py +243 -0
- bililive/utils/captcha_solver.py +130 -0
- bililive/utils/fonts_provider.py +28 -0
- bililive/utils/mobile.js +239 -0
- bililive/version.py +4 -0
- nonebot_plugin_bililive/__init__.py +25 -0
- nonebot_plugin_bililive-2.0.1.dist-info/METADATA +199 -0
- nonebot_plugin_bililive-2.0.1.dist-info/RECORD +52 -0
- nonebot_plugin_bililive-2.0.1.dist-info/WHEEL +4 -0
- nonebot_plugin_bililive-2.0.1.dist-info/entry_points.txt +5 -0
- 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
bililive/cli/__init__.py
ADDED
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
|
bililive/database/db.py
ADDED
|
@@ -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
|