From 920173c8faaab5a65459ce176d36812bac6feb08 Mon Sep 17 00:00:00 2001
From: Yuki-Asuuna <10174503104@stu.ecnu.edu.cn>
Date: Fri, 25 Feb 2022 21:22:48 +0800
Subject: =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0b=E7=AB=99=E5=8A=A8=E6=80=81?=
 =?UTF-8?q?=E8=AE=A2=E9=98=85=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Change-Id: I8b74e3a286901379b8337e33d1b581524cb80d97
---
 ATRI/plugins/bilibili_dynamic/__init__.py    | 169 +++++++++++++
 ATRI/plugins/bilibili_dynamic/data_source.py | 349 +++++++++++++++++++++++++++
 ATRI/plugins/bilibili_dynamic/user.json      | 244 +++++++++++++++++++
 3 files changed, 762 insertions(+)
 create mode 100644 ATRI/plugins/bilibili_dynamic/__init__.py
 create mode 100644 ATRI/plugins/bilibili_dynamic/data_source.py
 create mode 100644 ATRI/plugins/bilibili_dynamic/user.json

(limited to 'ATRI/plugins')

diff --git a/ATRI/plugins/bilibili_dynamic/__init__.py b/ATRI/plugins/bilibili_dynamic/__init__.py
new file mode 100644
index 0000000..87854c0
--- /dev/null
+++ b/ATRI/plugins/bilibili_dynamic/__init__.py
@@ -0,0 +1,169 @@
+from ATRI.utils.apscheduler import scheduler
+from ATRI.utils import timestamp2datetime
+
+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 .data_source import BilibiliDynamicSubscriptor
+import re
+from tabulate import tabulate
+
+bilibili_dynamic = BilibiliDynamicSubscriptor().on_command(
+    "/bilibili_dynamic", "b站动态订阅助手", aliases={"b站动态"}
+)
+
+__help__ = """欢迎使用【b站动态订阅助手】~
+目前支持的功能如下...请选择:
+1.添加订阅
+2.取消订阅
+3.订阅列表
+-----------------------------------
+用法示例1:/bilibili_dynamic 添加订阅
+用法示例2:/bilibili_dynamic 取消订阅 401742377(数字uid)
+用法示例3:/bilibili_dynamic 订阅列表"""
+
+
+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:]
+    # print(args)
+    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()
+        # print(event.group_id)
+        r = await subscriptor.get_subscriptions(query_map={"groupid": event.group_id})
+        subs = []
+        for s in r:
+            subs.append([s.nickname, s.uid, s.last_update])
+        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("已经成功退出订阅~")
+    # print(state)
+    # print(uid)
+    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}
+    )
+    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)
+        print(success)
+        success = success and (
+            await subscriptor.update_subscription_by_uid(
+                uid=uid, update_map={"nickname": up_name}
+            )
+        )
+    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("诶...因为神奇的原因失败了")
+
+
+from queue import Queue
+
+# 任务队列(taskQueue)
+tq = Queue()
+
+
+# 业务逻辑
+# 每10s从任务队列中拉一个uid出来,调用api进行查询
+# 当任务队列为空时,从数据库读取订阅列表,并塞入任务队列tq中
+@scheduler.scheduled_job(
+    "interval", name="b站动态检查", seconds=10, max_instances=3, misfire_grace_time=60
+)
+async def _check_dynamic():
+    from ATRI.database.models import Subscription
+
+    subscriptor = BilibiliDynamicSubscriptor()
+    all_dynamic = await subscriptor.get_all_subscriptions()
+    if tq.empty():
+        for d in all_dynamic:
+            tq.put(d)
+    else:
+        d: Subscription = tq.get()
+        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"))
+        res = res[::-1]
+        for i in res:
+            i["name"] = d.nickname
+            if ts < i["timestamp"]:
+                text, pic_url = subscriptor.generate_output(pattern=i)
+                # print(text,pic_url)
+                output = Message(
+                    [MessageSegment.text(text), MessageSegment.image(pic_url)]
+                )
+                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"])},
+                )
+                break
diff --git a/ATRI/plugins/bilibili_dynamic/data_source.py b/ATRI/plugins/bilibili_dynamic/data_source.py
new file mode 100644
index 0000000..e4eb5d7
--- /dev/null
+++ b/ATRI/plugins/bilibili_dynamic/data_source.py
@@ -0,0 +1,349 @@
+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
+
+__doc__ = """b站订阅动态助手
+"""
+
+__session_pool = {}
+
+
+def get_api(field: str):
+    """
+    获取 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())
+
+
+API = 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 = None,
+    data: Any = None,
+    no_csrf: bool = False,
+    json_body: bool = False,
+    **kwargs,
+):
+    """
+    向接口发送请求。
+
+    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
+
+    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"
+
+    config = {
+        "method": method,
+        "url": url,
+        "params": params,
+        "data": data,
+        "headers": headers,
+        "cookies": "",
+    }
+
+    config.update(kwargs)
+
+    if json_body:
+        config["headers"]["Content-Type"] = "application/json"
+        config["data"] = json.dumps(config["data"])
+
+    session = get_session()
+
+    async with session.request(**config) as resp:
+
+        # 检查状态码
+        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 None
+
+        # 检查响应头 Content-Type
+        content_type = resp.headers.get("content-type")
+
+        # 不是 application/json
+        if content_type.lower().index("application/json") == -1:
+            raise Exception("响应不是 application/json 类型")
+
+        raw_data = await resp.text()
+        resp_data: dict
+
+        if "callback" in params:
+            # JSONP 请求
+            resp_data = json.loads(re.match("^.*?({.*}).*$", raw_data, re.S).group(1))
+        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
+
+
+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):
+        """
+        获取用户信息(昵称,性别,生日,签名,头像 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.
+
+        Returns:
+            dict: 调用接口返回的内容。
+        """
+        api = API["info"]["dynamic"]
+        params = {
+            "host_uid": self.uid,
+            "offset_dynamic_id": offset,
+            "need_top": 1 if need_top else 0,
+        }
+        data = await bilibili_request("GET", url=api["url"], params=params)
+        # card 字段自动转换成 JSON。
+        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站动态订阅", __doc__, 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 = 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:
+            pattern = {}
+            desc = d["desc"]
+            card = d["card"]
+            type = desc["type"]
+
+            # common 部分
+            pattern["type"] = desc["type"]
+            pattern["uid"] = desc["uid"]
+            pattern["view"] = desc["view"]
+            pattern["repost"] = desc["repost"]
+            pattern["like"] = desc["like"]
+            pattern["dynamic_id"] = desc["dynamic_id"]
+            pattern["timestamp"] = desc["timestamp"]
+            pattern["time"] = timestamp2datetime(desc["timestamp"])
+            pattern["type_zh"] = ""
+
+            # alternative 部分
+            pattern["content"] = ""
+            pattern["pic"] = ""
+
+            # 根据type区分 提取content
+            if type == 1:  # 转发动态
+                pattern["type_zh"] = "转发动态"
+                pattern["content"] = card["item"]["content"]
+                pattern["pic"] = card["user"]["face"]
+
+            elif type == 2:  # 普通动态(带多张图片)
+                pattern["type_zh"] = "普通动态(附图)"
+                pattern["content"] = card["item"]["description"]
+                if card["item"]["pictures_count"] > 0:
+                    if isinstance(card["item"]["pictures"][0], str):
+                        pattern["pic"] = card["item"]["pictures"][0]
+                    else:
+                        pattern["pic"] = card["item"]["pictures"][0]["img_src"]
+
+            elif type == 4:  # 普通动态(纯文字)
+                pattern["type_zh"] = "普通动态(纯文字)"
+                pattern["content"] = card["item"]["content"]
+                # 无图片
+
+            elif type == 8:  # 视频动态
+                pattern["type_zh"] = "视频动态"
+                pattern["content"] = card["dynamic"]
+                pattern["pic"] = card["pic"]
+
+            elif type == 64:  # 文章
+                pattern["type_zh"] = "文章"
+                pattern["content"] = card["title"] + card["summary"]
+                if len(card["image_urls"]) > 0:
+                    pattern["pic"] = card["image_urls"][0]
+
+            ret.append(pattern)
+
+        return ret
+
+    def generate_output(self, pattern: dict) -> (str, str):
+        text_part = """【UP名称】{name}\n【动态类型】{dynamic_type}\n【动态ID】{dynamic_id}\n【时间】{time}\n【UID】{uid}\n【当前阅读次数】{view}\n【当前转发次数】{repost}\n【当前点赞次数】{like}\n【内容摘要】{content}\n""".format(
+            name=pattern["name"],
+            dynamic_type=pattern["type_zh"],
+            dynamic_id=pattern["dynamic_id"],
+            time=pattern["time"],
+            uid=pattern["uid"],
+            view=pattern["view"],
+            repost=pattern["repost"],
+            like=pattern["like"],
+            content=pattern["content"],
+        )
+        pic_part = pattern["pic"]
+        return text_part, pic_part
diff --git a/ATRI/plugins/bilibili_dynamic/user.json b/ATRI/plugins/bilibili_dynamic/user.json
new file mode 100644
index 0000000..ff4f49b
--- /dev/null
+++ b/ATRI/plugins/bilibili_dynamic/user.json
@@ -0,0 +1,244 @@
+{
+  "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


From 6eab8302e8c5f21c908cf45d705d755ca767d2ca Mon Sep 17 00:00:00 2001
From: Yuki-Asuuna <10174503104@stu.ecnu.edu.cn>
Date: Tue, 1 Mar 2022 10:54:35 +0800
Subject: fix: add title for video dynamic info

Change-Id: If5d85327e661487cccb6a26d833da08527bd79e7
---
 ATRI/plugins/bilibili_dynamic/data_source.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'ATRI/plugins')

diff --git a/ATRI/plugins/bilibili_dynamic/data_source.py b/ATRI/plugins/bilibili_dynamic/data_source.py
index e4eb5d7..a6a9a67 100644
--- a/ATRI/plugins/bilibili_dynamic/data_source.py
+++ b/ATRI/plugins/bilibili_dynamic/data_source.py
@@ -320,7 +320,7 @@ class BilibiliDynamicSubscriptor(Service):
 
             elif type == 8:  # 视频动态
                 pattern["type_zh"] = "视频动态"
-                pattern["content"] = card["dynamic"]
+                pattern["content"] = card["title"] + card["dynamic"]
                 pattern["pic"] = card["pic"]
 
             elif type == 64:  # 文章
-- 
cgit v1.2.3


From 3db938fc748a56e6de666e7c298b1e6858052bdb Mon Sep 17 00:00:00 2001
From: Yuki-Asuuna <10174503104@stu.ecnu.edu.cn>
Date: Tue, 1 Mar 2022 15:44:04 +0800
Subject: fix: sort to ensure dynamic timestamp with time chronological order

Change-Id: I105fc15904591cd89bef7bade38f48c7a7b834eb
---
 ATRI/plugins/bilibili_dynamic/__init__.py    |  1 -
 ATRI/plugins/bilibili_dynamic/data_source.py | 17 +++++++++--------
 2 files changed, 9 insertions(+), 9 deletions(-)

(limited to 'ATRI/plugins')

diff --git a/ATRI/plugins/bilibili_dynamic/__init__.py b/ATRI/plugins/bilibili_dynamic/__init__.py
index 87854c0..fa9f8c3 100644
--- a/ATRI/plugins/bilibili_dynamic/__init__.py
+++ b/ATRI/plugins/bilibili_dynamic/__init__.py
@@ -151,7 +151,6 @@ async def _check_dynamic():
         if info:
             if info.get("cards") is not None:
                 res = subscriptor.extract_dynamics_detail(info.get("cards"))
-        res = res[::-1]
         for i in res:
             i["name"] = d.nickname
             if ts < i["timestamp"]:
diff --git a/ATRI/plugins/bilibili_dynamic/data_source.py b/ATRI/plugins/bilibili_dynamic/data_source.py
index a6a9a67..bb2684f 100644
--- a/ATRI/plugins/bilibili_dynamic/data_source.py
+++ b/ATRI/plugins/bilibili_dynamic/data_source.py
@@ -9,6 +9,7 @@ import os
 import re
 import asyncio
 from typing import Any
+from operator import itemgetter
 
 __doc__ = """b站订阅动态助手
 """
@@ -54,13 +55,13 @@ def get_session():
 
 
 async def bilibili_request(
-    method: str,
-    url: str,
-    params: dict = None,
-    data: Any = None,
-    no_csrf: bool = False,
-    json_body: bool = False,
-    **kwargs,
+        method: str,
+        url: str,
+        params: dict = None,
+        data: Any = None,
+        no_csrf: bool = False,
+        json_body: bool = False,
+        **kwargs,
 ):
     """
     向接口发送请求。
@@ -330,7 +331,7 @@ class BilibiliDynamicSubscriptor(Service):
                     pattern["pic"] = card["image_urls"][0]
 
             ret.append(pattern)
-
+        ret = sorted(ret, key=itemgetter("timestamp"))
         return ret
 
     def generate_output(self, pattern: dict) -> (str, str):
-- 
cgit v1.2.3


From bca241cdfca4ba0e45e3e9159ddc13922eb52683 Mon Sep 17 00:00:00 2001
From: Lint Action <noreply@github.com>
Date: Tue, 1 Mar 2022 07:45:31 +0000
Subject: =?UTF-8?q?:rotating=5Flight:=20=E8=87=AA=E5=8A=A8=E8=BF=9B?=
 =?UTF-8?q?=E8=A1=8C=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ATRI/plugins/bilibili_dynamic/data_source.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

(limited to 'ATRI/plugins')

diff --git a/ATRI/plugins/bilibili_dynamic/data_source.py b/ATRI/plugins/bilibili_dynamic/data_source.py
index bb2684f..25c2d8d 100644
--- a/ATRI/plugins/bilibili_dynamic/data_source.py
+++ b/ATRI/plugins/bilibili_dynamic/data_source.py
@@ -55,13 +55,13 @@ def get_session():
 
 
 async def bilibili_request(
-        method: str,
-        url: str,
-        params: dict = None,
-        data: Any = None,
-        no_csrf: bool = False,
-        json_body: bool = False,
-        **kwargs,
+    method: str,
+    url: str,
+    params: dict = None,
+    data: Any = None,
+    no_csrf: bool = False,
+    json_body: bool = False,
+    **kwargs,
 ):
     """
     向接口发送请求。
-- 
cgit v1.2.3