diff --git a/nonebot-plugin-ncm/__init__.py b/nonebot-plugin-ncm/__init__.py index 7c9b293..491bf4e 100644 --- a/nonebot-plugin-ncm/__init__.py +++ b/nonebot-plugin-ncm/__init__.py @@ -1,71 +1,98 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from typing import Tuple, Any, Union + import nonebot -from nonebot.rule import Rule -from nonebot.log import logger from nonebot import on_regex, on_command, on_message from nonebot.adapters.onebot.v11 import (Message, Bot, MessageSegment, - ActionFailed, GroupMessageEvent, PrivateMessageEvent) -import re - +from nonebot.log import logger from nonebot.matcher import Matcher from nonebot.params import CommandArg, RegexGroup, Arg -from .data_source import nncm, music, ncm_config, playlist, setting, Q, cmd +from nonebot.rule import Rule + +from .data_source import nncm, ncm_config, setting, Q, cmd # =======nonebot-plugin-help======= __plugin_meta__ = nonebot.plugin.PluginMetadata( name='✨ 基于go-cqhttp与nonebot2的 网易云 无损音乐下载 ✨', - description='您的简单插件描述', + description='网易云 无损音乐下载', usage=f'''将网易云歌曲/歌单分享到群聊即可自动解析\n回复机器人解析消息即可自动下载(需要时间)\n - {cmd}ncm t 开启解析\n{cmd}ncm t 关闭解析\n{cmd}点歌 歌名:点歌''', - extra={'version': '1.4.0'} + {cmd}ncm t:开启解析\n{cmd}ncm t:关闭解析\n{cmd}点歌 歌名:点歌''', + extra={'version': '1.5.0'} ) - # ========nonebot-plugin-ncm====== +# ===========Constant============= +TRUE = ["True", "T", "true", "t"] +FALSE = ["False", "F", "false", "f"] +ADMIN = ["owner", "admin", "member"] + + # ===============Rule============= -async def song_is_open(event: GroupMessageEvent) -> bool: - info = setting.search(Q["group_id"] == event.dict()["group_id"]) - if info: - if info[0]["song"]: - return ncm_config.ncm_song - else: - setting.insert({"group_id": event.dict()["group_id"], "song": False, "list": False}) - return False +async def song_is_open(event: Union[GroupMessageEvent, PrivateMessageEvent]) -> bool: + if isinstance(event, GroupMessageEvent): + info = setting.search(Q["group_id"] == event.group_id) + if info: + return info[0]["song"] + else: + setting.insert({"group_id": event.group_id, "song": False, "list": False}) + return False + elif isinstance(event, PrivateMessageEvent): + info = setting.search(Q["user_id"] == event.user_id) + if info: + return info[0]["song"] + else: + setting.insert({"user_id": event.user_id, "song": True, "list": True}) + return True -async def playlist_is_open(event: GroupMessageEvent) -> bool: - info = setting.search(Q["group_id"] == event.dict()["group_id"]) +async def playlist_is_open(event: Union[GroupMessageEvent, PrivateMessageEvent]) -> bool: + if isinstance(event, GroupMessageEvent): + info = setting.search(Q["group_id"] == event.group_id) + if info: + return info[0]["list"] + else: + setting.insert({"group_id": event.group_id, "song": False, "list": False}) + return False + elif isinstance(event, PrivateMessageEvent): + info = setting.search(Q["user_id"] == event.user_id) + if info: + return info[0]["list"] + else: + setting.insert({"user_id": event.user_id, "song": True, "list": True}) + return True + + +async def check_search() -> bool: + info = setting.search(Q["global"] == "search") if info: - if info[0]["list"]: - return ncm_config.ncm_list + return info[0]["value"] else: - setting.insert({"group_id": event.dict()["group_id"], "song": False, "list": False}) - return False + setting.insert({"global": "search", "value": True}) + return True -async def search_check() -> bool: - return ncm_config.ncm_search +async def music_set_rule(event: Union[GroupMessageEvent, PrivateMessageEvent]) -> bool: + # 权限设置 + return event.sender.role in ADMIN[:ncm_config.ncm_admin_level] or event.get_user_id() in ncm_config.superusers -async def music_reply_rule(event: GroupMessageEvent): - return event.reply and event.reply.sender.user_id == event.self_id +async def music_reply_rule(event: Union[GroupMessageEvent, PrivateMessageEvent]): + return event.reply and event.get_plaintext() == "下载" # ============Matcher============= ncm_set = on_command("ncm", + rule=Rule(music_set_rule), priority=1, block=False) '''功能设置''' music_regex = on_regex("(song|url)\?id=([0-9]+)(|&)", - rule=Rule(song_is_open), priority=2, block=False) '''歌曲id识别''' playlist_regex = on_regex("playlist\?id=([0-9]+)&", - rule=Rule(playlist_is_open), priority=2, block=False) '''歌单识别''' music_reply = on_message(priority=2, @@ -73,7 +100,7 @@ async def music_reply_rule(event: GroupMessageEvent): block=False) '''回复下载''' search = on_command("点歌", - rule=Rule(search_check), + rule=Rule(check_search), priority=2, block=False) '''点歌''' @@ -90,103 +117,134 @@ async def receive_song(bot: Bot, song: Message = Arg(), ): nncm.get_session(bot, event) - _id = await nncm.search_song(keyword=song.__str__(), limit=1) - try: - await bot.send(event=event, message=Message(MessageSegment.music(type_="163", id_=_id))) - if isinstance(event, GroupMessageEvent): - await nncm.parse_song(_id) - except ActionFailed: - await search.finish(event=event, message="[WARNING]: 合并转发(群)消息发送失败: 账号可能被风控") + _id = await nncm.search_song(keyword=str(song), limit=1) + message_id = await bot.send(event=event, message=Message(MessageSegment.music(type_="163", id_=_id))) + nncm.get_song(message_id=message_id["message_id"], nid=_id) + # try: + + # except ActionFailed as e: + # logger.error(e.info) + # await search.finish(event=event, message=f"[WARNING]: 网易云卡片消息发送失败: 账号可能被风控") @music_regex.handle() -async def music_receive(bot: Bot, event: GroupMessageEvent, regroup: Tuple[Any, ...] = RegexGroup()): +async def music_receive(bot: Bot, event: Union[GroupMessageEvent, PrivateMessageEvent], regroup: Tuple[Any, ...] = RegexGroup()): nid = regroup[1] - logger.debug(f"已识别NID:{nid}的歌曲") + logger.info(f"已识别NID:{nid}的歌曲") nncm.get_session(bot, event) - await nncm.parse_song(nid) + nncm.get_song(nid) @playlist_regex.handle() -async def music_receive(bot: Bot, event: GroupMessageEvent, regroup: Tuple[Any, ...] = RegexGroup()): +async def music_list_receive(bot: Bot, event: Union[GroupMessageEvent, PrivateMessageEvent], regroup: Tuple[Any, ...] = RegexGroup()): lid = regroup[0] - logger.debug(f"已识别LID:{lid}的歌单") + logger.info(f"已识别LID:{lid}的歌单") nncm.get_session(bot, event) msg = await nncm.playlist(lid=lid) await bot.send(event=event, message=Message(MessageSegment.text(msg))) @music_reply.handle() -async def music_reply_receive(bot: Bot, event: GroupMessageEvent): - try: # 防止其他回复状况报错 - message: str = event.dict()["reply"]["message"][0].data["text"] - except Exception: - return +async def music_reply_receive(bot: Bot, event: Union[GroupMessageEvent, PrivateMessageEvent]): + # logger.info(event.dict()["reply"]["message_id"]) nncm.get_session(bot, event) - nid = re.search("ID:([0-9]*)", message) - if nid: + info = nncm.check_message() + if info is None: + return + if info["type"] == "song" and await song_is_open(event): await bot.send(event=event, message="少女祈祷中🙏...") - await nncm.download(ids=[int(nid[1])]) - data = music.search(Q["id"] == int(nid[1])) + await nncm.download(ids=[int(info["nid"])]) + data = await nncm.music_check(info["nid"]) if data: - await nncm.upload_group_file(data) + if isinstance(event, GroupMessageEvent): + await nncm.upload_group_file(data) + elif isinstance(event, PrivateMessageEvent): + await nncm.upload_private_file(data) else: logger.error("数据库中未有该音乐地址数据") - else: - lid = re.search("LIST:([0-9]*)", message) - info = playlist.search(Q["playlist_id"] == lid[1]) - if info: - await nncm.download(ids=info[0]["ids"]) - for i in info[0]["ids"]: - data = music.search(Q["id"] == i) - if data: - await nncm.upload_group_file(data) - else: - logger.error("数据库中未有该音乐地址数据") - else: - logger.error("数据库中未发现该歌单ID") + + elif info["type"] == "playlist" and await playlist_is_open(event): + await nncm.download(ids=info["ids"]) + for i in info["ids"]: + data = await nncm.music_check(i) + if data: + await nncm.upload_group_file(data) + else: + logger.error("数据库中未有该音乐地址数据") @ncm_set.handle() -async def set_receive(bot: Bot, event: GroupMessageEvent, args: Message = CommandArg()): # 功能设置接收 - true = ["True", "T", "true", "t"] - false = ["False", "F", "false", "f"] +async def set_receive(bot: Bot, event: Union[GroupMessageEvent, PrivateMessageEvent], args: Message = CommandArg()): # 功能设置接收 logger.debug(f"权限为{event.sender.role}的用户<{event.sender.nickname}>尝试使用命令{cmd}ncm {args}") - if event.sender.role not in ncm_config.ncm_admin: - logger.debug(f"执行错误:用户<{event.sender.nickname}>权限{event.sender.role}不在{ncm_config.ncm_admin}中") - elif event.get_user_id() not in ncm_config.superusers: - logger.debug(f"执行错误:用户<{event.sender.nickname}>非超级管理员(SUPERUSERS)") - if event.sender.role in ncm_config.ncm_admin or event.get_user_id() in ncm_config.superusers: - if args: - args = args.__str__().split() + if args: + args = str(args).split() + if len(args) == 1: mold = args[0] - else: - msg = f"{cmd}ncm:获取命令菜单\r\n说明:网易云歌曲分享到群内后回复机器人即可下载\r\n" \ - f"{cmd}ncm t:开启解析\r\n{cmd}ncm f:关闭解析\n{cmd}点歌 歌名:点歌" - return await ncm_set.finish(message=MessageSegment.text(msg)) - - info = setting.search(Q["group_id"] == event.dict()["group_id"]) - # logger.info(info) - if info: - if mold in true: + if isinstance(event, GroupMessageEvent): + info = setting.search(Q["group_id"] == event.group_id) # logger.info(info) - info[0]["song"] = True - info[0]["list"] = True - setting.update(info[0], Q["group_id"] == event.dict()["group_id"]) - msg = "已开启自动下载功能" - await bot.send(event=event, message=Message(MessageSegment.text(msg))) - elif mold in false: - info[0]["song"] = False - info[0]["list"] = False - setting.update(info[0], Q["group_id"] == event.dict()["group_id"]) - msg = "已关闭自动下载功能" - await bot.send(event=event, message=Message(MessageSegment.text(msg))) - logger.debug(f"用户<{event.sender.nickname}>执行操作成功") - else: - if mold in true: - setting.insert({"group_id": event.dict()["group_id"], "song": True, "list": True}) - elif mold in false: - setting.insert({"group_id": event.dict()["group_id"], "song": False, "list": False}) - + if info: + if mold in TRUE: + info[0]["song"] = True + info[0]["list"] = True + setting.update(info[0], Q["group_id"] == event.group_id) + msg = "已开启自动下载功能" + await bot.send(event=event, message=Message(MessageSegment.text(msg))) + elif mold in FALSE: + info[0]["song"] = False + info[0]["list"] = False + setting.update(info[0], Q["group_id"] == event.group_id) + msg = "已关闭自动下载功能" + await bot.send(event=event, message=Message(MessageSegment.text(msg))) + logger.debug(f"用户<{event.sender.nickname}>执行操作成功") + else: + if mold in TRUE: + setting.insert({"group_id": event.group_id, "song": True, "list": True}) + elif mold in FALSE: + setting.insert({"group_id": event.group_id, "song": False, "list": False}) + elif isinstance(event, PrivateMessageEvent): + info = setting.search(Q["user_id"] == event.user_id) + # logger.info(info) + if info: + if mold in TRUE: + info[0]["song"] = True + info[0]["list"] = True + setting.update(info[0], Q["user_id"] == event.user_id) + msg = "已开启自动下载功能" + await bot.send(event=event, message=Message(MessageSegment.text(msg))) + elif mold in FALSE: + info[0]["song"] = False + info[0]["list"] = False + setting.update(info[0], Q["user_id"] == event.user_id) + msg = "已关闭自动下载功能" + await bot.send(event=event, message=Message(MessageSegment.text(msg))) + logger.debug(f"用户<{event.sender.nickname}>执行操作成功") + else: + if mold in TRUE: + setting.insert({"user_id": event.user_id, "song": True, "list": True}) + elif mold in FALSE: + setting.insert({"user_id": event.user_id, "song": False, "list": False}) + elif len(args) == 2 and args[0] == "search": + mold = args[1] + info = setting.search(Q["global"] == "search") + if info: + if mold in TRUE: + info[0]["value"] = True + setting.update(info[0], Q["global"] == "search") + msg = "已开启点歌功能" + await bot.send(event=event, message=Message(MessageSegment.text(msg))) + elif mold in FALSE: + info[0]["value"] = False + setting.update(info[0], Q["global"] == "search") + msg = "已关闭点歌功能" + await bot.send(event=event, message=Message(MessageSegment.text(msg))) + logger.debug(f"用户<{event.sender.nickname}>执行操作成功") + else: + if mold in TRUE: + setting.insert({"global": "search", "value": True}) + elif mold in FALSE: + setting.insert({"global": "search", "value": False}) else: - await bot.send(event=event, message=Message(MessageSegment.text("你咩有权限哦~"))) + msg = f"{cmd}ncm:获取命令菜单\r\n说明:网易云歌曲分享到群内后回复机器人即可下载\r\n" \ + f"{cmd}ncm t:开启解析\r\n{cmd}ncm f:关闭解析\n{cmd}点歌 歌名:点歌" + return await ncm_set.finish(message=MessageSegment.text(msg)) diff --git a/nonebot-plugin-ncm/config.py b/nonebot-plugin-ncm/config.py index d25bd4d..234b68c 100644 --- a/nonebot-plugin-ncm/config.py +++ b/nonebot-plugin-ncm/config.py @@ -6,8 +6,8 @@ class Config(BaseModel, extra=Extra.ignore): superusers: list = [] - ncm_admin: list = ["owner", "admin"] - '''设置命令权限(非解析下载,仅解析功能开关设置)''' + ncm_admin_level: int = 1 + '''设置命令权限(1:仅限superusers和群主,2:在1的基础上管理员,3:所有用户)''' ncm_phone: str = "" '''手机号''' @@ -18,18 +18,6 @@ class Config(BaseModel, extra=Extra.ignore): ncm_password: str = "" '''密码''' - ncm_song: bool = True - '''单曲解析总开关''' - - ncm_list: bool = True - '''歌单解析总开关''' - - whitelist: list = [] - '''白名单(一键导入)''' - - ncm_search: bool = True - '''点歌总开关''' - global_config = nonebot.get_driver().config ncm_config = Config(**global_config.dict()) # 载入配置 diff --git a/nonebot-plugin-ncm/data_source.py b/nonebot-plugin-ncm/data_source.py index 56de17a..45231e2 100644 --- a/nonebot-plugin-ncm/data_source.py +++ b/nonebot-plugin-ncm/data_source.py @@ -37,7 +37,8 @@ music = TinyDB("./db/musics.json") playlist = TinyDB("./db/playlist.json") setting = TinyDB("./db/ncm_setting.json") -ncm_cache = TinyDB("./db/ncm_cache.json") +ncm_user_cache = TinyDB("./db/ncm_cache.json") +ncm_check_cache = TinyDB("./db/ncm_check_cache.json") Q = Query() cmd = list(nonebot.get_driver().config.command_start)[0] @@ -45,17 +46,6 @@ class NcmLoginFailedException(Exception): pass -# ============白名单导入============= -for ids in ncm_config.whitelist: - info = setting.search(Q["group_id"] == ids) - if info: - info[0]["song"] = True - info[0]["list"] = True - setting.update(info[0], Q["group_id"] == ids) - else: - setting.insert({"group_id": ids, "song": True, "list": True}) - - # ============主类============= class Ncm: def __init__(self): @@ -69,23 +59,22 @@ def get_session(self, bot: Bot, event: Union[GroupMessageEvent, PrivateMessageEv @staticmethod def save_user(session: str): - info = ncm_cache.search(Q["uid"] == "user") + info = ncm_user_cache.search(Q["uid"] == "user") if info: info[0]['session'] = session - ncm_cache.update(info[0], Q["uid"] == "user") + ncm_user_cache.update(info[0], Q["uid"] == "user") else: - ncm_cache.insert({"uid": "user", "session": session}) + ncm_user_cache.insert({"uid": "user", "session": session}) @staticmethod def load_user(info): - SetCurrentSession(LoadSessionFromString(info[0]['session'])) - + SetCurrentSession(LoadSessionFromString(info[0]['session'])) def login(self): try: self.api.login.LoginViaCellphone(phone=ncm_config.ncm_phone, password=ncm_config.ncm_password) self.get_user_info() - + except Exception as e: if str(e) == str({'code': 400, 'message': '登陆失败,请进行安全验证'}): logger.error("缺少安全验证,请将账号留空进行二维码登录") @@ -99,19 +88,19 @@ def get_user_info(self): self.save_user(DumpSessionAsString(GetCurrentSession())) def get_phone_login(self): - phone=ncm_config.ncm_phone - ctcode=int(ncm_config.ncm_ctcode) - result = self.api.login.SetSendRegisterVerifcationCodeViaCellphone(cell=phone,ctcode=ctcode) - if not result.get('code',0) == 200: + phone = ncm_config.ncm_phone + ctcode = int(ncm_config.ncm_ctcode) + result = self.api.login.SetSendRegisterVerifcationCodeViaCellphone(cell=phone, ctcode=ctcode) + if not result.get('code', 0) == 200: logger.error(result) else: - logger.success('已发送验证码,输入验证码:') + logger.success('已发送验证码,输入验证码:') while True: captcha = int(input()) - verified = self.api.login.GetRegisterVerifcationStatusViaCellphone(phone,captcha,ctcode) - if verified.get('code',0) == 200: + verified = self.api.login.GetRegisterVerifcationStatusViaCellphone(phone, captcha, ctcode) + if verified.get('code', 0) == 200: break - result = self.api.login.LoginViaCellphone(phone,captcha=captcha,ctcode=ctcode) + result = self.api.login.LoginViaCellphone(phone, captcha=captcha, ctcode=ctcode) self.get_user_info() def get_qrcode(self): @@ -144,6 +133,18 @@ def detail(self, ids: list) -> list: detail = [(data["name"] + "-" + ",".join([names["name"] for names in data["ar"]])) for data in songs] return detail + async def music_check(self, nid): + nid = int(nid) + info = music.search(Q["id"] == nid) + if info: + path = Path(info[0]["file"]) + if path.is_file(): + return info[0] + else: + return await self.download(ids=[nid], check=True) + else: + return None + async def search_song(self, keyword: str, limit: int = 1) -> int: # 搜索歌曲 res = self.api.cloudsearch.GetSearchResult(keyword=keyword, stype=SONG, limit=limit) logger.debug(f"搜索歌曲{keyword},返回结果:{res}") @@ -164,11 +165,54 @@ async def parse_song(self, nid: Union[int, str]): msg = f"歌曲ID:{nid}\r\n如需下载请回复该条消息\r\n关闭解析请使用指令\r\n{cmd}ncm f" await self.bot.send(event=self.event, message=Message(MessageSegment.text(msg))) - async def upload_group_file(self, data: list): + def check_message(self): + """检查缓存中是否存在解析 + + :return: + """ + info = ncm_check_cache.search(Q.message_id == self.event.dict()["reply"]["message_id"]) + return info[0] if info else None + + def get_song(self, nid: Union[int, str], message_id=None): + """解析歌曲id,并且加入缓存 + + :param message_id: + :param nid: + :return: + """ + if message_id: + mid = message_id + else: + mid = self.event.message_id + ncm_check_cache.insert({"message_id": mid, + "type": "song", + "nid": nid, + "ids": [], + "time": int(time.time())}) + + def get_playlist(self, ids: list, message_id=None): + """解析歌曲id,并且加入缓存 + + :param message_id: + :param ids: + :return: + """ + if message_id: + mid = message_id + else: + mid = self.event.message_id + ncm_check_cache.insert({"message_id": mid, + "type": "playlist", + "nid": 0, + "ids": ids, + "time": int(time.time())}) + + async def upload_group_file(self, data): try: await self.bot.call_api('upload_group_file', group_id=self.event.group_id, - file=data[0]["file"], name=data[0]["filename"]) + file=data["file"], name=data["filename"]) except (ActionFailed, NetworkError) as e: + logger.error(e.info) if isinstance(e, ActionFailed) and e.info["wording"] == "server" \ " requires unsupported ftn upload": await self.bot.send(event=self.event, message=Message(MessageSegment.text( @@ -176,7 +220,17 @@ async def upload_group_file(self, data: list): "请将机器人设置为管理员或者允许群员上传文件"))) elif isinstance(e, NetworkError): await self.bot.send(event=self.event, message=Message(MessageSegment.text( - "[ERROR] 文件上传失败\r\n[原因] 上传超时"))) + "[ERROR] 文件上传失败\r\n[原因] 上传超时(一般来说还在传,建议等待五分钟)"))) + + async def upload_private_file(self, data): + try: + await self.bot.call_api('upload_private_file', user_id=self.event.get_user_id(), + file=data["file"], name=data["filename"]) + except (ActionFailed, NetworkError) as e: + logger.error(e.info) + if isinstance(e, NetworkError): + await self.bot.send(event=self.event, message=Message(MessageSegment.text( + "[ERROR] 文件上传失败\r\n[原因] 上传超时(一般来说还在传,建议等待五分钟)"))) async def playlist(self, lid: Union[int, str]): # 下载歌单 data = self.api.playlist.GetPlaylistInfo(lid) @@ -203,7 +257,7 @@ async def playlist(self, lid: Union[int, str]): # 下载歌单 playlist.insert(config) return msg - async def download(self, ids: list): # 下载音乐 + async def download(self, ids: list, check=False): # 下载音乐 data: list = self.api.track.GetTrackAudio(song_ids=ids, bitrate=3200 * 1000)["data"] # logger.info(data) name: list = self.detail(ids) @@ -219,8 +273,8 @@ async def download(self, ids: list): # 下载音乐 filename = re.sub('[\/:*?"<>|]', '-', filename) file = Path.cwd().joinpath("music").joinpath(filename) config = { - "id": nid, - "file": file.__str__(), # 获取文件位置 + "id": int(nid), + "file": str(file), # 获取文件位置 "filename": filename, # 获取文件名 "from": "song" if len(ids) == 1 else "list", # 判断来自单曲还是歌单 "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 获取时间 @@ -235,15 +289,15 @@ async def download(self, ids: list): # 下载音乐 async with async_open(file, 'wb') as out_file: async for chunk in r.aiter_bytes(): await out_file.write(chunk) - if len(ids) > 1: + if not check and len(ids) > 1: if num // 10 == 0 or num == len(ids): await self.bot.send(event=self.event, message=Message(MessageSegment.text(f"下载进度:{num}/{len(ids)}"))) num += 1 - + return config nncm = Ncm() -info = ncm_cache.search(Q.uid == "user") +info = ncm_user_cache.search(Q.uid == "user") if info: logger.info("检测到缓存,自动加载用户") nncm.load_user(info) @@ -256,4 +310,3 @@ async def download(self, ids: list): # 下载音乐 nncm.get_phone_login() else: nncm.login() - diff --git a/pyproject.toml b/pyproject.toml index 6436377..741925e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot-plugin-ncm" -version = "1.4.2" +version = "1.5.0" description = "基于go-cqhttp与nonebot2的 网易云 无损音乐下载" license = "Apache License 2.0" authors = ["kitUIN "] @@ -14,11 +14,11 @@ repository = "https://github.com/kitUIN/nonebot-plugin-ncm" keywords = ["netease-cloud-music", "netease", "go-cqhttp", "nonebot2"] [tool.poetry.dependencies] python = "^3.8" -nonebot2 = "^2.0.0b1" +nonebot2 = "^2.0.0rc2" tinydb = "^4.7.0" -pyncm ="^1.6.8.2.2" +pyncm ="^1.6.8.3" aiofile ="^3.7.4" -nonebot-adapter-onebot = "^2.0.0b1" +nonebot-adapter-onebot = "^2.1.5" qrcode = "^7.3.1" [tool.poetry.dev-dependencies]