summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ATRI/database/__init__.py1
-rw-r--r--ATRI/database/db.py77
-rw-r--r--ATRI/database/models.py22
-rw-r--r--ATRI/plugins/bilibili_dynamic/__init__.py169
-rw-r--r--ATRI/plugins/bilibili_dynamic/data_source.py349
-rw-r--r--ATRI/plugins/bilibili_dynamic/user.json244
-rw-r--r--ATRI/utils/__init__.py12
-rw-r--r--requirements.txt3
8 files changed, 876 insertions, 1 deletions
diff --git a/ATRI/database/__init__.py b/ATRI/database/__init__.py
new file mode 100644
index 0000000..6840881
--- /dev/null
+++ b/ATRI/database/__init__.py
@@ -0,0 +1 @@
+from .db import DB
diff --git a/ATRI/database/db.py b/ATRI/database/db.py
new file mode 100644
index 0000000..c2aa015
--- /dev/null
+++ b/ATRI/database/db.py
@@ -0,0 +1,77 @@
+from tortoise import Tortoise
+
+from ATRI.database import models
+from nonebot import get_driver
+
+
+# 关于数据库的操作类,只实现与数据库有关的CRUD
+# 请不要把业务逻辑写进去
+class DB:
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ async def init(self):
+ from ATRI.database import models
+
+ await Tortoise.init(
+ db_url="sqlite://ATRI/database/db.sqlite3",
+ modules={"models": [locals()["models"]]},
+ )
+ # Generate the schema
+ await Tortoise.generate_schemas()
+
+ async def add_subscription(self, uid: int, groupid: int) -> bool:
+ try:
+ _ = await models.Subscription.create(uid=uid, groupid=groupid)
+ return True
+ except:
+ return False
+
+ async def get_all_subscriptions_by_gid(self, groupid: int) -> list:
+ try:
+ subs = await self.get_subscriptions(query_map={"groupid": groupid})
+ return subs
+ except:
+ return []
+
+ async def remove_subscription(self, query_map: dict) -> bool:
+ try:
+ ret = await models.Subscription.filter(**query_map).delete()
+ return True
+ except:
+ return False
+
+ async def get_subscriptions(self, query_map: dict) -> list:
+ try:
+ ret = await models.Subscription.filter(**query_map)
+ return ret
+ except:
+ return []
+
+ async def get_all_subscriptions(self) -> list:
+ try:
+ ret = await models.Subscription.all()
+ return ret
+ except:
+ return []
+
+ async def update_subscriptions_by_uid(self, uid: int, update_map: dict) -> bool:
+ try:
+ # why use ** ?
+ # Reference: https://stackoverflow.com/questions/5710391/converting-python-dict-to-kwargs
+ _ = await models.Subscription.filter(uid=uid).update(**update_map)
+ return True
+ except:
+ return False
+
+
+async def init():
+ async with DB() as db:
+ await db.init()
+
+
+driver = get_driver()
+driver.on_startup(init)
diff --git a/ATRI/database/models.py b/ATRI/database/models.py
new file mode 100644
index 0000000..b3953df
--- /dev/null
+++ b/ATRI/database/models.py
@@ -0,0 +1,22 @@
+"""
+ 定义SQLITE数据库的关系模式(表)
+ 数据库采用了tortoise orm,可以很好地支持异步
+"""
+
+from tortoise.models import Model
+from tortoise import fields
+from datetime import datetime
+
+# b站订阅表
+class Subscription(Model):
+ uid = fields.IntField(pk=True) # up的uid
+ groupid = fields.IntField() # 群号
+ nickname = fields.TextField(null=True) # 订阅up的名称
+ last_update = fields.DatetimeField(
+ default=datetime.fromordinal(1)
+ ) # 上一条动态更新时间 默认0001-01-01 00:00:00
+
+ def __str__(self):
+ return "[{nickname}|{uid}|{groupid}]".format(
+ nickname=self.nickname, uid=self.uid, groupid=self.groupid
+ )
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中
+ "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
diff --git a/ATRI/utils/__init__.py b/ATRI/utils/__init__.py
index 599e229..9c7f759 100644
--- a/ATRI/utils/__init__.py
+++ b/ATRI/utils/__init__.py
@@ -2,6 +2,7 @@ import os
import re
import yaml
import aiofiles
+import time
from pathlib import Path
from aiohttp import FormData
from datetime import datetime
@@ -11,6 +12,17 @@ from aiofiles.threadpool.text import AsyncTextIOWrapper
from . import request
+def timestamp2datetimestr(timestamp: int) -> str:
+ format = "%Y-%m-%d %H:%M:%S"
+ timestamp = time.localtime(timestamp)
+ dt = time.strftime(format, timestamp)
+ return dt
+
+
+def timestamp2datetime(value: int) -> datetime:
+ return datetime.fromtimestamp(value)
+
+
def now_time() -> float:
"""获取当前时间的整数."""
now_ = datetime.now()
diff --git a/requirements.txt b/requirements.txt
index 1655d22..c304c4d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,4 +14,5 @@ tensorflow>=2.6.0
jieba>=0.42.1
tabulate>=0.8.9
wcwidth>=0.2.5
-aiohttp>=3.8.1 \ No newline at end of file
+aiohttp>=3.7.4.post0
+tortoise-orm>=0.18.1 \ No newline at end of file