diff options
-rw-r--r-- | ATRI/database/__init__.py | 1 | ||||
-rw-r--r-- | ATRI/database/db.py | 77 | ||||
-rw-r--r-- | ATRI/database/models.py | 22 | ||||
-rw-r--r-- | ATRI/plugins/bilibili_dynamic/__init__.py | 169 | ||||
-rw-r--r-- | ATRI/plugins/bilibili_dynamic/data_source.py | 349 | ||||
-rw-r--r-- | ATRI/plugins/bilibili_dynamic/user.json | 244 | ||||
-rw-r--r-- | ATRI/utils/__init__.py | 12 | ||||
-rw-r--r-- | requirements.txt | 3 |
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中 [email protected]_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 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 |