From 59c923bbc3e91ad5a3482ec81c9192297ebbebe8 Mon Sep 17 00:00:00 2001
From: Kyomotoi <kyomotoiowo@gmail.com>
Date: Mon, 13 Jun 2022 19:37:34 +0800
Subject: =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E9=87=8D=E6=9E=84=E6=8F=92?=
 =?UTF-8?q?=E4=BB=B6:=20b=E7=AB=99=E5=8A=A8=E6=80=81=E8=AE=A2=E9=98=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ATRI/plugins/bilibili_dynamic/__init__.py          | 287 ++++++++--------
 ATRI/plugins/bilibili_dynamic/api.py               |  36 ++
 ATRI/plugins/bilibili_dynamic/data_source.py       | 373 ++++++---------------
 ATRI/plugins/bilibili_dynamic/database/__init__.py |   1 +
 ATRI/plugins/bilibili_dynamic/database/db.py       |  49 +++
 ATRI/plugins/bilibili_dynamic/database/models.py   |  14 +
 ATRI/plugins/bilibili_dynamic/user.json            | 244 --------------
 7 files changed, 327 insertions(+), 677 deletions(-)
 create mode 100644 ATRI/plugins/bilibili_dynamic/api.py
 create mode 100644 ATRI/plugins/bilibili_dynamic/database/__init__.py
 create mode 100644 ATRI/plugins/bilibili_dynamic/database/db.py
 create mode 100644 ATRI/plugins/bilibili_dynamic/database/models.py
 delete mode 100644 ATRI/plugins/bilibili_dynamic/user.json

diff --git a/ATRI/plugins/bilibili_dynamic/__init__.py b/ATRI/plugins/bilibili_dynamic/__init__.py
index a42a0e3..0a8afc3 100644
--- a/ATRI/plugins/bilibili_dynamic/__init__.py
+++ b/ATRI/plugins/bilibili_dynamic/__init__.py
@@ -1,192 +1,167 @@
 import re
+import pytz
+import asyncio
 from tabulate import tabulate
 from datetime import datetime, timedelta
 
-import pytz
 from apscheduler.triggers.base import BaseTrigger
 from apscheduler.triggers.combining import AndTrigger
 from apscheduler.triggers.interval import IntervalTrigger
 
-from nonebot.params import State
-from nonebot.adapters.onebot.v11 import MessageSegment, GroupMessageEvent, Message
-from nonebot.typing import T_State
 from nonebot import get_bot
+from nonebot.matcher import Matcher
+from nonebot.params import CommandArg, ArgPlainText
+from nonebot.adapters.onebot.v11 import Message, GroupMessageEvent
 
-from ATRI.utils.apscheduler import scheduler
+from ATRI.log import logger as log
 from ATRI.utils import timestamp2datetime
-from ATRI.log import logger
+from ATRI.utils.apscheduler import scheduler
 
 from .data_source import BilibiliDynamicSubscriptor
+from .database.models import Subscription
 
 
-bilibili_dynamic = BilibiliDynamicSubscriptor().on_command(
-    "/bilibili_dynamic", "b站动态订阅助手", aliases={"/bd", "b站动态"}
-)
+add_sub = BilibiliDynamicSubscriptor().cmd_as_group("add", "添加b站up主订阅")
 
-__help__ = """好哦!是b站动态订阅诶~
-目前支持的功能如下...请键入对应关键词:
-1.添加订阅
-2.取消订阅
-3.订阅列表
------------------------------------
-用法示例1:/bd 添加订阅
-用法示例2:/bd 取消订阅 401742377(数字uid)
-用法示例3:/bd 订阅列表"""
-
-
-def help() -> str:
-    return __help__
-
-
-@bilibili_dynamic.handle()
-async def _menu(event: GroupMessageEvent, state: T_State = State()):
-    args = str(event.get_plaintext()).strip().lower().split()[1:]
-    if not args:
-        await bilibili_dynamic.finish(help())
-    elif args and len(args) == 1:
-        state["sub_command"] = args[0]
-    elif args and len(args) == 2:
-        state["sub_command"] = args[0]
-        state["uid"] = args[1]
-    else:
-        await bilibili_dynamic.finish("参数错误QAQ 请检查您的输入~")
-
-
-@bilibili_dynamic.got("sub_command", prompt="您要执行操作是?\n【添加订阅/取消订阅/订阅列表】")
-async def handle_subcommand(event: GroupMessageEvent, state: T_State = State()):
-    if state["sub_command"] not in ["添加订阅", "取消订阅", "订阅列表"]:
-        await bilibili_dynamic.finish("没有这个命令哦, 请在【添加订阅/取消订阅/订阅列表】中选择并重新发送")
-
-    if state["sub_command"] == "订阅列表":
-        subscriptor = BilibiliDynamicSubscriptor()
-        r = await subscriptor.get_subscriptions(query_map={"groupid": event.group_id})
-        subs = []
-        for s in r:
-            tm = s.last_update.replace(tzinfo=pytz.timezone("Asia/Shanghai"))
-            subs.append([s.nickname, s.uid, tm + timedelta(hours=8)])
-        output = "本群订阅的UP列表如下~\n" + tabulate(
-            subs, headers=["up名称", "UID", "上次更新时间"], tablefmt="plain", showindex=True
-        )
-        await bilibili_dynamic.finish(output)
-
-
-@bilibili_dynamic.got("uid", prompt="请输入b站UID(输入-1取消):")
-async def handle_uid(event: GroupMessageEvent, state: T_State = State()):
-    sub_command = state["sub_command"]
-    if isinstance(state["uid"], list):
-        uid = str(state["uid"][0])
-    else:
-        uid = state["uid"]
-
-    if uid == "-1":
-        await bilibili_dynamic.finish("已经成功退出订阅~")
-    if not re.match(r"^\d+$", uid):
-        await bilibili_dynamic.reject("这似乎不是UID呢, 请重新输入:")
-    uid = int(uid)
-    subscriptor = BilibiliDynamicSubscriptor()
-    up_name = await subscriptor.get_upname_by_uid(uid)
-    if up_name == "":
-        await bilibili_dynamic.finish(f"无法获取uid={uid}的up信息...订阅失败了".format(uid=uid))
-    else:
-        await bilibili_dynamic.send(
-            f"uid为{uid}的UP主是【{up_name}】\n{sub_command}操作中...".format(
-                uid=uid, up_name=up_name, sub_command=sub_command
-            )
-        )
-    query_result = await subscriptor.get_subscriptions(
-        query_map={"uid": uid, "groupid": event.group_id}
+
+@add_sub.handle()
+async def _bd_add_sub(matcher: Matcher, args: Message = CommandArg()):
+    msg = args.extract_plain_text()
+    if msg:
+        matcher.set_arg("bd_add_sub_id", args)
+
+
+@add_sub.got("bd_add_sub_id", "up主id呢?速速")
+async def _bd_deal_add_sub(
+    event: GroupMessageEvent, _id: str = ArgPlainText("bd_add_sub_id")
+):
+    patt = r"^\d+$"
+    if not re.match(patt, _id):
+        await add_sub.reject("这似乎不是id呢,请重新输入:")
+
+    __id = int(_id)
+    group_id = event.group_id
+    sub = BilibiliDynamicSubscriptor()
+
+    up_nickname = await sub.get_up_nickname(__id)
+    if not up_nickname:
+        await add_sub.finish(f"无法获取id为 {_id} 的up主信息...操作失败了")
+
+    query_result = await sub.get_sub_list(__id, group_id)
+    if len(query_result):
+        await add_sub.finish(f"该up主[{up_nickname}]已在本群订阅列表中啦!")
+
+    await sub.add_sub(__id, group_id)
+    await sub.update_sub(
+        __id, {"up_nickname": up_nickname, "last_update": datetime.utcnow()}
     )
-    success = True
-    if sub_command == "添加订阅":
-        if len(query_result) > 0:
-            await bilibili_dynamic.finish(
-                f"订阅失败,因为uid={uid}的UP主【{up_name}】已在本群订阅列表中".format(
-                    uid=uid, up_name=up_name
-                )
-            )
-        success = await subscriptor.add_subscription(uid, event.group_id)
-        success = success and (
-            await subscriptor.update_subscription_by_uid(
-                uid=uid,
-                update_map={"nickname": up_name, "last_update": datetime.utcnow()},
-            )
-        )
-    elif sub_command == "取消订阅":
-        if len(query_result) == 0:
-            await bilibili_dynamic.finish(
-                f"取消订阅失败,因为uid={uid}的UP主【{up_name}】不在本群订阅列表中".format(
-                    uid=uid, up_name=up_name
-                )
-            )
-        success = await subscriptor.remove_subscription(uid, event.group_id)
-    if success:
-        await bilibili_dynamic.finish(
-            f"成功{sub_command}【{up_name}】的动态!".format(
-                sub_command=sub_command, up_name=up_name
-            )
-        )
-    else:
-        await bilibili_dynamic.finish("诶...因为神奇的原因失败了")
+    await add_sub.finish(f"成功订阅名为[{up_nickname}]up主的动态~!")
+
 
+del_sub = BilibiliDynamicSubscriptor().cmd_as_group("del", "删除b站up主订阅")
 
-from queue import Queue
 
-# 任务队列(taskQueue)
-tq = Queue()
+@del_sub.handle()
+async def _bd_del_sub(matcher: Matcher, args: Message = CommandArg()):
+    msg = args.extract_plain_text()
+    if msg:
+        matcher.set_arg("bd_del_sub_id", args)
 
 
-class BilibiliDynamicCheckEnabledTrigger(BaseTrigger):
-    # 自定义trigger 保证服务开启
-    # 实现abstract方法 <get_next_fire_time>
+@del_sub.got("bd_del_sub_id", "up主id呢?速速")
+async def _bd_deal_del_sub(
+    event: GroupMessageEvent, _id: str = ArgPlainText("bd_del_sub_id")
+):
+    patt = r"^\d+$"
+    if not re.match(patt, _id):
+        await add_sub.reject("这似乎不是id呢,请重新输入:")
+
+    __id = int(_id)
+    group_id = event.group_id
+    sub = BilibiliDynamicSubscriptor()
+
+    up_nickname = await sub.get_up_nickname(__id)
+    if not up_nickname:
+        await add_sub.finish(f"无法获取id为 {__id} 的up主信息...操作失败了")
+
+    query_result = await sub.get_sub_list(__id, group_id)
+    if not query_result:
+        await del_sub.finish(f"取消订阅失败...该up主[{up_nickname}]并不在本群订阅列表中")
+
+    await sub.del_sub(__id, group_id)
+    await del_sub.finish(f"成功取消该up主[{up_nickname}]的订阅~")
+
+
+get_sub_list = BilibiliDynamicSubscriptor().cmd_as_group("list", "获取b站up主订阅列表")
+
+
+@get_sub_list.handle()
+async def _get_sub_list(event: GroupMessageEvent):
+    group_id = event.group_id
+    sub = BilibiliDynamicSubscriptor()
+
+    query_result = await sub.get_sub_list(group_id=group_id)
+    if not query_result:
+        await get_sub_list.finish("本群还未订阅任何up主呢...")
+
+    subs = list()
+    for i in query_result:
+        tm = i.last_update.replace(tzinfo=pytz.timezone("Asia/Shanghai"))
+        subs.append([i.up_nickname, i.uid, tm + timedelta(hours=8)])
+
+    output = "本群订阅的up列表如下~\n" + tabulate(
+        subs, headers=["up主", "uid", "最后更新时间"], tablefmt="plain", showindex=True
+    )
+    await get_sub_list.finish(output)
+
+
+tq = asyncio.Queue()
+
+
+class BilibiliDynamicChecker(BaseTrigger):
     def get_next_fire_time(self, previous_fire_time, now):
-        subscriptor = BilibiliDynamicSubscriptor()
-        config = subscriptor.load_service("b站动态订阅")
-        if config["enabled"] == False:
-            return None
-        else:
+        sub = BilibiliDynamicSubscriptor()
+        conf = sub.load_service("b站动态订阅-rebu")
+        if conf.get("enabled"):
             return now
 
 
-# 业务逻辑
-# 每10s从任务队列中拉一个uid出来,调用api进行查询
-# 当任务队列为空时,从数据库读取订阅列表,并塞入任务队列tq中
 @scheduler.scheduled_job(
-    AndTrigger([IntervalTrigger(seconds=10), BilibiliDynamicCheckEnabledTrigger()]),
-    name="b站动态检查",
+    AndTrigger([IntervalTrigger(seconds=10), BilibiliDynamicChecker()]),
+    name="b站动态更新检查",
     max_instances=3,  # type: ignore
     misfire_grace_time=60,  # type: ignore
 )
-async def _check_dynamic():
-    from ATRI.database.models import Subscription
-
-    subscriptor = BilibiliDynamicSubscriptor()
-    all_dynamic = await subscriptor.get_all_subscriptions()
+async def _check_dynamic_rebu():
+    sub = BilibiliDynamicSubscriptor()
+    all_dy = await sub.get_all_subs()
     if tq.empty():
-        for d in all_dynamic:
-            tq.put(d)
+        for i in all_dy:
+            await tq.put(i)
     else:
-        d: Subscription = tq.get()
-        logger.info("准备查询UP【{up}】的动态 队列剩余{size}".format(up=d.nickname, size=tq.qsize()))
-        ts = int(d.last_update.timestamp())
-        info: dict = await subscriptor.get_recent_dynamic_by_uid(d.uid)
-        res = []
-        if info:
-            if info.get("cards") is not None:
-                res = subscriptor.extract_dynamics_detail(info.get("cards"))
-
-        if len(res) == 0:
-            logger.warning("获取UP【{up}】的动态为空".format(up=d.nickname))
-        for i in res:
-            i["name"] = d.nickname
+        m: Subscription = tq.get_nowait()
+        log.info(f"准备查询up主[{m.up_nickname}]的动态,队列剩余 {tq.qsize()}")
+
+        ts = int(m.last_update.timestamp())
+        info: dict = await sub.get_up_recent_dynamic(m.uid)
+        result = list()
+        if info.get("cards", list()):
+            result = sub.extract_dyanmic(info["cards"])
+        if not result:
+            log.warning(f"无法获取up主[{m.up_nickname}]的动态")
+
+        for i in result:
+            i["name"] = m.up_nickname
             if ts < i["timestamp"]:
-                text, pic_url = subscriptor.generate_output(pattern=i)
-                output = Message(
-                    [MessageSegment.text(text), MessageSegment.image(pic_url)]
-                )
+                content = Message(sub.gen_output(i))
+
                 bot = get_bot()
-                await bot.send_group_msg(group_id=d.groupid, message=output)
-                _ = await subscriptor.update_subscription_by_uid(
-                    uid=d.uid,
-                    update_map={"last_update": timestamp2datetime(i["timestamp"])},
+                await bot.send_group_msg(group_id=m.group_id, message=content)
+                await sub.update_sub(
+                    m.uid,
+                    {
+                        "group_id": m.group_id,
+                        "last_update": timestamp2datetime(i["timestamp"]),
+                    },
                 )
                 break
diff --git a/ATRI/plugins/bilibili_dynamic/api.py b/ATRI/plugins/bilibili_dynamic/api.py
new file mode 100644
index 0000000..a455805
--- /dev/null
+++ b/ATRI/plugins/bilibili_dynamic/api.py
@@ -0,0 +1,36 @@
+from ATRI.utils import request
+from ATRI.exceptions import RequestError
+
+
+class API:
+    def __init__(self, uid: int):
+        self.uid = uid
+
+    async def _request(self, url: str, params: dict = dict()) -> dict:
+        headers = {
+            "Referer": "https://www.bilibili.com",
+            "User-Agent": "Mozilla/5.0",
+        }
+
+        try:
+            resp = await request.get(url, params=params, headers=headers)
+        except RequestError:
+            raise RequestError("Request failed!")
+
+        return resp.json()
+
+    async def get_user_info(self) -> dict:
+        url = "https://api.bilibili.com/x/space/acc/info"
+        params = {"mid": self.uid}
+        return await self._request(url, params)
+
+    async def get_user_dynamics(
+        self, offset: int = int(), need_top: bool = False
+    ) -> dict:
+        url = "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history"
+        params = {
+            "host_uid": self.uid,
+            "offset_dynamic_id": offset,
+            "need_top": 1 if need_top else 0,
+        }
+        return await self._request(url, params)
diff --git a/ATRI/plugins/bilibili_dynamic/data_source.py b/ATRI/plugins/bilibili_dynamic/data_source.py
index 14143e6..5233692 100644
--- a/ATRI/plugins/bilibili_dynamic/data_source.py
+++ b/ATRI/plugins/bilibili_dynamic/data_source.py
@@ -1,286 +1,99 @@
-from ATRI.service import Service
-from ATRI.rule import is_in_service
-from ATRI.database.db import DB
-from ATRI.utils import timestamp2datetime
-
 import json
-import aiohttp
-import os
-import re
-import asyncio
-from typing import Any
 from operator import itemgetter
 
-__session_pool = {}
-
-
-def get_api(field: str) -> dict:
-    """
-    获取 API。
-
-    Args:
-        field (str): API 所属分类,即 data/api 下的文件名(不含后缀名)
-
-    Returns:
-        dict, 该 API 的内容。
-    """
-    path = os.path.abspath(
-        os.path.join(os.path.dirname(__file__), f"{field.lower()}.json")
-    )
-    if os.path.exists(path):
-        with open(path, encoding="utf8") as f:
-            return json.loads(f.read())
-    else:
-        return dict()
-
-
-API: dict = get_api("user")
-
-
-def get_session():
-    """
-    获取当前模块的 aiohttp.ClientSession 对象,用于自定义请求
-
-    Returns:
-        aiohttp.ClientSession
-    """
-    loop = asyncio.get_event_loop()
-    session = __session_pool.get(loop, None)
-    if session is None:
-        session = aiohttp.ClientSession(loop=loop)
-        __session_pool[loop] = session
-
-    return session
-
-
-async def bilibili_request(
-    method: str,
-    url: str,
-    params: dict = dict(),
-    data: Any = None,
-    no_csrf: bool = False,
-    json_body: bool = False,
-    **kwargs,
-) -> dict:
-    """
-    向接口发送请求。
-
-    Args:
-        method     (str)                 : 请求方法。
-        url        (str)                 : 请求 URL。
-        params     (dict, optional)      : 请求参数。
-        data       (Any, optional)       : 请求载荷。
-        no_csrf    (bool, optional)      : 不要自动添加 CSRF。
-        json_body (bool, optional) 载荷是否为 JSON
-
-    Returns:
-        接口未返回数据时,返回 None,否则返回该接口提供的 data 或 result 字段的数据。
-    """
-
-    method = method.upper()
-
-    # 使用 Referer 和 UA 请求头以绕过反爬虫机制
-    DEFAULT_HEADERS = {
-        "Referer": "https://www.bilibili.com",
-        "User-Agent": "Mozilla/5.0",
-    }
-    headers = DEFAULT_HEADERS
+from nonebot.adapters.onebot.v11 import MessageSegment
+from nonebot.adapters.onebot.v11 import GROUP_OWNER, GROUP_ADMIN
 
-    if params is None:
-        params = {}
-
-    # 自动添加 csrf
-    if not no_csrf and method in ["POST", "DELETE", "PATCH"]:
-        if data is None:
-            data = {}
-        data["csrf"] = ""
-        data["csrf_token"] = ""
-
-    # jsonp
-
-    if params.get("jsonp", "") == "jsonp":
-        params["callback"] = "callback"
+from ATRI.service import Service
+from ATRI.rule import is_in_service
+from ATRI.utils import timestamp2datetime
+from ATRI.exceptions import BilibiliDynamicError
 
-    config = {
-        "method": method,
-        "url": url,
-        "params": params,
-        "data": data,
-        "headers": headers,
-        "cookies": "",
-    }
+from .database import DB
+from .api import API
 
-    config.update(kwargs)
 
-    if json_body:
-        config["headers"]["Content-Type"] = "application/json"
-        config["data"] = json.dumps(config["data"])
+_OUTPUT_FORMAT = """
+{up_nickname} 的{up_dy_type}更新了!
+{up_dy_content}
+{up_dy_media}
+链接: {up_dy_link}
+""".strip()
 
-    session = get_session()
 
-    async with session.request(**config) as resp:
+class BilibiliDynamicSubscriptor(Service):
+    def __init__(self):
+        Service.__init__(
+            self,
+            "b站动态订阅",
+            "b站动态订阅助手~",
+            rule=is_in_service("b站动态订阅"),
+            permission=GROUP_OWNER | GROUP_ADMIN,
+            main_cmd="/bd",
+        )
 
-        # 检查状态码
+    async def add_sub(self, uid: int, group_id: int):
         try:
-            resp.raise_for_status()
-        except aiohttp.ClientResponseError as e:
-            raise Exception(e.message)
-
-        # 检查响应头 Content-Length
-        content_length = resp.headers.get("content-length")
-        if content_length and int(content_length) == 0:
-            return dict()
+            async with DB() as db:
+                await db.add_sub(uid, group_id)
+        except BilibiliDynamicError:
+            raise BilibiliDynamicError("添加订阅失败")
 
-        # 检查响应头 Content-Type
-        content_type = resp.headers.get("content-type")
-
-        # 不是 application/json
-        if content_type.lower().index("application/json") == -1:  # type: ignore
-            raise Exception("响应不是 application/json 类型")
-
-        raw_data = await resp.text()
-        resp_data: dict = dict()
+    async def update_sub(self, uid: int, update_map: dict):
+        try:
+            async with DB() as db:
+                await db.update_sub(uid, update_map)
+        except BilibiliDynamicError:
+            BilibiliDynamicError("更新订阅失败")
 
-        if "callback" in params:
-            # JSONP 请求
-            resp_data = json.loads(re.match("^.*?({.*}).*$", raw_data, re.S).group(1))  # type: ignore
+    async def del_sub(self, uid: int, group_id: int):
+        try:
+            async with DB() as db:
+                await db.del_sub({"uid": uid, "group_id": group_id})
+        except BilibiliDynamicError:
+            raise BilibiliDynamicError("删除订阅失败")
+
+    async def get_sub_list(self, uid: int = int(), group_id: int = int()) -> list:
+        if not uid:
+            query_map = {"group_id": group_id}
         else:
-            # JSON
-            resp_data = json.loads(raw_data)
-
-        # 检查 code
-        code = resp_data.get("code", None)
-
-        if code is None:
-            raise Exception("API 返回数据未含 code 字段")
-
-        if code != 0:
-            msg = resp_data.get("msg", None)
-            if msg is None:
-                msg = resp_data.get("message", None)
-            if msg is None:
-                msg = "接口未返回错误信息"
-            raise Exception(msg)
-
-        real_data = resp_data.get("data", None)
-        if real_data is None:
-            real_data = resp_data.get("result", None)
-        return real_data
-
+            query_map = {"uid": uid, "group_id": group_id}
 
-class User:
-    """
-    b站用户相关
-    """
-
-    def __init__(self, uid: int):
-        """
-        Args:
-            uid        (int)                 : 用户 UID
-        """
-        self.uid = uid
-
-        self.__self_info = None  # 暂时无用
-
-    async def get_user_info(self) -> dict:
-        """
-        获取用户信息(昵称,性别,生日,签名,头像 URL,空间横幅 URL 等)
-
-        Returns:
-            dict: 调用接口返回的内容。
-        """
-        api = API["info"]["info"]
-        params = {"mid": self.uid}
-        return await bilibili_request("GET", url=api["url"], params=params)
-
-    async def get_dynamics(self, offset: int = 0, need_top: bool = False):
-        """
-        获取用户动态。
-
-        Args:
-            offset (str, optional):     该值为第一次调用本方法时,数据中会有个 next_offset 字段,
-                                        指向下一动态列表第一条动态(类似单向链表)。
-                                        根据上一次获取结果中的 next_offset 字段值,
-                                        循环填充该值即可获取到全部动态。
-                                        0 为从头开始。
-                                        Defaults to 0.
-            need_top (bool, optional):  显示置顶动态. Defaults to False.
+        try:
+            async with DB() as db:
+                return await db.get_sub_list(query_map)
+        except BilibiliDynamicError:
+            raise BilibiliDynamicError("获取订阅列表失败")
 
-        Returns:
-            dict: 调用接口返回的内容。
-        """
-        api = API["info"]["dynamic"]
-        params = {
-            "host_uid": self.uid,
-            "offset_dynamic_id": offset,
-            "need_top": 1 if need_top else 0,
-        }
-        data: dict = await bilibili_request("GET", url=api["url"], params=params)
-        # card 字段自动转换成 JSON。
+    async def get_all_subs(self) -> list:
+        try:
+            async with DB() as db:
+                return await db.get_all_subs()
+        except BilibiliDynamicError:
+            raise BilibiliDynamicError("获取全部订阅列表失败")
+
+    async def get_up_nickname(self, uid: int) -> str:
+        api = API(uid)
+        resp = await api.get_user_info()
+        data = resp.get("data", dict())
+        return data.get("name", "unknown")
+
+    async def get_up_recent_dynamic(self, uid: int) -> dict:
+        api = API(uid)
+        resp = await api.get_user_dynamics()
+        data = resp.get("data", dict())
         if "cards" in data:
             for card in data["cards"]:
                 card["card"] = json.loads(card["card"])
                 card["extend_json"] = json.loads(card["extend_json"])
         return data
 
-
-class BilibiliDynamicSubscriptor(Service):
-    def __init__(self):
-        Service.__init__(self, "b站动态订阅", "b站订阅动态助手", rule=is_in_service("b站动态订阅"))
-
-    async def add_subscription(self, uid: int, groupid: int) -> bool:
-        async with DB() as db:
-            res = await db.add_subscription(uid=uid, groupid=groupid)
-            return res
-
-    async def remove_subscription(self, uid: int, groupid: int) -> bool:
-        async with DB() as db:
-            res = await db.remove_subscription(
-                query_map={"uid": uid, "groupid": groupid}
-            )
-            return res
-
-    async def get_subscriptions(self, query_map: dict) -> list:
-        async with DB() as db:
-            res = await db.get_subscriptions(query_map=query_map)
-            return res
-
-    async def update_subscription_by_uid(self, uid: int, update_map: dict) -> bool:
-        async with DB() as db:
-            res = await db.update_subscriptions_by_uid(uid=uid, update_map=update_map)
-            return res
-
-    async def get_all_subscriptions(self) -> list:
-        async with DB() as db:
-            res = await db.get_all_subscriptions()
-            return res
-
-    # bilibili network function
-
-    async def get_upname_by_uid(self, uid: int) -> str:
-        try:
-            u = User(uid)
-            info: dict = await u.get_user_info()
-            return info.get("name")
-        except:
-            return ""
-
-    async def get_recent_dynamic_by_uid(self, uid: int) -> dict:
-        try:
-            u = User(uid)
-            info = await u.get_dynamics()
-            return info
-        except:
-            return {}
-
-    def extract_dynamics_detail(self, dynamic_list: list) -> list:
-        import time
-
-        ret = []
-        for d in dynamic_list:
+    def extract_dyanmic(self, data: list) -> list:
+        result = list()
+        for i in data:
             pattern = {}
-            desc = d["desc"]
-            card = d["card"]
+            desc = i["desc"]
+            card = i["card"]
             type = desc["type"]
 
             # common 部分
@@ -292,11 +105,11 @@ class BilibiliDynamicSubscriptor(Service):
             pattern["dynamic_id"] = desc["dynamic_id"]
             pattern["timestamp"] = desc["timestamp"]
             pattern["time"] = timestamp2datetime(desc["timestamp"])
-            pattern["type_zh"] = ""
+            pattern["type_zh"] = str()
 
             # alternative 部分
-            pattern["content"] = ""
-            pattern["pic"] = ""
+            pattern["content"] = str()
+            pattern["pic"] = str()
 
             # 根据type区分 提取content
             if type == 1:  # 转发动态
@@ -329,19 +142,25 @@ class BilibiliDynamicSubscriptor(Service):
                 if len(card["image_urls"]) > 0:
                     pattern["pic"] = card["image_urls"][0]
 
-            ret.append(pattern)
-        ret = sorted(ret, key=itemgetter("timestamp"))
-        return ret
+            result.append(pattern)
+        return sorted(result, key=itemgetter("timestamp"))
+
+    def gen_output(self, data: dict, limit_content: int = 100) -> str:
+        """生成动态信息
 
-    def generate_output(self, pattern: dict) -> tuple:
-        # 限制摘要的字数
-        abstractLimit = 40
-        text_part = """【UP名称】{name}\n【动态类型】{dynamic_type}\n【时间】{time}\n【内容摘要】{content}\n【链接】{url}\n""".format(
-            name=pattern["name"],
-            dynamic_type=pattern["type_zh"],
-            time=pattern["time"],
-            content=pattern["content"][:abstractLimit],
-            url="https://t.bilibili.com/" + str(pattern["dynamic_id"]),
+        Args:
+            data (dict): dict形式的动态数据.
+            limit_content (int, optional): 内容字数限制. 默认 100.
+
+        Returns:
+            str: 动态信息
+        """
+        return _OUTPUT_FORMAT.format(
+            up_nickname=data["name"],
+            up_dy_type=data["type_zh"],
+            up_dy_content=str(data["content"][:limit_content] + "...")
+            .replace("https://", str())
+            .replace("http://", str()),
+            up_dy_media=MessageSegment.image(data["pic"]) if data.get("pic") else str(),
+            up_dy_link="https://t.bilibili.com/" + str(data["dynamic_id"]),
         )
-        pic_part = pattern["pic"]
-        return text_part, pic_part
diff --git a/ATRI/plugins/bilibili_dynamic/database/__init__.py b/ATRI/plugins/bilibili_dynamic/database/__init__.py
new file mode 100644
index 0000000..6840881
--- /dev/null
+++ b/ATRI/plugins/bilibili_dynamic/database/__init__.py
@@ -0,0 +1 @@
+from .db import DB
diff --git a/ATRI/plugins/bilibili_dynamic/database/db.py b/ATRI/plugins/bilibili_dynamic/database/db.py
new file mode 100644
index 0000000..37ed223
--- /dev/null
+++ b/ATRI/plugins/bilibili_dynamic/database/db.py
@@ -0,0 +1,49 @@
+from pathlib import Path
+from tortoise import Tortoise
+
+from ATRI import driver
+from .models import Subscription
+
+
+DB_DIR = Path(".") / "data" / "database" / "bilibili_dynamic"
+DB_DIR.mkdir(parents=True, exist_ok=True)
+
+
+class DB:
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        pass
+
+    async def init(self):
+        from . import models
+
+        await Tortoise.init(
+            db_url=f"sqlite://{DB_DIR}/db.sqlite3",
+            modules={"models": [locals()["models"]]},
+        )
+        await Tortoise.generate_schemas()
+
+    async def add_sub(self, uid: int, group_id: int):
+        await Subscription.create(uid=uid, group_id=group_id)
+
+    async def update_sub(self, uid: int, update_map: dict):
+        await Subscription.filter(uid=uid).update(**update_map)
+
+    async def del_sub(self, query_map: dict):
+        await Subscription.filter(**query_map).delete()
+
+    async def get_sub_list(self, query_map: dict) -> list:
+        return await Subscription.filter(**query_map)
+
+    async def get_all_subs(self) -> list:
+        return await Subscription.all()
+
+
+async def init():
+    async with DB() as db:
+        await db.init()
+
+
+driver().on_startup(init)
diff --git a/ATRI/plugins/bilibili_dynamic/database/models.py b/ATRI/plugins/bilibili_dynamic/database/models.py
new file mode 100644
index 0000000..d0cb3dd
--- /dev/null
+++ b/ATRI/plugins/bilibili_dynamic/database/models.py
@@ -0,0 +1,14 @@
+from datetime import datetime
+
+from tortoise import fields
+from tortoise.models import Model
+
+
+class Subscription(Model):
+    uid = fields.IntField()
+    group_id = fields.IntField()
+    up_nickname = fields.TextField(null=True)
+    last_update = fields.DatetimeField(default=datetime.fromordinal(1))
+
+    def __str__(self) -> str:
+        return f"[{self.uid}|{self.group_id}|{self.up_nickname}|{self.last_update}]"
diff --git a/ATRI/plugins/bilibili_dynamic/user.json b/ATRI/plugins/bilibili_dynamic/user.json
deleted file mode 100644
index ff4f49b..0000000
--- a/ATRI/plugins/bilibili_dynamic/user.json
+++ /dev/null
@@ -1,244 +0,0 @@
-{
-  "info": {
-    "my_info": {
-      "url": "https://api.bilibili.com/x/space/myinfo",
-      "method": "GET",
-      "verify": true,
-      "comment": "获取自己的信息"
-    },
-    "info": {
-      "url": "https://api.bilibili.com/x/space/acc/info",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "mid": "int: uid"
-      },
-      "comment": "用户基本信息"
-    },
-    "relation": {
-      "url": "https://api.bilibili.com/x/relation/stat",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "vmid": "int: uid"
-      },
-      "comment": "关注数,粉丝数"
-    },
-    "upstat": {
-      "url": "https://api.bilibili.com/x/space/upstat",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "mid": "int: uid"
-      },
-      "comment": "视频播放量,文章阅读量,总点赞数"
-    },
-    "live": {
-      "url": "https://api.bilibili.com/x/space/acc/info",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "mid": "int: uid"
-      },
-      "comment": "直播间基本信息"
-    },
-    "video": {
-      "url": "https://api.bilibili.com/x/space/arc/search",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "mid": "int: uid",
-        "ps": "const int: 30",
-        "tid": "int: 分区 ID,0 表示全部",
-        "pn": "int: 页码",
-        "keyword": "str: 关键词,可为空",
-        "order": "str: pubdate 上传日期,pubdate 播放量,pubdate 收藏量"
-      },
-      "comment": "搜索用户视频"
-    },
-    "audio": {
-      "url": "https://api.bilibili.com/audio/music-service/web/song/upper",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "uid": "int: uid",
-        "ps": "const int: 30",
-        "pn": "int: 页码",
-        "order": "int: 1 最新发布,2 最多播放,3 最多收藏"
-      },
-      "comment": "音频"
-    },
-    "article": {
-      "url": "https://api.bilibili.com/x/space/article",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "mid": "int: uid",
-        "ps": "const int: 30",
-        "pn": "int: 页码",
-        "sort": "str: publish_time 最新发布,publish_time 最多阅读,publish_time 最多收藏"
-      },
-      "comment": "专栏"
-    },
-    "article_lists": {
-      "url": "https://api.bilibili.com/x/article/up/lists",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "mid": "int: uid",
-        "sort": "int: 0 最近更新,1 最多阅读"
-      },
-      "comment": "专栏文集"
-    },
-    "dynamic": {
-      "url": "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "host_uid": "int: uid",
-        "offset_dynamic_id": "int: 动态偏移用,第一页为 0",
-        "need_top": "int bool: 是否显示置顶动态"
-      },
-      "comment": "用户动态信息"
-    },
-    "bangumi": {
-      "url": "https://api.bilibili.com/x/space/bangumi/follow/list",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "vmid": "int: uid",
-        "pn": "int: 页码",
-        "ps": "const int: 15",
-        "type": "int: 1 追番,2 追剧"
-      },
-      "comment": "用户追番列表"
-    },
-    "followings": {
-      "url": "https://api.bilibili.com/x/relation/followings",
-      "method": "GET",
-      "verify": true,
-      "params": {
-        "vmid": "int: uid",
-        "ps": "const int: 20",
-        "pn": "int: 页码",
-        "order": "str: desc 倒序, asc 正序"
-      },
-      "comment": "获取用户关注列表(不是自己只能访问前 5 页)"
-    },
-    "followers": {
-      "url": "https://api.bilibili.com/x/relation/followers",
-      "method": "GET",
-      "verify": true,
-      "params": {
-        "vmid": "int: uid",
-        "ps": "const int: 20",
-        "pn": "int: 页码",
-        "order": "str: desc 倒序, asc 正序"
-      },
-      "comment": "获取用户粉丝列表(不是自己只能访问前 5 页,是自己也不能获取全部的样子)"
-    },
-    "overview": {
-      "url": "https://api.bilibili.com/x/space/navnum",
-      "method": "GET",
-      "verify": false,
-      "params": {
-        "mid": "int: uid",
-        "jsonp": "const str: jsonp"
-      },
-      "comment": "获取用户的简易订阅和投稿信息(主要是这些的数量统计)"
-    },
-    "self_subscribe_group": {
-      "url": "https://api.bilibili.com/x/relation/tags",
-      "method": "GET",
-      "verify": true,
-      "params": {},
-      "comment": "获取自己的关注分组列表,用于操作关注"
-    },
-    "get_user_in_which_subscribe_groups": {
-      "url": "https://api.bilibili.com/x/relation/tag/user",
-      "method": "GET",
-      "verify": true,
-      "params": {
-        "fid": "int: uid"
-      },
-      "comment": "获取用户在哪一个分组"
-    },
-    "history": {
-      "url": "https://api.bilibili.com/x/v2/history",
-      "method": "GET",
-      "verify": true,
-      "params": {
-        "pn": "int: 页码",
-        "ps": "const int: 100"
-      },
-      "comment": "用户浏览历史记录"
-    }
-  },
-  "operate": {
-    "modify": {
-      "url": "https://api.bilibili.com/x/relation/modify",
-      "method": "POST",
-      "verify": true,
-      "data": {
-        "fid": "int: UID",
-        "act": "int: 1 关注 2 取关 3 悄悄关注 5 拉黑 6 取消拉黑 7 移除粉丝",
-        "re_src": "const int: 11"
-      },
-      "comment": "用户关系操作"
-    },
-    "send_msg": {
-      "url": "https://api.vc.bilibili.com/web_im/v1/web_im/send_msg",
-      "method": "POST",
-      "verify": true,
-      "data": {
-        "msg[sender_uid]": "int: 自己的 UID",
-        "msg[receiver_id]": "int: 对方 UID",
-        "msg[receiver_type]": "const int: 1",
-        "msg[msg_type]": "const int: 1",
-        "msg[msg_status]": "const int: 0",
-        "msg[content]": {
-          "content": "str: 文本内容"
-        }
-      },
-      "comment": "给用户发信息"
-    },
-    "create_subscribe_group": {
-      "url": "https://api.bilibili.com/x/relation/tag/create",
-      "method": "POST",
-      "verify": true,
-      "data": {
-        "tag": "str: 分组名"
-      },
-      "comment": "添加关注分组"
-    },
-    "del_subscribe_group": {
-      "url": "https://api.bilibili.com/x/relation/tag/del",
-      "method": "POST",
-      "verify": true,
-      "data": {
-        "tagid": "int: 分组 id"
-      },
-      "comment": "删除关注分组"
-    },
-    "rename_subscribe_group": {
-      "url": "https://api.bilibili.com/x/relation/tag/update",
-      "method": "POST",
-      "verify": true,
-      "data": {
-        "tagid": "int: 分组 id",
-        "name": "str: 新的分组名"
-      },
-      "comment": "重命名分组"
-    },
-    "set_user_subscribe_group": {
-      "url": "https://api.bilibili.com/x/relation/tags/addUsers",
-      "method": "POST",
-      "verify": true,
-      "data": {
-        "fids": "int: UID",
-        "tagids": "commaSeparatedList[int]: 分组的 tagids,逗号分隔"
-      },
-      "comment": "移动用户到关注分组"
-    }
-  }
-}
\ No newline at end of file
-- 
cgit v1.2.3