diff --git a/.env.dev b/.env.dev index 00d51e29..127b4891 100644 --- a/.env.dev +++ b/.env.dev @@ -10,7 +10,52 @@ NICKNAME=["真寻", "小真寻", "绪山真寻", "小寻子"] SESSION_EXPIRE_TIMEOUT=30 -DEBUG=False +# 全局图片统一使用bytes发送,当真寻与协议端不在同一服务器上时为True +IMAGE_TO_BYTES = False + +PLATFORM_SUPERUSERS = ' + { + "qq": [""], + "dodo": [""] + } +' + +DRIVER=~fastapi+~httpx+~websockets + +# kook adapter toekn +# kaiheila_bots =[{"token": ""}] + +# # discode adapter +# DISCORD_BOTS=' +# [ +# { +# "token": "", +# "intent": { +# "guild_messages": true, +# "direct_messages": true +# }, +# "application_commands": {"*": ["*"]} +# } +# ] +# ' +# DISCORD_PROXY='' + +# # dodo adapter +# DODO_BOTS=' +# [ +# { +# "client_id": "", +# "token": "" +# } +# ] +# ' + +# application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令 +# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册 + +# LOG_LEVEL=DEBUG # 服务器和端口 HOST = 127.0.0.1 PORT = 8080 + + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 94fc3aad..64932223 100644 --- a/.gitignore +++ b/.gitignore @@ -180,4 +180,5 @@ plugins/csgo_server/ plugins/activity/ !/resources/image/genshin/alc/back.png !/data/genshin_alc/ -.vscode/launch.json \ No newline at end of file +.vscode/launch.json +/resources/template/my_info \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..dc0ad84c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,29 @@ +{ + "C_Cpp.errorSquiggles": "enabled", + "terminal.integrated.env.linux": { + "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" + }, + "cSpell.words": [ + "aiofiles", + "Alconna", + "arclet", + "Arparma", + "displayname", + "flmt", + "getbbox", + "hibiapi", + "httpx", + "kaiheila", + "lolicon", + "nonebot", + "onebot", + "pixiv", + "Setu", + "tobytes", + "ujson", + "unban", + "userinfo", + "zhenxun" + ], + "python.analysis.autoImportCompletions": true +} diff --git a/README.md b/README.md index 4f540ca5..cc58a23e 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,54 @@ -
+
-![maven](https://img.shields.io/badge/python-3.8%2B-blue) -![maven](https://img.shields.io/badge/nonebot-2.0.0-yellow) -![maven](https://img.shields.io/badge/go--cqhttp-1.0.0-red) +
+ +![maven](https://img.shields.io/badge/python-3.9%2B-blue) +![maven](https://img.shields.io/badge/nonebot-2.1.3-yellow) + +
+ +
# 绪山真寻Bot +
+ **** +
+ “真寻是[椛椛](https://github.com/FloatTech/ZeroBot-Plugin)的好朋友!” +
+ **** -此项目基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库的QQ群娱乐机器人 ## 关于 用爱发电,某些功能学习借鉴了大佬们的代码,因为绪山真寻实在太可爱了因此开发了 绪山真寻bot,实现了一些对群友的娱乐功能和实用功能(大概)。 -如果该项目的图片等等侵犯猫豆腐老师权益请联系我删除! +如果该项目的图片等等侵犯猫豆腐老师权益请联系我删除! 讨论插件开发,nonebot2开发,或者有 安装使用问题开发建议,可以发送issues或加入[ [真寻酱的技术群](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) ] (在这里请不要吹水!) - 希望有个地方讨论绪山真寻Bot,渴望吹水聊天,可以加入[ [是真寻酱哒](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) ] + ## 声明 此项目仅用于学习交流,请勿用于非法用途 + +
+ # Nonebot2 - + 非常 [ **[NICE](https://github.com/nonebot/nonebot2)** ] 的OneBot框架 +
+ ## 未完成的文档 # [传送门](https://hibikier.github.io/zhenxun_bot/) @@ -50,15 +65,42 @@ ![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/html_help.png) -## Web UI -[zhenxun_bot_webui](https://github.com/HibiKier/zhenxun_bot_webui) +## 这是一份扩展 -## 一键安装脚本 +### 0. 体验一下? + +提供dev版本的zhenxun +``` +Url: 43.143.112.57:11451/onebot/v11/ws +AccessToken: PUBLIC_ZHENXUN_TEST + +注:你无法获得超级用户权限 +``` + +### 1. Web UI + +项目地址: [Web UI](https://github.com/HibiKier/zhenxun_bot_webui) + +
+后台示例图 + +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui1.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui2.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui3.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui4.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui5.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui6.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui7.png) + +
+ + +### 一键安装脚本(新版未测试) [zhenxun_bot-deploy](https://github.com/AkashiCoin/zhenxun_bot-deploy) -## 提供符合真寻标准的插件仓库 +### 提供符合真寻标准的插件仓库(旧版) [AkashiCoin/nonebot_plugins_zhenxun_bot](https://github.com/AkashiCoin/nonebot_plugins_zhenxun_bot) @@ -67,7 +109,7 @@ * 实现了许多功能,且提供了大量功能管理命令 * 通过Config配置项将所有插件配置统计保存至config.yaml,利于统一用户修改 * 方便增删插件,原生nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息` -* 提供了cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`__plugin_cd_limit__` +* 提供了cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等 * **..... 更多详细请通过`传送门`查看文档!** ## 功能列表 @@ -226,13 +268,10 @@ ``` -# 配置gocq - -在 https://github.com/Mrs4s/go-cqhttp 下载Releases最新版本,运行后选择反向代理, - 后将gocq的配置文件config.yml中的universal改为universal: ws://127.0.0.1:8080/onebot/v11/ws +# 使用napcat或拉格朗日 # 获取代码 -git clone https://github.com/HibiKier/zhenxun_bot.git +git clone https://github.com/HibiKier/zhenxun.git # 进入目录 cd zhenxun_bot @@ -247,6 +286,9 @@ poetry install # 安装依赖 # 开始运行 poetry shell # 进入虚拟环境 python bot.py + +# 运行后会在data目录下生成database.json文件,请根据自身数据库配置修改 +# 其他插件配置在data/config.yaml文件中(需要运行一次) ``` ## 简单配置 @@ -256,15 +298,32 @@ python bot.py SUPERUSERS = [""] # 填写你的QQ -2.在configs/config.py文件中 - * 数据库配置 + PLATFORM_SUPERUSERS = ' + { + "qq": [""], # 在此处填写你的qq + "dodo": [], + "kaiheila": [], + "discord": [] + } +' + +2.在data/database.json文件中修改数据库配置 +{ + "bind": "", + "sql_name": "postgres", + "user": "", # 用户们 + "password": "", # 密码 + "address": "", # 数据库地址ip + "port": "", # 数据库端口 + "database": "" # 数据库名称 +} 3.在configs/config.yaml文件中 # 该文件需要启动一次后生成 * 修改插件配置项 ``` -## 使用Docker +## 使用Docker (新版未测试过) **Docker 单机版(仅真寻Bot)** **点击下方的 GitHub 徽标查看教程** @@ -335,11 +394,15 @@ PS: **ARM平台** 请使用全量版 同时 **如果你的机器 RAM < 1G 可能 ## 更新 -### 2024/1/25 +### 2024/8/11 -* 重构webui +* 更新dev -### 2023/12/28 + + +
diff --git a/basic_plugins/admin_bot_manage/__init__.py b/basic_plugins/admin_bot_manage/__init__.py deleted file mode 100755 index 79aa5bca..00000000 --- a/basic_plugins/admin_bot_manage/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from pathlib import Path - -import nonebot - -from configs.config import Config - -Config.add_plugin_config( - "admin_bot_manage:custom_welcome_message", - "SET_GROUP_WELCOME_MESSAGE_LEVEL [LEVEL]", - 2, - name="群管理员操作", - help_="设置群欢迎消息权限", - default_value=2, - type=int, -) - -Config.add_plugin_config( - "admin_bot_manage:switch_rule", - "CHANGE_GROUP_SWITCH_LEVEL [LEVEL]", - 2, - help_="开关群功能权限", - default_value=2, - type=int, -) - -Config.add_plugin_config( - "admin_bot_manage", - "ADMIN_DEFAULT_AUTH", - 5, - help_="默认群管理员权限", - default_value=5, - type=int, -) - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/basic_plugins/admin_bot_manage/_data_source.py b/basic_plugins/admin_bot_manage/_data_source.py deleted file mode 100644 index b9f3a7de..00000000 --- a/basic_plugins/admin_bot_manage/_data_source.py +++ /dev/null @@ -1,391 +0,0 @@ -import asyncio -import os -import time -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import List, Union - -import ujson as json -from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment - -from configs.config import Config -from configs.path_config import DATA_PATH, IMAGE_PATH -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.manager import group_manager, plugins2settings_manager, plugins_manager -from utils.message_builder import image -from utils.typing import BLOCK_TYPE -from utils.utils import get_matchers - -CUSTOM_WELCOME_FILE = Path() / "data" / "custom_welcome_msg" / "custom_welcome_msg.json" -CUSTOM_WELCOME_FILE.parent.mkdir(parents=True, exist_ok=True) - -ICON_PATH = IMAGE_PATH / "other" - -GROUP_HELP_PATH = DATA_PATH / "group_help" - - -async def group_current_status(group_id: str) -> str: - """ - 说明: - 获取当前群聊所有通知的开关 - 参数: - :param group_id: 群号 - """ - _data = group_manager.get_task_data() - image_list = [] - for i, task in enumerate(_data): - name = _data[task] - name_image = BuildImage(0, 0, plain_text=f"{i+1}.{name}", font_size=20) - bk = BuildImage( - name_image.w + 200, name_image.h + 20, color=(103, 177, 109), font_size=15 - ) - await bk.apaste(name_image, (10, 0), True, "by_height") - a_icon = BuildImage(40, 40, background=ICON_PATH / "btn_false.png") - if group_manager.check_group_task_status(group_id, task): - a_icon = BuildImage(40, 40, background=ICON_PATH / "btn_true.png") - b_icon = BuildImage(40, 40, background=ICON_PATH / "btn_false.png") - if group_manager.check_task_super_status(task): - b_icon = BuildImage(40, 40, background=ICON_PATH / "btn_true.png") - await bk.atext((name_image.w + 20, 10), "状态") - await bk.apaste(a_icon, (name_image.w + 50, 0), True) - await bk.atext((name_image.w + 100, 10), "全局") - await bk.apaste(b_icon, (name_image.w + 130, 0), True) - image_list.append(bk) - w = max([x.w for x in image_list]) - h = sum([x.h + 10 for x in image_list]) - A = BuildImage(w + 20, h + 70, font_size=30, color=(119, 97, 177)) - await A.atext((15, 20), "群被动状态") - curr_h = 75 - for img in image_list: - # await img.acircle_corner() - await A.apaste(img, (0, curr_h), True) - curr_h += img.h + 10 - return A.pic2bs4() - - -async def custom_group_welcome( - msg: str, img_list: List[str], user_id: str, group_id: str -) -> Union[str, Message]: - """ - 说明: - 替换群欢迎消息 - 参数: - :param msg: 欢迎消息文本 - :param img_list: 欢迎消息图片 - :param user_id: 用户id,用于log记录 - :param group_id: 群号 - """ - img_result = "" - result = "" - img = img_list[0] if img_list else "" - msg_image = DATA_PATH / "custom_welcome_msg" / f"{group_id}.jpg" - if msg_image.exists(): - msg_image.unlink() - data = {} - if CUSTOM_WELCOME_FILE.exists(): - data = json.load(CUSTOM_WELCOME_FILE.open("r", encoding="utf8")) - try: - if msg: - data[group_id] = msg - json.dump( - data, - CUSTOM_WELCOME_FILE.open("w", encoding="utf8"), - indent=4, - ensure_ascii=False, - ) - logger.info(f"更换群欢迎消息 {msg}", "更换群欢迎信息", user_id, group_id) - result += msg - if img: - await AsyncHttpx.download_file(img, msg_image) - img_result = image(msg_image) - logger.info(f"更换群欢迎消息图片", "更换群欢迎信息", user_id, group_id) - except Exception as e: - logger.error(f"替换群消息失败", "更换群欢迎信息", user_id, group_id, e=e) - return "替换群消息失败..." - return f"替换群欢迎消息成功:\n{result}" + img_result - - -task_data = None - - -def change_global_task_status(cmd: str) -> str: - """ - 说明: - 修改全局被动任务状态 - 参数: - :param cmd: 功能名称 - """ - global task_data - if not task_data: - task_data = group_manager.get_task_data() - status = cmd[:2] - _cmd = cmd[4:] - if "全部被动" in cmd: - for task in task_data: - if status == "开启": - group_manager.open_global_task(task) - else: - group_manager.close_global_task(task) - group_manager.save() - return f"已 {status} 全局全部被动技能!" - else: - modules = [x for x in task_data if task_data[x].lower() == _cmd.lower()] - if not modules: - return "未查询到该被动任务" - if status == "开启": - group_manager.open_global_task(modules[0]) - else: - group_manager.close_global_task(modules[0]) - group_manager.save() - return f"已 {status} 全局{_cmd}" - - -async def change_group_switch(cmd: str, group_id: str, is_super: bool = False) -> str: - """ - 说明: - 修改群功能状态 - 参数: - :param cmd: 功能名称 - :param group_id: 群号 - :param is_super: 是否为超级用户,超级用户用于私聊开关功能状态 - """ - global task_data - if not task_data: - task_data = group_manager.get_task_data() - help_path = GROUP_HELP_PATH / f"{group_id}.png" - status = cmd[:2] - cmd = cmd[2:] - type_ = "plugin" - modules = plugins2settings_manager.get_plugin_module(cmd, True) - if cmd == "全部被动": - for task in task_data: - if status == "开启": - if not group_manager.check_group_task_status(group_id, task): - group_manager.open_group_task(group_id, task) - else: - if group_manager.check_group_task_status(group_id, task): - group_manager.close_group_task(group_id, task) - if help_path.exists(): - help_path.unlink() - return f"已 {status} 全部被动技能!" - if cmd == "全部功能": - for f in plugins2settings_manager.get_data(): - if status == "开启": - group_manager.unblock_plugin(f, group_id, False) - else: - group_manager.block_plugin(f, group_id, False) - group_manager.save() - if help_path.exists(): - help_path.unlink() - return f"已 {status} 全部功能!" - if cmd.lower() in [task_data[x].lower() for x in task_data.keys()]: - type_ = "task" - modules = [x for x in task_data.keys() if task_data[x].lower() == cmd.lower()] - for module in modules: - if is_super: - module = f"{module}:super" - if status == "开启": - if type_ == "task": - if group_manager.check_group_task_status(group_id, module): - return f"被动 {task_data[module]} 正处于开启状态!不要重复开启." - group_manager.open_group_task(group_id, module) - else: - if group_manager.get_plugin_status(module, group_id): - return f"功能 {cmd} 正处于开启状态!不要重复开启." - group_manager.unblock_plugin(module, group_id) - else: - if type_ == "task": - if not group_manager.check_group_task_status(group_id, module): - return f"被动 {task_data[module]} 正处于关闭状态!不要重复关闭." - group_manager.close_group_task(group_id, module) - else: - if not group_manager.get_plugin_status(module, group_id): - return f"功能 {cmd} 正处于关闭状态!不要重复关闭." - group_manager.block_plugin(module, group_id) - if help_path.exists(): - help_path.unlink() - if is_super: - for file in os.listdir(GROUP_HELP_PATH): - file = GROUP_HELP_PATH / file - file.unlink() - else: - if help_path.exists(): - help_path.unlink() - return f"{status} {cmd} 功能!" - - -def set_plugin_status(cmd: str, block_type: BLOCK_TYPE = "all"): - """ - 说明: - 设置插件功能状态(超级用户使用) - 参数: - :param cmd: 功能名称 - :param block_type: 限制类型, 'all': 全局, 'private': 私聊, 'group': 群聊 - """ - if block_type not in ["all", "private", "group"]: - raise TypeError("block_type类型错误, 可选值: ['all', 'private', 'group']") - status = cmd[:2] - cmd = cmd[2:] - module = plugins2settings_manager.get_plugin_module(cmd) - if status == "开启": - plugins_manager.unblock_plugin(module) - else: - plugins_manager.block_plugin(module, block_type=block_type) - for file in os.listdir(GROUP_HELP_PATH): - file = GROUP_HELP_PATH / file - file.unlink() - - -async def get_plugin_status(): - """ - 说明: - 获取功能状态 - """ - return await asyncio.get_event_loop().run_in_executor(None, _get_plugin_status) - - -def _get_plugin_status() -> MessageSegment: - """ - 说明: - 合成功能状态图片 - """ - rst = "\t功能\n" - flag_str = "状态".rjust(4) + "\n" - for matcher in get_matchers(True): - if module := matcher.plugin_name: - flag = plugins_manager.get_plugin_block_type(module) - flag = flag.upper() + " CLOSE" if flag else "OPEN" - try: - plugin_name = plugins_manager.get(module).plugin_name - if ( - "[Hidden]" in plugin_name - or "[Admin]" in plugin_name - or "[Superuser]" in plugin_name - ): - continue - rst += f"{plugin_name}" - except KeyError: - rst += f"{module}" - if plugins_manager.get(module).error: - rst += "[ERROR]" - rst += "\n" - flag_str += f"{flag}\n" - height = len(rst.split("\n")) * 24 - a = BuildImage(250, height, font_size=20) - a.text((10, 10), rst) - b = BuildImage(200, height, font_size=20) - b.text((10, 10), flag_str) - A = BuildImage(500, height) - A.paste(a) - A.paste(b, (270, 0)) - return image(b64=A.pic2bs4()) - - -async def update_member_info( - bot: Bot, group_id: int, remind_superuser: bool = False -) -> bool: - """ - 说明: - 更新群成员信息 - 参数: - :param group_id: 群号 - :param remind_superuser: 失败信息提醒超级用户 - """ - _group_user_list = await bot.get_group_member_list(group_id=group_id) - _error_member_list = [] - _exist_member_list = [] - # try: - admin_default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") - if admin_default_auth is not None: - for user_info in _group_user_list: - nickname = user_info["card"] or user_info["nickname"] - # 更新权限 - if user_info["role"] in [ - "owner", - "admin", - ] and not await LevelUser.is_group_flag( - user_info["user_id"], str(group_id) - ): - await LevelUser.set_level( - user_info["user_id"], - user_info["group_id"], - admin_default_auth, - ) - if str(user_info["user_id"]) in bot.config.superusers: - await LevelUser.set_level( - user_info["user_id"], user_info["group_id"], 9 - ) - user = await GroupInfoUser.get_or_none( - user_id=str(user_info["user_id"]), group_id=str(user_info["group_id"]) - ) - if user: - if user.user_name != nickname: - user.user_name = nickname - await user.save(update_fields=["user_name"]) - logger.debug( - f"更新群昵称成功", - "更新群组成员信息", - user_info["user_id"], - user_info["group_id"], - ) - _exist_member_list.append(str(user_info["user_id"])) - continue - join_time = datetime.strptime( - time.strftime( - "%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"]) - ), - "%Y-%m-%d %H:%M:%S", - ) - await GroupInfoUser.update_or_create( - user_id=str(user_info["user_id"]), - group_id=str(user_info["group_id"]), - defaults={ - "user_name": nickname, - "user_join_time": join_time.replace( - tzinfo=timezone(timedelta(hours=8)) - ), - }, - ) - _exist_member_list.append(str(user_info["user_id"])) - logger.debug("更新成功", "更新成员信息", user_info["user_id"], user_info["group_id"]) - _del_member_list = list( - set(_exist_member_list).difference( - set(await GroupInfoUser.get_group_member_id_list(group_id)) - ) - ) - if _del_member_list: - for del_user in _del_member_list: - await GroupInfoUser.filter( - user_id=str(del_user), group_id=str(group_id) - ).delete() - logger.info(f"删除已退群用户", "更新群组成员信息", del_user, group_id) - if _error_member_list and remind_superuser: - result = "" - for error_user in _error_member_list: - result += error_user - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), message=result[:-1] - ) - return True - - -def set_group_bot_status(group_id: str, status: bool) -> str: - """ - 说明: - 设置群聊bot开关状态 - 参数: - :param group_id: 群号 - :param status: 状态 - """ - if status: - if group_manager.check_group_bot_status(group_id): - return "我还醒着呢!" - group_manager.turn_on_group_bot_status(group_id) - return "呜..醒来了..." - else: - group_manager.shutdown_group_bot_status(group_id) - return "那我先睡觉了..." diff --git a/basic_plugins/admin_bot_manage/admin_config.py b/basic_plugins/admin_bot_manage/admin_config.py deleted file mode 100755 index ee751ff9..00000000 --- a/basic_plugins/admin_bot_manage/admin_config.py +++ /dev/null @@ -1,45 +0,0 @@ -from nonebot import on_notice -from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent - -from configs.config import Config -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from services.log import logger - -__zx_plugin_name__ = "群管理员变动监测 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -admin_notice = on_notice(priority=5) - - -@admin_notice.handle() -async def _(event: GroupAdminNoticeEvent): - if user := await GroupInfoUser.get_or_none( - user_id=str(event.user_id), group_id=str(event.group_id) - ): - nickname = user.user_name - else: - nickname = event.user_id - if event.sub_type == "set": - admin_default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") - if admin_default_auth is not None: - await LevelUser.set_level( - event.user_id, - event.group_id, - admin_default_auth, - ) - logger.info( - f"成为管理员,添加权限: {admin_default_auth}", - "群管理员变动监测", - event.user_id, - event.group_id, - ) - else: - logger.warning( - f"配置项 MODULE: [admin_bot_manage] | KEY: [ADMIN_DEFAULT_AUTH] 为空" - ) - elif event.sub_type == "unset": - await LevelUser.delete_level(event.user_id, event.group_id) - logger.info("撤销管理员,,取消权限等级", "群管理员变动监测", event.user_id, event.group_id) diff --git a/basic_plugins/admin_bot_manage/custom_welcome_message.py b/basic_plugins/admin_bot_manage/custom_welcome_message.py deleted file mode 100755 index 7f890bb4..00000000 --- a/basic_plugins/admin_bot_manage/custom_welcome_message.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg - -from configs.config import Config -from services.log import logger -from utils.depends import ImageList, OneCommand - -from ._data_source import custom_group_welcome - -__zx_plugin_name__ = "自定义进群欢迎消息 [Admin]" -__plugin_usage__ = """ -usage: - 指令: - 自定义进群欢迎消息 ?[文本] ?[图片] - Note:可以通过[at]来确认是否艾特新成员 - 示例:自定义进群欢迎消息 欢迎新人![图片] - 示例:自定义进群欢迎消息 欢迎你[at] -""".strip() -__plugin_des__ = "简易的自定义群欢迎消息" -__plugin_cmd__ = ["自定义群欢迎消息 ?[文本] ?[图片]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config( - "admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL" - ), -} - -custom_welcome = on_command( - "自定义进群欢迎消息", - aliases={"自定义欢迎消息", "自定义群欢迎消息", "设置群欢迎消息"}, - permission=GROUP, - priority=5, - block=True, -) - - -@custom_welcome.handle() -async def _( - event: GroupMessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - img: List[str] = ImageList(), -): - msg = arg.extract_plain_text().strip() - if not msg and not img: - await custom_welcome.finish(__plugin_usage__) - try: - await custom_welcome.send( - await custom_group_welcome( - msg, img, str(event.user_id), str(event.group_id) - ), - at_sender=True, - ) - logger.info(f"自定义群欢迎消息:{msg}", cmd, event.user_id, event.group_id) - except Exception as e: - logger.error( - f"自定义进群欢迎消息发生错误", cmd, event.user_id, getattr(event, "group_id", None), e=e - ) - await custom_welcome.send("发生了一些未知错误...") diff --git a/basic_plugins/admin_bot_manage/rule.py b/basic_plugins/admin_bot_manage/rule.py deleted file mode 100755 index a0e54efe..00000000 --- a/basic_plugins/admin_bot_manage/rule.py +++ /dev/null @@ -1,55 +0,0 @@ -import time - -from nonebot.adapters.onebot.v11 import Event - -from services.log import logger -from utils.manager import group_manager, plugins2settings_manager -from utils.utils import get_message_text - -cmd = [] - -v = time.time() - - -def switch_rule(event: Event) -> bool: - """ - 说明: - 检测文本是否是关闭功能命令 - 参数: - :param event: pass - """ - global cmd, v - try: - if not cmd or time.time() - v > 60 * 60: - cmd = ["关闭全部被动", "开启全部被动", "开启全部功能", "关闭全部功能"] - _data = group_manager.get_task_data() - for key in _data: - cmd.append(f"开启{_data[key]}") - cmd.append(f"关闭{_data[key]}") - cmd.append(f"开启被动{_data[key]}") - cmd.append(f"关闭被动{_data[key]}") - cmd.append(f"开启 {_data[key]}") - cmd.append(f"关闭 {_data[key]}") - _data = plugins2settings_manager.get_data() - for key in _data.keys(): - try: - if isinstance(_data[key].cmd, list): - for x in _data[key].cmd: - cmd.append(f"开启{x}") - cmd.append(f"关闭{x}") - cmd.append(f"开启 {x}") - cmd.append(f"关闭 {x}") - else: - cmd.append(f"开启{key}") - cmd.append(f"关闭{key}") - cmd.append(f"开启 {key}") - cmd.append(f"关闭 {key}") - except KeyError: - pass - v = time.time() - msg = get_message_text(event.json()).split() - msg = msg[0] if msg else "" - return msg in cmd - except Exception as e: - logger.error(f"检测是否为功能开关命令发生错误", e=e) - return False diff --git a/basic_plugins/admin_bot_manage/switch_rule.py b/basic_plugins/admin_bot_manage/switch_rule.py deleted file mode 100755 index c4ae3c3c..00000000 --- a/basic_plugins/admin_bot_manage/switch_rule.py +++ /dev/null @@ -1,144 +0,0 @@ -from typing import Any, Tuple - -from nonebot import on_command, on_message, on_regex -from nonebot.adapters.onebot.v11 import ( - GROUP, - Bot, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, -) -from nonebot.params import CommandArg, RegexGroup -from nonebot.permission import SUPERUSER - -from configs.config import NICKNAME, Config -from services.log import logger -from utils.message_builder import image -from utils.utils import get_message_text, is_number - -from ._data_source import ( - change_global_task_status, - change_group_switch, - get_plugin_status, - group_current_status, - set_group_bot_status, - set_plugin_status, -) -from .rule import switch_rule - -__zx_plugin_name__ = "群功能开关 [Admin]" - -__plugin_usage__ = """ -usage: - 群内功能与被动技能开关 - 指令: - 开启/关闭[功能] - 群被动状态 - 开启全部被动 - 关闭全部被动 - 醒来/休息吧 - 示例:开启/关闭色图 -""".strip() -__plugin_superuser_usage__ = """ -usage: - (私聊)功能总开关与指定群禁用 - 指令: - 功能状态 - 开启/关闭[功能] [group] - 开启/关闭[功能] ['private'/'group'] - 开启被动/关闭被动[被动名称] # 全局被动控制 -""".strip() -__plugin_des__ = "群内功能开关" -__plugin_cmd__ = [ - "开启/关闭[功能]", - "群被动状态", - "开启全部被动", - "关闭全部被动", - "醒来/休息吧", - "功能状态 [_superuser]", - "开启/关闭[功能] [group] [_superuser]", - "开启/关闭[功能] ['private'/'group'] [_superuser]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), - "cmd": ["开启功能", "关闭功能", "开关"], -} - -switch_rule_matcher = on_message(rule=switch_rule, priority=4, block=True) - -plugins_status = on_command("功能状态", permission=SUPERUSER, priority=5, block=True) - -group_task_status = on_command("群被动状态", permission=GROUP, priority=5, block=True) - -group_status = on_regex("^(休息吧|醒来)$", permission=GROUP, priority=5, block=True) - - -@switch_rule_matcher.handle() -async def _( - bot: Bot, - event: MessageEvent, -): - msg = get_message_text(event.message).strip() - msg_split = msg.split() - _cmd = msg_split[0] - if isinstance(event, GroupMessageEvent): - await switch_rule_matcher.send(await change_group_switch(_cmd, event.group_id)) - logger.info(f"使用群功能管理命令 {_cmd}", "功能管理", event.user_id, event.group_id) - else: - if str(event.user_id) in bot.config.superusers: - block_type = " ".join(msg_split[1:]) - block_type = block_type if block_type else "a" - if ("关闭被动" in _cmd or "开启被动" in _cmd) and isinstance( - event, PrivateMessageEvent - ): - await switch_rule_matcher.send(change_global_task_status(_cmd)) - elif is_number(block_type): - if not int(block_type) in [ - g["group_id"] for g in await bot.get_group_list() - ]: - await switch_rule_matcher.finish(f"{NICKNAME}未加入群聊:{block_type}") - await change_group_switch(_cmd, int(block_type), True) - group_name = (await bot.get_group_info(group_id=int(block_type)))[ - "group_name" - ] - await switch_rule_matcher.send( - f"已{_cmd[:2]}群聊 {group_name}({block_type}) 的 {_cmd[2:]} 功能" - ) - elif block_type in ["all", "private", "group", "a", "p", "g"]: - block_type = "all" if block_type == "a" else block_type - block_type = "private" if block_type == "p" else block_type - block_type = "group" if block_type == "g" else block_type - set_plugin_status(_cmd, block_type) # type: ignore - if block_type == "all": - await switch_rule_matcher.send(f"已{_cmd[:2]}功能:{_cmd[2:]}") - elif block_type == "private": - await switch_rule_matcher.send(f"已在私聊中{_cmd[:2]}功能:{_cmd[2:]}") - else: - await switch_rule_matcher.send(f"已在群聊中{_cmd[:2]}功能:{_cmd[2:]}") - else: - await switch_rule_matcher.finish("格式错误:关闭[功能] [group]/[p/g]") - logger.info(f"使用功能管理命令 {_cmd} | {block_type}", f"{_cmd}", event.user_id) - - -@plugins_status.handle() -async def _(): - await plugins_status.send(await get_plugin_status()) - - -@group_task_status.handle() -async def _(event: GroupMessageEvent): - await group_task_status.send(image(b64=await group_current_status(str(event.group_id)))) - - -@group_status.handle() -async def _(event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - cmd = reg_group[0] - if cmd == "休息吧": - msg = set_group_bot_status(str(event.group_id), False) - else: - msg = set_group_bot_status(str(event.group_id), True) - await group_status.send(msg) - logger.info(f"使用总开关命令: {cmd}", cmd, event.user_id, event.group_id) diff --git a/basic_plugins/admin_bot_manage/timing_task.py b/basic_plugins/admin_bot_manage/timing_task.py deleted file mode 100755 index fcae1e74..00000000 --- a/basic_plugins/admin_bot_manage/timing_task.py +++ /dev/null @@ -1,48 +0,0 @@ -from nonebot import get_bots - -from services.log import logger -from utils.utils import scheduler - -from ._data_source import update_member_info - -__zx_plugin_name__ = "管理方面定时任务 [Hidden]" -__plugin_usage__ = "无" -__plugin_des__ = "成员信息和管理权限的定时更新" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -async def update(): - bot_list = get_bots() - if bot_list: - used_group = [] - for key in bot_list: - bot = bot_list[key] - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl if g["group_id"] not in used_group] - for g in gl: - used_group.append(g) - try: - await update_member_info(bot, g) # type: ignore - logger.debug(f"更新群组成员信息成功", "自动更新群组成员信息", group_id=g) - except Exception as e: - logger.error(f"更新群组成员信息错误", "自动更新群组成员信息", group_id=g, e=e) - - -# 自动更新群员信息 -@scheduler.scheduled_job( - "cron", - hour=2, - minute=1, -) -async def _(): - await update() - - -# 快速更新群员信息以及管理员权限 -@scheduler.scheduled_job( - "interval", - minutes=5, -) -async def _(): - await update() diff --git a/basic_plugins/admin_bot_manage/update_group_member_info.py b/basic_plugins/admin_bot_manage/update_group_member_info.py deleted file mode 100755 index 6a59f418..00000000 --- a/basic_plugins/admin_bot_manage/update_group_member_info.py +++ /dev/null @@ -1,51 +0,0 @@ -from nonebot import on_command, on_notice -from nonebot.adapters.onebot.v11 import ( - GROUP, - Bot, - GroupIncreaseNoticeEvent, - GroupMessageEvent, -) - -from services.log import logger - -from ._data_source import update_member_info - -__zx_plugin_name__ = "更新群组成员列表 [Admin]" -__plugin_usage__ = """ -usage: - 更新群组成员的基本信息 - 指令: - 更新群组成员列表/更新群组成员信息 -""".strip() -__plugin_des__ = "更新群组成员列表" -__plugin_cmd__ = ["更新群组成员列表"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": 1, -} - - -refresh_member_group = on_command( - "更新群组成员列表", aliases={"更新群组成员信息"}, permission=GROUP, priority=5, block=True -) - - -@refresh_member_group.handle() -async def _(bot: Bot, event: GroupMessageEvent): - if await update_member_info(bot, event.group_id): - await refresh_member_group.send("更新群员信息成功!", at_sender=True) - logger.info("更新群员信息成功!", "更新群组成员列表", event.user_id, event.group_id) - else: - await refresh_member_group.send("更新群员信息失败!", at_sender=True) - logger.info("更新群员信息失败!", "更新群组成员列表", event.user_id, event.group_id) - - -group_increase_handle = on_notice(priority=1, block=False) - - -@group_increase_handle.handle() -async def _(bot: Bot, event: GroupIncreaseNoticeEvent): - if str(event.user_id) == bot.self_id: - await update_member_info(bot, event.group_id) - logger.info("{NICKNAME}加入群聊更新群组信息", "更新群组成员列表", event.user_id, event.group_id) diff --git a/basic_plugins/admin_help/__init__.py b/basic_plugins/admin_help/__init__.py deleted file mode 100755 index 938aaab2..00000000 --- a/basic_plugins/admin_help/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from nonebot import on_command -from nonebot.typing import T_State -from nonebot.adapters import Bot -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from utils.message_builder import image -from .data_source import create_help_image, ADMIN_HELP_IMAGE - - -__zx_plugin_name__ = '管理帮助 [Admin]' -__plugin_usage__ = '管理员帮助,在群内回复“管理员帮助”' -__plugin_version__ = 0.1 -__plugin_author__ = 'HibiKier' -__plugin_settings__ = { - "admin_level": 1, -} - -admin_help = on_command("管理员帮助", aliases={"管理帮助"}, priority=5, block=True) - -if ADMIN_HELP_IMAGE.exists(): - ADMIN_HELP_IMAGE.unlink() - - -@admin_help.handle() -async def _(bot: Bot, event: GroupMessageEvent, state: T_State): - if not ADMIN_HELP_IMAGE.exists(): - await create_help_image() - await admin_help.send(image(ADMIN_HELP_IMAGE)) diff --git a/basic_plugins/admin_help/data_source.py b/basic_plugins/admin_help/data_source.py deleted file mode 100755 index 7474f44d..00000000 --- a/basic_plugins/admin_help/data_source.py +++ /dev/null @@ -1,81 +0,0 @@ -import nonebot -from nonebot import Driver - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.image_template import help_template -from utils.image_utils import BuildImage, build_sort_image, group_image, text2image -from utils.manager import group_manager, plugin_data_manager -from utils.manager.models import PluginType - -driver: Driver = nonebot.get_driver() - - -ADMIN_HELP_IMAGE = IMAGE_PATH / "admin_help_img.png" - - -@driver.on_bot_connect -async def init_task(): - if not group_manager.get_task_data(): - group_manager.load_task() - logger.info(f"已成功加载 {len(group_manager.get_task_data())} 个被动技能.") - - -async def create_help_image(): - """ - 创建管理员帮助图片 - """ - if ADMIN_HELP_IMAGE.exists(): - return - plugin_data_ = plugin_data_manager.get_data() - image_list = [] - task_list = [] - for plugin_data in [plugin_data_[x] for x in plugin_data_]: - try: - usage = None - if plugin_data.plugin_type == PluginType.ADMIN and plugin_data.usage: - usage = await text2image( - plugin_data.usage, padding=5, color=(204, 196, 151) - ) - if usage: - await usage.acircle_corner() - level = 5 - if plugin_data.plugin_setting: - level = plugin_data.plugin_setting.level or level - image = await help_template(plugin_data.name + f"[{level}]", usage) - image_list.append(image) - if plugin_data.task: - for x in plugin_data.task.keys(): - task_list.append(plugin_data.task[x]) - except Exception as e: - logger.warning( - f"获取群管理员插件 {plugin_data.model}: {plugin_data.name} 设置失败...", - "管理员帮助", - e=e, - ) - task_str = "\n".join(task_list) - task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str - task_image = await text2image(task_str, padding=5, color=(204, 196, 151)) - task_image = await help_template("被动任务", task_image) - image_list.append(task_image) - image_group, _ = group_image(image_list) - A = await build_sort_image(image_group, color="#f9f6f2", padding_top=180) - await A.apaste( - BuildImage(0, 0, font="CJGaoDeGuo.otf", plain_text="群管理员帮助", font_size=50), - (50, 30), - True, - ) - await A.apaste( - BuildImage( - 0, - 0, - font="CJGaoDeGuo.otf", - plain_text="注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", - font_size=30, - font_color="red", - ), - (50, 90), - True, - ) - await A.asave(ADMIN_HELP_IMAGE) - logger.info(f"已成功加载 {len(image_list)} 条管理员命令") diff --git a/basic_plugins/apscheduler/__init__.py b/basic_plugins/apscheduler/__init__.py deleted file mode 100755 index de0a1962..00000000 --- a/basic_plugins/apscheduler/__init__.py +++ /dev/null @@ -1,250 +0,0 @@ -import shutil -from pathlib import Path -from typing import List - -import nonebot -from nonebot import get_bots, on_message - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from services.log import logger -from utils.message_builder import image -from utils.utils import broadcast_group, scheduler - -__zx_plugin_name__ = "定时任务相关 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"zwa": "早晚安"} - - -Config.add_plugin_config( - "_task", "DEFAULT_ZWA", True, help_="被动 早晚安 进群默认开关状态", default_value=True, type=bool -) - -Config.add_plugin_config( - "_backup", "BACKUP_FLAG", True, help_="是否开启文件备份", default_value=True, type=bool -) - -Config.add_plugin_config( - "_backup", - "BACKUP_DIR_OR_FILE", - [ - "data/black_word", - "data/configs", - "data/statistics", - "data/word_bank", - "data/manager", - "configs", - ], - name="文件备份", - help_="备份的文件夹或文件", - default_value=[], - type=List[str], -) - - -cx = on_message(priority=9999, block=False, rule=lambda: False) - - -# 早上好 -@scheduler.scheduled_job( - "cron", - hour=6, - minute=1, -) -async def _(): - img = image(IMAGE_PATH / "zhenxun" / "zao.jpg") - await broadcast_group("[[_task|zwa]]早上好" + img, log_cmd="被动早晚安") - logger.info("每日早安发送...") - - -# 睡觉了 -@scheduler.scheduled_job( - "cron", - hour=23, - minute=59, -) -async def _(): - img = image(IMAGE_PATH / "zhenxun" / "sleep.jpg") - await broadcast_group( - f"[[_task|zwa]]{NICKNAME}要睡觉了,你们也要早点睡呀" + img, log_cmd="被动早晚安" - ) - logger.info("每日晚安发送...") - - -# 自动更新群组信息 -@scheduler.scheduled_job( - "cron", - hour=3, - minute=1, -) -async def _(): - bots = nonebot.get_bots() - _used_group = [] - for bot in bots.values(): - try: - group_list = await bot.get_group_list() - gl = [g["group_id"] for g in group_list if g["group_id"] not in _used_group] - for g in gl: - _used_group.append(g) - group_info = await bot.get_group_info(group_id=g) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) - logger.debug("自动更新群组信息成功", "自动更新群组", group_id=g) - except Exception as e: - logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e) - logger.info("自动更新群组成员信息成功...") - - -# 自动更新好友信息 -@scheduler.scheduled_job( - "cron", - hour=3, - minute=1, -) -async def _(): - bots = nonebot.get_bots() - for key in bots: - try: - bot = bots[key] - fl = await bot.get_friend_list() - for f in fl: - if FriendUser.exists(user_id=str(f["user_id"])): - await FriendUser.create( - user_id=str(f["user_id"]), user_name=f["nickname"] - ) - logger.debug(f"更新好友信息成功", "自动更新好友", f["user_id"]) - else: - logger.debug(f"好友信息已存在", "自动更新好友", f["user_id"]) - except Exception as e: - logger.error(f"自动更新好友信息错误", "自动更新好友", e=e) - logger.info("自动更新好友信息成功...") - - -# 自动备份 -@scheduler.scheduled_job( - "cron", - hour=3, - minute=25, -) -async def _(): - if Config.get_config("_backup", "BACKUP_FLAG"): - _backup_path = Path() / "backup" - _backup_path.mkdir(exist_ok=True, parents=True) - if backup_dir_or_file := Config.get_config("_backup", "BACKUP_DIR_OR_FILE"): - for path_file in backup_dir_or_file: - try: - path = Path(path_file) - _p = _backup_path / path_file - if path.exists(): - if path.is_dir(): - if _p.exists(): - shutil.rmtree(_p, ignore_errors=True) - shutil.copytree(path_file, _p) - else: - if _p.exists(): - _p.unlink() - shutil.copy(path_file, _p) - logger.debug(f"已完成自动备份:{path_file}", "自动备份") - except Exception as e: - logger.error(f"自动备份文件 {path_file} 发生错误", "自动备份", e=e) - logger.info("自动备份成功...", "自动备份") - - # 一次性任务 - - -# 固定时间触发,仅触发一次: -# -# from datetime import datetime -# -# @nonebot.scheduler.scheduled_job( -# 'date', -# run_date=datetime(2021, 1, 1, 0, 0), -# # timezone=None, -# ) -# async def _(): -# await bot.send_group_msg(group_id=123456, -# message="2021,新年快乐!") - -# 定期任务 -# 从 start_date 开始到 end_date 结束,根据类似 Cron -# -# 的规则触发任务: -# -# @nonebot.scheduler.scheduled_job( -# 'cron', -# # year=None, -# # month=None, -# # day=None, -# # week=None, -# day_of_week="mon,tue,wed,thu,fri", -# hour=7, -# # minute=None, -# # second=None, -# # start_date=None, -# # end_date=None, -# # timezone=None, -# ) -# async def _(): -# await bot.send_group_msg(group_id=123456, -# message="起床啦!") - -# 间隔任务 -# -# interval 触发器 -# -# 从 start_date 开始,每间隔一段时间触发,到 end_date 结束: -# -# @nonebot.scheduler.scheduled_job( -# 'interval', -# # weeks=0, -# # days=0, -# # hours=0, -# minutes=5, -# # seconds=0, -# # start_date=time.now(), -# # end_date=None, -# ) -# async def _(): -# has_new_item = check_new_item() -# if has_new_item: -# await bot.send_group_msg(group_id=123456, -# message="XX有更新啦!") - - -# 动态的计划任务 -# import datetime -# -# from apscheduler.triggers.date import DateTrigger # 一次性触发器 -# # from apscheduler.triggers.cron import CronTrigger # 定期触发器 -# # from apscheduler.triggers.interval import IntervalTrigger # 间隔触发器 -# from nonebot import on_command, scheduler -# -# @on_command('赖床') -# async def _(session: CommandSession): -# await session.send('我会在5分钟后再喊你') -# -# # 制作一个“5分钟后”触发器 -# delta = datetime.timedelta(minutes=5) -# trigger = DateTrigger( -# run_date=datetime.datetime.now() + delta -# ) -# -# # 添加任务 -# scheduler.add_job( -# func=session.send, # 要添加任务的函数,不要带参数 -# trigger=trigger, # 触发器 -# args=('不要再赖床啦!',), # 函数的参数列表,注意:只有一个值时,不能省略末尾的逗号 -# # kwargs=None, -# misfire_grace_time=60, # 允许的误差时间,建议不要省略 -# # jobstore='default', # 任务储存库,在下一小节中说明 -# ) diff --git a/basic_plugins/ban/__init__.py b/basic_plugins/ban/__init__.py deleted file mode 100755 index ead19ac2..00000000 --- a/basic_plugins/ban/__init__.py +++ /dev/null @@ -1,179 +0,0 @@ -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import ( - Bot, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, -) -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from configs.config import NICKNAME, Config -from models.ban_user import BanUser -from models.level_user import LevelUser -from services.log import logger -from utils.depends import AtList, OneCommand -from utils.utils import is_number - -from .data_source import a_ban, parse_ban_time - -__zx_plugin_name__ = "封禁Ban用户 [Admin]" -__plugin_usage__ = """ -usage: - 将用户拉入或拉出黑名单 - 指令: - .ban [at] ?[小时] ?[分钟] - .unban - 示例:.ban @user - 示例:.ban @user 6 - 示例:.ban @user 3 10 - 示例:.unban @user -""".strip() -__plugin_superuser_usage__ = """ -usage: - b了=屏蔽用户消息,相当于最上级.ban - 跨群ban以及跨群b了 - 指令: - b了 [at/qq] - .ban [user_id] ?[小时] ?[分钟] - 示例:b了 @user - 示例:b了 1234567 - 示例:.ban 12345567 -""".strip() -__plugin_des__ = "你被逮捕了!丢进小黑屋!" -__plugin_cmd__ = [".ban [at] ?[小时] ?[分钟]", ".unban [at]", "b了 [at] [_superuser]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("ban", "BAN_LEVEL"), - "cmd": [".ban", ".unban", "ban", "unban"], -} -__plugin_configs__ = { - "BAN_LEVEL [LEVEL]": { - "value": 5, - "help": "ban/unban所需要的管理员权限等级", - "default_value": 5, - "type": int, - } -} - - -ban = on_command( - ".ban", - aliases={".unban", "/ban", "/unban"}, - priority=5, - block=True, -) - -super_ban = on_command("b了", permission=SUPERUSER, priority=5, block=True) - - -@ban.handle() -async def _( - bot: Bot, - event: GroupMessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - at_list: List[int] = AtList(), -): - result = "" - if at_list: - qq = at_list[0] - user = await bot.get_group_member_info(group_id=event.group_id, user_id=qq) - user_name = user["card"] or user["nickname"] - msg = arg.extract_plain_text().strip() - time = parse_ban_time(msg) - if isinstance(time, str): - await ban.finish(time, at_sender=True) - user_level = await LevelUser.get_user_level(event.user_id, event.group_id) - is_not_superuser = str(event.user_id) not in bot.config.superusers - if cmd in [".ban", "/ban"]: - at_user_level = await LevelUser.get_user_level(qq, event.group_id) - if user_level <= at_user_level and is_not_superuser: - await ban.finish( - f"您的权限等级比对方低或相等, {NICKNAME}不能为您使用此功能!", - at_sender=True, - ) - logger.info(f"用户封禁 时长: {time}", cmd, event.user_id, event.group_id, qq) - result = await a_ban(qq, time, user_name, event) - else: - if await BanUser.check_ban_level(qq, user_level) and is_not_superuser: - await ban.finish( - f"ban掉 {user_name} 的管理员权限比您高,无法进行unban", at_sender=True - ) - if await BanUser.unban(qq): - logger.info(f"解除用户封禁", cmd, event.user_id, event.group_id, qq) - result = f"已经将 {user_name} 从黑名单中删除了!" - else: - result = f"{user_name} 不在黑名单!" - else: - await ban.finish("艾特人了吗??", at_sender=True) - await ban.send(result, at_sender=True) - - -@ban.handle() -async def _( - bot: Bot, - event: PrivateMessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), -): - msg = arg.extract_plain_text().strip() - if msg and str(event.user_id) in bot.config.superusers: - msg_split = msg.split() - if msg_split and is_number(msg_split[0]): - qq = int(msg_split[0]) - param = msg_split[1:] - if cmd in [".ban", "/ban"]: - time = parse_ban_time(" ".join(param)) - if isinstance(time, str): - logger.info(time, cmd, event.user_id, target=qq) - await ban.finish(time) - result = await a_ban(qq, time, str(qq), event, 9) - else: - if await BanUser.unban(qq): - result = f"已经把 {qq} 从黑名单中删除了!" - else: - result = f"{qq} 不在黑名单!" - await ban.send(result) - logger.info(result, cmd, event.user_id, target=qq) - else: - await ban.send("参数不正确!\n格式:.ban [qq] [hour]? [minute]?", at_sender=True) - - -@super_ban.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - at_list: List[int] = AtList(), -): - user_name = "" - qq = None - if isinstance(event, GroupMessageEvent): - if at_list: - qq = at_list[0] - user = await bot.get_group_member_info(group_id=event.group_id, user_id=qq) - user_name = user["card"] or user["nickname"] - else: - msg = arg.extract_plain_text().strip() - if not is_number(msg): - await super_ban.finish("对象qq必须为纯数字...") - qq = int(msg) - user_name = msg - if qq: - await BanUser.ban(qq, 10, 99999999) - await ban.send(f"已将 {user_name} 拉入黑名单!") - logger.info( - f"已将 {user_name} 拉入黑名单!", - cmd, - event.user_id, - event.group_id if isinstance(event, GroupMessageEvent) else None, - qq, - ) - else: - await super_ban.send("需要提供被super ban的对象,可以使用at或者指定qq...") diff --git a/basic_plugins/ban/data_source.py b/basic_plugins/ban/data_source.py deleted file mode 100644 index b25a29c4..00000000 --- a/basic_plugins/ban/data_source.py +++ /dev/null @@ -1,77 +0,0 @@ -from typing import Optional, Union - -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent - -from configs.config import NICKNAME -from models.ban_user import BanUser -from models.level_user import LevelUser -from services.log import logger -from utils.utils import is_number - - -def parse_ban_time(msg: str) -> Union[int, str]: - """ - 解析ban时长 - :param msg: 文本消息 - """ - try: - if not msg: - return -1 - msg_split = msg.split() - if len(msg_split) == 1: - if not is_number(msg_split[0].strip()): - return "参数必须是数字!" - return int(msg_split[0]) * 60 * 60 - else: - if not is_number(msg_split[0].strip()) or not is_number( - msg_split[1].strip() - ): - return "参数必须是数字!" - return int(msg_split[0]) * 60 * 60 + int(msg_split[1]) * 60 - except ValueError as e: - logger.error("解析ban时长错误", ".ban", e=e) - return "时间解析错误!" - - -async def a_ban( - qq: int, - time: int, - user_name: str, - event: MessageEvent, - ban_level: Optional[int] = None, -) -> str: - """ - ban - :param qq: qq - :param time: ban时长 - :param user_name: ban用户昵称 - :param event: event - :param ban_level: ban级别 - """ - group_id = None - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - ban_level = await LevelUser.get_user_level(event.user_id, event.group_id) - if not ban_level: - return "未查询到ban级用户权限" - if await BanUser.ban(qq, ban_level, time): - logger.info( - f"封禁 时长 {time / 60} 分钟", ".ban", event.user_id, group_id, qq - ) - result = f"已经将 {user_name} 加入{NICKNAME}的黑名单了!" - if time != -1: - result += f"将在 {time / 60} 分钟后解封" - else: - result += f"将在 ∞ 分钟后解封" - else: - ban_time = await BanUser.check_ban_time(qq) - if isinstance(ban_time, int): - ban_time = abs(float(ban_time)) - if ban_time < 60: - ban_time = str(ban_time) + " 秒" - else: - ban_time = str(int(ban_time / 60)) + " 分钟" - else: - ban_time += " 分钟" - result = f"{user_name} 已在黑名单!预计 {ban_time}后解封" - return result diff --git a/basic_plugins/broadcast/__init__.py b/basic_plugins/broadcast/__init__.py deleted file mode 100755 index 4f8cb9cb..00000000 --- a/basic_plugins/broadcast/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -import asyncio -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from configs.config import Config -from services.log import logger -from utils.depends import ImageList -from utils.manager import group_manager -from utils.message_builder import image - -__zx_plugin_name__ = "广播 [Superuser]" -__plugin_usage__ = """ -usage: - 指令: - 广播- ?[消息] ?[图片] - 示例:广播- 你们好! -""".strip() -__plugin_des__ = "昭告天下!" -__plugin_cmd__ = ["广播-"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"broadcast": "广播"} -Config.add_plugin_config( - "_task", - "DEFAULT_BROADCAST", - True, - help_="被动 广播 进群默认开关状态", - default_value=True, - type=bool, -) - -broadcast = on_command("广播-", priority=1, permission=SUPERUSER, block=True) - - -@broadcast.handle() -async def _( - bot: Bot, - event: MessageEvent, - arg: Message = CommandArg(), - img_list: List[str] = ImageList(), -): - msg = arg.extract_plain_text().strip() - rst = "" - for img in img_list: - rst += image(img) - gl = [ - g["group_id"] - for g in await bot.get_group_list() - if group_manager.check_group_task_status(str(g["group_id"]), "broadcast") - ] - g_cnt = len(gl) - cnt = 0 - error = "" - x = 0.25 - for g in gl: - cnt += 1 - if cnt / g_cnt > x: - await broadcast.send(f"已播报至 {int(cnt / g_cnt * 100)}% 的群聊") - x += 0.25 - try: - await bot.send_group_msg(group_id=g, message=msg + rst) - logger.info(f"投递广播成功", "广播", group_id=g) - except Exception as e: - logger.error(f"投递广播失败", "广播", group_id=g, e=e) - error += f"GROUP {g} 投递广播失败:{type(e)}\n" - await asyncio.sleep(0.5) - await broadcast.send(f"已播报至 100% 的群聊") - if error: - await broadcast.send(f"播报时错误:{error}") diff --git a/basic_plugins/chat_history/_rule.py b/basic_plugins/chat_history/_rule.py deleted file mode 100644 index f794a177..00000000 --- a/basic_plugins/chat_history/_rule.py +++ /dev/null @@ -1,9 +0,0 @@ -from nonebot.adapters.onebot.v11 import Event, MessageEvent - -from configs.config import Config - - -def rule(event: Event) -> bool: - return bool( - Config.get_config("chat_history", "FLAG") and isinstance(event, MessageEvent) - ) diff --git a/basic_plugins/chat_history/chat_message.py b/basic_plugins/chat_history/chat_message.py deleted file mode 100644 index 2cefb2d7..00000000 --- a/basic_plugins/chat_history/chat_message.py +++ /dev/null @@ -1,72 +0,0 @@ -from nonebot import on_message -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent - -from configs.config import Config -from models.chat_history import ChatHistory -from services.log import logger -from utils.depends import PlaintText -from utils.utils import scheduler - -from ._rule import rule - -__zx_plugin_name__ = "消息存储 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -Config.add_plugin_config( - "chat_history", - "FLAG", - True, - help_="是否开启消息自从存储", - name="消息存储", - default_value=True, - type=bool, -) - - -chat_history = on_message(rule=rule, priority=1, block=False) - - -TEMP_LIST = [] - - -@chat_history.handle() -async def _(bot: Bot, event: MessageEvent, msg: str = PlaintText()): - group_id = None - if isinstance(event, GroupMessageEvent): - group_id = str(event.group_id) - TEMP_LIST.append( - ChatHistory( - user_id=str(event.user_id), - group_id=group_id, - text=str(event.get_message()), - plain_text=msg, - bot_id=str(bot.self_id), - ) - ) - - -@scheduler.scheduled_job( - "interval", - minutes=1, -) -async def _(): - try: - message_list = TEMP_LIST.copy() - TEMP_LIST.clear() - if message_list: - await ChatHistory.bulk_create(message_list) - logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务") - except Exception as e: - logger.error(f"定时批量添加聊天记录", "定时任务", e=e) - - -# @test.handle() -# async def _(event: MessageEvent): -# print(await ChatHistory.get_user_msg(event.user_id, "private")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) -# print(await ChatHistory.get_user_msg(event.user_id, "group")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) -# print(await ChatHistory.get_group_msg(event.group_id)) -# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/basic_plugins/chat_history/chat_message_handle.py b/basic_plugins/chat_history/chat_message_handle.py deleted file mode 100644 index 99d2f40a..00000000 --- a/basic_plugins/chat_history/chat_message_handle.py +++ /dev/null @@ -1,113 +0,0 @@ -from datetime import datetime, timedelta -from typing import Any, Tuple - -import pytz -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.params import RegexGroup - -from models.chat_history import ChatHistory -from models.group_member_info import GroupInfoUser -from utils.image_utils import BuildImage, text2image -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "消息统计" -__plugin_usage__ = """ -usage: - 发言记录统计 - regex:(周|月|日)?消息排行(des|DES)?(n=[0-9]{1,2})? - 指令: - 消息统计?(des)?(n=?) - 周消息统计?(des)?(n=?) - 月消息统计?(des)?(n=?) - 日消息统计?(des)?(n=?) - 示例: - 消息统计 - 消息统计des - 消息统计DESn=15 - 消息统计n=15 -""".strip() -__plugin_des__ = "发言消息排行" -__plugin_cmd__ = ["消息统计", "周消息统计", "月消息统计", "日消息统计"] -__plugin_type__ = ("数据统计", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "cmd": ["消息统计"], -} - - -msg_handler = on_regex( - r"^(周|月|日)?消息统计(des|DES)?(n=[0-9]{1,2})?$", priority=5, block=True -) - - -@msg_handler.handle() -async def _(event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - gid = event.group_id - date_scope = None - date, order, num = reg_group - num = num.split("=")[-1] if num else 10 - if num and is_number(num) and 10 < int(num) < 50: - num = int(num) - time_now = datetime.now() - zero_today = time_now - timedelta( - hours=time_now.hour, minutes=time_now.minute, seconds=time_now.second - ) - if date in ["日"]: - date_scope = (zero_today, time_now) - elif date in ["周"]: - date_scope = (time_now - timedelta(days=7), time_now) - elif date in ["月"]: - date_scope = (time_now - timedelta(days=30), time_now) - if rank_data := await ChatHistory.get_group_msg_rank( - gid, num, order or "DESC", date_scope - ): - name = "昵称:\n\n" - num_str = "发言次数:\n\n" - idx = 1 - for uid, num in rank_data: - if user := await GroupInfoUser.filter(user_id=uid, group_id=gid).first(): - user_name = user.user_name - else: - user_name = uid - name += f"\t{idx}.{user_name} \n\n" - num_str += f"\t{num}\n\n" - idx += 1 - name_img = await text2image(name.strip(), padding=10, color="#f9f6f2") - num_img = await text2image(num_str.strip(), padding=10, color="#f9f6f2") - if not date_scope: - if date_scope := await ChatHistory.get_group_first_msg_datetime(gid): - date_scope = date_scope.astimezone( - pytz.timezone("Asia/Shanghai") - ).replace(microsecond=0) - else: - date_scope = time_now.replace(microsecond=0) - date_str = f"日期:{date_scope} - 至今" - else: - date_str = f"日期:{date_scope[0].replace(microsecond=0)} - {date_scope[1].replace(microsecond=0)}" - date_w = BuildImage(0, 0, font_size=15).getsize(date_str)[0] - img_w = date_w if date_w > name_img.w + num_img.w else name_img.w + num_img.w - A = BuildImage( - img_w + 15, - num_img.h + 30, - color="#f9f6f2", - font="CJGaoDeGuo.otf", - font_size=15, - ) - await A.atext((10, 10), date_str) - await A.apaste(name_img, (0, 30)) - await A.apaste(num_img, (name_img.w, 30)) - await msg_handler.send(image(b64=A.pic2bs4())) - - -# @test.handle() -# async def _(event: MessageEvent): -# print(await ChatHistory.get_user_msg(event.user_id, "private")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) -# print(await ChatHistory.get_user_msg(event.user_id, "group")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) -# print(await ChatHistory.get_group_msg(event.group_id)) -# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/basic_plugins/group_handle/__init__.py b/basic_plugins/group_handle/__init__.py deleted file mode 100755 index fbc912f8..00000000 --- a/basic_plugins/group_handle/__init__.py +++ /dev/null @@ -1,236 +0,0 @@ -import os -import random -from datetime import datetime -from pathlib import Path - -import ujson as json -from nonebot import on_notice, on_request -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - GroupDecreaseNoticeEvent, - GroupIncreaseNoticeEvent, -) - -from configs.config import NICKNAME, Config -from configs.path_config import DATA_PATH, IMAGE_PATH -from models.group_info import GroupInfo -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from services.log import logger -from utils.depends import GetConfig -from utils.manager import group_manager, plugins2settings_manager, requests_manager -from utils.message_builder import image -from utils.utils import FreqLimiter - -__zx_plugin_name__ = "群事件处理 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"group_welcome": "进群欢迎", "refund_group_remind": "退群提醒"} -Config.add_plugin_config( - "invite_manager", "message", f"请不要未经同意就拉{NICKNAME}入群!告辞!", help_="强制拉群后进群回复的内容.." -) -Config.add_plugin_config( - "invite_manager", "flag", True, help_="被强制拉群后是否直接退出", default_value=True, type=bool -) -Config.add_plugin_config( - "invite_manager", "welcome_msg_cd", 5, help_="群欢迎消息cd", default_value=5, type=int -) -Config.add_plugin_config( - "_task", - "DEFAULT_GROUP_WELCOME", - True, - help_="被动 进群欢迎 进群默认开关状态", - default_value=True, - type=bool, -) -Config.add_plugin_config( - "_task", - "DEFAULT_REFUND_GROUP_REMIND", - True, - help_="被动 退群提醒 进群默认开关状态", - default_value=True, - type=bool, -) - - -_flmt = FreqLimiter(Config.get_config("invite_manager", "welcome_msg_cd") or 5) - - -# 群员增加处理 -group_increase_handle = on_notice(priority=1, block=False) -# 群员减少处理 -group_decrease_handle = on_notice(priority=1, block=False) -# (群管理)加群同意请求 -add_group = on_request(priority=1, block=False) - - -@group_increase_handle.handle() -async def _(bot: Bot, event: GroupIncreaseNoticeEvent): - if event.user_id == int(bot.self_id): - group = await GroupInfo.get_or_none(group_id=str(event.group_id)) - # 群聊不存在或被强制拉群,退出该群 - if (not group or group.group_flag == 0) and Config.get_config( - "invite_manager", "flag" - ): - try: - msg = Config.get_config("invite_manager", "message") - if msg: - await bot.send_group_msg(group_id=event.group_id, message=msg) - await bot.set_group_leave(group_id=event.group_id) - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"触发强制入群保护,已成功退出群聊 {event.group_id}...", - ) - logger.info(f"强制拉群或未有群信息,退出群聊成功", "入群检测", group_id=event.group_id) - requests_manager.remove_request("group", event.group_id) - except Exception as e: - logger.info(f"强制拉群或未有群信息,退出群聊失败", "入群检测", group_id=event.group_id, e=e) - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...", - ) - # 默认群功能开关 - elif event.group_id not in group_manager.get_data().group_manager.keys(): - data = plugins2settings_manager.get_data() - for plugin in data.keys(): - if not data[plugin].default_status: - group_manager.block_plugin(plugin, str(event.group_id)) - admin_default_auth = Config.get_config( - "admin_bot_manage", "ADMIN_DEFAULT_AUTH" - ) - # 即刻刷新权限 - for user_info in await bot.get_group_member_list(group_id=event.group_id): - if ( - user_info["role"] - in [ - "owner", - "admin", - ] - and not await LevelUser.is_group_flag( - user_info["user_id"], event.group_id - ) - and admin_default_auth is not None - ): - await LevelUser.set_level( - user_info["user_id"], - user_info["group_id"], - admin_default_auth, - ) - logger.debug( - f"添加默认群管理员权限: {admin_default_auth}", - "入群检测", - user_info["user_id"], - user_info["group_id"], - ) - if str(user_info["user_id"]) in bot.config.superusers: - await LevelUser.set_level( - user_info["user_id"], user_info["group_id"], 9 - ) - logger.debug( - f"添加超级用户权限: 9", - "入群检测", - user_info["user_id"], - user_info["group_id"], - ) - else: - join_time = datetime.now() - user_info = await bot.get_group_member_info( - group_id=event.group_id, user_id=event.user_id - ) - await GroupInfoUser.update_or_create( - user_id=str(user_info["user_id"]), - group_id=str(user_info["group_id"]), - defaults={"user_name": user_info["nickname"], "user_join_time": join_time}, - ) - logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新成功") - - # 群欢迎消息 - if _flmt.check(event.group_id): - _flmt.start_cd(event.group_id) - msg = "" - img = "" - at_flag = False - custom_welcome_msg_json = ( - Path() / "data" / "custom_welcome_msg" / "custom_welcome_msg.json" - ) - if custom_welcome_msg_json.exists(): - data = json.load(open(custom_welcome_msg_json, "r")) - if data.get(str(event.group_id)): - msg = data[str(event.group_id)] - if "[at]" in msg: - msg = msg.replace("[at]", "") - at_flag = True - if (DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg").exists(): - img = image(DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg") - if msg or img: - msg = msg.strip() + img - msg = "\n" + msg if at_flag else msg - await group_increase_handle.send( - "[[_task|group_welcome]]" + msg, at_sender=at_flag - ) - else: - await group_increase_handle.send( - "[[_task|group_welcome]]新人快跑啊!!本群现状↓(快使用自定义!)" - + image( - IMAGE_PATH - / "qxz" - / random.choice(os.listdir(IMAGE_PATH / "qxz")) - ) - ) - - -@group_decrease_handle.handle() -async def _(bot: Bot, event: GroupDecreaseNoticeEvent): - # 被踢出群 - if event.sub_type == "kick_me": - group_id = event.group_id - operator_id = event.operator_id - if user := await GroupInfoUser.get_or_none( - user_id=str(event.operator_id), group_id=str(event.group_id) - ): - operator_name = user.user_name - else: - operator_name = "None" - group = await GroupInfo.filter(group_id=str(group_id)).first() - group_name = group.group_name if group else "" - coffee = int(list(bot.config.superusers)[0]) - await bot.send_private_msg( - user_id=coffee, - message=f"****呜..一份踢出报告****\n" - f"我被 {operator_name}({operator_id})\n" - f"踢出了 {group_name}({group_id})\n" - f"日期:{str(datetime.now()).split('.')[0]}", - ) - return - if event.user_id == int(bot.self_id): - group_manager.delete_group(event.group_id) - return - if user := await GroupInfoUser.get_or_none( - user_id=str(event.user_id), group_id=str(event.group_id) - ): - user_name = user.user_name - else: - user_name = f"{event.user_id}" - await GroupInfoUser.filter( - user_id=str(event.user_id), group_id=str(event.group_id) - ).delete() - logger.info( - f"名称: {user_name} 退出群聊", - "group_decrease_handle", - event.user_id, - event.group_id, - ) - rst = "" - if event.sub_type == "leave": - rst = f"{user_name}离开了我们..." - if event.sub_type == "kick": - operator = await bot.get_group_member_info( - user_id=event.operator_id, group_id=event.group_id - ) - operator_name = operator["card"] if operator["card"] else operator["nickname"] - rst = f"{user_name} 被 {operator_name} 送走了." - try: - await group_decrease_handle.send(f"[[_task|refund_group_remind]]{rst}") - except ActionFailed: - return diff --git a/basic_plugins/help/__init__.py b/basic_plugins/help/__init__.py deleted file mode 100755 index d804cf08..00000000 --- a/basic_plugins/help/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.rule import to_me - -from configs.path_config import DATA_PATH, IMAGE_PATH -from services.log import logger -from utils.message_builder import image - -from ._data_source import create_help_img, get_plugin_help -from ._utils import GROUP_HELP_PATH - -__zx_plugin_name__ = "帮助" - -__plugin_configs__ = { - "TYPE": { - "value": "normal", - "help": "帮助图片样式 ['normal', 'HTML']", - "default_value": "normal", - "type": str, - } -} - -simple_help_image = IMAGE_PATH / "simple_help.png" -if simple_help_image.exists(): - simple_help_image.unlink() - - -simple_help = on_command( - "功能", rule=to_me(), aliases={"help", "帮助"}, priority=1, block=True -) - - -@simple_help.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - is_super = False - if msg: - if "-super" in msg: - if str(event.user_id) in bot.config.superusers: - is_super = True - msg = msg.replace("-super", "").strip() - img_msg = get_plugin_help(msg, is_super) - if img_msg: - await simple_help.send(image(b64=img_msg)) - else: - await simple_help.send("没有此功能的帮助信息...") - logger.info( - f"查看帮助详情: {msg}", "帮助", event.user_id, getattr(event, "group_id", None) - ) - else: - if isinstance(event, GroupMessageEvent): - _image_path = GROUP_HELP_PATH / f"{event.group_id}.png" - if not _image_path.exists(): - await create_help_img(event.group_id) - await simple_help.send(image(_image_path)) - else: - if not simple_help_image.exists(): - if simple_help_image.exists(): - simple_help_image.unlink() - await create_help_img(None) - await simple_help.finish(image("simple_help.png")) diff --git a/basic_plugins/help/_data_source.py b/basic_plugins/help/_data_source.py deleted file mode 100644 index be61b50b..00000000 --- a/basic_plugins/help/_data_source.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Optional - -from configs.path_config import IMAGE_PATH -from utils.image_utils import BuildImage -from utils.manager import admin_manager, plugin_data_manager, plugins2settings_manager - -from ._utils import HelpImageBuild - -random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help" - -background = IMAGE_PATH / "background" / "0.png" - - -async def create_help_img(group_id: Optional[int]): - """ - 说明: - 生成帮助图片 - 参数: - :param group_id: 群号 - """ - await HelpImageBuild().build_image(group_id) - - -def get_plugin_help(msg: str, is_super: bool = False) -> Optional[str]: - """ - 说明: - 获取功能的帮助信息 - 参数: - :param msg: 功能cmd - :param is_super: 是否为超级用户 - """ - module = plugins2settings_manager.get_plugin_module( - msg - ) or admin_manager.get_plugin_module(msg) - if module and (plugin_data := plugin_data_manager.get(module)): - plugin_data.superuser_usage - if is_super: - result = plugin_data.superuser_usage - else: - result = plugin_data.usage - if result: - width = 0 - for x in result.split("\n"): - _width = len(x) * 24 - width = width if width > _width else _width - height = len(result.split("\n")) * 45 - A = BuildImage(width, height, font_size=24) - bk = BuildImage( - width, - height, - background=IMAGE_PATH / "background" / "1.png", - ) - A.paste(bk, alpha=True) - A.text((int(width * 0.048), int(height * 0.21)), result) - return A.pic2bs4() - return None diff --git a/basic_plugins/hooks/__init__.py b/basic_plugins/hooks/__init__.py deleted file mode 100755 index b236939d..00000000 --- a/basic_plugins/hooks/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -from configs.config import Config - -Config.add_plugin_config( - "hook", - "CHECK_NOTICE_INFO_CD", - 300, - name="基础hook配置", - help_="群检测,个人权限检测等各种检测提示信息cd", - default_value=300, - type=int, -) - -Config.add_plugin_config( - "hook", - "MALICIOUS_BAN_TIME", - 30, - help_="恶意命令触发检测触发后ban的时长(分钟)", - default_value=30, - type=int, -) - -Config.add_plugin_config( - "hook", - "MALICIOUS_CHECK_TIME", - 5, - help_="恶意命令触发检测规定时间内(秒)", - default_value=5, - type=int, -) - -Config.add_plugin_config( - "hook", "MALICIOUS_BAN_COUNT", 6, help_="恶意命令触发检测最大触发次数", default_value=6, type=int -) diff --git a/basic_plugins/hooks/_utils.py b/basic_plugins/hooks/_utils.py deleted file mode 100644 index 109c3da2..00000000 --- a/basic_plugins/hooks/_utils.py +++ /dev/null @@ -1,580 +0,0 @@ -import time - -from nonebot.adapters.onebot.v11 import ( - Bot, - Event, - GroupMessageEvent, - Message, - MessageEvent, - PokeNotifyEvent, - PrivateMessageEvent, -) -from nonebot.exception import ActionFailed, IgnoredException -from nonebot.internal.matcher import Matcher - -from configs.config import Config -from models.bag_user import BagUser -from models.ban_user import BanUser -from models.friend_user import FriendUser -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from models.user_shop_gold_log import UserShopGoldLog -from services.log import logger -from utils.decorator import Singleton -from utils.manager import ( - StaticData, - admin_manager, - group_manager, - plugin_data_manager, - plugins2block_manager, - plugins2cd_manager, - plugins2count_manager, - plugins2settings_manager, - plugins_manager, -) -from utils.manager.models import PluginType -from utils.message_builder import at -from utils.utils import FreqLimiter - -ignore_rst_module = ["ai", "poke", "dialogue"] - -other_limit_plugins = ["poke"] - - -class StatusMessageManager(StaticData): - def __init__(self): - super().__init__(None) - - def add(self, id_: int): - self._data[id_] = time.time() - - def delete(self, id_: int): - if self._data.get(id_): - del self._data[id_] - - def check(self, id_: int, t: int = 30) -> bool: - if self._data.get(id_): - if time.time() - self._data[id_] > t: - del self._data[id_] - return True - return False - return True - - -status_message_manager = StatusMessageManager() - - -def set_block_limit_false(event, module): - """ - 设置用户block为false - :param event: event - :param module: 插件模块 - """ - if plugins2block_manager.check_plugin_block_status(module): - if plugin_block_data := plugins2block_manager.get_plugin_block_data(module): - check_type = plugin_block_data.check_type - limit_type = plugin_block_data.limit_type - if not ( - (isinstance(event, GroupMessageEvent) and check_type == "private") - or (isinstance(event, PrivateMessageEvent) and check_type == "group") - ): - block_type_ = event.user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - block_type_ = event.group_id - plugins2block_manager.set_false(block_type_, module) - - -async def send_msg(msg: str, bot: Bot, event: MessageEvent): - """ - 说明: - 发送信息 - 参数: - :param msg: pass - :param bot: pass - :param event: pass - """ - if "[uname]" in msg: - uname = event.sender.card or event.sender.nickname or "" - msg = msg.replace("[uname]", uname) - if "[nickname]" in msg: - if isinstance(event, GroupMessageEvent): - nickname = await GroupInfoUser.get_user_nickname( - event.user_id, event.group_id - ) - else: - nickname = await FriendUser.get_user_nickname(event.user_id) - msg = msg.replace("[nickname]", nickname) - if "[at]" in msg and isinstance(event, GroupMessageEvent): - msg = msg.replace("[at]", str(at(event.user_id))) - try: - if isinstance(event, GroupMessageEvent): - status_message_manager.add(event.group_id) - await bot.send_group_msg(group_id=event.group_id, message=Message(msg)) - else: - status_message_manager.add(event.user_id) - await bot.send_private_msg(user_id=event.user_id, message=Message(msg)) - except ActionFailed: - pass - - -class IsSuperuserException(Exception): - pass - - -@Singleton -class AuthChecker: - """ - 权限检查 - """ - - def __init__(self): - check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD") - if check_notice_info_cd is None or check_notice_info_cd < 0: - raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0") - self._flmt = FreqLimiter(check_notice_info_cd) - self._flmt_g = FreqLimiter(check_notice_info_cd) - self._flmt_s = FreqLimiter(check_notice_info_cd) - self._flmt_c = FreqLimiter(check_notice_info_cd) - - async def auth(self, matcher: Matcher, bot: Bot, event: Event): - """ - 说明: - 权限检查 - 参数: - :param matcher: matcher - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - try: - if plugin_name := matcher.plugin_name: - # self.auth_hidden(matcher, plugin_name) - cost_gold = await self.auth_cost(plugin_name, bot, event) - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - # if user_id and str(user_id) not in bot.config.superusers: - await self.auth_basic(plugin_name, bot, event) - self.auth_group(plugin_name, bot, event) - await self.auth_admin(plugin_name, matcher, bot, event) - await self.auth_plugin(plugin_name, matcher, bot, event) - await self.auth_limit(plugin_name, bot, event) - if cost_gold and user_id and group_id: - await BagUser.spend_gold(user_id, group_id, cost_gold) - logger.debug(f"调用功能花费金币: {cost_gold}", "HOOK", user_id, group_id) - except IsSuperuserException: - logger.debug(f"超级用户或被ban跳过权限检测...", "HOOK", user_id, group_id) - - # def auth_hidden(self, matcher: Matcher): - # if plugin_data := plugin_data_manager.get(matcher.plugin_name): # type: ignore - - async def auth_limit(self, plugin_name: str, bot: Bot, event: Event): - """ - 说明: - 插件限制 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return - group_id = getattr(event, "group_id", None) - if plugins2cd_manager.check_plugin_cd_status(plugin_name): - if ( - plugin_cd_data := plugins2cd_manager.get_plugin_cd_data(plugin_name) - ) and (plugin_data := plugins2cd_manager.get_plugin_data(plugin_name)): - check_type = plugin_cd_data.check_type - limit_type = plugin_cd_data.limit_type - msg = plugin_cd_data.rst - if ( - (isinstance(event, PrivateMessageEvent) and check_type == "private") - or (isinstance(event, GroupMessageEvent) and check_type == "group") - or plugin_data.check_type == "all" - ): - cd_type_ = user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - cd_type_ = event.group_id - if not plugins2cd_manager.check(plugin_name, cd_type_): - if msg: - await send_msg(msg, bot, event) # type: ignore - logger.debug( - f"{plugin_name} 正在cd中...", "HOOK", user_id, group_id - ) - raise IgnoredException(f"{plugin_name} 正在cd中...") - else: - plugins2cd_manager.start_cd(plugin_name, cd_type_) - # Block - if plugins2block_manager.check_plugin_block_status(plugin_name): - if plugin_block_data := plugins2block_manager.get_plugin_block_data( - plugin_name - ): - check_type = plugin_block_data.check_type - limit_type = plugin_block_data.limit_type - msg = plugin_block_data.rst - if ( - (isinstance(event, PrivateMessageEvent) and check_type == "private") - or (isinstance(event, GroupMessageEvent) and check_type == "group") - or check_type == "all" - ): - block_type_ = user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - block_type_ = event.group_id - if plugins2block_manager.check(block_type_, plugin_name): - if msg: - await send_msg(msg, bot, event) # type: ignore - logger.debug(f"正在调用{plugin_name}...", "HOOK", user_id, group_id) - raise IgnoredException(f"{user_id}正在调用{plugin_name}....") - else: - plugins2block_manager.set_true(block_type_, plugin_name) - # Count - if ( - plugins2count_manager.check_plugin_count_status(plugin_name) - and user_id not in bot.config.superusers - ): - if plugin_count_data := plugins2count_manager.get_plugin_count_data( - plugin_name - ): - limit_type = plugin_count_data.limit_type - msg = plugin_count_data.rst - count_type_ = user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - count_type_ = event.group_id - if not plugins2count_manager.check(plugin_name, count_type_): - if msg: - await send_msg(msg, bot, event) # type: ignore - logger.debug( - f"{plugin_name} count次数限制...", "HOOK", user_id, group_id - ) - raise IgnoredException(f"{plugin_name} count次数限制...") - else: - plugins2count_manager.increase(plugin_name, count_type_) - - async def auth_plugin( - self, plugin_name: str, matcher: Matcher, bot: Bot, event: Event - ): - """ - 说明: - 插件状态 - 参数: - :param plugin_name: 模块名 - :param matcher: matcher - :param bot: bot - :param event: event - """ - if plugin_name in plugins2settings_manager.keys() and matcher.priority not in [ - 1, - 999, - ]: - user_id = getattr(event, "user_id", None) - if not user_id: - return - group_id = getattr(event, "group_id", None) - # 戳一戳单独判断 - if ( - isinstance(event, GroupMessageEvent) - or isinstance(event, PokeNotifyEvent) - or matcher.plugin_name in other_limit_plugins - ) and group_id: - if status_message_manager.get(group_id) is None: - status_message_manager.delete(group_id) - if plugins2settings_manager[ - plugin_name - ].level > group_manager.get_group_level(group_id): - try: - if ( - self._flmt_g.check(user_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_g.start_cd(user_id) - await bot.send_group_msg( - group_id=group_id, message="群权限不足..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug(f"{plugin_name} 群权限不足...", "HOOK", user_id, group_id) - raise IgnoredException("群权限不足") - # 插件状态 - if not group_manager.get_plugin_status(plugin_name, group_id): - try: - if plugin_name not in ignore_rst_module and self._flmt_s.check( - group_id - ): - self._flmt_s.start_cd(group_id) - await bot.send_group_msg( - group_id=group_id, message="该群未开启此功能.." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug(f"{plugin_name} 未开启此功能...", "HOOK", user_id, group_id) - raise IgnoredException("未开启此功能...") - # 管理员禁用 - if not group_manager.get_plugin_status( - f"{plugin_name}:super", group_id - ): - try: - if ( - self._flmt_s.check(group_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_s.start_cd(group_id) - await bot.send_group_msg( - group_id=group_id, message="管理员禁用了此群该功能..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug( - f"{plugin_name} 管理员禁用了此群该功能...", "HOOK", user_id, group_id - ) - raise IgnoredException("管理员禁用了此群该功能...") - # 群聊禁用 - if not plugins_manager.get_plugin_status( - plugin_name, block_type="group" - ): - try: - if ( - self._flmt_c.check(group_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_c.start_cd(group_id) - await bot.send_group_msg( - group_id=group_id, message="该功能在群聊中已被禁用..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug( - f"{plugin_name} 该插件在群聊中已被禁用...", "HOOK", user_id, group_id - ) - raise IgnoredException("该插件在群聊中已被禁用...") - else: - # 私聊禁用 - if not plugins_manager.get_plugin_status( - plugin_name, block_type="private" - ): - try: - if self._flmt_c.check(user_id): - self._flmt_c.start_cd(user_id) - await bot.send_private_msg( - user_id=user_id, message="该功能在私聊中已被禁用..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(user_id) - set_block_limit_false(event, plugin_name) - logger.debug( - f"{plugin_name} 该插件在私聊中已被禁用...", "HOOK", user_id, group_id - ) - raise IgnoredException("该插件在私聊中已被禁用...") - # 维护 - if not plugins_manager.get_plugin_status(plugin_name, block_type="all"): - if isinstance( - event, GroupMessageEvent - ) and group_manager.check_group_is_white(event.group_id): - raise IsSuperuserException() - try: - if isinstance(event, GroupMessageEvent): - if ( - self._flmt_c.check(event.group_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_c.start_cd(event.group_id) - await bot.send_group_msg( - group_id=event.group_id, message="此功能正在维护..." - ) - else: - await bot.send_private_msg( - user_id=user_id, message="此功能正在维护..." - ) - except ActionFailed: - pass - if event.is_tome(): - id_ = group_id or user_id - status_message_manager.add(id_) - set_block_limit_false(event, plugin_name) - logger.debug(f"{plugin_name} 此功能正在维护...", "HOOK", user_id, group_id) - raise IgnoredException("此功能正在维护...") - - async def auth_admin( - self, plugin_name: str, matcher: Matcher, bot: Bot, event: Event - ): - """ - 说明: - 管理员命令 个人权限 - 参数: - :param plugin_name: 模块名 - :param matcher: matcher - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return - group_id = getattr(event, "group_id", None) - if plugin_name in admin_manager.keys() and matcher.priority not in [1, 999]: - if isinstance(event, GroupMessageEvent): - # 个人权限 - if ( - not await LevelUser.check_level( - event.user_id, - event.group_id, - admin_manager.get_plugin_level(plugin_name), - ) - and admin_manager.get_plugin_level(plugin_name) > 0 - ): - try: - if self._flmt.check(event.user_id): - self._flmt.start_cd(event.user_id) - await bot.send_group_msg( - group_id=event.group_id, - message=f"{at(event.user_id)}你的权限不足喔,该功能需要的权限等级:" - f"{admin_manager.get_plugin_level(plugin_name)}", - ) - except ActionFailed: - pass - set_block_limit_false(event, plugin_name) - if event.is_tome(): - status_message_manager.add(event.group_id) - logger.debug(f"{plugin_name} 管理员权限不足...", "HOOK", user_id, group_id) - raise IgnoredException("管理员权限不足") - else: - if not await LevelUser.check_level( - user_id, 0, admin_manager.get_plugin_level(plugin_name) - ): - try: - await bot.send_private_msg( - user_id=user_id, - message=f"你的权限不足喔,该功能需要的权限等级:{admin_manager.get_plugin_level(plugin_name)}", - ) - except ActionFailed: - pass - set_block_limit_false(event, plugin_name) - if event.is_tome(): - status_message_manager.add(user_id) - logger.debug(f"{plugin_name} 管理员权限不足...", "HOOK", user_id, group_id) - raise IgnoredException("权限不足") - - def auth_group(self, plugin_name: str, bot: Bot, event: Event): - """ - 说明: - 群黑名单检测 群总开关检测 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - if not group_id: - return - if ( - group_manager.get_group_level(group_id) < 0 - and str(user_id) not in bot.config.superusers - ): - logger.debug(f"{plugin_name} 群黑名单, 群权限-1...", "HOOK", user_id, group_id) - raise IgnoredException("群黑名单") - if not group_manager.check_group_bot_status(group_id): - try: - if str(event.get_message()) != "醒来": - logger.debug( - f"{plugin_name} 功能总开关关闭状态...", "HOOK", user_id, group_id - ) - raise IgnoredException("功能总开关关闭状态") - except ValueError: - logger.debug(f"{plugin_name} 功能总开关关闭状态...", "HOOK", user_id, group_id) - raise IgnoredException("功能总开关关闭状态") - - async def auth_basic(self, plugin_name: str, bot: Bot, event: Event): - """ - 说明: - 检测是否满足超级用户权限,是否被ban等 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return - plugin_setting = plugins2settings_manager.get_plugin_data(plugin_name) - if ( - ( - not isinstance(event, MessageEvent) - and plugin_name not in other_limit_plugins - ) - or await BanUser.is_ban(user_id) - and str(user_id) not in bot.config.superusers - ) or ( - str(user_id) in bot.config.superusers - and plugin_setting - and not plugin_setting.limit_superuser - ): - raise IsSuperuserException() - if plugin_data := plugin_data_manager.get(plugin_name): - if ( - plugin_data.plugin_type == PluginType.SUPERUSER - and str(user_id) in bot.config.superusers - ): - raise IsSuperuserException() - - async def auth_cost(self, plugin_name: str, bot: Bot, event: Event) -> int: - """ - 说明: - 检测是否满足金币条件 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return 0 - group_id = getattr(event, "group_id", None) - cost_gold = 0 - if isinstance(event, GroupMessageEvent) and ( - psm := plugins2settings_manager.get_plugin_data(plugin_name) - ): - if psm.cost_gold > 0: - if ( - await BagUser.get_gold(event.user_id, event.group_id) - < psm.cost_gold - ): - await send_msg(f"金币不足..该功能需要{psm.cost_gold}金币..", bot, event) - logger.debug( - f"{plugin_name} 金币限制..该功能需要{psm.cost_gold}金币..", - "HOOK", - user_id, - group_id, - ) - raise IgnoredException(f"{plugin_name} 金币限制...") - # 当插件不阻塞超级用户时,超级用户提前扣除金币 - if ( - str(event.user_id) in bot.config.superusers - and not psm.limit_superuser - ): - await BagUser.spend_gold( - event.user_id, event.group_id, psm.cost_gold - ) - await UserShopGoldLog.create( - user_id=str(event.user_id), - group_id=str(event.group_id), - type=2, - name=plugin_name, - num=1, - spend_gold=psm.cost_gold, - ) - cost_gold = psm.cost_gold - return cost_gold diff --git a/basic_plugins/hooks/auth_hook.py b/basic_plugins/hooks/auth_hook.py deleted file mode 100755 index a51e8fbe..00000000 --- a/basic_plugins/hooks/auth_hook.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Optional - -from nonebot.adapters.onebot.v11 import ( - Bot, - MessageEvent, - Event, -) -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor, run_postprocessor -from nonebot.typing import T_State - -from ._utils import ( - set_block_limit_false, - AuthChecker, -) - - -# # 权限检测 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: Event): - await AuthChecker().auth(matcher, bot, event) - - -# 解除命令block阻塞 -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: Event, - state: T_State, -): - if not isinstance(event, MessageEvent) and matcher.plugin_name != "poke": - return - module = matcher.plugin_name - set_block_limit_false(event, module) diff --git a/basic_plugins/hooks/ban_hook.py b/basic_plugins/hooks/ban_hook.py deleted file mode 100755 index 40d11b05..00000000 --- a/basic_plugins/hooks/ban_hook.py +++ /dev/null @@ -1,83 +0,0 @@ -from nonebot.adapters.onebot.v11 import ActionFailed, Bot, Event, GroupMessageEvent -from nonebot.matcher import Matcher -from nonebot.message import IgnoredException, run_preprocessor -from nonebot.typing import T_State - -from configs.config import Config -from models.ban_user import BanUser -from services.log import logger -from utils.message_builder import at -from utils.utils import FreqLimiter, is_number, static_flmt - -from ._utils import ignore_rst_module, other_limit_plugins - -Config.add_plugin_config( - "hook", - "BAN_RESULT", - "才不会给你发消息.", - help_="对被ban用户发送的消息", -) - -_flmt = FreqLimiter(300) - - -# 检查是否被ban -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: Event, state: T_State): - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - if user_id and ( - matcher.priority not in [1, 999] or matcher.plugin_name in other_limit_plugins - ): - if ( - await BanUser.is_super_ban(user_id) - and str(user_id) not in bot.config.superusers - ): - logger.debug(f"用户处于超级黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于超级黑名单中") - if await BanUser.is_ban(user_id) and str(user_id) not in bot.config.superusers: - time = await BanUser.check_ban_time(user_id) - if isinstance(time, int): - time = abs(int(time)) - if time < 60: - time = str(time) + " 秒" - else: - time = str(int(time / 60)) + " 分钟" - else: - time = str(time) + " 分钟" - if isinstance(event, GroupMessageEvent): - if not static_flmt.check(user_id): - logger.debug(f"用户处于黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于黑名单中") - static_flmt.start_cd(user_id) - if matcher.priority != 999: - try: - ban_result = Config.get_config("hook", "BAN_RESULT") - if ( - ban_result - and _flmt.check(user_id) - and matcher.plugin_name not in ignore_rst_module - ): - _flmt.start_cd(user_id) - await bot.send_group_msg( - group_id=event.group_id, - message=at(user_id) - + ban_result - + f" 在..在 {time} 后才会理你喔", - ) - except ActionFailed: - pass - else: - if not static_flmt.check(user_id): - logger.debug(f"用户处于黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于黑名单中") - static_flmt.start_cd(user_id) - if matcher.priority != 999: - ban_result = Config.get_config("hook", "BAN_RESULT") - if ban_result and matcher.plugin_name not in ignore_rst_module: - await bot.send_private_msg( - user_id=user_id, - message=at(user_id) + ban_result + f" 在..在 {time}后才会理你喔", - ) - logger.debug(f"用户处于黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于黑名单中") diff --git a/basic_plugins/hooks/chkdsk_hook.py b/basic_plugins/hooks/chkdsk_hook.py deleted file mode 100755 index 5b506c97..00000000 --- a/basic_plugins/hooks/chkdsk_hook.py +++ /dev/null @@ -1,72 +0,0 @@ -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - GroupMessageEvent, - MessageEvent, -) -from nonebot.matcher import Matcher -from nonebot.message import IgnoredException, run_preprocessor -from nonebot.typing import T_State - -from configs.config import Config -from models.ban_user import BanUser -from services.log import logger -from utils.message_builder import at -from utils.utils import BanCheckLimiter - -malicious_check_time = Config.get_config("hook", "MALICIOUS_CHECK_TIME") -malicious_ban_count = Config.get_config("hook", "MALICIOUS_BAN_COUNT") - -if not malicious_check_time: - raise ValueError("模块: [hook], 配置项: [MALICIOUS_CHECK_TIME] 为空或小于0") -if not malicious_ban_count: - raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_COUNT] 为空或小于0") - -_blmt = BanCheckLimiter( - malicious_check_time, - malicious_ban_count, -) - - -# 恶意触发命令检测 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: GroupMessageEvent, state: T_State): - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - if not isinstance(event, MessageEvent): - return - malicious_ban_time = Config.get_config("hook", "MALICIOUS_BAN_TIME") - if not malicious_ban_time: - raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0") - if matcher.type == "message" and matcher.priority not in [1, 999]: - if state["_prefix"]["raw_command"]: - if _blmt.check(f'{event.user_id}{state["_prefix"]["raw_command"]}'): - await BanUser.ban( - event.user_id, - 9, - malicious_ban_time * 60, - ) - logger.info( - f"触发了恶意触发检测: {matcher.plugin_name}", "HOOK", user_id, group_id - ) - if isinstance(event, GroupMessageEvent): - try: - await bot.send_group_msg( - group_id=event.group_id, - message=at(event.user_id) + "检测到恶意触发命令,您将被封禁 30 分钟", - ) - except ActionFailed: - pass - else: - try: - await bot.send_private_msg( - user_id=event.user_id, - message=at(event.user_id) + "检测到恶意触发命令,您将被封禁 30 分钟", - ) - except ActionFailed: - pass - logger.debug( - f"触发了恶意触发检测: {matcher.plugin_name}", "HOOK", user_id, group_id - ) - raise IgnoredException("检测到恶意触发命令") - _blmt.add(f'{event.user_id}{state["_prefix"]["raw_command"]}') diff --git a/basic_plugins/hooks/other_hook.py b/basic_plugins/hooks/other_hook.py deleted file mode 100755 index 9010c84b..00000000 --- a/basic_plugins/hooks/other_hook.py +++ /dev/null @@ -1,43 +0,0 @@ -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor, IgnoredException -from nonebot.typing import T_State -from ._utils import status_message_manager -from nonebot.adapters.onebot.v11 import ( - Bot, - MessageEvent, - PrivateMessageEvent, - GroupMessageEvent, -) - - -# 为什么AI会自己和自己聊天 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: PrivateMessageEvent, state: T_State): - if not isinstance(event, MessageEvent): - return - if event.user_id == int(bot.self_id): - raise IgnoredException("为什么AI会自己和自己聊天") - - -# 有命令就别说话了 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): - if not isinstance(event, MessageEvent): - return - if matcher.type == "message" and matcher.plugin_name == "ai": - if ( - isinstance(event, GroupMessageEvent) - and not status_message_manager.check(event.group_id) - ): - status_message_manager.delete(event.group_id) - raise IgnoredException("有命令就别说话了") - elif ( - isinstance(event, PrivateMessageEvent) - and not status_message_manager.check(event.user_id) - ): - status_message_manager.delete(event.user_id) - raise IgnoredException("有命令就别说话了") - - - - diff --git a/basic_plugins/hooks/task_hook.py b/basic_plugins/hooks/task_hook.py deleted file mode 100644 index f346052c..00000000 --- a/basic_plugins/hooks/task_hook.py +++ /dev/null @@ -1,46 +0,0 @@ -import re -from typing import Any, Dict - -from nonebot.adapters.onebot.v11 import Bot, Message, unescape -from nonebot.exception import MockApiException - -from services.log import logger -from utils.manager import group_manager - - -@Bot.on_calling_api -async def _(bot: Bot, api: str, data: Dict[str, Any]): - r = None - task = None - group_id = None - try: - if ( - api == "send_msg" and data.get("message_type") == "group" - ) or api == "send_group_msg": - msg = unescape( - data["message"].strip() - if isinstance(data["message"], str) - else str(data["message"]["text"]).strip() - ) - if r := re.search( - "^\[\[_task\|(.*)]]", - msg, - ): - if r.group(1) in group_manager.get_task_data().keys(): - task = r.group(1) - group_id = data["group_id"] - except Exception as e: - logger.error(f"TaskHook ERROR", "HOOK", e=e) - else: - if task and group_id: - if group_manager.get_group_level( - group_id - ) < 0 or not group_manager.check_task_status(task, group_id): - logger.debug(f"被动技能 {task} 处于关闭状态") - raise MockApiException(f"被动技能 {task} 处于关闭状态...") - else: - msg = str(data["message"]).strip() - msg = msg.replace(f"[[_task|{task}]]", "").replace( - f"[[_task|{task}]]", "" - ) - data["message"] = Message(msg) diff --git a/basic_plugins/hooks/withdraw_message_hook.py b/basic_plugins/hooks/withdraw_message_hook.py deleted file mode 100755 index db7d5651..00000000 --- a/basic_plugins/hooks/withdraw_message_hook.py +++ /dev/null @@ -1,32 +0,0 @@ -import asyncio -from typing import Optional - -from nonebot.adapters.onebot.v11 import Bot, Event -from nonebot.matcher import Matcher -from nonebot.message import run_postprocessor -from nonebot.typing import T_State - -from services.log import logger -from utils.manager import withdraw_message_manager - - -# 消息撤回 -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: Event, - state: T_State, -): - tasks = [] - for id_, time in withdraw_message_manager.data: - tasks.append(asyncio.ensure_future(_withdraw_message(bot, id_, time))) - withdraw_message_manager.remove((id_, time)) - await asyncio.gather(*tasks) - - -async def _withdraw_message(bot: Bot, id_: int, time: int): - await asyncio.sleep(time) - logger.debug(f"撤回消息ID: {id_}", "HOOK") - await bot.delete_msg(message_id=id_) diff --git a/basic_plugins/init_plugin_config/__init__.py b/basic_plugins/init_plugin_config/__init__.py deleted file mode 100755 index 355239fe..00000000 --- a/basic_plugins/init_plugin_config/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -import nonebot -from nonebot import Driver -from nonebot.adapters.onebot.v11 import Bot - -from configs.path_config import DATA_PATH -from services.log import logger - -from .check_plugin_status import check_plugin_status -from .init import init -from .init_none_plugin_count_manager import init_none_plugin_count_manager -from .init_plugin_info import init_plugin_info -from .init_plugins_config import init_plugins_config -from .init_plugins_data import init_plugins_data, plugins_manager -from .init_plugins_limit import ( - init_plugins_block_limit, - init_plugins_cd_limit, - init_plugins_count_limit, -) -from .init_plugins_resources import init_plugins_resources -from .init_plugins_settings import init_plugins_settings - -__zx_plugin_name__ = "初始化插件数据 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - """ - 初始化数据 - """ - config_file = DATA_PATH / "configs" / "plugins2config.yaml" - _flag = not config_file.exists() - init() - init_plugin_info() - init_plugins_settings() - init_plugins_cd_limit() - init_plugins_block_limit() - init_plugins_count_limit() - init_plugins_data() - init_plugins_config() - init_plugins_resources() - init_none_plugin_count_manager() - if _flag: - raise Exception("首次运行,已在configs目录下生成配置文件config.yaml,修改后重启即可...") - logger.info("初始化数据完成...") - - -@driver.on_bot_connect -async def _(bot: Bot): - # await init_group_manager() - await check_plugin_status(bot) diff --git a/basic_plugins/init_plugin_config/check_plugin_status.py b/basic_plugins/init_plugin_config/check_plugin_status.py deleted file mode 100755 index 5f61e0c2..00000000 --- a/basic_plugins/init_plugin_config/check_plugin_status.py +++ /dev/null @@ -1,18 +0,0 @@ -from utils.manager import plugins_manager -from nonebot.adapters.onebot.v11 import Bot - - -async def check_plugin_status(bot: Bot): - """ - 遍历查看插件加载情况 - """ - msg = "" - for plugin in plugins_manager.keys(): - data = plugins_manager.get(plugin) - if data.error: - msg += f'{plugin}:{data.plugin_name}\n' - if msg and bot.config.superusers: - msg = "以下插件加载失败..\n" + msg - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), message=msg.strip() - ) diff --git a/basic_plugins/init_plugin_config/init.py b/basic_plugins/init_plugin_config/init.py deleted file mode 100644 index d8640828..00000000 --- a/basic_plugins/init_plugin_config/init.py +++ /dev/null @@ -1,14 +0,0 @@ -from utils.manager import plugins2settings_manager - - -def init(): - if plugins2settings_manager.get("update_pic"): - plugins2settings_manager["update_picture"] = plugins2settings_manager["update_pic"] - plugins2settings_manager.delete("update_pic") - if plugins2settings_manager.get("white2black_img"): - plugins2settings_manager["white2black_image"] = plugins2settings_manager["white2black_img"] - plugins2settings_manager.delete("white2black_img") - if plugins2settings_manager.get("send_img"): - plugins2settings_manager["send_image"] = plugins2settings_manager["send_img"] - plugins2settings_manager.delete("send_img") - diff --git a/basic_plugins/init_plugin_config/init_none_plugin_count_manager.py b/basic_plugins/init_plugin_config/init_none_plugin_count_manager.py deleted file mode 100755 index 29d6b3a9..00000000 --- a/basic_plugins/init_plugin_config/init_none_plugin_count_manager.py +++ /dev/null @@ -1,62 +0,0 @@ -from utils.manager import ( - none_plugin_count_manager, - plugins2count_manager, - plugins2cd_manager, - plugins2settings_manager, - plugins2block_manager, - plugins_manager, -) -from services.log import logger -from utils.utils import get_matchers - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -def init_none_plugin_count_manager(): - """ - 清除已删除插件数据 - """ - modules = [x.plugin_name for x in get_matchers(True)] - plugins_manager_list = list(plugins_manager.keys()) - for module in plugins_manager_list: - try: - if module not in modules or none_plugin_count_manager.check(module): - try: - plugin_name = plugins_manager.get(module).plugin_name - except (AttributeError, KeyError): - plugin_name = "" - if none_plugin_count_manager.check(module): - try: - plugins2settings_manager.delete(module) - plugins2count_manager.delete(module) - plugins2cd_manager.delete(module) - plugins2block_manager.delete(module) - plugins_manager.delete(module) - plugins_manager.save() - # resources_manager.remove_resource(module) - none_plugin_count_manager.delete(module) - logger.info(f"{module}:{plugin_name} 插件疑似已删除,清除对应插件数据...") - except Exception as e: - logger.exception( - f"{module}:{plugin_name} 插件疑似已删除,清除对应插件数据失败...{type(e)}:{e}" - ) - else: - none_plugin_count_manager.add_count(module) - logger.info( - f"{module}:{plugin_name} 插件疑似已删除," - f"加载{none_plugin_count_manager._max_count}次失败后将清除对应插件数据," - f"当前次数:{none_plugin_count_manager.get(module)}" - ) - else: - none_plugin_count_manager.reset(module) - except Exception as e: - logger.error(f"清除插件数据错误 {type(e)}:{e}") - plugins2settings_manager.save() - plugins2count_manager.save() - plugins2cd_manager.save() - plugins2block_manager.save() - plugins_manager.save() - none_plugin_count_manager.save() diff --git a/basic_plugins/init_plugin_config/init_plugin_info.py b/basic_plugins/init_plugin_config/init_plugin_info.py deleted file mode 100644 index 744f50d7..00000000 --- a/basic_plugins/init_plugin_config/init_plugin_info.py +++ /dev/null @@ -1,149 +0,0 @@ -import random -from types import ModuleType -from typing import Any, Dict - -from configs.config import Config -from services import logger -from utils.manager import ( - plugin_data_manager, - plugins2block_manager, - plugins2cd_manager, - plugins2count_manager, - plugins2settings_manager, - plugins_manager, -) -from utils.manager.models import ( - Plugin, - PluginBlock, - PluginCd, - PluginCount, - PluginData, - PluginSetting, - PluginType, -) -from utils.utils import get_matchers - - -def get_attr(module: ModuleType, name: str, default: Any = None) -> Any: - """ - 说明: - 获取属性 - 参数: - :param module: module - :param name: name - :param default: default - """ - return getattr(module, name, None) or default - - -def init_plugin_info(): - - for matcher in [x for x in get_matchers(True)]: - try: - if (plugin := matcher.plugin) and matcher.plugin_name: - metadata = plugin.metadata - extra = metadata.extra if metadata else {} - if hasattr(plugin, "module"): - module = plugin.module - plugin_model = matcher.plugin_name - plugin_name = ( - metadata.name - if metadata and metadata.name - else get_attr(module, "__zx_plugin_name__", matcher.plugin_name) - ) - if not plugin_name: - logger.warning(f"配置文件 模块:{plugin_model} 获取 plugin_name 失败...") - continue - if "[Admin]" in plugin_name: - plugin_type = PluginType.ADMIN - plugin_name = plugin_name.replace("[Admin]", "").strip() - elif "[Hidden]" in plugin_name: - plugin_type = PluginType.HIDDEN - plugin_name = plugin_name.replace("[Hidden]", "").strip() - elif "[Superuser]" in plugin_name: - plugin_type = PluginType.SUPERUSER - plugin_name = plugin_name.replace("[Superuser]", "").strip() - else: - plugin_type = PluginType.NORMAL - plugin_usage = ( - metadata.usage - if metadata and metadata.usage - else get_attr(module, "__plugin_usage__") - ) - plugin_des = ( - metadata.description - if metadata and metadata.description - else get_attr(module, "__plugin_des__") - ) - menu_type = get_attr(module, "__plugin_type__") or ("normal",) - plugin_setting = get_attr(module, "__plugin_settings__") - if plugin_setting: - plugin_setting = PluginSetting(**plugin_setting) - plugin_setting.plugin_type = menu_type - plugin_superuser_usage = get_attr( - module, "__plugin_superuser_usage__" - ) - plugin_task = get_attr(module, "__plugin_task__") - plugin_version = extra.get("__plugin_version__") or get_attr( - module, "__plugin_version__" - ) - plugin_author = extra.get("__plugin_author__") or get_attr( - module, "__plugin_author__" - ) - plugin_cd = get_attr(module, "__plugin_cd_limit__") - if plugin_cd: - plugin_cd = PluginCd(**plugin_cd) - plugin_block = get_attr(module, "__plugin_block_limit__") - if plugin_block: - plugin_block = PluginBlock(**plugin_block) - plugin_count = get_attr(module, "__plugin_count_limit__") - if plugin_count: - plugin_count = PluginCount(**plugin_count) - plugin_resources = get_attr(module, "__plugin_resources__") - plugin_configs = get_attr(module, "__plugin_configs__") - if settings := plugins2settings_manager.get(plugin_model): - plugin_setting = settings - if plugin_cd_limit := plugins2cd_manager.get(plugin_model): - plugin_cd = plugin_cd_limit - if plugin_block_limit := plugins2block_manager.get(plugin_model): - plugin_block = plugin_block_limit - if plugin_count_limit := plugins2count_manager.get(plugin_model): - plugin_count = plugin_count_limit - if plugin_cfg := Config.get(plugin_model): - if plugin_configs: - for config_name in plugin_configs: - config: Dict[str, Any] = plugin_configs[config_name] # type: ignore - Config.add_plugin_config( - plugin_model, - config_name, - config.get("value"), - help_=config.get("help"), - default_value=config.get("default_value"), - type=config.get("type"), - ) - plugin_configs = plugin_cfg.configs - plugin_status = plugins_manager.get(plugin_model) - if not plugin_status: - plugin_status = Plugin(plugin_name=plugin_model) - plugin_status.author = plugin_author - plugin_status.version = plugin_version - plugin_data = PluginData( - model=plugin_model, - name=plugin_name.strip(), - plugin_type=plugin_type, - usage=plugin_usage, - superuser_usage=plugin_superuser_usage, - des=plugin_des, - task=plugin_task, - menu_type=menu_type, - plugin_setting=plugin_setting, - plugin_cd=plugin_cd, - plugin_block=plugin_block, - plugin_count=plugin_count, - plugin_resources=plugin_resources, - plugin_configs=plugin_configs, # type: ignore - plugin_status=plugin_status, - ) - plugin_data_manager.add_plugin_info(plugin_data) - except Exception as e: - logger.error(f"构造插件数据失败 {matcher.plugin_name}", e=e) diff --git a/basic_plugins/init_plugin_config/init_plugins_config.py b/basic_plugins/init_plugin_config/init_plugins_config.py deleted file mode 100755 index 203c3941..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_config.py +++ /dev/null @@ -1,173 +0,0 @@ -from pathlib import Path - -from ruamel import yaml -from ruamel.yaml import YAML, round_trip_dump, round_trip_load - -from configs.config import Config -from configs.path_config import DATA_PATH -from services.log import logger -from utils.manager import admin_manager, plugin_data_manager, plugins_manager -from utils.text_utils import prompt2cn -from utils.utils import get_matchers - -_yaml = YAML(typ="safe") - - -def init_plugins_config(): - """ - 初始化插件数据配置 - """ - plugins2config_file = DATA_PATH / "configs" / "plugins2config.yaml" - _data = Config.get_data() - # 优先使用 metadata 数据 - for matcher in get_matchers(True): - if matcher.plugin_name: - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - # 插件配置版本更新或为Version为None或不在存储配置内,当使用metadata时,必定更新 - version = plugin_data.plugin_status.version - config = _data.get(matcher.plugin_name) - plugin = plugins_manager.get(matcher.plugin_name) - if plugin_data.plugin_configs and ( - isinstance(version, str) - or ( - version is None - or ( - config - and config.configs.keys() - != plugin_data.plugin_configs.keys() - ) - or version > int(plugin.version or 0) - or matcher.plugin_name not in _data.keys() - ) - ): - plugin_configs = plugin_data.plugin_configs - for key in plugin_configs: - if isinstance(plugin_data.plugin_configs[key], dict): - Config.add_plugin_config( - matcher.plugin_name, - key, - plugin_configs[key].get("value"), - help_=plugin_configs[key].get("help"), - default_value=plugin_configs[key].get("default_value"), - _override=True, - type=plugin_configs[key].get("type"), - ) - else: - config = plugin_configs[key] - Config.add_plugin_config( - matcher.plugin_name, - key, - config.value, - name=config.name, - help_=config.help, - default_value=config.default_value, - _override=True, - type=config.type, - ) - elif plugin_configs := _data.get(matcher.plugin_name): - for key in plugin_configs.configs: - Config.add_plugin_config( - matcher.plugin_name, - key, - plugin_configs.configs[key].value, - help_=plugin_configs.configs[key].help, - default_value=plugin_configs.configs[key].default_value, - _override=True, - type=plugin_configs.configs[key].type, - ) - if not Config.is_empty(): - Config.save() - _data = round_trip_load(open(plugins2config_file, encoding="utf8")) - for plugin in _data.keys(): - try: - plugin_name = plugins_manager.get(plugin).plugin_name - except (AttributeError, TypeError): - plugin_name = plugin - _data[plugin].yaml_set_start_comment(plugin_name, indent=2) - # 初始化未设置的管理员权限等级 - for k, v in Config.get_admin_level_data(): - admin_manager.set_admin_level(k, v) - # 存完插件基本设置 - with open(plugins2config_file, "w", encoding="utf8") as wf: - round_trip_dump( - _data, wf, indent=2, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - _replace_config() - - -def _replace_config(): - """ - 说明: - 定时任务加载的配置读取替换 - """ - # 再开始读取用户配置 - user_config_file = Path() / "configs" / "config.yaml" - _data = {} - _tmp_data = {} - if user_config_file.exists(): - with open(user_config_file, "r", encoding="utf8") as f: - _data = _yaml.load(f) - # 数据替换 - for plugin in Config.keys(): - _tmp_data[plugin] = {} - for k in Config[plugin].configs.keys(): - try: - if _data.get(plugin) and k in _data[plugin].keys(): - Config.set_config(plugin, k, _data[plugin][k]) - if level2module := Config.get_level2module(plugin, k): - try: - admin_manager.set_admin_level( - level2module, _data[plugin][k] - ) - except KeyError: - logger.warning( - f"{level2module} 设置权限等级失败:{_data[plugin][k]}" - ) - _tmp_data[plugin][k] = Config.get_config(plugin, k) - except AttributeError as e: - raise AttributeError( - f"{e}\n" + prompt2cn("可能为config.yaml配置文件填写不规范", 46) - ) - Config.save() - temp_file = Path() / "configs" / "temp_config.yaml" - try: - with open(temp_file, "w", encoding="utf8") as wf: - yaml.dump(_tmp_data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True) - with open(temp_file, "r", encoding="utf8") as rf: - _data = round_trip_load(rf) - # 添加注释 - for plugin in _data.keys(): - rst = "" - plugin_name = None - try: - if config_group := Config.get(plugin): - for key in list(config_group.configs.keys()): - try: - if config := config_group.configs[key]: - if config.name: - plugin_name = config.name - except AttributeError: - pass - except (KeyError, AttributeError): - plugin_name = None - if not plugin_name: - try: - plugin_name = plugins_manager.get(plugin).plugin_name - except (AttributeError, TypeError): - plugin_name = plugin - plugin_name = ( - plugin_name.replace("[Hidden]", "") - .replace("[Superuser]", "") - .replace("[Admin]", "") - .strip() - ) - rst += plugin_name + "\n" - for k in _data[plugin].keys(): - rst += f"{k}: {Config[plugin].configs[k].help}" + "\n" - _data[plugin].yaml_set_start_comment(rst[:-1], indent=2) - with open(Path() / "configs" / "config.yaml", "w", encoding="utf8") as wf: - round_trip_dump(_data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True) - except Exception as e: - logger.error(f"生成简易配置注释错误 {type(e)}:{e}") - if temp_file.exists(): - temp_file.unlink() diff --git a/basic_plugins/init_plugin_config/init_plugins_data.py b/basic_plugins/init_plugin_config/init_plugins_data.py deleted file mode 100755 index 41ded12c..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_data.py +++ /dev/null @@ -1,57 +0,0 @@ - -from ruamel.yaml import YAML -from utils.manager import plugins_manager, plugin_data_manager -from utils.utils import get_matchers -from services.log import logger - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -_yaml = YAML(typ="safe") - - -def init_plugins_data(): - """ - 初始化插件数据信息 - """ - for matcher in get_matchers(True): - _plugin = matcher.plugin - if not _plugin: - continue - try: - _module = _plugin.module - except AttributeError: - if matcher.plugin_name not in plugins_manager.keys(): - plugins_manager.add_plugin_data( - matcher.plugin_name, matcher.plugin_name, error=True - ) - else: - plugins_manager[matcher.plugin_name].error = True - else: - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - try: - plugin_version = plugin_data.plugin_status.version - plugin_name = plugin_data.name - plugin_author = plugin_data.plugin_status.author - if matcher.plugin_name in plugins_manager.keys(): - plugins_manager[matcher.plugin_name].error = False - if matcher.plugin_name not in plugins_manager.keys(): - plugins_manager.add_plugin_data( - matcher.plugin_name, - plugin_name=plugin_name, - author=plugin_author, - version=plugin_version, - ) - elif isinstance(plugin_version, str) or plugins_manager[matcher.plugin_name].version is None or ( - plugin_version is not None - and plugin_version > float(plugins_manager[matcher.plugin_name].version) - ): - plugins_manager[matcher.plugin_name].plugin_name = plugin_name - plugins_manager[matcher.plugin_name].author = plugin_author - plugins_manager[matcher.plugin_name].version = plugin_version - except Exception as e: - logger.error(f"插件数据 {matcher.plugin_name} 加载发生错误 {type(e)}:{e}") - plugins_manager.save() diff --git a/basic_plugins/init_plugin_config/init_plugins_limit.py b/basic_plugins/init_plugin_config/init_plugins_limit.py deleted file mode 100755 index 2de6c010..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_limit.py +++ /dev/null @@ -1,64 +0,0 @@ -from utils.manager import ( - plugins2cd_manager, - plugins2block_manager, - plugins2count_manager, - plugin_data_manager, -) -from utils.utils import get_matchers -from configs.path_config import DATA_PATH - - -def init_plugins_cd_limit(): - """ - 加载 cd 限制 - """ - plugins2cd_file = DATA_PATH / "configs" / "plugins2cd.yaml" - plugins2cd_file.parent.mkdir(exist_ok=True, parents=True) - for matcher in get_matchers(True): - if not plugins2cd_manager.get_plugin_cd_data(matcher.plugin_name) and ( - plugin_data := plugin_data_manager.get(matcher.plugin_name) - ): - if plugin_data.plugin_cd: - plugins2cd_manager.add_cd_limit( - matcher.plugin_name, plugin_data.plugin_cd - ) - if not plugins2cd_manager.keys(): - plugins2cd_manager.add_cd_limit("这是一个示例") - plugins2cd_manager.save() - plugins2cd_manager.reload_cd_limit() - - -def init_plugins_block_limit(): - """ - 加载阻塞限制 - """ - for matcher in get_matchers(True): - if not plugins2block_manager.get_plugin_block_data(matcher.plugin_name) and ( - plugin_data := plugin_data_manager.get(matcher.plugin_name) - ): - if plugin_data.plugin_block: - plugins2block_manager.add_block_limit( - matcher.plugin_name, plugin_data.plugin_block - ) - if not plugins2block_manager.keys(): - plugins2block_manager.add_block_limit("这是一个示例") - plugins2block_manager.save() - plugins2block_manager.reload_block_limit() - - -def init_plugins_count_limit(): - """ - 加载次数限制 - """ - for matcher in get_matchers(True): - if not plugins2count_manager.get_plugin_count_data(matcher.plugin_name) and ( - plugin_data := plugin_data_manager.get(matcher.plugin_name) - ): - if plugin_data.plugin_count: - plugins2count_manager.add_count_limit( - matcher.plugin_name, plugin_data.plugin_count - ) - if not plugins2count_manager.keys(): - plugins2count_manager.add_count_limit("这是一个示例") - plugins2count_manager.save() - plugins2count_manager.reload_count_limit() diff --git a/basic_plugins/init_plugin_config/init_plugins_resources.py b/basic_plugins/init_plugin_config/init_plugins_resources.py deleted file mode 100755 index 936d5038..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_resources.py +++ /dev/null @@ -1,25 +0,0 @@ -from utils.manager import resources_manager, plugin_data_manager -from utils.utils import get_matchers -from services.log import logger -from pathlib import Path - - -def init_plugins_resources(): - """ - 资源文件路径的移动 - """ - for matcher in get_matchers(True): - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - try: - _module = matcher.plugin.module - except AttributeError: - logger.warning(f"插件 {matcher.plugin_name} 加载失败...,资源控制未加载...") - else: - if resources := plugin_data.plugin_resources: - path = Path(_module.__getattribute__("__file__")).parent - for resource in resources.keys(): - resources_manager.add_resource( - matcher.plugin_name, path / resource, resources[resource] - ) - resources_manager.save() - resources_manager.start_move() diff --git a/basic_plugins/init_plugin_config/init_plugins_settings.py b/basic_plugins/init_plugin_config/init_plugins_settings.py deleted file mode 100755 index cdf7b372..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_settings.py +++ /dev/null @@ -1,56 +0,0 @@ -import nonebot - -from services.log import logger -from utils.manager import admin_manager, plugin_data_manager, plugins2settings_manager -from utils.manager.models import PluginType -from utils.utils import get_matchers - - -def init_plugins_settings(): - """ - 初始化插件设置,从插件中获取 __zx_plugin_name__,__plugin_cmd__,__plugin_settings__ - """ - # for x in plugins2settings_manager.keys(): - # try: - # _plugin = nonebot.plugin.get_plugin(x) - # _module = _plugin.module - # _module.__getattribute__("__zx_plugin_name__") - # except (KeyError, AttributeError) as e: - # logger.warning(f"配置文件 模块:{x} 获取 plugin_name 失败...{e}") - for matcher in get_matchers(True): - try: - if ( - matcher.plugin_name - and matcher.plugin_name not in plugins2settings_manager.keys() - ): - if _plugin := matcher.plugin: - try: - _module = _plugin.module - except AttributeError: - logger.warning(f"插件 {matcher.plugin_name} 加载失败...,插件控制未加载.") - else: - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - if plugin_settings := plugin_data.plugin_setting: - if ( - name := _module.__getattribute__( - "__zx_plugin_name__" - ) - ) not in plugin_settings.cmd: - plugin_settings.cmd.append(name) - # 管理员命令 - if plugin_data.plugin_type == PluginType.ADMIN: - admin_manager.add_admin_plugin_settings( - matcher.plugin_name, - plugin_settings.cmd, - plugin_settings.level, - ) - else: - plugins2settings_manager.add_plugin_settings( - matcher.plugin_name, plugin_settings - ) - except Exception as e: - logger.error( - f"{matcher.plugin_name} 初始化 plugin_settings 发生错误 {type(e)}:{e}" - ) - plugins2settings_manager.save() - logger.info(f"已成功加载 {len(plugins2settings_manager.get_data())} 个非限制插件.") diff --git a/basic_plugins/invite_manager/__init__.py b/basic_plugins/invite_manager/__init__.py deleted file mode 100755 index 0317713b..00000000 --- a/basic_plugins/invite_manager/__init__.py +++ /dev/null @@ -1,170 +0,0 @@ -import asyncio -import re -import time -from datetime import datetime - -from nonebot import on_message, on_request -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - FriendRequestEvent, - GroupRequestEvent, - MessageEvent, -) - -from configs.config import NICKNAME, Config -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from services.log import logger -from utils.manager import requests_manager -from utils.utils import scheduler - -from .utils import time_manager - -__zx_plugin_name__ = "好友群聊处理请求 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "AUTO_ADD_FRIEND": { - "value": False, - "help": "是否自动同意好友添加", - "default_value": False, - "type": bool, - } -} - -friend_req = on_request(priority=5, block=True) -group_req = on_request(priority=5, block=True) -x = on_message(priority=999, block=False, rule=lambda: False) - - -@friend_req.handle() -async def _(bot: Bot, event: FriendRequestEvent): - if time_manager.add_user_request(event.user_id): - logger.debug(f"收录好友请求...", "好友请求", target=event.user_id) - user = await bot.get_stranger_info(user_id=event.user_id) - nickname = user["nickname"] - sex = user["sex"] - age = str(user["age"]) - comment = event.comment - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"*****一份好友申请*****\n" - f"昵称:{nickname}({event.user_id})\n" - f"自动同意:{'√' if Config.get_config('invite_manager', 'AUTO_ADD_FRIEND') else '×'}\n" - f"日期:{str(datetime.now()).split('.')[0]}\n" - f"备注:{event.comment}", - ) - if Config.get_config("invite_manager", "AUTO_ADD_FRIEND"): - logger.debug(f"已开启好友请求自动同意,成功通过该请求", "好友请求", target=event.user_id) - await bot.set_friend_add_request(flag=event.flag, approve=True) - await FriendUser.create( - user_id=str(user["user_id"]), user_name=user["nickname"] - ) - else: - requests_manager.add_request( - str(bot.self_id), - event.user_id, - "private", - event.flag, - nickname=nickname, - sex=sex, - age=age, - comment=comment, - ) - else: - logger.debug(f"好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id) - - -@group_req.handle() -async def _(bot: Bot, event: GroupRequestEvent): - # 邀请 - if event.sub_type == "invite": - if str(event.user_id) in bot.config.superusers: - try: - logger.debug( - f"超级用户自动同意加入群聊", "群聊请求", event.user_id, target=event.group_id - ) - await bot.set_group_add_request( - flag=event.flag, sub_type="invite", approve=True - ) - group_info = await bot.get_group_info(group_id=event.group_id) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) - except ActionFailed as e: - logger.error( - "超级用户自动同意加入群聊发生错误", - "群聊请求", - event.user_id, - target=event.group_id, - e=e, - ) - else: - if time_manager.add_group_request(event.user_id, event.group_id): - logger.debug( - f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求", "群聊请求" - ) - user = await bot.get_stranger_info(user_id=event.user_id) - sex = user["sex"] - age = str(user["age"]) - nickname = await FriendUser.get_user_name(event.user_id) - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"*****一份入群申请*****\n" - f"申请人:{nickname}({event.user_id})\n" - f"群聊:{event.group_id}\n" - f"邀请日期:{datetime.now().replace(microsecond=0)}", - ) - await bot.send_private_msg( - user_id=event.user_id, - message=f"想要邀请我偷偷入群嘛~已经提醒{NICKNAME}的管理员大人了\n" - "请确保已经群主或群管理沟通过!\n" - "等待管理员处理吧!", - ) - requests_manager.add_request( - str(bot.self_id), - event.user_id, - "group", - event.flag, - invite_group=event.group_id, - nickname=nickname, - sex=sex, - age=age, - ) - else: - logger.debug( - f"群聊请求五分钟内重复, 已忽略", - "群聊请求", - target=f"{event.user_id}:{event.group_id}", - ) - - -@x.handle() -async def _(event: MessageEvent): - await asyncio.sleep(0.1) - r = re.search(r'groupcode="(.*?)"', str(event.get_message())) - if r: - group_id = int(r.group(1)) - else: - return - r = re.search(r'groupname="(.*?)"', str(event.get_message())) - if r: - group_name = r.group(1) - else: - group_name = "None" - requests_manager.set_group_name(group_name, group_id) - - -@scheduler.scheduled_job( - "interval", - minutes=5, -) -async def _(): - time_manager.clear() diff --git a/basic_plugins/invite_manager/utils.py b/basic_plugins/invite_manager/utils.py deleted file mode 100644 index aee7c6f1..00000000 --- a/basic_plugins/invite_manager/utils.py +++ /dev/null @@ -1,87 +0,0 @@ -import time -from dataclasses import dataclass -from typing import Dict - - -@dataclass -class PrivateRequest: - - """ - 好友请求 - """ - - user_id: int - time: float = time.time() - - -@dataclass -class GroupRequest: - - """ - 群聊请求 - """ - - user_id: int - group_id: int - time: float = time.time() - - -class RequestTimeManage: - - """ - 过滤五分钟以内的重复请求 - """ - - def __init__(self): - - self._group: Dict[str, GroupRequest] = {} - self._user: Dict[int, PrivateRequest] = {} - - def add_user_request(self, user_id: int) -> bool: - """ - 添加请求时间 - - Args: - user_id (int): 用户id - - Returns: - bool: 是否满足时间 - """ - if user := self._user.get(user_id): - if time.time() - user.time < 60 * 5: - return False - self._user[user_id] = PrivateRequest(user_id) - return True - - def add_group_request(self, user_id: int, group_id: int) -> bool: - """ - 添加请求时间 - - Args: - user_id (int): 用户id - group_id (int): 邀请群聊 - - Returns: - bool: 是否满足时间 - """ - key = f"{user_id}:{group_id}" - if group := self._group.get(key): - if time.time() - group.time < 60 * 5: - return False - self._group[key] = GroupRequest(user_id=user_id, group_id=group_id) - return True - - def clear(self): - """ - 清理过期五分钟请求 - """ - now = time.time() - for user_id in self._user: - if now - self._user[user_id].time < 60 * 5: - del self._user[user_id] - for key in self._group: - if now - self._group[key].time < 60 * 5: - del self._group[key] - - -time_manager = RequestTimeManage() diff --git a/basic_plugins/nickname.py b/basic_plugins/nickname.py deleted file mode 100755 index d9e104cc..00000000 --- a/basic_plugins/nickname.py +++ /dev/null @@ -1,207 +0,0 @@ -import random -from typing import Any, List, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.internal.matcher import Matcher -from nonebot.internal.params import Depends -from nonebot.params import CommandArg, RegexGroup -from nonebot.rule import to_me - -from configs.config import NICKNAME -from models.ban_user import BanUser -from models.friend_user import FriendUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.depends import GetConfig - -__zx_plugin_name__ = "昵称系统" -__plugin_usage__ = f""" -usage: - 个人昵称,将替换真寻称呼你的名称,群聊 与 私聊 昵称相互独立,全局昵称设置将更改您目前所有群聊中及私聊的昵称 - 指令: - 以后叫我 [昵称]: 设置当前群聊/私聊的昵称 - 全局昵称设置 [昵称]: 设置当前所有群聊和私聊的昵称 - {NICKNAME}我是谁 -""".strip() -__plugin_des__ = "区区昵称,才不想叫呢!" -__plugin_cmd__ = ["以后叫我 [昵称]", f"{NICKNAME}我是谁", "全局昵称设置 [昵称]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["昵称"], -} -__plugin_configs__ = { - "BLACK_WORD": { - "value": ["爸", "爹", "爷", "父"], - "help": "昵称所屏蔽的关键词,已设置的昵称会被替换为 *,未设置的昵称会在设置时提示", - "default_value": None, - "type": List[str], - } -} - -nickname = on_regex( - "(?:以后)?(?:叫我|请叫我|称呼我)(.*)", - rule=to_me(), - priority=5, - block=True, -) - -my_nickname = on_command( - "我叫什么", aliases={"我是谁", "我的名字"}, rule=to_me(), priority=5, block=True -) - -global_nickname = on_regex("设置全局昵称(.*)", rule=to_me(), priority=5, block=True) - - -cancel_nickname = on_command("取消昵称", rule=to_me(), priority=5, block=True) - - -def CheckNickname(): - """ - 说明: - 检查名称是否合法 - """ - - async def dependency( - bot: Bot, - matcher: Matcher, - event: MessageEvent, - reg_group: Tuple[Any, ...] = RegexGroup(), - black_word: Any = GetConfig(config="BLACK_WORD"), - ): - (msg,) = reg_group - logger.debug(f"昵称检查: {msg}", "昵称设置", event.user_id) - if not msg: - await matcher.finish("叫你空白?叫你虚空?叫你无名??", at_sender=True) - if str(event.user_id) in bot.config.superusers: - logger.debug(f"超级用户设置昵称, 跳过合法检测: {msg}", "昵称设置", event.user_id) - return - if len(msg) > 20: - await nickname.finish("昵称可不能超过20个字!", at_sender=True) - if msg in bot.config.nickname: - await nickname.finish("笨蛋!休想占用我的名字!#", at_sender=True) - if black_word: - for x in msg: - if x in black_word: - logger.debug("昵称设置禁止字符: [{x}]", "昵称设置", event.user_id) - await nickname.finish(f"字符 [{x}] 为禁止字符!", at_sender=True) - for word in black_word: - if word in msg: - logger.debug("昵称设置禁止字符: [{word}]", "昵称设置", event.user_id) - await nickname.finish(f"字符 [{word}] 为禁止字符!", at_sender=True) - - return Depends(dependency) - - -@global_nickname.handle(parameterless=[CheckNickname()]) -async def _( - event: MessageEvent, - reg_group: Tuple[Any, ...] = RegexGroup(), -): - (msg,) = reg_group - await FriendUser.set_user_nickname(event.user_id, msg) - await GroupInfoUser.filter(user_id=str(event.user_id)).update(nickname=msg) - logger.info(f"设置全局昵称成功: {msg}", "设置全局昵称", event.user_id) - await global_nickname.send(f"设置全局昵称成功!亲爱的{msg}") - - -@nickname.handle(parameterless=[CheckNickname()]) -async def _( - event: MessageEvent, - reg_group: Tuple[Any, ...] = RegexGroup(), -): - (msg,) = reg_group - if isinstance(event, GroupMessageEvent): - await GroupInfoUser.set_user_nickname(event.user_id, event.group_id, msg) - if len(msg) < 5: - if random.random() < 0.3: - msg = "~".join(msg) - await nickname.send( - random.choice( - [ - f"好啦好啦,我知道啦,{msg},以后就这么叫你吧", - f"嗯嗯,{NICKNAME}记住你的昵称了哦,{msg}", - f"好突然,突然要叫你昵称什么的...{msg}..", - f"{NICKNAME}会好好记住{msg}的,放心吧", - f"好..好.,那窝以后就叫你{msg}了.", - ] - ) - ) - logger.info(f"设置群昵称成功: {msg}", "昵称设置", event.user_id, event.group_id) - else: - await FriendUser.set_user_nickname(event.user_id, msg) - await nickname.send( - random.choice( - [ - f"好啦好啦,我知道啦,{msg},以后就这么叫你吧", - f"嗯嗯,{NICKNAME}记住你的昵称了哦,{msg}", - f"好突然,突然要叫你昵称什么的...{msg}..", - f"{NICKNAME}会好好记住{msg}的,放心吧", - f"好..好.,那窝以后就叫你{msg}了.", - ] - ) - ) - logger.info(f"设置私聊昵称成功: {msg}", "昵称设置", event.user_id) - - -@my_nickname.handle() -async def _(event: MessageEvent): - nickname_ = None - card = None - if isinstance(event, GroupMessageEvent): - nickname_ = await GroupInfoUser.get_user_nickname(event.user_id, event.group_id) - card = event.sender.card or event.sender.nickname - else: - nickname_ = await FriendUser.get_user_nickname(event.user_id) - card = event.sender.nickname - if nickname_: - await my_nickname.send( - random.choice( - [ - f"我肯定记得你啊,你是{nickname_}啊", - f"我不会忘记你的,你也不要忘记我!{nickname_}", - f"哼哼,{NICKNAME}记忆力可是很好的,{nickname_}", - f"嗯?你是失忆了嘛...{nickname_}..", - f"不要小看{NICKNAME}的记忆力啊!笨蛋{nickname_}!QAQ", - f"哎?{nickname_}..怎么了吗..突然这样问..", - ] - ) - ) - else: - await my_nickname.send( - random.choice( - ["没..没有昵称嘛,{}", "啊,你是{}啊,我想叫你的昵称!", "是{}啊,有什么事吗?", "你是{}?"] - ).format(card) - ) - - -@cancel_nickname.handle() -async def _(event: MessageEvent): - nickname_ = None - if isinstance(event, GroupMessageEvent): - nickname_ = await GroupInfoUser.get_user_nickname(event.user_id, event.group_id) - else: - nickname_ = await FriendUser.get_user_nickname(event.user_id) - if nickname_: - await cancel_nickname.send( - random.choice( - [ - f"呜..{NICKNAME}睡一觉就会忘记的..和梦一样..{nickname_}", - f"窝知道了..{nickname_}..", - f"是{NICKNAME}哪里做的不好嘛..好吧..晚安{nickname_}", - f"呃,{nickname_},下次我绝对绝对绝对不会再忘记你!", - f"可..可恶!{nickname_}!太可恶了!呜", - ] - ) - ) - if isinstance(event, GroupMessageEvent): - await GroupInfoUser.set_user_nickname(event.user_id, event.group_id, "") - else: - await FriendUser.set_user_nickname(event.user_id, "") - await BanUser.ban(event.user_id, 9, 60) - else: - await cancel_nickname.send("你在做梦吗?你没有昵称啊", at_sender=True) diff --git a/basic_plugins/plugin_shop/__init__.py b/basic_plugins/plugin_shop/__init__.py deleted file mode 100644 index 0ef17cb9..00000000 --- a/basic_plugins/plugin_shop/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.message_builder import image - -from .data_source import ( - download_json, - install_plugin, - show_plugin_repo, - uninstall_plugin, -) - -__zx_plugin_name__ = "插件商店 [Superuser]" -__plugin_usage__ = """ -usage: - 下载安装插件 - 指令: - 查看插件仓库 - 更新插件仓库 - 安装插件 [name/id] (重新安装等同于更新) - 卸载插件 [name/id] -""".strip() -__plugin_des__ = "从真寻适配仓库中下载插件" -__plugin_cmd__ = ["查看插件仓库", "更新插件仓库", "安装插件 [name/id]", "卸载插件 [name/id]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -show_repo = on_regex("^查看插件仓库$", priority=1, block=True, permission=SUPERUSER) - -update_repo = on_regex("^更新插件仓库$", priority=1, block=True, permission=SUPERUSER) - -install_plugin_matcher = on_command( - "安装插件", priority=1, block=True, permission=SUPERUSER -) - -uninstall_plugin_matcher = on_command( - "卸载插件", priority=1, block=True, permission=SUPERUSER -) - - -@install_plugin_matcher.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - name = arg.extract_plain_text().strip() - msg = await install_plugin(name) - await install_plugin_matcher.send(msg) - logger.info(f"安装插件: {name}", "安装插件", event.user_id) - - -@uninstall_plugin_matcher.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - name = arg.extract_plain_text().strip() - msg = await uninstall_plugin(name) - await install_plugin_matcher.send(msg) - logger.info(f"卸载插件: {name}", "卸载插件", event.user_id) - - -@update_repo.handle() -async def _(bot: Bot, event: MessageEvent): - code = await download_json() - if code == 200: - await update_repo.finish("更新插件仓库信息成功!") - await update_repo.send("更新插件仓库信息失败!") - logger.info("更新插件仓库信息", "更新插件仓库信息", event.user_id) - - -@show_repo.handle() -async def _(bot: Bot, event: MessageEvent): - msg = await show_plugin_repo() - if isinstance(msg, int): - await show_repo.finish("文件下载失败或解压失败..") - await show_repo.send(image(msg)) - logger.info("查看插件仓库", "查看插件仓库", event.user_id) diff --git a/basic_plugins/plugin_shop/data_source.py b/basic_plugins/plugin_shop/data_source.py deleted file mode 100644 index 815527df..00000000 --- a/basic_plugins/plugin_shop/data_source.py +++ /dev/null @@ -1,200 +0,0 @@ -import os -import shutil -import zipfile -from pathlib import Path -from typing import Tuple, Union - -import ujson as json - -from configs.path_config import DATA_PATH, TEMP_PATH -from services import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage, text2image -from utils.manager import plugins_manager -from utils.utils import is_number - -path = DATA_PATH / "plugin_shop" -if not path.exists(): - path.mkdir(parents=True, exist_ok=True) - -repo_json_url = "https://github.com/zhenxun-org/nonebot_plugins_zhenxun_bot/archive/refs/heads/index.zip" -repo_zip_path = TEMP_PATH / "plugin_repo_json.zip" -plugin_json = path / "zhenxun_plugins.json" - -extensive_plugin_path = Path() / "extensive_plugin" - -path = DATA_PATH / "plugin_shop" -if not path.exists(): - path.mkdir(parents=True, exist_ok=True) -if not extensive_plugin_path.exists(): - extensive_plugin_path.mkdir(parents=True, exist_ok=True) - -data = {} - - -async def install_plugin(name: str) -> str: - """ - 安装插件 - :param name: 插件名或下标 - """ - try: - if is_number(name): - name, code = await get_plugin_name(int(name)) - if code != 200: - return name - plugin_url = data[name]["download_url"] - url = (await AsyncHttpx.get(plugin_url)).headers.get("Location") - zip_file = TEMP_PATH / f"{name}.zip" - if zip_file.exists(): - zip_file.unlink() - if await AsyncHttpx.download_file(url, zip_file): - logger.debug("开始解压插件压缩包...", "安装插件", target=name) - # 解压 - zf = zipfile.ZipFile(zip_file, "r") - extract_path = TEMP_PATH / f"{name}" - if extract_path.exists(): - shutil.rmtree(extract_path.absolute(), ignore_errors=True) - extract_path.mkdir(exist_ok=True, parents=True) - for file in zf.namelist(): - zf.extract(file, extract_path) - zf.close() - logger.debug("解压插件压缩包完成...", "安装插件", target=name) - logger.debug("开始移动插件文件夹...", "安装插件", target=name) - if (extensive_plugin_path / f"{name}").exists(): - logger.debug( - "extensive_plugin目录下文件夹已存在,删除该目录插件文件夹...", "安装插件", target=name - ) - shutil.rmtree( - (extensive_plugin_path / f"{name}").absolute(), ignore_errors=True - ) - extract_path.rename(extensive_plugin_path / f"{name}") - prompt = "" - if "pyproject.toml" in os.listdir(extensive_plugin_path / f"{name}"): - prompt = "检测到该插件含有额外依赖,当前安装无法保证依赖完全安装成功。" - os.system( - f"poetry run pip install -r {(extensive_plugin_path / f'{name}' / 'pyproject.toml').absolute()}" - ) - elif "requirements.txt" in os.listdir(extensive_plugin_path / f"{name}"): - prompt = "检测到该插件含有额外依赖,当前安装无法保证依赖完全安装成功。" - os.system( - f"poetry run pip install -r {(extensive_plugin_path / f'{name}' / 'requirements.txt').absolute()}" - ) - with open(extensive_plugin_path / f"{name}" / "plugin_info.json", "w") as f: - json.dump(data[name], f, ensure_ascii=False, indent=4) - logger.debug("移动插件文件夹完成...", "安装插件", target=name) - logger.info(f"成功安装插件 {name} 成功!\n{prompt}", "安装插件", target=name) - return f"成功安装插件 {name},请重启真寻!" - except Exception as e: - logger.error(f"安装插失败", "安装插件", target=name, e=e) - return f"安装插件 {name} 失败 {type(e)}:{e}" - - -async def uninstall_plugin(name: str) -> str: - """ - 删除插件 - :param name: 插件名或下标 - """ - try: - if is_number(name): - name, code = await get_plugin_name(int(name)) - if code != 200: - return name - if name not in os.listdir(extensive_plugin_path): - return f"未安装 {name} 插件!" - shutil.rmtree((extensive_plugin_path / name).absolute(), ignore_errors=True) - logger.info(f"插件 {name} 删除成功!") - return f"插件 {name} 删除成功!" - except Exception as e: - logger.error(f"删除插件失败", target=name, e=e) - return f"删除插件 {name} 失败 {type(e)}:{e}" - - -async def show_plugin_repo() -> Union[int, str]: - """ - 获取插件仓库数据并格式化 - """ - if not plugin_json.exists(): - code = await download_json() - if code != 200: - return code - plugin_info = json.load(open(plugin_json, "r", encoding="utf8")) - plugins_data = plugins_manager.get_data() - load_plugin_list = plugins_data.keys() - image_list = [] - w, h = 0, 0 - line_height = 10 - for i, key in enumerate(plugin_info.keys()): - data[key] = { - "名称": plugin_info[key]["plugin_name"], - "模块": key, - "作者": plugin_info[key]["author"], - "版本": plugin_info[key]["version"], - "简介": plugin_info[key]["introduction"], - "download_url": plugin_info[key]["download_url"], - "github_url": plugin_info[key]["github_url"], - } - status = "" - version = "" - if key in load_plugin_list: - status = "[已安装]" - version = f"[{plugins_data[key].version}]" - s = ( - f'id:{i+1}\n名称:{plugin_info[key]["plugin_name"]}' - f" \t\t{status}\n" - f"模块:{key}\n" - f'作者:{plugin_info[key]["author"]}\n' - f'版本:{plugin_info[key]["version"]} \t\t{version}\n' - f'简介:{plugin_info[key]["introduction"]}\n' - f"-------------------" - ) - img = await text2image(s, font_size=20, color="#f9f6f2") - w = w if w > img.w else img.w - h += img.h + line_height - image_list.append(img) - A = BuildImage(w + 50, h + 50, color="#f9f6f2") - cur_h = 25 - for img in image_list: - await A.apaste(img, (25, cur_h)) - cur_h += img.h + line_height - return A.pic2bs4() - - -async def download_json() -> int: - """ - 下载插件库json文件 - """ - try: - url = (await AsyncHttpx.get(repo_json_url)).headers.get("Location") - if repo_zip_path.exists(): - repo_zip_path.unlink() - if await AsyncHttpx.download_file(url, repo_zip_path): - zf = zipfile.ZipFile(repo_zip_path, "r") - extract_path = path / "temp" - for file in zf.namelist(): - zf.extract(file, extract_path) - zf.close() - if plugin_json.exists(): - plugin_json.unlink() - ( - extract_path - / "nonebot_plugins_zhenxun_bot-index" - / "zhenxun_plugins.json" - ).rename(plugin_json) - shutil.rmtree(extract_path.absolute(), ignore_errors=True) - return 200 - except Exception as e: - logger.error(f"下载插件库压缩包失败或解压失败", e=e) - return 999 - - -async def get_plugin_name(index: int) -> Tuple[str, int]: - """ - 通过下标获取插件名 - :param name: 下标 - """ - if not data: - await show_plugin_repo() - if index < 1 or index > len(data.keys()): - return "下标超过上下限!", 999 - name = list(data.keys())[index - 1] - return name, 200 diff --git a/basic_plugins/scripts.py b/basic_plugins/scripts.py deleted file mode 100755 index 1a6bc456..00000000 --- a/basic_plugins/scripts.py +++ /dev/null @@ -1,108 +0,0 @@ -import random -from asyncio.exceptions import TimeoutError - -import nonebot -from nonebot.adapters.onebot.v11 import Bot -from nonebot.drivers import Driver - -from configs.path_config import TEXT_PATH -from models.bag_user import BagUser -from models.group_info import GroupInfo -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.utils import GDict, scheduler - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def update_city(): - """ - 部分插件需要中国省份城市 - 这里直接更新,避免插件内代码重复 - """ - china_city = TEXT_PATH / "china_city.json" - data = {} - if not china_city.exists(): - try: - logger.debug("开始更新城市列表...") - res = await AsyncHttpx.get( - "http://www.weather.com.cn/data/city3jdata/china.html", timeout=5 - ) - res.encoding = "utf8" - provinces_data = json.loads(res.text) - for province in provinces_data.keys(): - data[provinces_data[province]] = [] - res = await AsyncHttpx.get( - f"http://www.weather.com.cn/data/city3jdata/provshi/{province}.html", - timeout=5, - ) - res.encoding = "utf8" - city_data = json.loads(res.text) - for city in city_data.keys(): - data[provinces_data[province]].append(city_data[city]) - with open(china_city, "w", encoding="utf8") as f: - json.dump(data, f, indent=4, ensure_ascii=False) - logger.info("自动更新城市列表完成.....") - except TimeoutError as e: - logger.warning("自动更新城市列表超时...", e=e) - except ValueError as e: - logger.warning("自动城市列表失败.....", e=e) - except Exception as e: - logger.error(f"自动城市列表未知错误", e=e) - - -@driver.on_bot_connect -async def _(bot: Bot): - """ - 版本某些需要的变换 - """ - # 清空不存在的群聊信息,并将已所有已存在的群聊group_flag设置为1(认证所有已存在的群) - if not await GroupInfo.get_or_none(group_id=114514): - # 标识符,该功能只需执行一次 - await GroupInfo.create( - group_id=114514, - group_name="114514", - max_member_count=114514, - member_count=114514, - group_flag=1, - ) - group_list = await bot.get_group_list() - group_list = [g["group_id"] for g in group_list] - _gl = [x.group_id for x in await GroupInfo.all()] - if 114514 in _gl: - _gl.remove(114514) - for group_id in _gl: - if group_id in group_list: - if group := await GroupInfo.get_or_none(group_id=group_id): - group.group_flag = 1 - await group.save(update_fields=["group_flag"]) - else: - group_info = await bot.get_group_info(group_id=group_id) - await GroupInfo.create( - group_id=group_info["group_id"], - group_name=group_info["group_name"], - max_member_count=group_info["max_member_count"], - member_count=group_info["member_count"], - group_flag=1, - ) - logger.info(f"已添加群认证...", group_id=group_id) - else: - await GroupInfo.filter(group_id=group_id).delete() - logger.info(f"移除不存在的群聊信息", group_id=group_id) - - -# 自动更新城市列表 -@scheduler.scheduled_job( - "cron", - hour=6, - minute=1, -) -async def _(): - await update_city() diff --git a/basic_plugins/shop/__init__.py b/basic_plugins/shop/__init__.py deleted file mode 100644 index f6a49be9..00000000 --- a/basic_plugins/shop/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from pathlib import Path - -import nonebot -from nonebot.drivers import Driver - -from configs.config import Config -from utils.decorator.shop import shop_register - -driver: Driver = nonebot.get_driver() - - -Config.add_plugin_config( - "shop", - "IMPORT_DEFAULT_SHOP_GOODS", - True, - help_="导入商店自带的三个商品", - default_value=True, - type=bool, -) - - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) - - -@driver.on_bot_connect -async def _(): - await shop_register.load_register() diff --git a/basic_plugins/shop/buy.py b/basic_plugins/shop/buy.py deleted file mode 100644 index 61701e00..00000000 --- a/basic_plugins/shop/buy.py +++ /dev/null @@ -1,109 +0,0 @@ -import time - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg - -from models.bag_user import BagUser -from models.goods_info import GoodsInfo -from models.user_shop_gold_log import UserShopGoldLog -from services.log import logger -from utils.utils import is_number - -__zx_plugin_name__ = "商店 - 购买道具" -__plugin_usage__ = """ -usage: - 购买道具 - 指令: - 购买 [序号或名称] ?[数量=1] - 示例:购买 好感双倍加持卡Ⅰ - 示例:购买 1 4 -""".strip() -__plugin_des__ = "商店 - 购买道具" -__plugin_cmd__ = ["购买 [序号或名称] ?[数量=1]"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "购买道具"], -} -__plugin_cd_limit__ = {"cd": 3} - - -buy = on_command("购买", aliases={"购买道具"}, priority=5, block=True, permission=GROUP) - - -@buy.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - goods = None - if arg.extract_plain_text().strip() in ["神秘药水"]: - await buy.finish("你们看看就好啦,这是不可能卖给你们的~", at_sender=True) - goods_list = [ - x - for x in await GoodsInfo.get_all_goods() - if x.goods_limit_time > time.time() or x.goods_limit_time == 0 - ] - goods_name_list = [x.goods_name for x in goods_list] - msg = arg.extract_plain_text().strip().split() - num = 1 - if len(msg) > 1: - if is_number(msg[1]) and int(msg[1]) > 0: - num = int(msg[1]) - else: - await buy.finish("购买的数量要是数字且大于0!", at_sender=True) - if is_number(msg[0]): - msg = int(msg[0]) - if msg > len(goods_name_list) or msg < 1: - await buy.finish("请输入正确的商品id!", at_sender=True) - goods = goods_list[msg - 1] - else: - if msg[0] in goods_name_list: - for i in range(len(goods_name_list)): - if msg[0] == goods_name_list[i]: - goods = goods_list[i] - break - else: - await buy.finish("请输入正确的商品名称!") - else: - await buy.finish("请输入正确的商品名称!", at_sender=True) - if ( - await BagUser.get_gold(event.user_id, event.group_id) - ) < goods.goods_price * num * goods.goods_discount: - await buy.finish("您的金币好像不太够哦", at_sender=True) - flag, n = await GoodsInfo.check_user_daily_purchase( - goods, event.user_id, event.group_id, num - ) - if flag: - await buy.finish(f"该次购买将超过每日次数限制,目前该道具还可以购买{n}次哦", at_sender=True) - spend_gold = int(goods.goods_discount * goods.goods_price * num) - await BagUser.spend_gold(event.user_id, event.group_id, spend_gold) - await BagUser.add_property(event.user_id, event.group_id, goods.goods_name, num) - await GoodsInfo.add_user_daily_purchase(goods, event.user_id, event.group_id, num) - await buy.send( - f"花费 {goods.goods_price * num * goods.goods_discount} 金币购买 {goods.goods_name} ×{num} 成功!", - at_sender=True, - ) - logger.info( - f"花费 {goods.goods_price*num} 金币购买 {goods.goods_name} ×{num} 成功!", - "购买道具", - event.user_id, - event.group_id, - ) - await UserShopGoldLog.create( - user_id=str(event.user_id), - group_id=str(event.group_id), - type=0, - name=goods.goods_name, - num=num, - spend_gold=goods.goods_price * num * goods.goods_discount, - ) - # else: - # await buy.send(f"{goods.goods_name} 购买失败!", at_sender=True) - # logger.info( - # f"USER {event.user_id} GROUP {event.group_id} " - # f"花费 {goods.goods_price * num * goods.goods_discount} 金币购买 {goods.goods_name} ×{num} 失败!" - # ) diff --git a/basic_plugins/shop/gold.py b/basic_plugins/shop/gold.py deleted file mode 100644 index dcc3def9..00000000 --- a/basic_plugins/shop/gold.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import ActionFailed, GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg - -from models.bag_user import BagUser -from utils.data_utils import init_rank -from utils.image_utils import text2image -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "商店 - 我的金币" -__plugin_usage__ = """ -usage: - 我的金币 - 指令: - 我的金币 -""".strip() -__plugin_des__ = "商店 - 我的金币" -__plugin_cmd__ = ["我的金币"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "我的金币"], -} - - -my_gold = on_command("我的金币", priority=5, block=True, permission=GROUP) - -gold_rank = on_command("金币排行", priority=5, block=True, permission=GROUP) - - -@my_gold.handle() -async def _(event: GroupMessageEvent): - msg = await BagUser.get_user_total_gold(event.user_id, event.group_id) - try: - await my_gold.send(msg) - except ActionFailed: - await my_gold.send( - image(b64=(await text2image(msg, color="#f9f6f2", padding=10)).pic2bs4()) - ) - - -@gold_rank.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - num = arg.extract_plain_text().strip() - if is_number(num) and 51 > int(num) > 10: - num = int(num) - else: - num = 10 - all_users = await BagUser.filter(group_id=event.group_id) - all_user_id = [user.user_id for user in all_users] - all_user_data = [user.gold for user in all_users] - rank_image = await init_rank( - "金币排行", all_user_id, all_user_data, event.group_id, num - ) - if rank_image: - await gold_rank.finish(image(b64=rank_image.pic2bs4())) diff --git a/basic_plugins/shop/my_props/__init__.py b/basic_plugins/shop/my_props/__init__.py deleted file mode 100644 index 07faf00c..00000000 --- a/basic_plugins/shop/my_props/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from models.bag_user import BagUser -from services.log import logger -from utils.message_builder import image - -from ._data_source import create_bag_image - -__zx_plugin_name__ = "商店 - 我的道具" -__plugin_usage__ = """ -usage: - 我的道具 - 指令: - 我的道具 -""".strip() -__plugin_des__ = "商店 - 我的道具" -__plugin_cmd__ = ["我的道具"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "我的道具"], -} - - -my_props = on_command("我的道具", priority=5, block=True, permission=GROUP) - - -@my_props.handle() -async def _(event: GroupMessageEvent): - props = await BagUser.get_property(str(event.user_id), str(event.group_id)) - if props: - await my_props.send(image(b64=await create_bag_image(props))) - logger.info(f"查看我的道具", "我的道具", event.user_id, event.group_id) - else: - await my_props.finish("您的背包里没有任何的道具噢~", at_sender=True) diff --git a/basic_plugins/shop/my_props/_data_source.py b/basic_plugins/shop/my_props/_data_source.py deleted file mode 100644 index dec64650..00000000 --- a/basic_plugins/shop/my_props/_data_source.py +++ /dev/null @@ -1,121 +0,0 @@ -from typing import Dict, List, Optional - -from models.bag_user import BagUser -from models.goods_info import GoodsInfo -from utils.image_utils import BuildImage -from configs.path_config import IMAGE_PATH - - -icon_path = IMAGE_PATH / "shop_icon" - - -async def create_bag_image(props: Dict[str, int]): - """ - 说明: - 创建背包道具图片 - 参数: - :param props: 道具仓库字典 - """ - goods_list = await GoodsInfo.get_all_goods() - active_props = await _init_prop(props, [x for x in goods_list if not x.is_passive]) - passive_props = await _init_prop(props, [x for x in goods_list if x.is_passive]) - active_w = 0 - active_h = 0 - passive_w = 0 - passive_h = 0 - if active_props: - img = BuildImage( - active_props.w, - active_props.h + 70, - font="CJGaoDeGuo.otf", - font_size=30, - color="#f9f6f2", - ) - await img.apaste(active_props, (0, 70)) - await img.atext((0, 30), "主动道具") - active_props = img - active_w = img.w - active_h = img.h - if passive_props: - img = BuildImage( - passive_props.w, - passive_props.h + 70, - font="CJGaoDeGuo.otf", - font_size=30, - color="#f9f6f2", - ) - await img.apaste(passive_props, (0, 70)) - await img.atext((0, 30), "被动道具") - passive_props = img - passive_w = img.w - passive_h = img.h - A = BuildImage( - active_w + passive_w + 100, - max(active_h, passive_h) + 60, - font="CJGaoDeGuo.otf", - font_size=30, - color="#f9f6f2", - ) - curr_w = 50 - if active_props: - await A.apaste(active_props, (curr_w, 0)) - curr_w += active_props.w + 10 - if passive_props: - await A.apaste(passive_props, (curr_w, 0)) - if active_props and passive_props: - await A.aline( - (active_props.w + 45, 70, active_props.w + 45, A.h - 20), fill=(0, 0, 0) - ) - return A.pic2bs4() - - -async def _init_prop( - props: Dict[str, int], _props: List[GoodsInfo] -) -> Optional[BuildImage]: - """ - 说明: - 构造道具列表图片 - 参数: - :param props: 道具仓库字典 - :param _props: 道具列表 - """ - active_name = [x.goods_name for x in _props] - name_list = [x for x in props.keys() if x in active_name] - if not name_list: - return None - temp_img = BuildImage(0, 0, font_size=20) - image_list = [] - num_list = [] - for i, name in enumerate(name_list): - img = BuildImage( - temp_img.getsize(name)[0] + 50, - 30, - font="msyh.ttf", - font_size=20, - color="#f9f6f2", - ) - await img.atext((30, 5), f"{i + 1}.{name}") - goods = [x for x in _props if x.goods_name == name][0] - if goods.icon and (icon_path / goods.icon).exists(): - icon = BuildImage(30, 30, background=icon_path / goods.icon) - await img.apaste(icon, alpha=True) - image_list.append(img) - num_list.append( - BuildImage( - 30, 30, font_size=20, font="msyh.ttf", plain_text=f"×{props[name]}" - ) - ) - max_w = 0 - num_max_w = 0 - h = 0 - for img, num in zip(image_list, num_list): - h += img.h - max_w = max_w if max_w > img.w else img.w - num_max_w = num_max_w if num_max_w > num.w else num.w - A = BuildImage(max_w + num_max_w + 30, h, color="#f9f6f2") - curr_h = 0 - for img, num in zip(image_list, num_list): - await A.apaste(img, (0, curr_h)) - await A.apaste(num, (max_w + 20, curr_h + 5), True) - curr_h += img.h - return A diff --git a/basic_plugins/shop/shop_handle/__init__.py b/basic_plugins/shop/shop_handle/__init__.py deleted file mode 100644 index d436f78b..00000000 --- a/basic_plugins/shop/shop_handle/__init__.py +++ /dev/null @@ -1,162 +0,0 @@ -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from configs.path_config import IMAGE_PATH -from models.bag_user import BagUser -from services.log import logger -from utils.message_builder import image -from utils.utils import is_number, scheduler - -from .data_source import ( - GoodsInfo, - create_shop_help, - delete_goods, - parse_goods_info, - register_goods, - update_goods, -) - -__zx_plugin_name__ = "商店" -__plugin_usage__ = """ -usage: - 商店项目,这可不是奸商 - 指令: - 商店 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 商品操作 - 指令: - 添加商品 name:[名称] price:[价格] des:[描述] ?discount:[折扣](小数) ?limit_time:[限时时间](小时) - 删除商品 [名称或序号] - 修改商品 name:[名称或序号] price:[价格] des:[描述] discount:[折扣] limit_time:[限时] - 示例:添加商品 name:萝莉酒杯 price:9999 des:普通的酒杯,但是里面.. discount:0.4 limit_time:90 - 示例:添加商品 name:可疑的药 price:5 des:效果未知 - 示例:删除商品 2 - 示例:修改商品 name:1 price:900 修改序号为1的商品的价格为900 - * 修改商品只需添加需要值即可 * -""".strip() -__plugin_des__ = "商店系统[金币回收计划]" -__plugin_cmd__ = [ - "商店", - "添加商品 name:[名称] price:[价格] des:[描述] ?discount:[折扣](小数) ?limit_time:[限时时间](小时)) [_superuser]", - "删除商品 [名称或序号] [_superuser]", - "修改商品 name:[名称或序号] price:[价格] des:[描述] discount:[折扣] limit_time:[限时] [_superuser]", -] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店"], -} -__plugin_block_limit__ = {"limit_type": "group"} - - -shop_help = on_command("商店", priority=5, block=True) - -shop_add_goods = on_command("添加商品", priority=5, permission=SUPERUSER, block=True) - -shop_del_goods = on_command("删除商品", priority=5, permission=SUPERUSER, block=True) - -shop_update_goods = on_command("修改商品", priority=5, permission=SUPERUSER, block=True) - - -@shop_help.handle() -async def _(): - await shop_help.send(image(b64=await create_shop_help())) - - -@shop_add_goods.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - data = parse_goods_info(msg) - if isinstance(data, str): - await shop_add_goods.finish(data) - if not data.get("name") or not data.get("price") or not data.get("des"): - await shop_add_goods.finish("name:price:des 参数不可缺少!") - if await register_goods(**data): - await shop_add_goods.send( - f"添加商品 {data['name']} 成功!\n" - f"名称:{data['name']}\n" - f"价格:{data['price']}金币\n" - f"简介:{data['des']}\n" - f"折扣:{data.get('discount')}\n" - f"限时:{data.get('limit_time')}", - at_sender=True, - ) - logger.info(f"添加商品 {msg} 成功", "添加商品", event.user_id) - else: - await shop_add_goods.send(f"添加商品 {msg} 失败了...", at_sender=True) - logger.warning(f"添加商品 {msg} 失败", "添加商品", event.user_id) - - -@shop_del_goods.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - name = "" - id_ = 0 - if is_number(msg): - id_ = int(msg) - else: - name = msg - rst, goods_name, code = await delete_goods(name, id_) - if code == 200: - await shop_del_goods.send(f"删除商品 {goods_name} 成功了...", at_sender=True) - if os.path.exists(f"{IMAGE_PATH}/shop_help.png"): - os.remove(f"{IMAGE_PATH}/shop_help.png") - logger.info(f"删除商品成功", "删除商品", event.user_id, target=goods_name) - else: - await shop_del_goods.send(f"删除商品 {goods_name} 失败了...", at_sender=True) - logger.info(f"删除商品失败", "删除商品", event.user_id, target=goods_name) - - -@shop_update_goods.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - data = parse_goods_info(msg) - if isinstance(data, str): - await shop_add_goods.finish(data) - if not data.get("name"): - await shop_add_goods.finish("name 参数不可缺少!") - flag, name, text = await update_goods(**data) - if flag: - await shop_update_goods.send(f"修改商品 {name} 成功了...\n{text}", at_sender=True) - logger.info(f"修改商品数据 {text} 成功", "修改商品", event.user_id, target=name) - else: - await shop_update_goods.send(name, at_sender=True) - logger.info(f"修改商品数据 {text} 失败", "修改商品", event.user_id, target=name) - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=0, -) -async def _(): - try: - await GoodsInfo.all().update(daily_purchase_limit={}) - logger.info("商品每日限购次数重置成功...") - except Exception as e: - logger.error(f"商品每日限购次数重置出错", e=e) - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=1, -) -async def _(): - try: - await BagUser.all().update(get_today_gold=0, spend_today_gold=0) - except Exception as e: - logger.error(f"重置每日金币", "定时任务", e=e) diff --git a/basic_plugins/shop/shop_handle/data_source.py b/basic_plugins/shop/shop_handle/data_source.py deleted file mode 100644 index 2d92e202..00000000 --- a/basic_plugins/shop/shop_handle/data_source.py +++ /dev/null @@ -1,416 +0,0 @@ -import time -from typing import List, Optional, Tuple, Union - -from PIL import Image - -from configs.path_config import IMAGE_PATH -from models.goods_info import GoodsInfo -from utils.image_utils import BuildImage, text2image -from utils.utils import GDict, is_number - -icon_path = IMAGE_PATH / "shop_icon" - -# 创建商店界面 -async def create_shop_help() -> str: - """ - 制作商店图片 - :return: 图片base64 - """ - goods_lst = await GoodsInfo.get_all_goods() - _dc = {} - font_h = BuildImage(0, 0).getsize("正")[1] - h = 10 - _list: List[GoodsInfo] = [] - for goods in goods_lst: - if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time: - _list.append(goods) - # A = BuildImage(1100, h, color="#f9f6f2") - total_n = 0 - image_list = [] - for idx, goods in enumerate(_list): - name_image = BuildImage( - 580, 40, font_size=25, color="#e67b6b", font="CJGaoDeGuo.otf" - ) - await name_image.atext( - (15, 0), f"{idx + 1}.{goods.goods_name}", center_type="by_height" - ) - await name_image.aline((380, -5, 280, 45), "#a29ad6", 5) - await name_image.atext((390, 0), "售价:", center_type="by_height") - if goods.goods_discount != 1: - discount_price = int(goods.goods_discount * goods.goods_price) - old_price_image = BuildImage( - 0, - 0, - plain_text=str(goods.goods_price), - font_color=(194, 194, 194), - font="CJGaoDeGuo.otf", - font_size=15, - ) - await old_price_image.aline( - ( - 0, - int(old_price_image.h / 2), - old_price_image.w + 1, - int(old_price_image.h / 2), - ), - (0, 0, 0), - ) - await name_image.apaste(old_price_image, (440, 0), True) - await name_image.atext((440, 15), str(discount_price), (255, 255, 255)) - else: - await name_image.atext( - (440, 0), - str(goods.goods_price), - (255, 255, 255), - center_type="by_height", - ) - await name_image.atext( - ( - 440 - + BuildImage(0, 0, plain_text=str(goods.goods_price), font_size=25).w, - 0, - ), - f" 金币", - center_type="by_height", - ) - des_image = None - font_img = BuildImage( - 600, 80, font_size=20, color="#a29ad6", font="CJGaoDeGuo.otf" - ) - p = font_img.getsize("简介:")[0] + 20 - if goods.goods_description: - des_list = goods.goods_description.split("\n") - desc = "" - for des in des_list: - if font_img.getsize(des)[0] > font_img.w - p - 20: - msg = "" - tmp = "" - for i in range(len(des)): - if font_img.getsize(tmp)[0] < font_img.w - p - 20: - tmp += des[i] - else: - msg += tmp + "\n" - tmp = des[i] - desc += msg - if tmp: - desc += tmp - else: - desc += des + "\n" - if desc[-1] == "\n": - desc = desc[:-1] - des_image = await text2image(desc, color="#a29ad6") - goods_image = BuildImage( - 600, - (50 + des_image.h) if des_image else 50, - font_size=20, - color="#a29ad6", - font="CJGaoDeGuo.otf", - ) - if des_image: - await goods_image.atext((15, 50), "简介:") - await goods_image.apaste(des_image, (p, 50)) - await name_image.acircle_corner(5) - await goods_image.apaste(name_image, (0, 5), True, center_type="by_width") - await goods_image.acircle_corner(20) - bk = BuildImage( - 1180, - (50 + des_image.h) if des_image else 50, - font_size=15, - color="#f9f6f2", - font="CJGaoDeGuo.otf", - ) - if goods.icon and (icon_path / goods.icon).exists(): - icon = BuildImage(70, 70, background=icon_path / goods.icon) - await bk.apaste(icon) - await bk.apaste(goods_image, (70, 0), alpha=True) - n = 0 - _w = 650 - # 添加限时图标和时间 - if goods.goods_limit_time > 0: - n += 140 - _limit_time_logo = BuildImage( - 40, 40, background=f"{IMAGE_PATH}/other/time.png" - ) - await bk.apaste(_limit_time_logo, (_w + 50, 0), True) - await bk.apaste( - BuildImage(0, 0, plain_text="限时!", font_size=23, font="CJGaoDeGuo.otf"), - (_w + 90, 10), - True, - ) - limit_time = time.strftime( - "%Y-%m-%d %H:%M", time.localtime(goods.goods_limit_time) - ).split() - y_m_d = limit_time[0] - _h_m = limit_time[1].split(":") - h_m = _h_m[0] + "时 " + _h_m[1] + "分" - await bk.atext((_w + 55, 38), str(y_m_d)) - await bk.atext((_w + 65, 57), str(h_m)) - _w += 140 - if goods.goods_discount != 1: - n += 140 - _discount_logo = BuildImage( - 30, 30, background=f"{IMAGE_PATH}/other/discount.png" - ) - await bk.apaste(_discount_logo, (_w + 50, 10), True) - await bk.apaste( - BuildImage(0, 0, plain_text="折扣!", font_size=23, font="CJGaoDeGuo.otf"), - (_w + 90, 15), - True, - ) - await bk.apaste( - BuildImage( - 0, - 0, - plain_text=f"{10 * goods.goods_discount:.1f} 折", - font_size=30, - font="CJGaoDeGuo.otf", - font_color=(85, 156, 75), - ), - (_w + 50, 44), - True, - ) - _w += 140 - if goods.daily_limit != 0: - n += 140 - _daily_limit_logo = BuildImage( - 35, 35, background=f"{IMAGE_PATH}/other/daily_limit.png" - ) - await bk.apaste(_daily_limit_logo, (_w + 50, 10), True) - await bk.apaste( - BuildImage(0, 0, plain_text="限购!", font_size=23, font="CJGaoDeGuo.otf"), - (_w + 90, 20), - True, - ) - await bk.apaste( - BuildImage( - 0, - 0, - plain_text=f"{goods.daily_limit}", - font_size=30, - font="CJGaoDeGuo.otf", - ), - (_w + 72, 45), - True, - ) - if total_n < n: - total_n = n - if n: - await bk.aline((650, -1, 650 + n, -1), "#a29ad6", 5) - # await bk.aline((650, 80, 650 + n, 80), "#a29ad6", 5) - - # 添加限时图标和时间 - image_list.append(bk) - # await A.apaste(bk, (0, current_h), True) - # current_h += 90 - h = 0 - current_h = 0 - for img in image_list: - h += img.h + 10 - A = BuildImage(1100, h, color="#f9f6f2") - for img in image_list: - await A.apaste(img, (0, current_h), True) - current_h += img.h + 10 - w = 950 - if total_n: - w += total_n - h = A.h + 230 + 100 - h = 1000 if h < 1000 else h - shop_logo = BuildImage(100, 100, background=f"{IMAGE_PATH}/other/shop_text.png") - shop = BuildImage(w, h, font_size=20, color="#f9f6f2") - zx_img = BuildImage(0, 0, background=f"{IMAGE_PATH}/zhenxun/toukan_3.png") - zx_img.transpose(Image.FLIP_LEFT_RIGHT) - zx_img.replace_color_tran(((240, 240, 240), (255, 255, 255)), (249, 246, 242)) - await shop.apaste(zx_img, (0, 100)) - shop.paste(A, (20 + zx_img.w, 230)) - await shop.apaste(shop_logo, (450, 30), True) - shop.text( - (int((1000 - shop.getsize("注【通过 序号 或者 商品名称 购买】")[0]) / 2), 170), - "注【通过 序号 或者 商品名称 购买】", - ) - shop.text((20 + zx_img.w, h - 100), "神秘药水\t\t售价:9999999金币\n\t\t鬼知道会有什么效果~") - return shop.pic2bs4() - - -async def register_goods( - name: str, - price: int, - des: str, - discount: Optional[float] = 1, - limit_time: Optional[int] = 0, - daily_limit: Optional[int] = 0, - is_passive: Optional[bool] = False, - icon: Optional[str] = None, -) -> bool: - """ - 添加商品 - 例如: 折扣:可选参数↓ 限时时间:可选,单位为小时 - 添加商品 name:萝莉酒杯 price:9999 des:普通的酒杯,但是里面.. discount:0.4 limit_time:90 - 添加商品 name:可疑的药 price:5 des:效果未知 - :param name: 商品名称 - :param price: 商品价格 - :param des: 商品简介 - :param discount: 商品折扣 - :param limit_time: 商品限时销售时间,单位为小时 - :param daily_limit: 每日购买次数限制 - :param is_passive: 是否为被动 - :param icon: 图标 - :return: 是否添加成功 - """ - if not await GoodsInfo.get_or_none(goods_name=name): - limit_time_ = float(limit_time) if limit_time else limit_time - discount = discount if discount is not None else 1 - limit_time_ = ( - int(time.time() + limit_time_ * 60 * 60) - if limit_time_ is not None and limit_time_ != 0 - else 0 - ) - await GoodsInfo.create( - goods_name=name, - goods_price=int(price), - goods_description=des, - goods_discount=float(discount), - goods_limit_time=limit_time_, - daily_limit=daily_limit, - is_passive=is_passive, - icon=icon, - ) - return True - return False - - -# 删除商品 -async def delete_goods(name: str, id_: int) -> Tuple[str, str, int]: - """ - 删除商品 - :param name: 商品名称 - :param id_: 商品id - :return: 删除状况 - """ - goods_lst = await GoodsInfo.get_all_goods() - if id_: - if id_ < 1 or id_ > len(goods_lst): - return "序号错误,没有该序号商品...", "", 999 - goods_name = goods_lst[id_ - 1].goods_name - if await GoodsInfo.delete_goods(goods_name): - return f"删除商品 {goods_name} 成功!", goods_name, 200 - else: - return f"删除商品 {goods_name} 失败!", goods_name, 999 - if name: - if await GoodsInfo.delete_goods(name): - return f"删除商品 {name} 成功!", name, 200 - else: - return f"删除商品 {name} 失败!", name, 999 - return "获取商品失败", "", 999 - - -# 更新商品信息 -async def update_goods(**kwargs) -> Tuple[bool, str, str]: - """ - 更新商品信息 - :param kwargs: kwargs - :return: 更新状况 - """ - if kwargs: - goods_lst = await GoodsInfo.get_all_goods() - if is_number(kwargs["name"]): - if int(kwargs["name"]) < 1 or int(kwargs["name"]) > len(goods_lst): - return False, "序号错误,没有该序号的商品...", "" - goods = goods_lst[int(kwargs["name"]) - 1] - else: - goods = await GoodsInfo.filter(goods_name=kwargs["name"]).first() - if not goods: - return False, "名称错误,没有该名称的商品...", "" - name: str = goods.goods_name - price = goods.goods_price - des = goods.goods_description - discount = goods.goods_discount - limit_time = goods.goods_limit_time - daily_limit = goods.daily_limit - is_passive = goods.is_passive - new_time = 0 - tmp = "" - if kwargs.get("price"): - tmp += f'价格:{price} --> {kwargs["price"]}\n' - price = kwargs["price"] - if kwargs.get("des"): - tmp += f'描述:{des} --> {kwargs["des"]}\n' - des = kwargs["des"] - if kwargs.get("discount"): - tmp += f'折扣:{discount} --> {kwargs["discount"]}\n' - discount = kwargs["discount"] - if kwargs.get("limit_time"): - kwargs["limit_time"] = float(kwargs["limit_time"]) - new_time = ( - time.strftime( - "%Y-%m-%d %H:%M:%S", - time.localtime(time.time() + kwargs["limit_time"] * 60 * 60), - ) - if kwargs["limit_time"] != 0 - else 0 - ) - tmp += f"限时至: {new_time}\n" if new_time else "取消了限时\n" - limit_time = kwargs["limit_time"] - if kwargs.get("daily_limit"): - tmp += ( - f'每日购买限制:{daily_limit} --> {kwargs["daily_limit"]}\n' - if daily_limit - else "取消了购买限制\n" - ) - daily_limit = int(kwargs["daily_limit"]) - if kwargs.get("is_passive"): - tmp += f'被动道具:{is_passive} --> {kwargs["is_passive"]}\n' - des = kwargs["is_passive"] - await GoodsInfo.update_goods( - name, - int(price), - des, - float(discount), - int( - time.time() + limit_time * 60 * 60 - if limit_time != 0 and new_time - else 0 - ), - daily_limit, - is_passive, - ) - return ( - True, - name, - tmp[:-1], - ) - - -def parse_goods_info(msg: str) -> Union[dict, str]: - """ - 解析格式数据 - :param msg: 消息 - :return: 解析完毕的数据data - """ - if "name:" not in msg: - return "必须指定修改的商品名称或序号!" - data = {} - for x in msg.split(): - sp = x.split(":", maxsplit=1) - if str(sp[1]).strip(): - sp[1] = sp[1].strip() - if sp[0] == "name": - data["name"] = sp[1] - elif sp[0] == "price": - if not is_number(sp[1]) or int(sp[1]) < 0: - return "price参数不合法,必须大于等于0!" - data["price"] = sp[1] - elif sp[0] == "des": - data["des"] = sp[1] - elif sp[0] == "discount": - if not is_number(sp[1]) or float(sp[1]) < 0: - return "discount参数不合法,必须大于0!" - data["discount"] = sp[1] - elif sp[0] == "limit_time": - if not is_number(sp[1]) or float(sp[1]) < 0: - return "limit_time参数不合法,必须为数字且大于0!" - data["limit_time"] = sp[1] - elif sp[0] == "daily_limit": - if not is_number(sp[1]) or float(sp[1]) < 0: - return "daily_limit参数不合法,必须为数字且大于0!" - data["daily_limit"] = sp[1] - return data diff --git a/basic_plugins/shop/use/__init__.py b/basic_plugins/shop/use/__init__.py deleted file mode 100644 index 33a3a0b5..00000000 --- a/basic_plugins/shop/use/__init__.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import Any, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg, RegexGroup - -from models.bag_user import BagUser -from models.user_shop_gold_log import UserShopGoldLog -from services.log import logger -from utils.decorator.shop import NotMeetUseConditionsException -from utils.utils import is_number - -from .data_source import build_params, effect, func_manager, register_use - -__zx_plugin_name__ = "商店 - 使用道具" -__plugin_usage__ = """ -usage: - 普通的使用道具 - 指令: - 使用道具 [序号或道具名称] ?[数量]=1 ?[其他信息] - 示例:使用道具好感度双倍加持卡 使用道具好感度双倍加持卡 - 示例:使用道具1 使用第一个道具 - 示例:使用道具1 10 使用10个第一个道具 - 示例:使用道具1 1 来点色图 使用第一个道具并附带信息 - * 序号以 ”我的道具“ 为准 * -""".strip() -__plugin_des__ = "商店 - 使用道具" -__plugin_cmd__ = ["使用道具 [序号或道具名称]"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "使用道具"], -} - - -use_props = on_command(r"使用道具", priority=5, block=True, permission=GROUP) - - -@use_props.handle() -async def _(bot: Bot, event: GroupMessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text() - num = 1 - text = "" - prop_n = None - index = None - split = msg.split() - if size := len(split): - if size == 1: - prop_n = split[0].strip() - index = 1 - if size > 1 and is_number(split[1].strip()): - prop_n = split[0].strip() - num = int(split[1].strip()) - index = 2 - else: - await use_props.finish("缺少参数,请查看帮助", at_sender=True) - if index: - text = " ".join(split[index:]) - property_ = await BagUser.get_property(event.user_id, event.group_id, True) - if property_: - name = None - if prop_n and is_number(prop_n): - if 0 < int(prop_n) <= len(property_): - name = list(property_.keys())[int(prop_n) - 1] - else: - await use_props.finish("仔细看看自己的道具仓库有没有这个道具?", at_sender=True) - else: - if prop_n not in property_.keys(): - await use_props.finish("道具名称错误!", at_sender=True) - name = prop_n - if not name: - await use_props.finish("未获取到道具名称", at_sender=True) - _user_prop_count = property_[name] - if num > _user_prop_count: - await use_props.finish(f"道具数量不足,无法使用{num}次!") - if num > (n := func_manager.get_max_num_limit(name)): - await use_props.finish(f"该道具单次只能使用 {n} 个!") - try: - model, kwargs = build_params(bot, event, name, num, text) - except KeyError: - logger.warning(f"{name} 未注册使用函数") - await use_props.finish(f"{name} 未注册使用方法") - else: - try: - await func_manager.run_handle( - type_="before_handle", param=model, **kwargs - ) - except NotMeetUseConditionsException as e: - await use_props.finish(e.get_info(), at_sender=True) - if await BagUser.delete_property(event.user_id, event.group_id, name, num): - if func_manager.check_send_success_message(name): - await use_props.send(f"使用道具 {name} {num} 次成功!", at_sender=True) - if msg := await effect(bot, event, name, num, text, event.message): - await use_props.send(msg, at_sender=True) - logger.info(f"使用道具 {name} {num} 次成功", event.user_id, event.group_id) - await UserShopGoldLog.create( - user_id=event.user_id, - group_id=event.group_id, - type=1, - name=name, - num=num, - ) - else: - await use_props.send(f"使用道具 {name} {num} 次失败!", at_sender=True) - logger.info( - f"使用道具 {name} {num} 次失败", "使用道具", event.user_id, event.group_id - ) - await func_manager.run_handle(type_="after_handle", param=model, **kwargs) - else: - await use_props.send("您的背包里没有任何的道具噢", at_sender=True) diff --git a/basic_plugins/shop/use/data_source.py b/basic_plugins/shop/use/data_source.py deleted file mode 100644 index b52d2c52..00000000 --- a/basic_plugins/shop/use/data_source.py +++ /dev/null @@ -1,235 +0,0 @@ -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageSegment, Message -from services.log import logger -from nonebot.adapters.onebot.v11 import Bot -from pydantic import create_model -from utils.models import ShopParam -from typing import Optional, Union, Callable, List, Tuple, Dict, Any -from types import MappingProxyType -import inspect -import asyncio - - -class GoodsUseFuncManager: - def __init__(self): - self._data = {} - - def register_use_before_handle(self, goods_name: str, fun_list: List[Callable]): - """ - 说明: - 注册商品使用前函数 - 参数: - :param goods_name: 商品名称 - :param fun_list: 函数列表 - """ - if fun_list: - self._data[goods_name]["before_handle"] = fun_list - logger.info(f"register_use_before_handle 成功注册商品:{goods_name} 的{len(fun_list)}个使用前函数") - - def register_use_after_handle(self, goods_name: str, fun_list: List[Callable]): - """ - 说明: - 注册商品使用后函数 - 参数: - :param goods_name: 商品名称 - :param fun_list: 函数列表 - """ - if fun_list: - self._data[goods_name]["after_handle"] = fun_list - logger.info(f"register_use_after_handle 成功注册商品:{goods_name} 的{len(fun_list)}个使用后函数") - - def register_use(self, goods_name: str, **kwargs): - """ - 注册商品使用方法 - :param goods_name: 商品名称 - :param kwargs: kwargs - """ - self._data[goods_name] = kwargs - - def exists(self, goods_name: str) -> bool: - """ - 判断商品使用方法是否被注册 - :param goods_name: 商品名称 - """ - return bool(self._data.get(goods_name)) - - def get_max_num_limit(self, goods_name: str) -> int: - """ - 获取单次商品使用数量 - :param goods_name: 商品名称 - """ - if self.exists(goods_name): - return self._data[goods_name]["kwargs"]["max_num_limit"] - return 1 - - def _parse_args(self, args_: MappingProxyType, param: ShopParam, **kwargs): - param_list_ = [] - _bot = param.bot - param.bot = None - param_json = param.dict() - param_json["bot"] = _bot - for par in args_.keys(): - if par in ["shop_param"]: - param_list_.append(param) - elif par not in ["args", "kwargs"]: - param_list_.append(param_json.get(par)) - if kwargs.get(par) is not None: - del kwargs[par] - return param_list_ - - async def use( - self, param: ShopParam, **kwargs - ) -> Optional[Union[str, MessageSegment]]: - """ - 使用道具 - :param param: BaseModel - :param kwargs: kwargs - """ - goods_name = param.goods_name - if self.exists(goods_name): - # 使用方法 - args = inspect.signature(self._data[goods_name]["func"]).parameters - if args and list(args.keys())[0] != "kwargs": - if asyncio.iscoroutinefunction(self._data[goods_name]["func"]): - return await self._data[goods_name]["func"]( - *self._parse_args(args, param, **kwargs) - ) - else: - return self._data[goods_name]["func"]( - *self._parse_args(args, param, **kwargs) - ) - else: - if asyncio.iscoroutinefunction(self._data[goods_name]["func"]): - return await self._data[goods_name]["func"]( - **kwargs, - ) - else: - return self._data[goods_name]["func"]( - **kwargs, - ) - - async def run_handle(self, goods_name: str, type_: str, param: ShopParam, **kwargs): - if self._data.get(goods_name) and self._data[goods_name].get(type_): - for func in self._data[goods_name].get(type_): - args = inspect.signature(func).parameters - if args and list(args.keys())[0] != "kwargs": - if asyncio.iscoroutinefunction(func): - await func(*self._parse_args(args, param, **kwargs)) - else: - func(*self._parse_args(args, param, **kwargs)) - else: - if asyncio.iscoroutinefunction(func): - await func(**kwargs) - else: - func(**kwargs) - - def check_send_success_message(self, goods_name: str) -> bool: - """ - 检查是否发送使用成功信息 - :param goods_name: 商品名称 - """ - if self.exists(goods_name): - return bool(self._data[goods_name]["kwargs"]["send_success_msg"]) - return False - - def get_kwargs(self, goods_name: str) -> dict: - """ - 获取商品使用方法的kwargs - :param goods_name: 商品名称 - """ - if self.exists(goods_name): - return self._data[goods_name]["kwargs"] - return {} - - def init_model(self, goods_name: str, bot: Bot, event: GroupMessageEvent, num: int, text: str): - return self._data[goods_name]["model"]( - **{ - "goods_name": goods_name, - "bot": bot, - "event": event, - "user_id": event.user_id, - "group_id": event.group_id, - "num": num, - "message": event.message, - "text": text - } - ) - - -func_manager = GoodsUseFuncManager() - - -def build_params( - bot: Bot, event: GroupMessageEvent, goods_name: str, num: int, text: str -) -> Tuple[ShopParam, Dict[str, Any]]: - """ - 说明: - 构造参数 - 参数: - :param bot: bot - :param event: event - :param goods_name: 商品名称 - :param num: 数量 - :param text: 其他信息 - """ - _kwargs = func_manager.get_kwargs(goods_name) - return ( - func_manager.init_model(goods_name, bot, event, num, text), - { - **_kwargs, - "_bot": bot, - "event": event, - "group_id": event.group_id, - "user_id": event.user_id, - "num": num, - "text": text, - "message": event.message, - "goods_name": goods_name, - }, - ) - - -async def effect( - bot: Bot, event: GroupMessageEvent, goods_name: str, num: int, text: str, message: Message -) -> Optional[Union[str, MessageSegment]]: - """ - 商品生效 - :param bot: Bot - :param event: GroupMessageEvent - :param goods_name: 商品名称 - :param num: 使用数量 - :param text: 其他信息 - :param message: Message - :return: 使用是否成功 - """ - # 优先使用注册的商品插件 - # try: - if func_manager.exists(goods_name): - _kwargs = func_manager.get_kwargs(goods_name) - model, kwargs = build_params(bot, event, goods_name, num, text) - return await func_manager.use(model, **kwargs) - # except Exception as e: - # logger.error(f"use 商品生效函数effect 发生错误 {type(e)}:{e}") - return None - - -def register_use(goods_name: str, func: Callable, **kwargs): - """ - 注册商品使用方法 - :param goods_name: 商品名称 - :param func: 使用函数 - :param kwargs: kwargs - """ - if func_manager.exists(goods_name): - raise ValueError("该商品使用函数已被注册!") - # 发送使用成功信息 - kwargs["send_success_msg"] = kwargs.get("send_success_msg", True) - kwargs["max_num_limit"] = kwargs.get("max_num_limit", 1) - func_manager.register_use( - goods_name, - **{ - "func": func, - "model": create_model(f"{goods_name}_model", __base__=ShopParam, **kwargs), - "kwargs": kwargs, - }, - ) - logger.info(f"register_use 成功注册商品:{goods_name} 的使用函数") diff --git a/basic_plugins/super_cmd/bot_friend_group.py b/basic_plugins/super_cmd/bot_friend_group.py deleted file mode 100755 index eef776d3..00000000 --- a/basic_plugins/super_cmd/bot_friend_group.py +++ /dev/null @@ -1,158 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from models.group_info import GroupInfo -from services.log import logger -from utils.depends import OneCommand -from utils.manager import requests_manager -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "显示所有好友群组 [Superuser]" -__plugin_usage__ = """ -usage: - 显示所有好友群组 - 指令: - 查看所有好友/查看所有群组 - 同意好友请求 [id] - 拒绝好友请求 [id] - 同意群聊请求 [id] - 拒绝群聊请求 [id] - 查看所有请求 - 清空所有请求 -""".strip() -__plugin_des__ = "显示所有好友群组" -__plugin_cmd__ = [ - "查看所有好友/查看所有群组", - "同意好友请求 [id]", - "拒绝好友请求 [id]", - "同意群聊请求 [id]", - "拒绝群聊请求 [id]", - "查看所有请求", - "清空所有请求", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -cls_group = on_command( - "查看所有群组", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) -cls_friend = on_command( - "查看所有好友", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - -friend_handle = on_command( - "同意好友请求", aliases={"拒绝好友请求"}, permission=SUPERUSER, priority=1, block=True -) - -group_handle = on_command( - "同意群聊请求", aliases={"拒绝群聊请求"}, permission=SUPERUSER, priority=1, block=True -) - -clear_request = on_command("清空所有请求", permission=SUPERUSER, priority=1, block=True) - -cls_request = on_command("查看所有请求", permission=SUPERUSER, priority=1, block=True) - - -@cls_group.handle() -async def _(bot: Bot): - gl = await bot.get_group_list() - msg = ["{group_id} {group_name}".format_map(g) for g in gl] - msg = "\n".join(msg) - msg = f"bot:{bot.self_id}\n| 群号 | 群名 | 共{len(gl)}个群\n" + msg - await cls_group.send(msg) - - -@cls_friend.handle() -async def _(bot: Bot): - gl = await bot.get_friend_list() - msg = ["{user_id} {nickname}".format_map(g) for g in gl] - msg = "\n".join(msg) - msg = f"| QQ号 | 昵称 | 共{len(gl)}个好友\n" + msg - await cls_friend.send(msg) - - -@friend_handle.handle() -async def _(bot: Bot, cmd: str = OneCommand(), arg: Message = CommandArg()): - id_ = arg.extract_plain_text().strip() - if is_number(id_): - id_ = int(id_) - if cmd[:2] == "同意": - flag = await requests_manager.approve(bot, id_, "private") - else: - flag = await requests_manager.refused(bot, id_, "private") - if flag == 1: - await friend_handle.send(f"{cmd[:2]}好友请求失败,该请求已失效..") - requests_manager.delete_request(id_, "private") - elif flag == 2: - await friend_handle.send(f"{cmd[:2]}好友请求失败,未找到此id的请求..") - else: - await friend_handle.send(f"{cmd[:2]}好友请求成功!") - else: - await friend_handle.send("id必须为纯数字!") - - -@group_handle.handle() -async def _( - bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg() -): - id_ = arg.extract_plain_text().strip() - flag = None - if is_number(id_): - id_ = int(id_) - if cmd[:2] == "同意": - rid = requests_manager.get_group_id(id_) - if rid: - if group := await GroupInfo.get_or_none(group_id=str(rid)): - group.group_flag = 1 - await group.save(update_fields=["group_flag"]) - else: - group_info = await bot.get_group_info(group_id=rid) - await GroupInfo.create( - group_id=str(rid), - group_name=group_info["group_name"], - max_member_count=group_info["max_member_count"], - member_count=group_info["member_count"], - group_flag=1, - ) - flag = await requests_manager.approve(bot, id_, "group") - else: - await group_handle.send("同意群聊请求失败,未找到此id的请求..") - logger.info("同意群聊请求失败,未找到此id的请求..", cmd, event.user_id) - else: - flag = await requests_manager.refused(bot, id_, "group") - if flag == 1: - await group_handle.send(f"{cmd[:2]}群聊请求失败,该请求已失效..") - requests_manager.delete_request(id_, "group") - elif flag == 2: - await group_handle.send(f"{cmd[:2]}群聊请求失败,未找到此id的请求..") - else: - await group_handle.send(f"{cmd[:2]}群聊请求成功!") - else: - await group_handle.send("id必须为纯数字!") - - -@cls_request.handle() -async def _(): - _str = "" - for type_ in ["private", "group"]: - msg = await requests_manager.show(type_) - if msg: - _str += image(msg) - else: - _str += "没有任何好友请求.." if type_ == "private" else "没有任何群聊请求.." - if type_ == "private": - _str += "\n--------------------\n" - await cls_request.send(Message(_str)) - - -@clear_request.handle() -async def _(): - requests_manager.clear() - await clear_request.send("已清空所有好友/群聊请求..") diff --git a/basic_plugins/super_cmd/clear_data.py b/basic_plugins/super_cmd/clear_data.py deleted file mode 100755 index effca772..00000000 --- a/basic_plugins/super_cmd/clear_data.py +++ /dev/null @@ -1,75 +0,0 @@ -import asyncio -import os -import time - -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.manager import resources_manager -from utils.utils import scheduler - -__zx_plugin_name__ = "清理临时数据 [Superuser]" -__plugin_usage__ = """ -usage: - 清理临时数据 - 指令: - 清理临时数据 -""".strip() -__plugin_des__ = "清理临时数据" -__plugin_cmd__ = [ - "清理临时数据", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -clear_data = on_command( - "清理临时数据", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -resources_manager.add_temp_dir(TEMP_PATH) - - -@clear_data.handle() -async def _(): - await clear_data.send("开始清理临时数据....") - size = await asyncio.get_event_loop().run_in_executor(None, _clear_data) - await clear_data.send("共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)) - logger.info("清理临时数据完成," + "共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)) - - -def _clear_data() -> float: - logger.debug("开始清理临时文件...") - size = 0 - dir_list = [dir_ for dir_ in resources_manager.get_temp_data_dir() if dir_.exists()] - for dir_ in dir_list: - logger.debug(f"尝试清理文件夹: {dir_.absolute()}", "清理临时数据") - dir_size = 0 - for file in os.listdir(dir_): - file = dir_ / file - if file.is_file(): - try: - if time.time() - os.path.getatime(file) > 10: - file_size = os.path.getsize(file) - file.unlink() - size += file_size - dir_size += file_size - logger.debug(f"移除临时文件: {file.absolute()}", "清理临时数据") - except Exception as e: - logger.error(f"清理临时数据错误,临时文件夹: {dir_.absolute()}...", "清理临时数据", e=e) - logger.debug("清理临时文件夹大小: {:.2f}MB".format(size / 1024 / 1024), "清理临时数据") - return float(size) - - -@scheduler.scheduled_job( - "cron", - hour=1, - minute=1, -) -async def _(): - size = await asyncio.get_event_loop().run_in_executor(None, _clear_data) - logger.info("自动清理临时数据完成," + "共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)) diff --git a/basic_plugins/super_cmd/exec_sql.py b/basic_plugins/super_cmd/exec_sql.py deleted file mode 100644 index 26763de9..00000000 --- a/basic_plugins/super_cmd/exec_sql.py +++ /dev/null @@ -1,92 +0,0 @@ -import asyncio - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me -from tortoise import Tortoise - -from services.db_context import TestSQL -from services.log import logger -from utils.message_builder import custom_forward_msg - -__zx_plugin_name__ = "执行sql [Superuser]" -__plugin_usage__ = """ -usage: - 执行一段sql语句 - 指令: - exec [sql语句] ([查询页数,19条/页]) - 查看所有表 -""".strip() -__plugin_des__ = "执行一段sql语句" -__plugin_cmd__ = ["exec [sql语句]", "查看所有表"] -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier" - - -exec_ = on_command("exec", rule=to_me(), permission=SUPERUSER, priority=1, block=True) -tables = on_command("查看所有表", rule=to_me(), permission=SUPERUSER, priority=1, block=True) - - -@exec_.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - sql = arg.extract_plain_text().strip() - if not sql: - await exec_.finish("未接受到sql语句") - db = Tortoise.get_connection("default") - # try: - # 判断是否为SELECT语句 - if sql.lower().startswith("select"): - pass - # # 分割语句 - try: - page = int(sql.split(" ")[-1]) - 1 - sql_list = sql.split(" ")[:-1] - except ValueError: - page = 0 - sql_list = sql.split(" ") - # 拼接语句 - sql = " ".join(sql_list) - res = await db.execute_query_dict(sql) - msg_list = [f"第{page+1}页查询结果:"] - # logger.info(res) - # 获取所有字段 - keys = res[0].keys() - # 每页10条 - for i in res[page * 10 : (page + 1) * 10]: - msg = "" - for key in keys: - msg += f"{key}: {i[key]}\n" - msg += f"第{page+1}页第{res.index(i)+1}条" - msg_list.append(msg) - # 检查是私聊还是群聊 - if isinstance(event, GroupMessageEvent): - forward_msg_list = custom_forward_msg(msg_list, bot.self_id) - await bot.send_group_forward_msg( - group_id=event.group_id, messages=forward_msg_list - ) - else: - for msg in msg_list: - await exec_.send(msg) - await asyncio.sleep(0.2) - return - else: - await TestSQL.raw(sql) - await exec_.send("执行 sql 语句成功.") - # except Exception as e: - # await exec_.send(f"执行 sql 语句失败 {type(e)}:{e}") - # logger.error(f"执行 sql 语句失败 {type(e)}:{e}") - - -@tables.handle() -async def _(bot: Bot, event: MessageEvent): - # 获取所有表 - db = Tortoise.get_connection("default") - query = await db.execute_query_dict( - "select tablename from pg_tables where schemaname = 'public'" - ) - msg = "数据库中的所有表名:\n" - for tablename in query: - msg += str(tablename["tablename"]) + "\n" - await tables.finish(msg) diff --git a/basic_plugins/super_cmd/manager_group.py b/basic_plugins/super_cmd/manager_group.py deleted file mode 100755 index f1a1195b..00000000 --- a/basic_plugins/super_cmd/manager_group.py +++ /dev/null @@ -1,235 +0,0 @@ -from typing import Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import ( - GROUP, - Bot, - GroupMessageEvent, - Message, - MessageEvent, -) -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import NICKNAME -from models.group_info import GroupInfo -from services.log import logger -from utils.depends import OneCommand -from utils.image_utils import text2image -from utils.manager import group_manager, plugins2settings_manager -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "管理群操作 [Superuser]" -__plugin_usage__ = """ -usage: - 群权限 | 群白名单 | 退出群 操作 - 退群,添加/删除群白名单,添加/删除群认证,当在群聊中这五个命令且没有指定群号时,默认指定当前群聊 - 指令: - 退群 ?[group_id] - 修改群权限 [group_id] [等级] - 修改群权限 [等级]: 该命令仅在群聊时生效,默认修改当前群聊 - 添加群白名单 ?*[group_id] - 删除群白名单 ?*[group_id] - 添加群认证 ?*[group_id] - 删除群认证 ?*[group_id] - 查看群白名单 -""".strip() -__plugin_des__ = "管理群操作" -__plugin_cmd__ = [ - "退群 [group_id]", - "修改群权限 [group_id] [等级]", - "添加群白名单 *[group_id]", - "删除群白名单 *[group_id]", - "添加群认证 *[group_id]", - "删除群认证 *[group_id]", - "查看群白名单", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -del_group = on_command("退群", rule=to_me(), permission=SUPERUSER, priority=1, block=True) - -add_group_level = on_command("修改群权限", priority=1, permission=SUPERUSER, block=True) -my_group_level = on_command( - "查看群权限", aliases={"群权限"}, priority=5, permission=GROUP, block=True -) -what_up_group_level = on_regex( - "(:?提高|提升|升高|增加|加上).*?群权限", - rule=to_me(), - priority=5, - permission=GROUP, - block=True, -) -manager_group_whitelist = on_command( - "添加群白名单", aliases={"删除群白名单"}, priority=1, permission=SUPERUSER, block=True -) - -show_group_whitelist = on_command( - "查看群白名单", priority=1, permission=SUPERUSER, block=True -) - -group_auth = on_command( - "添加群认证", aliases={"删除群认证"}, priority=1, permission=SUPERUSER, block=True -) - - -@del_group.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - group_id = arg.extract_plain_text().strip() - if not group_id and isinstance(event, GroupMessageEvent): - group_id = event.group_id - if group_id: - if is_number(group_id): - group_list = [x["group_id"] for x in await bot.get_group_list()] - group_id = int(group_id) - if group_id not in group_list: - logger.debug("群聊不存在", "退群", event.user_id, target=group_id) - await del_group.finish(f"{NICKNAME}未在该群聊中...") - try: - await bot.set_group_leave(group_id=group_id) - logger.info(f"{NICKNAME}退出群聊成功", "退群", event.user_id, target=group_id) - await del_group.send(f"退出群聊 {group_id} 成功", at_sender=True) - group_manager.delete_group(group_id) - await GroupInfo.filter(group_id=group_id).delete() - except Exception as e: - logger.error(f"退出群聊失败", "退群", event.user_id, target=group_id, e=e) - await del_group.send(f"退出群聊 {group_id} 失败", at_sender=True) - else: - await del_group.send(f"请输入正确的群号", at_sender=True) - else: - await del_group.send(f"请输入群号", at_sender=True) - - -@add_group_level.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - msg = msg.split() - group_id = 0 - level = 0 - if isinstance(event, GroupMessageEvent) and len(msg) == 1: - msg = [event.group_id, msg[0]] - if not msg: - await add_group_level.finish("缺失参数...") - if len(msg) < 2: - await add_group_level.finish("缺失参数...") - if is_number(msg[0]) and is_number(msg[1]): - group_id = str(msg[0]) - level = int(msg[1]) - else: - await add_group_level.finish("参数错误...群号和等级必须是数字..") - old_level = group_manager.get_group_level(group_id) - group_manager.set_group_level(group_id, level) - await add_group_level.send("修改成功...", at_sender=True) - if level > -1: - await bot.send_group_msg( - group_id=int(group_id), message=f"管理员修改了此群权限:{old_level} -> {level}" - ) - logger.info(f"修改群权限:{level}", "修改群权限", event.user_id, target=group_id) - - -@my_group_level.handle() -async def _(event: GroupMessageEvent): - level = group_manager.get_group_level(event.group_id) - tmp = "" - data = plugins2settings_manager.get_data() - for module in data: - if data[module].level > level: - plugin_name = data[module].cmd[0] - if plugin_name == "pixiv": - plugin_name = "搜图 p站排行" - tmp += f"{plugin_name}\n" - if not tmp: - await my_group_level.finish(f"当前群权限:{level}") - await my_group_level.finish( - f"当前群权限:{level}\n目前无法使用的功能:\n" - + image(await text2image(tmp, padding=10, color="#f9f6f2")) - ) - - -@what_up_group_level.handle() -async def _(): - await what_up_group_level.finish( - f"[此功能用于防止内鬼,如果引起不便那真是抱歉了]\n" f"目前提高群权限的方法:\n" f"\t1.超级管理员修改权限" - ) - - -@manager_group_whitelist.handle() -async def _( - bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg() -): - msg = arg.extract_plain_text().strip().split() - if not msg and isinstance(event, GroupMessageEvent): - msg = [event.group_id] - if not msg: - await manager_group_whitelist.finish("请输入群号") - all_group = [g["group_id"] for g in await bot.get_group_list()] - error_group = [] - group_list = [] - for group_id in msg: - if is_number(group_id) and int(group_id) in all_group: - group_list.append(int(group_id)) - else: - logger.debug(f"群号不合法或不存在", cmd, target=group_id) - error_group.append(group_id) - if group_list: - for group_id in group_list: - if cmd in ["添加群白名单"]: - group_manager.add_group_white_list(group_id) - else: - group_manager.delete_group_white_list(group_id) - group_list = [str(x) for x in group_list] - await manager_group_whitelist.send("已成功将 " + "\n".join(group_list) + " " + cmd) - group_manager.save() - if error_group: - await manager_group_whitelist.send("以下群聊不合法或不存在:\n" + "\n".join(error_group)) - - -@show_group_whitelist.handle() -async def _(): - group = [str(g) for g in group_manager.get_group_white_list()] - if not group: - await show_group_whitelist.finish("没有任何群在群白名单...") - await show_group_whitelist.send("目前的群白名单:\n" + "\n".join(group)) - - -@group_auth.handle() -async def _( - bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg() -): - msg = arg.extract_plain_text().strip().split() - if isinstance(event, GroupMessageEvent) and not msg: - msg = [event.group_id] - if not msg: - await manager_group_whitelist.finish("请输入群号") - error_group = [] - for group_id in msg: - group_id = int(group_id) - if cmd[:2] == "添加": - try: - await GroupInfo.update_or_create( - group_id=group_id, - defaults={ - "group_flag": 1, - }, - ) - except Exception as e: - await group_auth.send(f"添加群认证 {group_id} 发生错误!") - logger.error(f"添加群认证发生错误", cmd, target=group_id, e=e) - else: - await group_auth.send(f"已为 {group_id} {cmd[:2]}群认证..") - logger.info(f"添加群认证成功", cmd, target=group_id) - else: - if group := await GroupInfo.filter(group_id=group_id).first(): - await group.update_or_create( - group_id=group_id, defaults={"group_flag": 0} - ) - await group_auth.send(f"已删除 {group_id} 群认证..") - logger.info(f"删除群认证成功", cmd, target=group_id) - else: - await group_auth.send(f"未查找到群聊: {group_id}") - logger.info(f"未找到群聊", cmd, target=group_id) - if error_group: - await manager_group_whitelist.send("以下群聊不合法或不存在:\n" + "\n".join(error_group)) diff --git a/basic_plugins/super_cmd/reload_setting.py b/basic_plugins/super_cmd/reload_setting.py deleted file mode 100755 index c6460b40..00000000 --- a/basic_plugins/super_cmd/reload_setting.py +++ /dev/null @@ -1,64 +0,0 @@ -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import Config -from services.log import logger -from utils.manager import ( - group_manager, - plugins2block_manager, - plugins2cd_manager, - plugins2settings_manager, -) -from utils.utils import scheduler - -__zx_plugin_name__ = "重载配置 [Superuser]" -__plugin_usage__ = """ -usage: - 重载配置 - plugins2settings, - plugins2cd - plugins2block - group_manager - 指令: - 重载配置 -""".strip() -__plugin_des__ = "重载配置" -__plugin_cmd__ = [ - "重载配置", -] -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "AUTO_RELOAD": {"value": False, "help": "自动重载配置文件", "default_value": False, "type": bool}, - "AUTO_RELOAD_TIME": {"value": 180, "help": "控制自动重载配置文件时长", "default_value": 180, "type": int}, -} - - -reload_plugins_manager = on_command( - "重载配置", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -@reload_plugins_manager.handle() -async def _(): - plugins2settings_manager.reload() - plugins2cd_manager.reload() - plugins2block_manager.reload() - group_manager.reload() - Config.reload() - await reload_plugins_manager.send("重载完成...") - - -@scheduler.scheduled_job( - "interval", - seconds=Config.get_config("reload_setting", "AUTO_RELOAD_TIME", 180), -) -async def _(): - if Config.get_config("reload_setting", "AUTO_RELOAD"): - plugins2settings_manager.reload() - plugins2cd_manager.reload() - plugins2block_manager.reload() - group_manager.reload() - Config.reload() - logger.debug("已自动重载所有配置文件...") diff --git a/basic_plugins/super_cmd/set_admin_permissions.py b/basic_plugins/super_cmd/set_admin_permissions.py deleted file mode 100755 index 03835f76..00000000 --- a/basic_plugins/super_cmd/set_admin_permissions.py +++ /dev/null @@ -1,114 +0,0 @@ -from typing import List, Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.exception import ActionFailed -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from models.level_user import LevelUser -from services.log import logger -from utils.depends import AtList, OneCommand -from utils.message_builder import at -from utils.utils import is_number - -__zx_plugin_name__ = "用户权限管理 [Superuser]" -__plugin_usage__ = """ -usage: - 增删改用户的权限 - 指令: - 添加权限 [at] [权限] - 添加权限 [qq] [group_id] [权限] - 删除权限 [at] - 删除权限 [qq] [group_id] -""".strip() -__plugin_des__ = "增删改用户的权限" -__plugin_cmd__ = [ - "添加权限 [at] [权限]", - "添加权限 [qq] [group_id] [权限]", - "删除权限 [at]", - "删除权限 [qq] [group_id]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -super_cmd = on_command( - "添加管理", - aliases={"删除管理", "添加权限", "删除权限"}, - priority=1, - permission=SUPERUSER, - block=True, -) - - -@super_cmd.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - at_list: List[int] = AtList(), -): - group_id = event.group_id if isinstance(event, GroupMessageEvent) else -1 - level = None - args = arg.extract_plain_text().strip().split() - flag = 2 - qq = None - try: - if at_list: - qq = at_list[0] - if cmd[:2] == "添加" and args and is_number(args[0]): - level = int(args[0]) - else: - if cmd[:2] == "添加": - if ( - len(args) > 2 - and is_number(args[0]) - and is_number(args[1]) - and is_number(args[2]) - ): - qq = int(args[0]) - group_id = int(args[1]) - level = int(args[2]) - else: - if len(args) > 1 and is_number(args[0]) and is_number(args[1]): - qq = int(args[0]) - group_id = int(args[1]) - flag = 1 - level = -1 if cmd[:2] == "删除" else level - if group_id == -1 or not level or not qq: - raise IndexError() - except IndexError: - await super_cmd.finish(__plugin_usage__) - if not qq: - await super_cmd.finish("未指定对象...") - try: - if cmd[:2] == "添加": - await LevelUser.set_level(qq, group_id, level, 1) - result = f"设置权限成功, 权限: {level}" - else: - if await LevelUser.delete_level(qq, group_id): - result = "删除管理成功!" - else: - result = "该账号无管理权限!" - if flag == 2: - await super_cmd.send(result) - elif flag == 1: - try: - await bot.send_group_msg( - group_id=group_id, - message=Message( - f"{at(qq)}管理员修改了你的权限" - f"\n--------\n你当前的权限等级:{level if level != -1 else 0}" - ), - ) - except ActionFailed: - pass - await super_cmd.send("修改成功") - logger.info( - f"修改权限: {level if level != -1 else 0}", cmd, event.user_id, group_id, qq - ) - except Exception as e: - await super_cmd.send("执行指令失败!") - logger.error(f"执行指令失败", cmd, event.user_id, group_id, qq, e=e) diff --git a/basic_plugins/super_cmd/update_friend_group_info.py b/basic_plugins/super_cmd/update_friend_group_info.py deleted file mode 100755 index 5d4b1089..00000000 --- a/basic_plugins/super_cmd/update_friend_group_info.py +++ /dev/null @@ -1,77 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, MessageEvent -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from services.log import logger - -__zx_plugin_name__ = "更新群/好友信息 [Superuser]" -__plugin_usage__ = """ -usage: - 更新群/好友信息 - 指令: - 更新群信息 - 更新好友信息 -""".strip() -__plugin_des__ = "更新群/好友信息" -__plugin_cmd__ = [ - "更新群信息", - "更新好友信息", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -update_group_info = on_command( - "更新群信息", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) -update_friend_info = on_command( - "更新好友信息", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -@update_group_info.handle() -async def _(bot: Bot, event: MessageEvent): - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - num = 0 - for g in gl: - try: - group_info = await bot.get_group_info(group_id=g) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - }, - ) - num += 1 - logger.debug( - "群聊信息更新成功", "更新群信息", event.user_id, target=group_info["group_id"] - ) - except Exception as e: - logger.error(f"更新群聊信息失败", "更新群信息", event.user_id, target=g, e=e) - await update_group_info.send(f"成功更新了 {len(gl)} 个群的信息") - logger.info(f"更新群聊信息完成,共更新了 {len(gl)} 个群的信息", "更新群信息", event.user_id) - - -@update_friend_info.handle() -async def _(bot: Bot, event: MessageEvent): - num = 0 - error_list = [] - fl = await bot.get_friend_list() - for f in fl: - try: - await FriendUser.update_or_create( - user_id=str(f["user_id"]), defaults={"nickname": f["nickname"]} - ) - logger.debug(f"更新好友信息成功", "更新好友信息", event.user_id, target=f["user_id"]) - num += 1 - except Exception as e: - logger.error(f"更新好友信息失败", "更新好友信息", event.user_id, target=f["user_id"], e=e) - await update_friend_info.send(f"成功更新了 {num} 个好友的信息") - if error_list: - await update_friend_info.send(f"以下好友更新失败:\n" + "\n".join(error_list)) - logger.info(f"更新好友信息完成,共更新了 {num} 个群的信息", "更新好友信息", event.user_id) diff --git a/basic_plugins/super_help/__init__.py b/basic_plugins/super_help/__init__.py deleted file mode 100755 index 574b3d1b..00000000 --- a/basic_plugins/super_help/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from utils.message_builder import image - -from .data_source import SUPERUSER_HELP_IMAGE, create_help_image - -__zx_plugin_name__ = "超级用户帮助 [Superuser]" - - -if SUPERUSER_HELP_IMAGE.exists(): - SUPERUSER_HELP_IMAGE.unlink() - -super_help = on_command( - "超级用户帮助", rule=to_me(), priority=1, permission=SUPERUSER, block=True -) - - -@super_help.handle() -async def _(): - if not SUPERUSER_HELP_IMAGE.exists(): - await create_help_image() - await super_help.finish(image(SUPERUSER_HELP_IMAGE)) diff --git a/basic_plugins/super_help/data_source.py b/basic_plugins/super_help/data_source.py deleted file mode 100755 index 655ea5a1..00000000 --- a/basic_plugins/super_help/data_source.py +++ /dev/null @@ -1,81 +0,0 @@ -import nonebot -from nonebot import Driver - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.image_template import help_template -from utils.image_utils import BuildImage, build_sort_image, group_image, text2image -from utils.manager import plugin_data_manager -from utils.manager.models import PluginType - -driver: Driver = nonebot.get_driver() - -SUPERUSER_HELP_IMAGE = IMAGE_PATH / "superuser_help.png" - - -@driver.on_bot_connect -async def create_help_image(): - """ - 创建超级用户帮助图片 - """ - if SUPERUSER_HELP_IMAGE.exists(): - return - plugin_data_ = plugin_data_manager.get_data() - image_list = [] - task_list = [] - for plugin_data in [ - plugin_data_[x] - for x in plugin_data_ - if plugin_data_[x].name != "超级用户帮助 [Superuser]" - ]: - try: - if plugin_data.plugin_type in [PluginType.SUPERUSER, PluginType.ADMIN]: - usage = None - if ( - plugin_data.plugin_type == PluginType.SUPERUSER - and plugin_data.usage - ): - usage = await text2image( - plugin_data.usage, padding=5, color=(204, 196, 151) - ) - if plugin_data.superuser_usage: - usage = await text2image( - plugin_data.superuser_usage, padding=5, color=(204, 196, 151) - ) - if usage: - await usage.acircle_corner() - image = await help_template(plugin_data.name, usage) - image_list.append(image) - if plugin_data.task: - for x in plugin_data.task.keys(): - task_list.append(plugin_data.task[x]) - except Exception as e: - logger.warning( - f"获取超级用户插件 {plugin_data.model}: {plugin_data.name} 设置失败...", e=e - ) - task_str = "\n".join(task_list) - task_str = "通过私聊 开启被动/关闭被动 + [被动名称] 来控制全局被动\n----------\n" + task_str - task_image = await text2image(task_str, padding=5, color=(204, 196, 151)) - task_image = await help_template("被动任务", task_image) - image_list.append(task_image) - image_group, _ = group_image(image_list) - A = await build_sort_image(image_group, color="#f9f6f2", padding_top=180) - await A.apaste( - BuildImage(0, 0, font="CJGaoDeGuo.otf", plain_text="超级用户帮助", font_size=50), - (50, 30), - True, - ) - await A.apaste( - BuildImage( - 0, - 0, - font="CJGaoDeGuo.otf", - plain_text="注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", - font_size=30, - font_color="red", - ), - (50, 90), - True, - ) - await A.asave(SUPERUSER_HELP_IMAGE) - logger.info(f"已成功加载 {len(image_list)} 条超级用户命令") diff --git a/basic_plugins/update_info.py b/basic_plugins/update_info.py deleted file mode 100755 index a5896f23..00000000 --- a/basic_plugins/update_info.py +++ /dev/null @@ -1,32 +0,0 @@ -from nonebot import on_command - -from utils.message_builder import image - -__zx_plugin_name__ = "更新信息" -__plugin_usage__ = """ -usage: - 更新信息 - 指令: - 更新信息 -""".strip() -__plugin_des__ = "当前版本的更新信息" -__plugin_cmd__ = ["更新信息"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["更新信息"], -} - - -update_info = on_command("更新信息", aliases={"更新日志"}, priority=5, block=True) - - -@update_info.handle() -async def _(): - if img := image("update_info.png"): - await update_info.finish(img) - else: - await update_info.finish("目前没有更新信息哦") diff --git a/bot.py b/bot.py index 4ff6efac..b3009b5c 100644 --- a/bot.py +++ b/bot.py @@ -1,20 +1,26 @@ import nonebot -from nonebot.adapters.onebot.v11 import Adapter -from services.db_context import init, disconnect + +# from nonebot.adapters.discord import Adapter as DiscordAdapter +from nonebot.adapters.dodo import Adapter as DoDoAdapter +from nonebot.adapters.kaiheila import Adapter as KaiheilaAdapter +from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter + +from zhenxun.services.db_context import disconnect, init nonebot.init() driver = nonebot.get_driver() -driver.register_adapter(Adapter) -config = driver.config +driver.register_adapter(OneBotV11Adapter) +driver.register_adapter(KaiheilaAdapter) +driver.register_adapter(DoDoAdapter) +# driver.register_adapter(DiscordAdapter) + + driver.on_startup(init) driver.on_shutdown(disconnect) -# 优先加载定时任务 -nonebot.load_plugin("nonebot_plugin_apscheduler") -nonebot.load_plugins("basic_plugins") -nonebot.load_plugins("plugins") -nonebot.load_plugins("extensive_plugin") -# 最后加载权限控制 -nonebot.load_plugins("basic_plugins/hooks") + +nonebot.load_builtin_plugins("echo") # 内置插件 +nonebot.load_plugins("zhenxun/builtin_plugins") +nonebot.load_plugins("zhenxun/plugins") if __name__ == "__main__": diff --git a/configs/path_config.py b/configs/path_config.py deleted file mode 100644 index 7f9b46bf..00000000 --- a/configs/path_config.py +++ /dev/null @@ -1,47 +0,0 @@ -from pathlib import Path -import os - -# 图片路径 -IMAGE_PATH = Path() / "resources" / "image" -# 语音路径 -RECORD_PATH = Path() / "resources" / "record" -# 文本路径 -TEXT_PATH = Path() / "resources" / "text" -# 日志路径 -LOG_PATH = Path() / "log" -# 字体路径 -FONT_PATH = Path() / "resources" / "font" -# 数据路径 -DATA_PATH = Path() / "data" -# 临时数据路径 -TEMP_PATH = Path() / "resources" / "temp" -# 网页模板路径 -TEMPLATE_PATH = Path() / "resources" / "template" - - -def load_path(): - old_img_dir = Path() / "resources" / "img" - if not IMAGE_PATH.exists() and old_img_dir.exists(): - os.rename(old_img_dir, IMAGE_PATH) - old_voice_dir = Path() / "resources" / "voice" - if not RECORD_PATH.exists() and old_voice_dir.exists(): - os.rename(old_voice_dir, RECORD_PATH) - old_ttf_dir = Path() / "resources" / "ttf" - if not FONT_PATH.exists() and old_ttf_dir.exists(): - os.rename(old_ttf_dir, FONT_PATH) - old_txt_dir = Path() / "resources" / "txt" - if not TEXT_PATH.exists() and old_txt_dir.exists(): - os.rename(old_txt_dir, TEXT_PATH) - IMAGE_PATH.mkdir(parents=True, exist_ok=True) - RECORD_PATH.mkdir(parents=True, exist_ok=True) - TEXT_PATH.mkdir(parents=True, exist_ok=True) - LOG_PATH.mkdir(parents=True, exist_ok=True) - FONT_PATH.mkdir(parents=True, exist_ok=True) - DATA_PATH.mkdir(parents=True, exist_ok=True) - TEMP_PATH.mkdir(parents=True, exist_ok=True) - - -load_path() - - - diff --git a/docs_image/tt.jpg b/docs_image/tt.jpg new file mode 100644 index 00000000..3f105a90 Binary files /dev/null and b/docs_image/tt.jpg differ diff --git a/docs_image/webui1.png b/docs_image/webui1.png new file mode 100644 index 00000000..2cfa5822 Binary files /dev/null and b/docs_image/webui1.png differ diff --git a/docs_image/webui2.png b/docs_image/webui2.png new file mode 100644 index 00000000..ff884972 Binary files /dev/null and b/docs_image/webui2.png differ diff --git a/docs_image/webui3.png b/docs_image/webui3.png new file mode 100644 index 00000000..f328df2d Binary files /dev/null and b/docs_image/webui3.png differ diff --git a/docs_image/webui4.png b/docs_image/webui4.png new file mode 100644 index 00000000..8bf31231 Binary files /dev/null and b/docs_image/webui4.png differ diff --git a/docs_image/webui5.png b/docs_image/webui5.png new file mode 100644 index 00000000..6b8d350e Binary files /dev/null and b/docs_image/webui5.png differ diff --git a/docs_image/webui6.png b/docs_image/webui6.png new file mode 100644 index 00000000..157ad8a6 Binary files /dev/null and b/docs_image/webui6.png differ diff --git a/docs_image/webui7.png b/docs_image/webui7.png new file mode 100644 index 00000000..ac82ab02 Binary files /dev/null and b/docs_image/webui7.png differ diff --git a/models/bag_user.py b/models/bag_user.py deleted file mode 100755 index 283a49f2..00000000 --- a/models/bag_user.py +++ /dev/null @@ -1,173 +0,0 @@ -from typing import Dict, Union - -from tortoise import fields - -from services.db_context import Model - -from .goods_info import GoodsInfo - - -class BagUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - gold = fields.IntField(default=100) - """金币数量""" - spend_total_gold = fields.IntField(default=0) - """花费金币总数""" - get_total_gold = fields.IntField(default=0) - """获取金币总数""" - get_today_gold = fields.IntField(default=0) - """今日获取金币""" - spend_today_gold = fields.IntField(default=0) - """今日获取金币""" - property: Dict[str, int] = fields.JSONField(default={}) - """道具""" - - class Meta: - table = "bag_users" - table_description = "用户道具数据表" - unique_together = ("user_id", "group_id") - - @classmethod - async def get_user_total_gold(cls, user_id: Union[int, str], group_id: Union[int, str]) -> str: - """ - 说明: - 获取金币概况 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - return ( - f"当前金币:{user.gold}\n今日获取金币:{user.get_today_gold}\n今日花费金币:{user.spend_today_gold}" - f"\n今日收益:{user.get_today_gold - user.spend_today_gold}" - f"\n总赚取金币:{user.get_total_gold}\n总花费金币:{user.spend_total_gold}" - ) - - @classmethod - async def get_gold(cls, user_id: Union[int, str], group_id: Union[int, str]) -> int: - """ - 说明: - 获取当前金币 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - return user.gold - - @classmethod - async def get_property( - cls, user_id: Union[int, str], group_id: Union[int, str], only_active: bool = False - ) -> Dict[str, int]: - """ - 说明: - 获取当前道具 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param only_active: 仅仅获取主动使用的道具 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - if only_active and user.property: - data = {} - name_list = [ - x.goods_name - for x in await GoodsInfo.get_all_goods() - if not x.is_passive - ] - for key in [x for x in user.property if x in name_list]: - data[key] = user.property[key] - return data - return user.property - - @classmethod - async def add_gold(cls, user_id: Union[int, str], group_id: Union[int, str], num: int): - """ - 说明: - 增加金币 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param num: 金币数量 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - user.gold = user.gold + num - user.get_total_gold = user.get_total_gold + num - user.get_today_gold = user.get_today_gold + num - await user.save(update_fields=["gold", "get_today_gold", "get_total_gold"]) - - @classmethod - async def spend_gold(cls, user_id: Union[int, str], group_id: Union[int, str], num: int): - """ - 说明: - 花费金币 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param num: 金币数量 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - user.gold = user.gold - num - user.spend_total_gold = user.spend_total_gold + num - user.spend_today_gold = user.spend_today_gold + num - await user.save(update_fields=["gold", "spend_total_gold", "spend_today_gold"]) - - @classmethod - async def add_property(cls, user_id: Union[int, str], group_id: Union[int, str], name: str, num: int = 1): - """ - 说明: - 增加道具 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param name: 道具名称 - :param num: 道具数量 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - property_ = user.property - if property_.get(name) is None: - property_[name] = 0 - property_[name] += num - user.property = property_ - await user.save(update_fields=["property"]) - - @classmethod - async def delete_property( - cls, user_id: Union[int, str], group_id: Union[int, str], name: str, num: int = 1 - ) -> bool: - """ - 说明: - 使用/删除 道具 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param name: 道具名称 - :param num: 使用个数 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - property_ = user.property - if name in property_: - if (n := property_.get(name, 0)) < num: - return False - if n == num: - del property_[name] - else: - property_[name] -= num - await user.save(update_fields=["property"]) - return True - return False - - @classmethod - async def _run_script(cls): - return ["ALTER TABLE bag_users DROP props;", # 删除 props 字段 - "ALTER TABLE bag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE bag_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE bag_users ALTER COLUMN group_id TYPE character varying(255);" - ] diff --git a/models/ban_user.py b/models/ban_user.py deleted file mode 100755 index e75c9552..00000000 --- a/models/ban_user.py +++ /dev/null @@ -1,131 +0,0 @@ -import time -from typing import Union - -from tortoise import fields - -from services.db_context import Model -from services.log import logger - - -class BanUser(Model): - - user_id = fields.CharField(255, pk=True) - """用户id""" - ban_level = fields.IntField() - """使用ban命令的用户等级""" - ban_time = fields.BigIntField() - """ban开始的时间""" - duration = fields.BigIntField() - """ban时长""" - - class Meta: - table = "ban_users" - table_description = ".ban/b了 封禁人员数据表" - - @classmethod - async def check_ban_level(cls, user_id: Union[int, str], level: int) -> bool: - """ - 说明: - 检测ban掉目标的用户与unban用户的权限等级大小 - 参数: - :param user_id: unban用户的用户id - :param level: ban掉目标用户的权限等级 - """ - user = await cls.filter(user_id=str(user_id)).first() - if user: - logger.debug( - f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", - target=str(user_id), - ) - return bool(user and user.ban_level > level) - return False - - @classmethod - async def check_ban_time(cls, user_id: Union[int, str]) -> Union[str, int]: - """ - 说明: - 检测用户被ban时长 - 参数: - :param user_id: 用户id - """ - logger.debug(f"获取用户ban时长", target=str(user_id)) - if user := await cls.filter(user_id=str(user_id)).first(): - if ( - time.time() - (user.ban_time + user.duration) > 0 - and user.duration != -1 - ): - return "" - if user.duration == -1: - return "∞" - return int(time.time() - user.ban_time - user.duration) - return "" - - @classmethod - async def is_ban(cls, user_id: Union[int, str]) -> bool: - """ - 说明: - 判断用户是否被ban - 参数: - :param user_id: 用户id - """ - logger.debug(f"检测是否被ban", target=str(user_id)) - if await cls.check_ban_time(str(user_id)): - return True - else: - await cls.unban(user_id) - return False - - @classmethod - async def is_super_ban(cls, user_id: Union[int, str]) -> bool: - """ - 说明: - 判断用户是否被超级用户ban / b了 - 参数: - :param user_id: 用户id - """ - logger.debug(f"检测是否被超级用户权限封禁", target=str(user_id)) - if user := await cls.filter(user_id=str(user_id)).first(): - if user.ban_level == 10: - return True - return False - - @classmethod - async def ban(cls, user_id: Union[int, str], ban_level: int, duration: int): - """ - 说明: - ban掉目标用户 - 参数: - :param user_id: 目标用户id - :param ban_level: 使用ban命令用户的权限 - :param duration: ban时长,秒 - """ - logger.debug(f"封禁用户,等级:{ban_level},时长: {duration}", target=str(user_id)) - if await cls.filter(user_id=str(user_id)).first(): - await cls.unban(user_id) - await cls.create( - user_id=str(user_id), - ban_level=ban_level, - ban_time=time.time(), - duration=duration, - ) - - @classmethod - async def unban(cls, user_id: Union[int, str]) -> bool: - """ - 说明: - unban用户 - 参数: - :param user_id: 用户id - """ - if user := await cls.filter(user_id=str(user_id)).first(): - logger.debug("解除封禁", target=str(user_id)) - await user.delete() - return True - return False - - @classmethod - async def _run_script(cls): - return ["ALTER TABLE ban_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE ban_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - ] diff --git a/models/friend_user.py b/models/friend_user.py deleted file mode 100755 index 9de696c9..00000000 --- a/models/friend_user.py +++ /dev/null @@ -1,65 +0,0 @@ -from tortoise import fields -from typing import Union -from configs.config import Config -from services.db_context import Model - - -class FriendUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255, unique=True) - """用户id""" - user_name = fields.CharField(max_length=255, default="") - """用户名称""" - nickname = fields.CharField(max_length=255, null=True) - """私聊下自定义昵称""" - - class Meta: - table = "friend_users" - table_description = "好友信息数据表" - - @classmethod - async def get_user_name(cls, user_id: Union[int, str]) -> str: - """ - 说明: - 获取好友用户名称 - 参数: - :param user_id: 用户id - """ - if user := await cls.get_or_none(user_id=str(user_id)): - return user.user_name - return "" - - @classmethod - async def get_user_nickname(cls, user_id: Union[int, str]) -> str: - """ - 说明: - 获取用户昵称 - 参数: - :param user_id: 用户id - """ - if user := await cls.get_or_none(user_id=str(user_id)): - if user.nickname: - _tmp = "" - if black_word := Config.get_config("nickname", "BLACK_WORD"): - for x in user.nickname: - _tmp += "*" if x in black_word else x - return _tmp - return "" - - @classmethod - async def set_user_nickname(cls, user_id: Union[int, str], nickname: str): - """ - 说明: - 设置用户昵称 - 参数: - :param user_id: 用户id - :param nickname: 昵称 - """ - await cls.update_or_create(user_id=str(user_id), defaults={"nickname": nickname}) - - @classmethod - async def _run_script(cls): - await cls.raw("ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);") - # 将user_id字段类型改为character varying(255)) diff --git a/models/goods_info.py b/models/goods_info.py deleted file mode 100644 index d0afcef2..00000000 --- a/models/goods_info.py +++ /dev/null @@ -1,205 +0,0 @@ -from typing import Dict, List, Optional, Tuple, Union - -from tortoise import fields - -from services.db_context import Model - - -class GoodsInfo(Model): - __tablename__ = "goods_info" - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - goods_name = fields.CharField(255, unique=True) - """商品名称""" - goods_price = fields.IntField() - """价格""" - goods_description = fields.TextField() - """描述""" - goods_discount = fields.FloatField(default=1) - """折扣""" - goods_limit_time = fields.BigIntField(default=0) - """限时""" - daily_limit = fields.IntField(default=0) - """每日限购""" - daily_purchase_limit: Dict[str, Dict[str, int]] = fields.JSONField(default={}) - """用户限购记录""" - is_passive = fields.BooleanField(default=False) - """是否为被动道具""" - icon = fields.TextField(null=True) - """图标路径""" - - class Meta: - table = "goods_info" - table_description = "商品数据表" - - @classmethod - async def add_goods( - cls, - goods_name: str, - goods_price: int, - goods_description: str, - goods_discount: float = 1, - goods_limit_time: int = 0, - daily_limit: int = 0, - is_passive: bool = False, - icon: Optional[str] = None, - ): - """ - 说明: - 添加商品 - 参数: - :param goods_name: 商品名称 - :param goods_price: 商品价格 - :param goods_description: 商品简介 - :param goods_discount: 商品折扣 - :param goods_limit_time: 商品限时 - :param daily_limit: 每日购买限制 - :param is_passive: 是否为被动道具 - :param icon: 图标 - """ - if not await cls.filter(goods_name=goods_name).first(): - await cls.create( - goods_name=goods_name, - goods_price=goods_price, - goods_description=goods_description, - goods_discount=goods_discount, - goods_limit_time=goods_limit_time, - daily_limit=daily_limit, - is_passive=is_passive, - icon=icon, - ) - - @classmethod - async def delete_goods(cls, goods_name: str) -> bool: - """ - 说明: - 删除商品 - 参数: - :param goods_name: 商品名称 - """ - if goods := await cls.get_or_none(goods_name=goods_name): - await goods.delete() - return True - return False - - @classmethod - async def update_goods( - cls, - goods_name: str, - goods_price: Optional[int] = None, - goods_description: Optional[str] = None, - goods_discount: Optional[float] = None, - goods_limit_time: Optional[int] = None, - daily_limit: Optional[int] = None, - is_passive: Optional[bool] = None, - icon: Optional[str] = None, - ): - """ - 说明: - 更新商品信息 - 参数: - :param goods_name: 商品名称 - :param goods_price: 商品价格 - :param goods_description: 商品简介 - :param goods_discount: 商品折扣 - :param goods_limit_time: 商品限时时间 - :param daily_limit: 每日次数限制 - :param is_passive: 是否为被动 - :param icon: 图标 - """ - if goods := await cls.get_or_none(goods_name=goods_name): - await cls.update_or_create( - goods_name=goods_name, - defaults={ - "goods_price": goods_price or goods.goods_price, - "goods_description": goods_description or goods.goods_description, - "goods_discount": goods_discount or goods.goods_discount, - "goods_limit_time": goods_limit_time - if goods_limit_time is not None - else goods.goods_limit_time, - "daily_limit": daily_limit - if daily_limit is not None - else goods.daily_limit, - "is_passive": is_passive - if is_passive is not None - else goods.is_passive, - "icon": icon or goods.icon, - }, - ) - - @classmethod - async def get_all_goods(cls) -> List["GoodsInfo"]: - """ - 说明: - 获得全部有序商品对象 - """ - query = await cls.all() - id_lst = [x.id for x in query] - goods_lst = [] - for _ in range(len(query)): - min_id = min(id_lst) - goods_lst.append([x for x in query if x.id == min_id][0]) - id_lst.remove(min_id) - return goods_lst - - @classmethod - async def add_user_daily_purchase( - cls, goods: "GoodsInfo", user_id_: Union[int, str], group_id_: Union[int, str], num: int = 1 - ): - """ - 说明: - 添加用户明日购买限制 - 参数: - :param goods: 商品 - :param user_id: 用户id - :param group_id: 群号 - :param num: 数量 - """ - user_id = str(user_id_) - group_id = str(group_id_) - if goods and goods.daily_limit and goods.daily_limit > 0: - if not goods.daily_purchase_limit.get(group_id): - goods.daily_purchase_limit[group_id] = {} - if not goods.daily_purchase_limit[group_id].get(user_id): - goods.daily_purchase_limit[group_id][user_id] = 0 - goods.daily_purchase_limit[group_id][user_id] += num - await goods.save(update_fields=["daily_purchase_limit"]) - - @classmethod - async def check_user_daily_purchase( - cls, goods: "GoodsInfo", user_id_: Union[int, str], group_id_: Union[int, str], num: int = 1 - ) -> Tuple[bool, int]: - """ - 说明: - 检测用户每日购买上限 - 参数: - :param goods: 商品 - :param user_id: 用户id - :param group_id: 群号 - :param num: 数量 - """ - user_id = str(user_id_) - group_id = str(group_id_) - if goods and goods.daily_limit > 0: - if ( - not goods.daily_limit - or not goods.daily_purchase_limit.get(group_id) - or not goods.daily_purchase_limit[group_id].get(user_id) - ): - return goods.daily_limit - num < 0, goods.daily_limit - if goods.daily_purchase_limit[group_id][user_id] + num > goods.daily_limit: - return ( - True, - goods.daily_limit - goods.daily_purchase_limit[group_id][user_id], - ) - return False, 0 - - @classmethod - def _run_script(cls): - return [ - "ALTER TABLE goods_info ADD daily_limit Integer DEFAULT 0;", - "ALTER TABLE goods_info ADD daily_purchase_limit Json DEFAULT '{}';", - "ALTER TABLE goods_info ADD is_passive boolean DEFAULT False;", - "ALTER TABLE goods_info ADD icon VARCHAR(255);", - ] diff --git a/models/group_info.py b/models/group_info.py deleted file mode 100755 index 85b2d468..00000000 --- a/models/group_info.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List, Optional - -from tortoise import fields - -from services.db_context import Model -from services.log import logger - - -class GroupInfo(Model): - - group_id = fields.CharField(255, pk=True) - """群聊id""" - group_name = fields.TextField(default="") - """群聊名称""" - max_member_count = fields.IntField(default=0) - """最大人数""" - member_count = fields.IntField(default=0) - """当前人数""" - group_flag: int = fields.IntField(default=0) - """群认证标记""" - - class Meta: - table = "group_info" - table_description = "群聊信息表" - - @classmethod - def _run_script(cls): - return ["ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag - "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);" - # 将group_id字段类型改为character varying(255) - ] diff --git a/models/group_member_info.py b/models/group_member_info.py deleted file mode 100755 index 687af219..00000000 --- a/models/group_member_info.py +++ /dev/null @@ -1,122 +0,0 @@ -from datetime import datetime -from typing import List, Optional, Set, Union - -from tortoise import fields - -from configs.config import Config -from services.db_context import Model -from services.log import logger - - -class GroupInfoUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - user_name = fields.CharField(255, default="") - """用户昵称""" - group_id = fields.CharField(255) - """群聊id""" - user_join_time: datetime = fields.DatetimeField(null=True) - """用户入群时间""" - nickname = fields.CharField(255, null=True) - """群聊昵称""" - uid = fields.BigIntField(null=True) - """用户uid""" - - class Meta: - table = "group_info_users" - table_description = "群员信息数据表" - unique_together = ("user_id", "group_id") - - @classmethod - async def get_group_member_id_list(cls, group_id: Union[int, str]) -> Set[int]: - """ - 说明: - 获取该群所有用户id - 参数: - :param group_id: 群号 - """ - return set( - await cls.filter(group_id=str(group_id)).values_list("user_id", flat=True) - ) # type: ignore - - @classmethod - async def set_user_nickname(cls, user_id: Union[int, str], group_id: Union[int, str], nickname: str): - """ - 说明: - 设置群员在该群内的昵称 - 参数: - :param user_id: 用户id - :param group_id: 群号 - :param nickname: 昵称 - """ - await cls.update_or_create( - user_id=str(user_id), - group_id=str(group_id), - defaults={"nickname": nickname}, - ) - - @classmethod - async def get_user_all_group(cls, user_id: Union[int, str]) -> List[int]: - """ - 说明: - 获取该用户所在的所有群聊 - 参数: - :param user_id: 用户id - """ - return list( - await cls.filter(user_id=str(user_id)).values_list("group_id", flat=True) - ) # type: ignore - - @classmethod - async def get_user_nickname(cls, user_id: Union[int, str], group_id: Union[int, str]) -> str: - """ - 说明: - 获取用户在该群的昵称 - 参数: - :param user_id: 用户id - :param group_id: 群号 - """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - if user.nickname: - nickname = "" - if black_word := Config.get_config("nickname", "BLACK_WORD"): - for x in user.nickname: - nickname += "*" if x in black_word else x - return nickname - return user.nickname - return "" - - @classmethod - async def get_group_member_uid(cls, user_id: Union[int, str], group_id: Union[int, str]) -> Optional[int]: - logger.debug( - f"GroupInfoUser 尝试获取 用户[{user_id}] 群聊[{group_id}] UID" - ) - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - _max_uid_user, _ = await cls.get_or_create(user_id="114514", group_id="114514") - _max_uid = _max_uid_user.uid - if not user.uid: - all_user = await cls.filter(user_id=str(user_id)).all() - for x in all_user: - if x.uid: - return x.uid - user.uid = _max_uid + 1 - _max_uid_user.uid = _max_uid + 1 - await cls.bulk_update([user, _max_uid_user], ["uid"]) - logger.debug( - f"GroupInfoUser 获取 用户[{user_id}] 群聊[{group_id}] UID: {user.uid}" - ) - return user.uid - - @classmethod - async def _run_script(cls): - return [ - "alter table group_info_users alter user_join_time drop not null;", # 允许 user_join_time 为空 - "ALTER TABLE group_info_users ALTER COLUMN user_join_time TYPE timestamp with time zone USING user_join_time::timestamp with time zone;", - "ALTER TABLE group_info_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE group_info_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE group_info_users ALTER COLUMN group_id TYPE character varying(255);" - ] diff --git a/models/level_user.py b/models/level_user.py deleted file mode 100755 index 0c67d8ac..00000000 --- a/models/level_user.py +++ /dev/null @@ -1,108 +0,0 @@ -from tortoise import fields - -from services.db_context import Model -from typing import Union - -class LevelUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - user_level = fields.BigIntField() - """用户权限等级""" - group_flag = fields.IntField(default=0) - """特殊标记,是否随群管理员变更而设置权限""" - - class Meta: - table = "level_users" - table_description = "用户权限数据库" - unique_together = ("user_id", "group_id") - - @classmethod - async def get_user_level(cls, user_id: Union[int, str], group_id: Union[int, str]) -> int: - """ - 说明: - 获取用户在群内的等级 - 参数: - :param user_id: 用户id - :param group_id: 群组id - """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - return user.user_level - return -1 - - @classmethod - async def set_level( - cls, user_id: Union[int, str], group_id: Union[int, str], level: int, group_flag: int = 0 - ): - """ - 说明: - 设置用户在群内的权限 - 参数: - :param user_id: 用户id - :param group_id: 群组id - :param level: 权限等级 - :param group_flag: 是否被自动更新刷新权限 0:是,1:否 - """ - await cls.update_or_create( - user_id=str(user_id), - group_id=str(group_id), - defaults={"user_level": level, "group_flag": group_flag}, - ) - - @classmethod - async def delete_level(cls, user_id: Union[int, str], group_id: Union[int, str]) -> bool: - """ - 说明: - 删除用户权限 - 参数: - :param user_id: 用户id - :param group_id: 群组id - """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - await user.delete() - return True - return False - - @classmethod - async def check_level(cls, user_id: Union[int, str], group_id: Union[int, str], level: int) -> bool: - """ - 说明: - 检查用户权限等级是否大于 level - 参数: - :param user_id: 用户id - :param group_id: 群组id - :param level: 权限等级 - """ - if group_id: - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - return user.user_level >= level - else: - user_list = await cls.filter(user_id=str(user_id)).all() - user = max(user_list, key=lambda x: x.user_level) - return user.user_level >= level - return False - - @classmethod - async def is_group_flag(cls, user_id: Union[int, str], group_id: Union[int, str]) -> bool: - """ - 说明: - 检测是否会被自动更新刷新权限 - 参数: - :param user_id: 用户id - :param group_id: 群组id - """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - return user.group_flag == 1 - return False - - @classmethod - async def _run_script(cls): - return ["ALTER TABLE level_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE level_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE level_users ALTER COLUMN group_id TYPE character varying(255);" - ] diff --git a/models/user_shop_gold_log.py b/models/user_shop_gold_log.py deleted file mode 100644 index 41c980f6..00000000 --- a/models/user_shop_gold_log.py +++ /dev/null @@ -1,38 +0,0 @@ -from datetime import datetime - -from tortoise import fields - -from services.db_context import Model - - -class UserShopGoldLog(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - type = fields.IntField() - """金币使用类型 0: 购买, 1: 使用, 2: 插件""" - name = fields.CharField(255) - """商品/插件 名称""" - spend_gold = fields.IntField(default=0) - """花费金币""" - num = fields.IntField() - """数量""" - create_time = fields.DatetimeField(auto_now_add=True) - """创建时间""" - - class Meta: - table = "user_shop_gold_log" - table_description = "金币使用日志表" - - @classmethod - def _run_script(cls): - return [ - "ALTER TABLE user_shop_gold_log RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE user_shop_gold_log ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE user_shop_gold_log ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/about.py b/plugins/about.py deleted file mode 100644 index 5feb5941..00000000 --- a/plugins/about.py +++ /dev/null @@ -1,43 +0,0 @@ -from nonebot import on_regex -from nonebot.rule import to_me -from pathlib import Path - - -__zx_plugin_name__ = "关于" -__plugin_usage__ = """ -usage: - 想要更加了解真寻吗 - 指令: - 关于 -""".strip() -__plugin_des__ = "想要更加了解真寻吗" -__plugin_cmd__ = ["关于"] -__plugin_version__ = 0.1 -__plugin_type__ = ("其他",) -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 1, - "default_status": True, - "limit_superuser": False, - "cmd": ["关于"], -} - - -about = on_regex("^关于$", priority=5, block=True, rule=to_me()) - - -@about.handle() -async def _(): - ver_file = Path() / '__version__' - version = None - if ver_file.exists(): - with open(ver_file, 'r', encoding='utf8') as f: - version = f.read().split(':')[-1].strip() - msg = f""" -『绪山真寻Bot』 -版本:{version} -简介:基于Nonebot2与go-cqhttp开发,是一个非常可爱的Bot呀,希望与大家要好好相处 -项目地址:https://github.com/HibiKier/zhenxun_bot -文档地址:https://hibikier.github.io/zhenxun_bot/ - """.strip() - await about.send(msg) diff --git a/plugins/aconfig/__init__.py b/plugins/aconfig/__init__.py deleted file mode 100755 index 3397495b..00000000 --- a/plugins/aconfig/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import random - -from nonebot import on_command, on_keyword -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.rule import to_me - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH -from utils.message_builder import image -from utils.utils import FreqLimiter - -__zx_plugin_name__ = "基本设置 [Hidden]" -__plugin_usage__ = "用法: 无" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -_flmt = FreqLimiter(300) - - -config_play_game = on_keyword({"打游戏"}, permission=GROUP, priority=1, block=True) - - -@config_play_game.handle() -async def _(event: GroupMessageEvent): - if not _flmt.check(event.group_id): - return - _flmt.start_cd(event.group_id) - await config_play_game.finish( - image(IMAGE_PATH / random.choice(os.listdir(IMAGE_PATH / "dayouxi"))) - ) - - -self_introduction = on_command( - "自我介绍", aliases={"介绍", "你是谁", "你叫什么"}, rule=to_me(), priority=5, block=True -) - - -@self_introduction.handle() -async def _(): - if NICKNAME.find("真寻") != -1: - result = ( - "我叫绪山真寻\n" - "你们可以叫我真寻,小真寻,哪怕你们叫我小寻子我也能接受!\n" - "年龄的话我还是个**岁初中生(至少现在是)\n" - "身高保密!!!(也就比美波里(姐姐..(妹妹))矮一点)\n" - "我生日是在3月6号, 能记住的话我会很高兴的\n现在是自宅警备系的现役JC\n" - "最好的朋友是椛!\n" + image("zhenxun.jpg") - ) - await self_introduction.finish(result) - - -my_wife = on_keyword({"老婆"}, rule=to_me(), priority=5, block=True) - - -@my_wife.handle() -async def _(): - await my_wife.finish(image(IMAGE_PATH / "other" / "laopo.jpg")) diff --git a/plugins/ai/__init__.py b/plugins/ai/__init__.py deleted file mode 100755 index 15ce091c..00000000 --- a/plugins/ai/__init__.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import List - -from nonebot import on_message -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.rule import to_me - -from configs.config import NICKNAME, Config -from models.friend_user import FriendUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.utils import get_message_img, get_message_text - -from .data_source import get_chat_result, hello, no_result - -__zx_plugin_name__ = "AI" -__plugin_usage__ = f""" -usage: - 与{NICKNAME}普普通通的对话吧! -""" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "cmd": ["Ai", "ai", "AI", "aI"], -} -__plugin_configs__ = { - "TL_KEY": {"value": [], "help": "图灵Key", "type": List[str]}, - "ALAPI_AI_CHECK": { - "value": False, - "help": "是否检测青云客骂娘回复", - "default_value": False, - "type": bool, - }, - "TEXT_FILTER": { - "value": ["鸡", "口交"], - "help": "文本过滤器,将敏感词更改为*", - "default_value": [], - "type": List[str], - }, -} -Config.add_plugin_config( - "alapi", "ALAPI_TOKEN", None, help_="在 https://admin.alapi.cn/user/login 登录后获取token" -) - -ai = on_message(rule=to_me(), priority=998) - - -@ai.handle() -async def _(bot: Bot, event: MessageEvent): - msg = get_message_text(event.json()) - img = get_message_img(event.json()) - if "CQ:xml" in str(event.get_message()): - return - # 打招呼 - if (not msg and not img) or msg in [ - "你好啊", - "你好", - "在吗", - "在不在", - "您好", - "您好啊", - "你好", - "在", - ]: - await ai.finish(hello()) - img = img[0] if img else "" - if isinstance(event, GroupMessageEvent): - nickname = await GroupInfoUser.get_user_nickname(event.user_id, event.group_id) - else: - nickname = await FriendUser.get_user_nickname(event.user_id) - if not nickname: - if isinstance(event, GroupMessageEvent): - nickname = event.sender.card or event.sender.nickname - else: - nickname = event.sender.nickname - result = await get_chat_result(msg, img, event.user_id, nickname) - logger.info( - f"USER {event.user_id} GROUP {event.group_id if isinstance(event, GroupMessageEvent) else ''} " - f"问题:{msg} ---- 回答:{result}" - ) - if result: - result = str(result) - for t in Config.get_config("ai", "TEXT_FILTER"): - result = result.replace(t, "*") - await ai.finish(Message(result)) - else: - await ai.finish(no_result()) diff --git a/plugins/alapi/__init__.py b/plugins/alapi/__init__.py deleted file mode 100755 index 749529c7..00000000 --- a/plugins/alapi/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path - -import nonebot - -from configs.config import Config - -Config.add_plugin_config( - "alapi", "ALAPI_TOKEN", None, help_="在https://admin.alapi.cn/user/login登录后获取token" -) - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/alapi/comments_163.py b/plugins/alapi/comments_163.py deleted file mode 100755 index 2176162f..00000000 --- a/plugins/alapi/comments_163.py +++ /dev/null @@ -1,45 +0,0 @@ -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from ._data_source import get_data -from services.log import logger - -__zx_plugin_name__ = "网易云热评" -__plugin_usage__ = """ -usage: - 到点了,还是防不了下塔 - 指令: - 网易云热评/到点了/12点了 -""".strip() -__plugin_des__ = "生了个人,我很抱歉" -__plugin_cmd__ = ["网易云热评", "到点了", "12点了"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["网易云热评", "网易云评论", "到点了", "12点了"], -} - - -comments_163 = on_regex( - "^(网易云热评|网易云评论|到点了|12点了)$", priority=5, block=True -) - - -comments_163_url = "https://v2.alapi.cn/api/comment" - - -@comments_163.handle() -async def _(event: MessageEvent): - data, code = await get_data(comments_163_url) - if code != 200: - await comments_163.finish(data, at_sender=True) - data = data["data"] - comment = data["comment_content"] - song_name = data["title"] - await comments_163.send(f"{comment}\n\t——《{song_name}》") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送网易云热评: {comment} \n\t\t————{song_name}" - ) diff --git a/plugins/alapi/cover.py b/plugins/alapi/cover.py deleted file mode 100755 index 75cd3713..00000000 --- a/plugins/alapi/cover.py +++ /dev/null @@ -1,47 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, Message, GroupMessageEvent -from utils.message_builder import image -from nonebot.params import CommandArg -from ._data_source import get_data -from services.log import logger - -__zx_plugin_name__ = "b封面" -__plugin_usage__ = """ -usage: - b封面 [链接/av/bv/cv/直播id] - 示例:b封面 av86863038 -""".strip() -__plugin_des__ = "快捷的b站视频封面获取方式" -__plugin_cmd__ = ["b封面/B封面"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["b封面", "B封面"], -} - - -cover = on_command("b封面", aliases={"B封面"}, priority=5, block=True) - - -cover_url = "https://v2.alapi.cn/api/bilibili/cover" - - -@cover.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - params = {"c": msg} - data, code = await get_data(cover_url, params) - if code != 200: - await cover.finish(data, at_sender=True) - data = data["data"] - title = data["title"] - img = data["cover"] - await cover.send(Message(f"title:{title}\n{image(img)}")) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 获取b站封面: {title} url:{img}" - ) diff --git a/plugins/alapi/jitang.py b/plugins/alapi/jitang.py deleted file mode 100755 index 4d907684..00000000 --- a/plugins/alapi/jitang.py +++ /dev/null @@ -1,45 +0,0 @@ -from nonebot import on_regex -from services.log import logger -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from ._data_source import get_data - - -__zx_plugin_name__ = "鸡汤" -__plugin_usage__ = """ -usage: - 不喝点什么感觉有点不舒服 - 指令: - 鸡汤 -""".strip() -__plugin_des__ = "喏,亲手为你煮的鸡汤" -__plugin_cmd__ = ["鸡汤"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["鸡汤", "毒鸡汤"], -} - -url = "https://v2.alapi.cn/api/soul" - - -jitang = on_regex("^毒?鸡汤$", priority=5, block=True) - - -@jitang.handle() -async def _(event: MessageEvent): - try: - data, code = await get_data(url) - if code != 200: - await jitang.finish(data, at_sender=True) - await jitang.send(data["data"]["content"]) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送鸡汤:" + data["data"]["content"] - ) - except Exception as e: - await jitang.send("鸡汤煮坏掉了...") - logger.error(f"鸡汤煮坏掉了 {type(e)}:{e}") diff --git a/plugins/alapi/poetry.py b/plugins/alapi/poetry.py deleted file mode 100755 index 1c6ff10d..00000000 --- a/plugins/alapi/poetry.py +++ /dev/null @@ -1,41 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from services.log import logger -from ._data_source import get_data - -__zx_plugin_name__ = "古诗" -__plugin_usage__ = """usage: - 平白无故念首诗 - 示例:念诗/来首诗/念首诗 -""" -__plugin_des__ = "为什么突然文艺起来了!" -__plugin_cmd__ = ["念诗/来首诗/念首诗"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["念诗", "来首诗", "念首诗"], -} - -poetry = on_command("念诗", aliases={"来首诗", "念首诗"}, priority=5, block=True) - - -poetry_url = "https://v2.alapi.cn/api/shici" - - -@poetry.handle() -async def _(event: MessageEvent): - data, code = await get_data(poetry_url) - if code != 200: - await poetry.finish(data, at_sender=True) - data = data["data"] - content = data["content"] - title = data["origin"] - author = data["author"] - await poetry.send(f"{content}\n\t——{author}《{title}》") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送古诗: f'{content}\n\t--{author}《{title}》'" - ) diff --git a/plugins/bilibili_sub/__init__.py b/plugins/bilibili_sub/__init__.py deleted file mode 100755 index 34879661..00000000 --- a/plugins/bilibili_sub/__init__.py +++ /dev/null @@ -1,307 +0,0 @@ -from typing import Any, Optional, Tuple - -import nonebot -from nonebot import Driver, on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import ArgStr, CommandArg, RegexGroup -from nonebot.typing import T_State - -from configs.config import Config -from models.level_user import LevelUser -from services.log import logger -from utils.depends import GetConfig -from utils.image_utils import text2image -from utils.manager import group_manager -from utils.message_builder import image -from utils.utils import get_bot, is_number, scheduler - -from .data_source import ( - BilibiliSub, - SubManager, - add_live_sub, - add_season_sub, - add_up_sub, - delete_sub, - get_media_id, - get_sub_status, -) - -__zx_plugin_name__ = "B站订阅" -__plugin_usage__ = """ -usage: - B站直播,番剧,UP动态开播等提醒 - 主播订阅相当于 直播间订阅 + UP订阅 - 指令:[示例Id乱打的,仅做示例] - 添加订阅 ['主播'/'UP'/'番剧'] [id/链接/番名] - 删除订阅 ['主播'/'UP'/'id'] [id] - 查看订阅 - 示例:添加订阅主播 2345344 <-(直播房间id) - 示例:添加订阅UP 2355543 <-(个人主页id) - 示例:添加订阅番剧 史莱姆 <-(支持模糊搜索) - 示例:添加订阅番剧 125344 <-(番剧id) - 示例:删除订阅id 2324344 <-(任意id,通过查看订阅获取) -""".strip() -__plugin_des__ = "非常便利的B站订阅通知" -__plugin_cmd__ = ["添加订阅 [主播/UP/番剧] [id/链接/番名]", "删除订阅 ['主播'/'UP'/'id'] [id]", "查看订阅"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier & NumberSir" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["B站订阅", "b站订阅", "添加订阅", "删除订阅", "查看订阅"], -} -__plugin_configs__ = { - "GROUP_BILIBILI_SUB_LEVEL": { - "value": 5, - "help": "群内bilibili订阅需要管理的权限", - "default_value": 5, - "type": int, - }, - "LIVE_MSG_AT_ALL": { - "value": False, - "help": "直播提醒是否AT全体(仅在真寻是管理员时生效)", - "default_value": False, - "type": bool, - }, - "UP_MSG_AT_ALL": { - "value": False, - "help": "UP动态投稿提醒是否AT全体(仅在真寻是管理员时生效)", - "default_value": False, - "type": bool, - }, - "DOWNLOAD_DYNAMIC_IMAGE": { - "value": True, - "help": "下载动态中的图片并发送在提醒消息中", - "default_value": True, - "type": bool, - }, -} - -add_sub = on_command("添加订阅", priority=5, block=True) -del_sub = on_command("删除订阅", priority=5, block=True) -show_sub_info = on_regex("^查看订阅$", priority=5, block=True) - -driver: Driver = nonebot.get_driver() - - -sub_manager: SubManager - - -@driver.on_startup -async def _(): - global sub_manager - sub_manager = SubManager() - - -@add_sub.handle() -@del_sub.handle() -async def _( - event: MessageEvent, - state: T_State, - arg: Message = CommandArg(), - sub_level: Optional[int] = GetConfig(config="GROUP_BILIBILI_SUB_LEVEL"), -): - msg = arg.extract_plain_text().strip().split() - if len(msg) < 2: - await add_sub.finish("参数不完全,请查看订阅帮助...") - sub_type = msg[0] - id_ = "" - if isinstance(event, GroupMessageEvent): - if not await LevelUser.check_level( - event.user_id, - event.group_id, - sub_level, # type: ignore - ): - await add_sub.finish( - f"您的权限不足,群内订阅的需要 {sub_level} 级权限..", - at_sender=True, - ) - sub_user = f"{event.user_id}:{event.group_id}" - else: - sub_user = f"{event.user_id}" - state["sub_type"] = sub_type - state["sub_user"] = sub_user - if len(msg) > 1: - if "http" in msg[1]: - msg[1] = msg[1].split("?")[0] - msg[1] = msg[1][:-1] if msg[1][-1] == "/" else msg[1] - msg[1] = msg[1].split("/")[-1] - id_ = msg[1][2:] if msg[1].startswith("md") else msg[1] - if not is_number(id_): - if sub_type in ["season", "动漫", "番剧"]: - rst = "*以为您找到以下番剧,请输入Id选择:*\n" - state["season_data"] = await get_media_id(id_) - if len(state["season_data"]) == 0: # type: ignore - await add_sub.finish(f"未找到番剧:{msg}") - for i, x in enumerate(state["season_data"]): # type: ignore - rst += f'{i + 1}.{state["season_data"][x]["title"]}\n----------\n' # type: ignore - await add_sub.send("\n".join(rst.split("\n")[:-1])) - else: - await add_sub.finish("Id 必须为全数字!") - else: - state["id"] = int(id_) - - -@add_sub.got("sub_type") -@add_sub.got("sub_user") -@add_sub.got("id") -async def _( - event: MessageEvent, - state: T_State, - id_: str = ArgStr("id"), - sub_type: str = ArgStr("sub_type"), - sub_user: str = ArgStr("sub_user"), -): - if sub_type in ["season", "动漫", "番剧"] and state.get("season_data"): - season_data = state["season_data"] - if not is_number(id_) or int(id_) < 1 or int(id_) > len(season_data): - await add_sub.reject_arg("id", "Id必须为数字且在范围内!请重新输入...") - id_ = season_data[int(id_) - 1]["media_id"] - if sub_type in ["主播", "直播"]: - await add_sub.send(await add_live_sub(id_, sub_user)) - elif sub_type.lower() in ["up", "用户"]: - await add_sub.send(await add_up_sub(id_, sub_user)) - elif sub_type in ["season", "动漫", "番剧"]: - await add_sub.send(await add_season_sub(id_, sub_user)) - else: - await add_sub.finish("参数错误,第一参数必须为:主播/up/番剧!") - logger.info( - f"添加订阅:{sub_type} -> {sub_user} -> {id_}", - "添加订阅", - event.user_id, - getattr(event, "group_id", None), - ) - - -@del_sub.got("sub_type") -@del_sub.got("sub_user") -@del_sub.got("id") -async def _( - event: MessageEvent, - id_: str = ArgStr("id"), - sub_type: str = ArgStr("sub_type"), - sub_user: str = ArgStr("sub_user"), -): - if sub_type in ["主播", "直播"]: - result = await BilibiliSub.delete_bilibili_sub(id_, sub_user, "live") - elif sub_type.lower() in ["up", "用户"]: - result = await BilibiliSub.delete_bilibili_sub(id_, sub_user, "up") - else: - result = await BilibiliSub.delete_bilibili_sub(id_, sub_user) - if result: - await del_sub.send(f"删除订阅id:{id_} 成功...") - logger.info( - f"删除订阅 {id_}", - "删除订阅", - event.user_id, - getattr(event, "group_id", None), - ) - else: - await del_sub.send(f"删除订阅id:{id_} 失败...") - logger.info( - f"删除订阅 {id_} 失败", - "删除订阅", - event.user_id, - getattr(event, "group_id", None), - ) - - -@show_sub_info.handle() -async def _(event: MessageEvent): - if isinstance(event, GroupMessageEvent): - id_ = f"{event.group_id}" - else: - id_ = f"{event.user_id}" - data = await BilibiliSub.filter(sub_users__contains=id_).all() - live_rst = "" - up_rst = "" - season_rst = "" - for x in data: - if x.sub_type == "live": - live_rst += ( - f"\t直播间id:{x.sub_id}\n" f"\t名称:{x.uname}\n" f"------------------\n" - ) - if x.sub_type == "up": - up_rst += f"\tUP:{x.uname}\n" f"\tuid:{x.uid}\n" f"------------------\n" - if x.sub_type == "season": - season_rst += ( - f"\t番剧id:{x.sub_id}\n" - f"\t番名:{x.season_name}\n" - f"\t当前集数:{x.season_current_episode}\n" - f"------------------\n" - ) - live_rst = "当前订阅的直播:\n" + live_rst if live_rst else live_rst - up_rst = "当前订阅的UP:\n" + up_rst if up_rst else up_rst - season_rst = "当前订阅的番剧:\n" + season_rst if season_rst else season_rst - if not live_rst and not up_rst and not season_rst: - live_rst = ( - "该群目前没有任何订阅..." if isinstance(event, GroupMessageEvent) else "您目前没有任何订阅..." - ) - await show_sub_info.send( - image( - await text2image( - live_rst + up_rst + season_rst, padding=10, color="#f9f6f2" - ) - ) - ) - - -# 推送 -@scheduler.scheduled_job( - "interval", - seconds=30, -) -async def _(): - bot = get_bot() - sub = None - if bot: - await sub_manager.reload_sub_data() - sub = await sub_manager.random_sub_data() - if sub: - try: - logger.debug(f"Bilibili订阅开始检测:{sub.sub_id}") - rst = await get_sub_status(sub.sub_id, sub.sub_id, sub.sub_type) - await send_sub_msg(rst, sub, bot) # type: ignore - if sub.sub_type == "live": - rst += "\n" + await get_sub_status(sub.uid, sub.sub_id, "up") - await send_sub_msg(rst, sub, bot) # type: ignore - except Exception as e: - logger.error(f"B站订阅推送发生错误 sub_id:{sub.sub_id}", e=e) - - -async def send_sub_msg(rst: str, sub: BilibiliSub, bot: Bot): - """ - 推送信息 - :param rst: 回复 - :param sub: BilibiliSub - :param bot: Bot - """ - temp_group = [] - if rst and rst.strip(): - for x in sub.sub_users.split(",")[:-1]: - try: - if ":" in x and x.split(":")[1] not in temp_group: - group_id = int(x.split(":")[1]) - temp_group.append(group_id) - if ( - await bot.get_group_member_info( - group_id=group_id, user_id=int(bot.self_id), no_cache=True - ) - )["role"] in ["owner", "admin"]: - if ( - sub.sub_type == "live" - and Config.get_config("bilibili_sub", "LIVE_MSG_AT_ALL") - ) or ( - sub.sub_type == "up" - and Config.get_config("bilibili_sub", "UP_MSG_AT_ALL") - ): - rst = "[CQ:at,qq=all]\n" + rst - if group_manager.get_plugin_status("bilibili_sub", group_id): - await bot.send_group_msg( - group_id=group_id, message=Message(rst) - ) - else: - await bot.send_private_msg(user_id=int(x), message=Message(rst)) - except Exception as e: - logger.error(f"B站订阅推送发生错误 sub_id: {sub.sub_id}", e=e) diff --git a/plugins/bilibili_sub/data_source.py b/plugins/bilibili_sub/data_source.py deleted file mode 100755 index d0958357..00000000 --- a/plugins/bilibili_sub/data_source.py +++ /dev/null @@ -1,451 +0,0 @@ -import random -from asyncio.exceptions import TimeoutError -from datetime import datetime -from typing import Optional, Tuple, Union - -# from .utils import get_videos -from bilireq import dynamic -from bilireq.exceptions import ResponseCodeError -from bilireq.grpc.dynamic import grpc_get_user_dynamics -from bilireq.grpc.protos.bilibili.app.dynamic.v2.dynamic_pb2 import DynamicType -from bilireq.live import get_room_info_by_id -from bilireq.user import get_videos -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.browser import get_browser -from utils.http_utils import AsyncHttpx, AsyncPlaywright -from utils.manager import resources_manager -from utils.message_builder import image -from utils.utils import get_bot, get_local_proxy - -from .model import BilibiliSub -from .utils import get_meta, get_user_card - -SEARCH_URL = "https://api.bilibili.com/x/web-interface/search/all/v2" - -DYNAMIC_PATH = IMAGE_PATH / "bilibili_sub" / "dynamic" -DYNAMIC_PATH.mkdir(exist_ok=True, parents=True) - - -TYPE2MSG = { - 0: "发布了新动态", - DynamicType.forward: "转发了一条动态", - DynamicType.word: "发布了新文字动态", - DynamicType.draw: "发布了新图文动态", - DynamicType.av: "发布了新投稿", - DynamicType.article: "发布了新专栏", - DynamicType.music: "发布了新音频", -} - - -resources_manager.add_temp_dir(DYNAMIC_PATH) - - -async def add_live_sub(live_id: str, sub_user: str) -> str: - """ - 添加直播订阅 - :param live_id: 直播房间号 - :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) - :return: - """ - try: - if await BilibiliSub.exists( - sub_type="live", sub_id=live_id, sub_users__contains=sub_user + "," - ): - return "该订阅Id已存在..." - try: - """bilibili_api.live库的LiveRoom类中get_room_info改为bilireq.live库的get_room_info_by_id方法""" - live_info = await get_room_info_by_id(live_id) - except ResponseCodeError: - return f"未找到房间号Id:{live_id} 的信息,请检查Id是否正确" - uid = str(live_info["uid"]) - room_id = live_info["room_id"] - short_id = live_info["short_id"] - title = live_info["title"] - live_status = live_info["live_status"] - try: - user_info = await get_user_card(uid) - except ResponseCodeError: - return f"未找到UpId:{uid} 的信息,请检查Id是否正确" - uname = user_info["name"] - dynamic_info = await dynamic.get_user_dynamics(int(uid)) - dynamic_upload_time = 0 - if dynamic_info.get("cards"): - dynamic_upload_time = dynamic_info["cards"][0]["desc"]["dynamic_id"] - if await BilibiliSub.sub_handle( - room_id, - "live", - sub_user, - uid=uid, - live_short_id=short_id, - live_status=live_status, - uname=uname, - dynamic_upload_time=dynamic_upload_time, - ): - if data := await BilibiliSub.get_or_none(sub_id=room_id): - uname = data.uname - return ( - "已成功订阅主播:\n" - f"\ttitle:{title}\n" - f"\tname: {uname}\n" - f"\tlive_id:{room_id}\n" - f"\tuid:{uid}" - ) - return "添加订阅失败..." - except Exception as e: - logger.error(f"订阅主播live_id: {live_id} 错误", e=e) - return "添加订阅失败..." - - -async def add_up_sub(uid: str, sub_user: str) -> str: - """ - 添加订阅 UP - :param uid: UP uid - :param sub_user: 订阅用户 - """ - uname = uid - dynamic_upload_time = 0 - latest_video_created = 0 - try: - if await BilibiliSub.exists( - sub_type="up", sub_id=uid, sub_users__contains=sub_user + "," - ): - return "该订阅Id已存在..." - try: - """bilibili_api.user库中User类的get_user_info改为bilireq.user库的get_user_info方法""" - user_info = await get_user_card(uid) - except ResponseCodeError: - return f"未找到UpId:{uid} 的信息,请检查Id是否正确" - uname = user_info["name"] - """bilibili_api.user库中User类的get_dynamics改为bilireq.dynamic库的get_user_dynamics方法""" - dynamic_info = await dynamic.get_user_dynamics(int(uid)) - if dynamic_info.get("cards"): - dynamic_upload_time = dynamic_info["cards"][0]["desc"]["dynamic_id"] - except Exception as e: - logger.error(f"订阅Up uid: {uid} 错误", e=e) - if await BilibiliSub.sub_handle( - uid, - "up", - sub_user, - uid=uid, - uname=uname, - dynamic_upload_time=dynamic_upload_time, - latest_video_created=latest_video_created, - ): - return "已成功订阅UP:\n" f"\tname: {uname}\n" f"\tuid:{uid}" - else: - return "添加订阅失败..." - - -async def add_season_sub(media_id: str, sub_user: str) -> str: - """ - 添加订阅 UP - :param media_id: 番剧 media_id - :param sub_user: 订阅用户 - """ - try: - if await BilibiliSub.exists( - sub_type="season", sub_id=media_id, sub_users__contains=sub_user + "," - ): - return "该订阅Id已存在..." - try: - """bilibili_api.bangumi库中get_meta改为bilireq.bangumi库的get_meta方法""" - season_info = await get_meta(media_id) - except ResponseCodeError: - return f"未找到media_id:{media_id} 的信息,请检查Id是否正确" - season_id = season_info["media"]["season_id"] - season_current_episode = season_info["media"]["new_ep"]["index"] - season_name = season_info["media"]["title"] - if await BilibiliSub.sub_handle( - media_id, - "season", - sub_user, - season_name=season_name, - season_id=season_id, - season_current_episode=season_current_episode, - ): - return ( - "已成功订阅番剧:\n" - f"\ttitle: {season_name}\n" - f"\tcurrent_episode: {season_current_episode}" - ) - else: - return "添加订阅失败..." - except Exception as e: - logger.error(f"订阅番剧 media_id: {media_id} 错误", e=e) - return "添加订阅失败..." - - -async def delete_sub(sub_id: str, sub_user: str) -> str: - """ - 删除订阅 - :param sub_id: 订阅 id - :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) - """ - if await BilibiliSub.delete_bilibili_sub(sub_id, sub_user): - return f"已成功取消订阅:{sub_id}" - else: - return f"取消订阅:{sub_id} 失败,请检查是否订阅过该Id...." - - -async def get_media_id(keyword: str) -> Optional[dict]: - """ - 获取番剧的 media_id - :param keyword: 番剧名称 - """ - params = {"keyword": keyword} - for _ in range(3): - try: - _season_data = {} - response = await AsyncHttpx.get(SEARCH_URL, params=params, timeout=5) - if response.status_code == 200: - data = response.json() - if data.get("data"): - for item in data["data"]["result"]: - if item["result_type"] == "media_bangumi": - idx = 0 - for x in item["data"]: - _season_data[idx] = { - "media_id": x["media_id"], - "title": x["title"] - .replace('', "") - .replace("", ""), - } - idx += 1 - return _season_data - except TimeoutError: - pass - return {} - - -async def get_sub_status(id_: str, sub_id: str, sub_type: str) -> Union[Message, str]: - """ - 获取订阅状态 - :param id_: 订阅 id - :param sub_type: 订阅类型 - """ - try: - if sub_type == "live": - return await _get_live_status(id_) - elif sub_type == "up": - return await _get_up_status(id_, sub_id) - elif sub_type == "season": - return await _get_season_status(id_) - except ResponseCodeError as e: - logger.error(f"Id:{id_} 获取信息失败...", e=e) - # return f"Id:{id_} 获取信息失败...请检查订阅Id是否存在或稍后再试..." - except Exception as e: - logger.error(f"获取订阅状态发生预料之外的错误 Id_:{id_}", e=e) - # return "发生了预料之外的错误..请稍后再试或联系管理员....." - return "" - - -async def _get_live_status(id_: str) -> str: - """ - 获取直播订阅状态 - :param id_: 直播间 id - """ - """bilibili_api.live库的LiveRoom类中get_room_info改为bilireq.live库的get_room_info_by_id方法""" - live_info = await get_room_info_by_id(id_) - title = live_info["title"] - room_id = live_info["room_id"] - live_status = live_info["live_status"] - cover = live_info["user_cover"] - if sub := await BilibiliSub.get_or_none(sub_id=id_): - if sub.live_status != live_status: - await BilibiliSub.sub_handle(id_, live_status=live_status) - if sub.live_status in [0, 2] and live_status == 1: - return ( - f"" - f"{image(cover)}\n" - f"{sub.uname} 开播啦!\n" - f"标题:{title}\n" - f"直链:https://live.bilibili.com/{room_id}" - ) - return "" - - -async def _get_up_status( - id_: str, live_id: Optional[str] = None -) -> Union[Message, str]: - """获取up动态 - - 参数: - id_: up的id - live_id: 直播间id,当订阅直播间时才有. - - 返回: - Union[Message, str]: 消息 - """ - rst = "" - if _user := await BilibiliSub.get_or_none(sub_id=live_id or id_): - dynamics = None - dynamic = None - uname = "" - try: - dynamics = ( - await grpc_get_user_dynamics(int(id_), proxy=get_local_proxy()) - ).list - except Exception as e: - logger.error("获取动态失败...", target=id_, e=e) - if dynamics: - uname = dynamics[0].modules[0].module_author.author.name - for dyn in dynamics: - if int(dyn.extend.dyn_id_str) > _user.dynamic_upload_time: - dynamic = dyn - break - if not dynamic: - logger.debug(f"{_user.sub_type}:{id_} 未有任何动态, 已跳过....") - return "" - if _user.uname != uname: - await BilibiliSub.sub_handle(live_id or id_, uname=uname) - dynamic_img, link = await get_user_dynamic(dynamic.extend.dyn_id_str, _user) - if not dynamic_img: - logger.debug(f"{id_} 未发布新动态或截图失败, 已跳过....") - return "" - await BilibiliSub.sub_handle( - live_id or id_, dynamic_upload_time=int(dynamic.extend.dyn_id_str) - ) - rst += ( - f"{uname} {TYPE2MSG.get(dynamic.card_type, TYPE2MSG[0])}!\n" - + dynamic_img - + f"\n{link}\n" - ) - video_info = "" - if video_list := [ - module - for module in dynamic.modules - if str(module.module_dynamic.dyn_archive) - ]: - video = video_list[0].module_dynamic.dyn_archive - video_info = ( - image(video.cover) - + f"标题: {video.title}\nBvid: {video.bvid}\n直链: https://www.bilibili.com/video/{video.bvid}" - ) - rst += video_info + "\n" - download_dynamic_image = Config.get_config( - "bilibili_sub", "DOWNLOAD_DYNAMIC_IMAGE" - ) - draw_info = "" - if download_dynamic_image and ( - draw_list := [ - module.module_dynamic.dyn_draw - for module in dynamic.modules - if str(module.module_dynamic.dyn_draw) - ] - ): - idx = 0 - for draws in draw_list: - for draw in list(draws.items): - path = ( - TEMP_PATH - / f"{_user.uid}_{dynamic.extend.dyn_id_str}_draw_{idx}.jpg" - ) - if await AsyncHttpx.download_file(draw.src, path): - draw_info += image(path) - idx += 1 - if draw_info: - rst += "动态图片\n" + draw_info + "\n" - return rst - - -async def _get_season_status(id_: str) -> str: - """ - 获取 番剧 更新状态 - :param id_: 番剧 id - """ - """bilibili_api.bangumi库中get_meta改为bilireq.bangumi库的get_meta方法""" - season_info = await get_meta(id_) - title = season_info["media"]["title"] - if data := await BilibiliSub.get_or_none(sub_id=id_): - _idx = data.season_current_episode - new_ep = season_info["media"]["new_ep"]["index"] - if new_ep != _idx: - await BilibiliSub.sub_handle( - id_, season_current_episode=new_ep, season_update_time=datetime.now() - ) - return ( - f'{image(season_info["media"]["cover"])}\n' - f"[{title}]更新啦\n" - f"最新集数:{new_ep}" - ) - return "" - - -async def get_user_dynamic( - dynamic_id: str, local_user: BilibiliSub -) -> Tuple[Optional[MessageSegment], str]: - """ - 获取用户动态 - :param dynamic_id: 动态id - :param local_user: 数据库存储的用户数据 - :return: 最新动态截图与时间 - """ - if local_user.dynamic_upload_time < int(dynamic_id): - image = await AsyncPlaywright.screenshot( - f"https://t.bilibili.com/{dynamic_id}", - DYNAMIC_PATH / f"sub_{local_user.sub_id}.png", - ".bili-dyn-item__main", - wait_until="networkidle", - ) - return ( - image, - f"https://t.bilibili.com/{dynamic_id}", - ) - return None, "" - - -class SubManager: - def __init__(self): - self.live_data = [] - self.up_data = [] - self.season_data = [] - self.current_index = -1 - - async def reload_sub_data(self): - """ - 重载数据 - """ - if not self.live_data or not self.up_data or not self.season_data: - ( - _live_data, - _up_data, - _season_data, - ) = await BilibiliSub.get_all_sub_data() - if not self.live_data: - self.live_data = _live_data - if not self.up_data: - self.up_data = _up_data - if not self.season_data: - self.season_data = _season_data - - async def random_sub_data(self) -> Optional[BilibiliSub]: - """ - 随机获取一条数据 - :return: - """ - sub = None - if not self.live_data and not self.up_data and not self.season_data: - return sub - self.current_index += 1 - if self.current_index == 0: - if self.live_data: - sub = random.choice(self.live_data) - self.live_data.remove(sub) - elif self.current_index == 1: - if self.up_data: - sub = random.choice(self.up_data) - self.up_data.remove(sub) - elif self.current_index == 2: - if self.season_data: - sub = random.choice(self.season_data) - self.season_data.remove(sub) - else: - self.current_index = -1 - if sub: - return sub - await self.reload_sub_data() - return await self.random_sub_data() diff --git a/plugins/bilibili_sub/model.py b/plugins/bilibili_sub/model.py deleted file mode 100755 index 28c6cf7f..00000000 --- a/plugins/bilibili_sub/model.py +++ /dev/null @@ -1,207 +0,0 @@ -from datetime import datetime -from typing import List, Optional, Tuple - -from tortoise import fields - -from services.db_context import Model -from services.log import logger - - -class BilibiliSub(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - sub_id = fields.CharField(255) - """订阅id""" - sub_type = fields.CharField(255) - """订阅类型""" - sub_users = fields.TextField() - """订阅用户""" - live_short_id = fields.CharField(255, null=True) - """直播短id""" - live_status = fields.IntField(null=True) - """直播状态 0: 停播 1: 直播""" - uid = fields.CharField(255, null=True) - """主播/UP UID""" - uname = fields.CharField(255, null=True) - """主播/UP 名称""" - latest_video_created = fields.BigIntField(null=True) - """最后视频上传时间""" - dynamic_upload_time = fields.BigIntField(null=True, default=0) - """动态发布时间""" - season_name = fields.CharField(255, null=True) - """番剧名称""" - season_id = fields.IntField(null=True) - """番剧id""" - season_current_episode = fields.CharField(255, null=True) - """番剧最新集数""" - season_update_time = fields.DateField(null=True) - """番剧更新日期""" - - class Meta: - table = "bilibili_sub" - table_description = "B站订阅数据表" - unique_together = ("sub_id", "sub_type") - - @classmethod - async def sub_handle( - cls, - sub_id: str, - sub_type: Optional[str] = None, - sub_user: str = "", - *, - live_short_id: Optional[str] = None, - live_status: Optional[int] = None, - dynamic_upload_time: int = 0, - uid: Optional[str] = None, - uname: Optional[str] = None, - latest_video_created: Optional[int] = None, - season_name: Optional[str] = None, - season_id: Optional[int] = None, - season_current_episode: Optional[str] = None, - season_update_time: Optional[datetime] = None, - ) -> bool: - """ - 说明: - 添加订阅 - 参数: - :param sub_id: 订阅名称,房间号,番剧号等 - :param sub_type: 订阅类型 - :param sub_user: 订阅此条目的用户 - :param live_short_id: 直接短 id - :param live_status: 主播开播状态 - :param dynamic_upload_time: 主播/UP最新动态时间 - :param uid: 主播/UP uid - :param uname: 用户名称 - :param latest_video_created: 最新视频上传时间 - :param season_name: 番剧名称 - :param season_id: 番剧 season_id - :param season_current_episode: 番剧最新集数 - :param season_update_time: 番剧更新时间 - """ - sub_id = str(sub_id) - try: - data = { - "sub_type": sub_type, - "sub_user": sub_user, - "live_short_id": live_short_id, - "live_status": live_status, - "dynamic_upload_time": dynamic_upload_time, - "uid": uid, - "uname": uname, - "latest_video_created": latest_video_created, - "season_name": season_name, - "season_id": season_id, - "season_current_episode": season_current_episode, - "season_update_time": season_update_time, - } - if sub_user: - sub_user = sub_user if sub_user[-1] == "," else f"{sub_user}," - sub = None - if sub_type: - sub = await cls.get_or_none(sub_id=sub_id, sub_type=sub_type) - else: - sub = await cls.get_or_none(sub_id=sub_id) - if sub: - sub_users = sub.sub_users + sub_user - data["sub_type"] = sub_type or sub.sub_type - data["sub_users"] = sub_users - data["live_short_id"] = live_short_id or sub.live_short_id - data["live_status"] = ( - live_status if live_status is not None else sub.live_status - ) - data["dynamic_upload_time"] = ( - dynamic_upload_time or sub.dynamic_upload_time - ) - data["uid"] = uid or sub.uid - data["uname"] = uname or sub.uname - data["latest_video_created"] = ( - latest_video_created or sub.latest_video_created - ) - data["season_name"] = season_name or sub.season_name - data["season_id"] = season_id or sub.season_id - data["season_current_episode"] = ( - season_current_episode or sub.season_current_episode - ) - data["season_update_time"] = ( - season_update_time or sub.season_update_time - ) - else: - await cls.create(sub_id=sub_id, sub_type=sub_type, sub_users=sub_user) - await cls.update_or_create(sub_id=sub_id, defaults=data) - return True - except Exception as e: - logger.error(f"添加订阅 Id: {sub_id} 错误", e=e) - return False - - @classmethod - async def delete_bilibili_sub( - cls, sub_id: str, sub_user: str, sub_type: Optional[str] = None - ) -> bool: - """ - 说明: - 删除订阅 - 参数: - :param sub_id: 订阅名称 - :param sub_user: 删除此条目的用户 - """ - try: - group_id = None - contains_str = sub_user - if ":" in sub_user: - group_id = sub_user.split(":")[1] - contains_str = f":{group_id}" - if sub_type: - sub = await cls.get_or_none( - sub_id=sub_id, sub_type=sub_type, sub_users__contains=contains_str - ) - else: - sub = await cls.get_or_none( - sub_id=sub_id, sub_users__contains=contains_str - ) - if not sub: - return False - if group_id: - sub.sub_users = ",".join( - [s for s in sub.sub_users.split(",") if f":{group_id}" not in s] - ) - else: - sub.sub_users = sub.sub_users.replace(f"{sub_user},", "") - if sub.sub_users.strip(): - await sub.save(update_fields=["sub_users"]) - else: - await sub.delete() - return True - except Exception as e: - logger.error(f"bilibili_sub 删除订阅错误", target=sub_id, e=e) - return False - - @classmethod - async def get_all_sub_data( - cls, - ) -> Tuple[List["BilibiliSub"], List["BilibiliSub"], List["BilibiliSub"]]: - """ - 说明: - 分类获取所有数据 - """ - live_data = [] - up_data = [] - season_data = [] - query = await cls.all() - for x in query: - if x.sub_type == "live": - live_data.append(x) - if x.sub_type == "up": - up_data.append(x) - if x.sub_type == "season": - season_data.append(x) - return live_data, up_data, season_data - - @classmethod - def _run_script(cls): - return [ - "ALTER TABLE bilibili_sub ALTER COLUMN season_update_time TYPE timestamp with time zone USING season_update_time::timestamp with time zone;", - "alter table bilibili_sub alter COLUMN sub_id type varchar(255);", # 将sub_id字段改为字符串 - "alter table bilibili_sub alter COLUMN live_short_id type varchar(255);", # 将live_short_id字段改为字符串 - "alter table bilibili_sub alter COLUMN uid type varchar(255);", # 将live_short_id字段改为字符串 - ] diff --git a/plugins/bilibili_sub/utils.py b/plugins/bilibili_sub/utils.py deleted file mode 100755 index ee2094f4..00000000 --- a/plugins/bilibili_sub/utils.py +++ /dev/null @@ -1,150 +0,0 @@ -from io import BytesIO - -# from bilibili_api import user -from bilireq.user import get_user_info -from httpx import AsyncClient - -from configs.path_config import IMAGE_PATH -from utils.http_utils import AsyncHttpx, get_user_agent -from utils.image_utils import BuildImage - -BORDER_PATH = IMAGE_PATH / "border" -BORDER_PATH.mkdir(parents=True, exist_ok=True) -BASE_URL = "https://api.bilibili.com" - - -async def get_pic(url: str) -> bytes: - """ - 获取图像 - :param url: 图像链接 - :return: 图像二进制 - """ - return (await AsyncHttpx.get(url, timeout=10)).content - - -async def create_live_des_image(uid: int, title: str, cover: str, tags: str, des: str): - """ - 生成主播简介图片 - :param uid: 主播 uid - :param title: 直播间标题 - :param cover: 直播封面 - :param tags: 直播标签 - :param des: 直播简介 - :return: - """ - - user_info = await get_user_info(uid) - name = user_info["name"] - sex = user_info["sex"] - face = user_info["face"] - sign = user_info["sign"] - ava = BuildImage(100, 100, background=BytesIO(await get_pic(face))) - ava.circle() - cover = BuildImage(470, 265, background=BytesIO(await get_pic(cover))) - - -def _create_live_des_image( - title: str, - cover: BuildImage, - tags: str, - des: str, - user_name: str, - sex: str, - sign: str, - ava: BuildImage, -): - """ - 生成主播简介图片 - :param title: 直播间标题 - :param cover: 直播封面 - :param tags: 直播标签 - :param des: 直播简介 - :param user_name: 主播名称 - :param sex: 主播性别 - :param sign: 主播签名 - :param ava: 主播头像 - :return: - """ - border = BORDER_PATH / "0.png" - border_img = None - if border.exists(): - border_img = BuildImage(1772, 2657, background=border) - bk = BuildImage(1772, 2657, font_size=30) - bk.paste(cover, (0, 100), center_type="by_width") - - -async def get_meta(media_id: str, auth=None, reqtype="both", **kwargs): - """ - 根据番剧 ID 获取番剧元数据信息, - 作为bilibili_api和bilireq的替代品。 - 如果bilireq.bangumi更新了,可以转为调用bilireq.bangumi的get_meta方法,两者完全一致。 - """ - from bilireq.utils import get - - url = f"{BASE_URL}/pgc/review/user" - params = {"media_id": media_id} - raw_json = await get( - url, raw=True, params=params, auth=auth, reqtype=reqtype, **kwargs - ) - return raw_json["result"] - - -async def get_videos( - uid: int, tid: int = 0, pn: int = 1, keyword: str = "", order: str = "pubdate" -): - """ - 获取用户投该视频信息 - 作为bilibili_api和bilireq的替代品。 - 如果bilireq.user更新了,可以转为调用bilireq.user的get_videos方法,两者完全一致。 - - :param uid: 用户 UID - :param tid: 分区 ID - :param pn: 页码 - :param keyword: 搜索关键词 - :param order: 排序方式,可以为 “pubdate(上传日期从新到旧), stow(收藏从多到少), click(播放量从多到少)” - """ - from bilireq.utils import ResponseCodeError - - url = f"{BASE_URL}/x/space/arc/search" - headers = get_user_agent() - headers["Referer"] = f"https://space.bilibili.com/{uid}/video" - async with AsyncClient() as client: - r = await client.head( - "https://space.bilibili.com", - headers=headers, - ) - params = { - "mid": uid, - "ps": 30, - "tid": tid, - "pn": pn, - "keyword": keyword, - "order": order, - } - raw_json = ( - await client.get(url, params=params, headers=headers, cookies=r.cookies) - ).json() - if raw_json["code"] != 0: - raise ResponseCodeError( - code=raw_json["code"], - msg=raw_json["message"], - data=raw_json.get("data", None), - ) - return raw_json["data"] - - -async def get_user_card( - mid: str, photo: bool = False, auth=None, reqtype="both", **kwargs -): - from bilireq.utils import get - - url = f"{BASE_URL}/x/web-interface/card" - return ( - await get( - url, - params={"mid": mid, "photo": photo}, - auth=auth, - reqtype=reqtype, - **kwargs, - ) - )["card"] diff --git a/plugins/black_word/__init__.py b/plugins/black_word/__init__.py deleted file mode 100644 index fde8e4dd..00000000 --- a/plugins/black_word/__init__.py +++ /dev/null @@ -1,278 +0,0 @@ -from datetime import datetime -from typing import Any, List, Tuple - -from nonebot import on_command, on_message, on_regex -from nonebot.adapters.onebot.v11 import ( - Bot, - Event, - GroupMessageEvent, - Message, - MessageEvent, -) -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor -from nonebot.params import CommandArg, RegexGroup -from nonebot.permission import SUPERUSER - -from configs.config import NICKNAME, Config -from models.ban_user import BanUser -from services.log import logger -from utils.image_utils import BuildImage -from utils.manager import group_manager -from utils.message_builder import image -from utils.utils import get_message_text, is_number - -from .data_source import set_user_punish, show_black_text_image -from .model import BlackWord -from .utils import black_word_manager - -__zx_plugin_name__ = "敏感词检测" -__plugin_usage__ = """ -usage: - 注意你的发言! - 指令: - 惩罚机制 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 查看和设置惩罚 - Regex:^记录名单(u:\d*)?(g:\d*)?(d[=><]\d*-\d{1,2}-\d{1,2})?$ - 设置惩罚id需要通过 '记录名单u:xxxxxxxx' 获取 - 指令: - 记录名单 - 设置惩罚 [user_id] [下标] [惩罚等级] - 示例:记录名单 - 示例:记录名单u:12345678 - 示例:设置惩罚 12345678 1 4 -""".strip() -__plugin_des__ = "请注意你的发言!!" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_cmd__ = ["惩罚机制", "记录名单 [_superuser]", "设置惩罚 [_superuser]"] -__plugin_settings__ = { - "cmd": ["敏感词检测"], -} - - -Config.add_plugin_config( - "black_word", - "CYCLE_DAYS", - 30, - name="敏感词检测与惩罚", - help_="黑名单词汇记录周期", - default_value=30, - type=int, -) - -Config.add_plugin_config( - "black_word", - "TOLERATE_COUNT", - [5, 1, 1, 1, 1], - help_="各个级别惩罚的容忍次数,依次为:1, 2, 3, 4, 5", - default_value=[5, 1, 1, 1, 1], - type=List[int], -) - -Config.add_plugin_config( - "black_word", "AUTO_PUNISH", True, help_="是否启动自动惩罚机制", default_value=True, type=bool -) - -# Config.add_plugin_config( -# "black_word", "IGNORE_GROUP", [], help_="退出群聊惩罚中忽略的群聊,即不会退出的群聊", default_value=[] -# ) - -Config.add_plugin_config( - "black_word", - "BAN_4_DURATION", - 360, - help_="Union[int, List[int, int]]Ban时长(分钟),四级惩罚,可以为指定数字或指定列表区间(随机),例如 [30, 360]", - default_value=360, - type=int, -) - -Config.add_plugin_config( - "black_word", - "BAN_3_DURATION", - 7, - help_="Union[int, List[int, int]]Ban时长(天),三级惩罚,可以为指定数字或指定列表区间(随机),例如 [7, 30]", - default_value=7, - type=int, -) - -Config.add_plugin_config( - "black_word", - "WARNING_RESULT", - f"请注意对{NICKNAME}的发言内容", - help_="口头警告内容", - default_value=f"请注意对{NICKNAME}的发言内容", -) - -Config.add_plugin_config( - "black_word", - "AUTO_ADD_PUNISH_LEVEL", - True, - help_="自动提级机制,当周期内处罚次数大于某一特定值就提升惩罚等级", - default_value=True, - type=bool, -) - -Config.add_plugin_config( - "black_word", - "ADD_PUNISH_LEVEL_TO_COUNT", - 3, - help_="在CYCLE_DAYS周期内触发指定惩罚次数后提升惩罚等级", - default_value=3, - type=int, -) - -Config.add_plugin_config( - "black_word", - "ALAPI_CHECK_FLAG", - False, - help_="当未检测到已收录的敏感词时,开启ALAPI文本检测并将疑似文本发送给超级用户", - default_value=False, - type=bool, -) - -Config.add_plugin_config( - "black_word", - "CONTAIN_BLACK_STOP_PROPAGATION", - True, - help_="当文本包含任意敏感词时,停止向下级插件传递,即不触发ai", - default_value=True, - type=bool, -) - -message_matcher = on_message(priority=1, block=False) - -set_punish = on_command("设置惩罚", priority=1, permission=SUPERUSER, block=True) - -show_black = on_regex( - r"^记录名单(u:\d*)?(g:\d*)?(d[=><]\d*-\d{1,2}-\d{1,2})?$", - priority=1, - permission=SUPERUSER, - block=True, -) - -show_punish = on_command("惩罚机制", aliases={"敏感词检测"}, priority=1, block=True) - - -# 黑名单词汇检测 -@run_preprocessor -async def _( - bot: Bot, - matcher: Matcher, - event: Event, -): - msg = get_message_text(event.json()) - if ( - isinstance(event, MessageEvent) - and event.is_tome() - and not msg.startswith("原神绑定") - ): - if str(event.user_id) in bot.config.superusers: - return logger.debug(f"超级用户跳过黑名单词汇检查 Message: {msg}", target=event.user_id) - if ( - event.is_tome() - and matcher.plugin_name == "black_word" - and not await BanUser.is_ban(event.user_id) - ): - # 屏蔽群权限-1的群 - if ( - isinstance(event, GroupMessageEvent) - and group_manager.get_group_level(event.group_id) < 0 - ): - return - user_id = str(event.user_id) - group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else None - msg = get_message_text(event.json()) - if await black_word_manager.check( - user_id, group_id, msg - ) and Config.get_config("black_word", "CONTAIN_BLACK_STOP_PROPAGATION"): - matcher.stop_propagation() - - -@show_black.handle() -async def _(bot: Bot, reg_group: Tuple[Any, ...] = RegexGroup()): - user_id, group_id, date = reg_group - date_type = "=" - if date: - date_type = date[1] - date = date[2:] - try: - date = datetime.strptime(date, "%Y-%m-%d") - except ValueError: - await show_black.finish("日期格式错误,需要:年-月-日") - pic = await show_black_text_image( - bot, - user_id.split(":")[1] if user_id else None, - group_id.split(":")[1] if group_id else None, - date, - date_type, - ) - await show_black.send(image(b64=pic.pic2bs4())) - - -@show_punish.handle() -async def _(): - text = f""" - ** 惩罚机制 ** - - 惩罚前包含容忍机制,在指定周期内会容忍偶尔少次数的敏感词只会进行警告提醒 - - 多次触发同级惩罚会使惩罚等级提高,即惩罚自动提级机制 - - 目前公开的惩罚等级: - - 1级:永久ban - - 2级:删除好友 - - 3级:ban指定/随机天数 - - 4级:ban指定/随机时长 - - 5级:警告 - - 备注: - - 该功能为测试阶段,如果你有被误封情况,请联系管理员,会从数据库中提取出你的数据进行审核后判断 - - 目前该功能暂不完善,部分情况会由管理员鉴定,请注意对真寻的发言 - - 关于敏感词: - - 记住不要骂{NICKNAME}就对了! - """.strip() - max_width = 0 - for m in text.split("\n"): - max_width = len(m) * 20 if len(m) * 20 > max_width else max_width - max_height = len(text.split("\n")) * 24 - A = BuildImage( - max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1" - ) - A.text((10, 10), text) - await show_punish.send(image(b64=A.pic2bs4())) - - -@set_punish.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - if ( - len(msg) < 3 - or not is_number(msg[0]) - or not is_number(msg[1]) - or not is_number(msg[2]) - ): - await set_punish.finish("参数错误,请查看帮助...", at_sender=True) - uid = msg[0] - id_ = int(msg[1]) - punish_level = int(msg[2]) - rst = await set_user_punish(uid, id_, punish_level) - await set_punish.send(rst) - logger.info( - f"设置惩罚 uid:{uid} id_:{id_} punish_level:{punish_level} --> {rst}", - "设置惩罚", - event.user_id, - ) diff --git a/plugins/black_word/data_source.py b/plugins/black_word/data_source.py deleted file mode 100644 index 2da848d2..00000000 --- a/plugins/black_word/data_source.py +++ /dev/null @@ -1,121 +0,0 @@ -from datetime import datetime -from typing import Optional - -from nonebot.adapters.onebot.v11 import Bot - -from services.log import logger -from utils.image_utils import BuildImage, text2image - -from .model import BlackWord -from .utils import Config, _get_punish - - -async def show_black_text_image( - bot: Bot, - user_id: Optional[str], - group_id: Optional[str], - date: Optional[datetime], - data_type: str = "=", -) -> BuildImage: - """ - 展示记录名单 - :param bot: bot - :param user: 用户qq - :param group_id: 群聊 - :param date: 日期 - :param data_type: 日期搜索类型 - :return: - """ - data = await BlackWord.get_black_data(user_id, group_id, date, data_type) - A = BuildImage(0, 0, color="#f9f6f2", font_size=20) - image_list = [] - friend_str = await bot.get_friend_list() - id_str = "" - uname_str = "" - uid_str = "" - gid_str = "" - plant_text_str = "" - black_word_str = "" - punish_str = "" - punish_level_str = "" - create_time_str = "" - for i, x in enumerate(data): - try: - if x.group_id: - user_name = ( - await bot.get_group_member_info( - group_id=int(x.group_id), user_id=int(x.user_id) - ) - )["card"] - else: - user_name = [ - u["nickname"] for u in friend_str if u["user_id"] == int(x.user_id) - ][0] - except Exception as e: - logger.warning( - f"show_black_text_image 获取 USER {x.user_id} user_name 失败", e=e - ) - user_name = x.user_id - id_str += f"{i}\n" - uname_str += f"{user_name}\n" - uid_str += f"{x.user_id}\n" - gid_str += f"{x.group_id}\n" - plant_text = " ".join(x.plant_text.split("\n")) - if A.getsize(plant_text)[0] > 200: - plant_text = plant_text[:20] + "..." - plant_text_str += f"{plant_text}\n" - black_word_str += f"{x.black_word}\n" - punish_str += f"{x.punish}\n" - punish_level_str += f"{x.punish_level}\n" - create_time_str += f"{x.create_time.replace(microsecond=0)}\n" - _tmp_img = BuildImage(0, 0, font_size=35, font="CJGaoDeGuo.otf") - for s, type_ in [ - (id_str, "Id"), - (uname_str, "昵称"), - (uid_str, "UID"), - (gid_str, "GID"), - (plant_text_str, "文本"), - (black_word_str, "检测"), - (punish_str, "惩罚"), - (punish_level_str, "等级"), - (create_time_str, "记录日期"), - ]: - img = await text2image(s, color="#f9f6f2", _add_height=2.1) - w = _tmp_img.getsize(type_)[0] if _tmp_img.getsize(type_)[0] > img.w else img.w - A = BuildImage(w + 11, img.h + 50, color="#f9f6f2", font_size=35, font="CJGaoDeGuo.otf") - await A.atext((10, 10), type_) - await A.apaste(img, (0, 50)) - image_list.append(A) - horizontal_line = [] - w, h = 0, 0 - for img in image_list: - w += img.w + 20 - h = img.h if img.h > h else h - horizontal_line.append(img.w) - A = BuildImage(w, h, color="#f9f6f2") - current_w = 0 - for img in image_list: - await A.apaste(img, (current_w, 0)) - current_w += img.w + 20 - return A - - -async def set_user_punish(user_id: str, id_: int, punish_level: int) -> str: - """ - 设置惩罚 - :param user_id: 用户id - :param id_: 记录下标 - :param punish_level: 惩罚等级 - """ - result = await _get_punish(punish_level, user_id) - punish = { - 1: "永久ban", - 2: "删除好友", - 3: f"ban {result} 天", - 4: f"ban {result} 分钟", - 5: "口头警告" - } - if await BlackWord.set_user_punish(user_id, punish[punish_level], id_=id_): - return f"已对 USER {user_id} 进行 {punish[punish_level]} 处罚。" - else: - return "操作失败,可能未找到用户,id或敏感词" diff --git a/plugins/bt/__init__.py b/plugins/bt/__init__.py deleted file mode 100755 index d2571441..00000000 --- a/plugins/bt/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -from asyncio.exceptions import TimeoutError - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Message, PrivateMessageEvent -from nonebot.adapters.onebot.v11.permission import PRIVATE -from nonebot.params import ArgStr, CommandArg -from nonebot.typing import T_State - -from services.log import logger -from utils.utils import is_number - -from .data_source import get_bt_info - -__zx_plugin_name__ = "磁力搜索" -__plugin_usage__ = """ -usage: - * 请各位使用后不要转发 * - * 拒绝反冲斗士! * - 指令: - bt [关键词] ?[页数] - 示例:bt 钢铁侠 - 示例:bt 钢铁侠 3 -""".strip() -__plugin_des__ = "bt(磁力搜索)[仅支持私聊,懂的都懂]" -__plugin_cmd__ = ["bt [关键词] ?[页数]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["bt", "磁力搜索", "Bt", "BT"], -} -__plugin_block_limit__ = {"rst": "您有bt任务正在进行,请等待结束."} -__plugin_configs__ = { - "BT_MAX_NUM": { - "value": 10, - "help": "单次BT搜索返回最大消息数量", - "default_value": 10, - "type": int, - } -} - - -bt = on_command("bt", permission=PRIVATE, priority=5, block=True) - - -@bt.handle() -async def _(state: T_State, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - if msg: - keyword = None - page = 1 - if n := len(msg): - keyword = msg[0] - if n > 1 and is_number(msg[1]) and int(msg[1]) > 0: - page = int(msg[1]) - state["keyword"] = keyword - state["page"] = page - else: - state["page"] = 1 - - -@bt.got("keyword", prompt="请输入要查询的内容!") -async def _( - event: PrivateMessageEvent, - state: T_State, - keyword: str = ArgStr("keyword"), - page: str = ArgStr("page"), -): - send_flag = False - try: - async for title, type_, create_time, file_size, link in get_bt_info( - keyword, page - ): - await bt.send( - f"标题:{title}\n" - f"类型:{type_}\n" - f"创建时间:{create_time}\n" - f"文件大小:{file_size}\n" - f"种子:{link}" - ) - send_flag = True - except TimeoutError: - await bt.finish(f"搜索 {keyword} 超时...") - except Exception as e: - logger.error(f"bt 错误 {type(e)}:{e}") - await bt.finish(f"bt 其他未知错误..") - if not send_flag: - await bt.send(f"{keyword} 未搜索到...") - logger.info(f"USER {event.user_id} BT搜索 {keyword} 第 {page} 页") diff --git a/plugins/check/__init__.py b/plugins/check/__init__.py deleted file mode 100755 index 153512ad..00000000 --- a/plugins/check/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -from nonebot import on_command -from .data_source import Check -from nonebot.rule import to_me -from nonebot.permission import SUPERUSER -from utils.message_builder import image - - -__zx_plugin_name__ = "服务器自我检查 [Superuser]" -__plugin_usage__ = """ -usage: - 查看服务器当前状态 - 指令: - 自检 -""" -__plugin_des__ = "查看服务器当前状态" -__plugin_cmd__ = ["自检/check"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -check = Check() - - -check_ = on_command( - "自检", aliases={"check"}, rule=to_me(), permission=SUPERUSER, block=True, priority=1 -) - - -@check_.handle() -async def _(): - await check_.send(image(b64=await check.show())) diff --git a/plugins/check_zhenxun_update/__init__.py b/plugins/check_zhenxun_update/__init__.py deleted file mode 100755 index 506ae25d..00000000 --- a/plugins/check_zhenxun_update/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -import os -import platform -from pathlib import Path - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, MessageEvent -from nonebot.params import ArgStr -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import NICKNAME, Config -from services.log import logger -from utils.utils import get_bot, scheduler - -from .data_source import check_update, get_latest_version_data - -__zx_plugin_name__ = "自动更新 [Superuser]" -__plugin_usage__ = """ -usage: - 检查更新真寻最新版本,包括了自动更新 - 指令: - 检查更新真寻 - 重启 -""".strip() -__plugin_des__ = "就算是真寻也会成长的" -__plugin_cmd__ = ["检查更新真寻", "重启"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "UPDATE_REMIND": { - "value": True, - "help": "真寻是否检测版本状态", - "default": True, - "type": bool, - }, - "AUTO_UPDATE_ZHENXUN": { - "value": False, - "help": "真寻是否自动检查更新", - "default": False, - "type": bool, - }, -} - -update_zhenxun = on_command("检查更新真寻", permission=SUPERUSER, priority=1, block=True) - -restart = on_command( - "重启", - aliases={"restart"}, - permission=SUPERUSER, - rule=to_me(), - priority=1, - block=True, -) - - -@update_zhenxun.handle() -async def _(bot: Bot, event: MessageEvent): - try: - code, error = await check_update(bot) - if error: - logger.error(f"错误: {error}", "检查更新真寻") - await bot.send_private_msg( - user_id=event.user_id, message=f"更新真寻未知错误 {error}" - ) - except Exception as e: - logger.error(f"更新真寻未知错误", "检查更新真寻", e=e) - await bot.send_private_msg( - user_id=event.user_id, - message=f"更新真寻未知错误 {type(e)}: {e}", - ) - else: - if code == 200: - await bot.send_private_msg(user_id=event.user_id, message=f"更新完毕,请重启真寻....") - - -@restart.got("flag", prompt=f"确定是否重启{NICKNAME}?确定请回复[是|好|确定](重启失败咱们将失去联系,请谨慎!)") -async def _(flag: str = ArgStr("flag")): - if flag.lower() in ["true", "是", "好", "确定", "确定是"]: - await restart.send(f"开始重启{NICKNAME}..请稍等...") - open("is_restart", "w") - if str(platform.system()).lower() == "windows": - import sys - - python = sys.executable - os.execl(python, python, *sys.argv) - else: - os.system("./restart.sh") - else: - await restart.send("已取消操作...") - - -@scheduler.scheduled_job( - "cron", - hour=12, - minute=0, -) -async def _(): - if Config.get_config("check_zhenxun_update", "UPDATE_REMIND"): - _version = "v0.0.0" - _version_file = Path() / "__version__" - if _version_file.exists(): - _version = ( - open(_version_file, "r", encoding="utf8") - .readline() - .split(":")[-1] - .strip() - ) - data = await get_latest_version_data() - if data: - latest_version = data["name"] - if _version.lower() != latest_version.lower(): - bot = get_bot() - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"检测到真寻版本更新\n" f"当前版本:{_version},最新版本:{latest_version}", - ) - if Config.get_config("check_zhenxun_update", "AUTO_UPDATE_ZHENXUN"): - try: - code = await check_update(bot) - except Exception as e: - logger.error(f"更新真寻未知错误 {type(e)}:{e}") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"更新真寻未知错误 {type(e)}:{e}\n", - ) - else: - if code == 200: - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"更新完毕,请重启{NICKNAME}....", - ) diff --git a/plugins/check_zhenxun_update/data_source.py b/plugins/check_zhenxun_update/data_source.py deleted file mode 100755 index fbb67555..00000000 --- a/plugins/check_zhenxun_update/data_source.py +++ /dev/null @@ -1,221 +0,0 @@ -import asyncio -import os -import platform -import shutil -import tarfile -from pathlib import Path -from typing import List, Tuple - -import nonebot -import ujson as json -from nonebot.adapters.onebot.v11 import Bot, Message - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.message_builder import image - -# if str(platform.system()).lower() == "windows": -# policy = asyncio.WindowsSelectorEventLoopPolicy() -# asyncio.set_event_loop_policy(policy) - - -driver = nonebot.get_driver() - -release_url = "https://api.github.com/repos/HibiKier/zhenxun_bot/releases/latest" - -_version_file = Path() / "__version__" -zhenxun_latest_tar_gz = Path() / "zhenxun_latest_file.tar.gz" -temp_dir = Path() / "temp" -backup_dir = Path() / "backup" - - -@driver.on_bot_connect -async def remind(bot: Bot): - if str(platform.system()).lower() != "windows": - restart = Path() / "restart.sh" - if not restart.exists(): - with open(restart, "w", encoding="utf8") as f: - f.write( - f"pid=$(netstat -tunlp | grep " - + str(bot.config.port) - + " | awk '{print $7}')\n" - "pid=${pid%/*}\n" - "kill -9 $pid\n" - "sleep 3\n" - "python3 bot.py" - ) - os.system("chmod +x ./restart.sh") - logger.info("已自动生成 restart.sh(重启) 文件,请检查脚本是否与本地指令符合...") - is_restart_file = Path() / "is_restart" - if is_restart_file.exists(): - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"真寻重启完毕...", - ) - is_restart_file.unlink() - - -async def check_update(bot: Bot) -> Tuple[int, str]: - logger.info("开始检查更新真寻酱....") - _version = "v0.0.0" - if _version_file.exists(): - _version = ( - open(_version_file, "r", encoding="utf8").readline().split(":")[-1].strip() - ) - data = await get_latest_version_data() - if data: - latest_version = data["name"] - if _version != latest_version: - tar_gz_url = data["tarball_url"] - logger.info(f"检测真寻已更新,当前版本:{_version},最新版本:{latest_version}") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"检测真寻已更新,当前版本:{_version},最新版本:{latest_version}\n" f"开始更新.....", - ) - logger.info(f"开始下载真寻最新版文件....") - tar_gz_url = (await AsyncHttpx.get(tar_gz_url)).headers.get("Location") - if await AsyncHttpx.download_file(tar_gz_url, zhenxun_latest_tar_gz): - logger.info("下载真寻最新版文件完成....") - error = await asyncio.get_event_loop().run_in_executor( - None, _file_handle, latest_version - ) - if error: - return 998, error - logger.info("真寻更新完毕,清理文件完成....") - logger.info("开始获取真寻更新日志.....") - update_info = data["body"] - width = 0 - height = len(update_info.split("\n")) * 24 - A = BuildImage(width, height, font_size=20) - for m in update_info.split("\n"): - w, h = A.getsize(m) - if w > width: - width = w - A = BuildImage(width + 50, height, font_size=20) - A.text((10, 10), update_info) - A.save(f"{IMAGE_PATH}/update_info.png") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=Message( - f"真寻更新完成,版本:{_version} -> {latest_version}\n" - f"更新日期:{data['created_at']}\n" - f"更新日志:\n" - f"{image('update_info.png')}" - ), - ) - return 200, "" - else: - logger.warning(f"下载真寻最新版本失败...版本号:{latest_version}") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"下载真寻最新版本失败...版本号:{latest_version}.", - ) - else: - logger.info(f"自动获取真寻版本成功:{latest_version},当前版本为最新版,无需更新...") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"自动获取真寻版本成功:{latest_version},当前版本为最新版,无需更新...", - ) - else: - logger.warning("自动获取真寻版本失败....") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), message=f"自动获取真寻版本失败...." - ) - return 999, "" - - -def _file_handle(latest_version: str) -> str: - if not temp_dir.exists(): - temp_dir.mkdir(exist_ok=True, parents=True) - if backup_dir.exists(): - shutil.rmtree(backup_dir) - tf = None - error = "" - # try: - backup_dir.mkdir(exist_ok=True, parents=True) - logger.info("开始解压真寻文件压缩包....") - tf = tarfile.open(zhenxun_latest_tar_gz) - tf.extractall(temp_dir) - logger.info("解压真寻文件压缩包完成....") - zhenxun_latest_file = Path(temp_dir) / os.listdir(temp_dir)[0] - update_info_file = Path(zhenxun_latest_file) / "update_info.json" - update_info = json.load(open(update_info_file, "r", encoding="utf8")) - update_file = update_info["update_file"] - add_file = update_info["add_file"] - delete_file = update_info["delete_file"] - config_file = Path() / "configs" / "config.py" - config_path_file = Path() / "configs" / "path_config.py" - # for file in [config_file.name]: - # tmp = "" - # new_file = Path(zhenxun_latest_file) / "configs" / file - # old_file = Path() / "configs" / file - # new_lines = open(new_file, "r", encoding="utf8").readlines() - # old_lines = open(old_file, "r", encoding="utf8").readlines() - # for nl in new_lines: - # tmp += check_old_lines(old_lines, nl) - # with open(old_file, "w", encoding="utf8") as f: - # f.write(tmp) - for file in delete_file + update_file: - if file != "configs": - file = Path() / file - backup_file = Path(backup_dir) / file - if file.exists(): - backup_file.parent.mkdir(parents=True, exist_ok=True) - if backup_file.exists(): - backup_file.unlink() - if file not in [config_file, config_path_file]: - shutil.move(file.absolute(), backup_file.absolute()) - else: - with open(file, "r", encoding="utf8") as rf: - data = rf.read() - with open(backup_file, "w", encoding="utf8") as wf: - wf.write(data) - logger.info(f"已备份文件:{file}") - for file in add_file + update_file: - new_file = Path(zhenxun_latest_file) / file - old_file = Path() / file - if old_file not in [config_file, config_path_file] and file != "configs": - if not old_file.exists() and new_file.exists(): - shutil.move(new_file.absolute(), old_file.absolute()) - logger.info(f"已更新文件:{file}") - # except Exception as e: - # error = f'{type(e)}:{e}' - if tf: - tf.close() - if temp_dir.exists(): - shutil.rmtree(temp_dir) - if zhenxun_latest_tar_gz.exists(): - zhenxun_latest_tar_gz.unlink() - local_update_info_file = Path() / "update_info.json" - if local_update_info_file.exists(): - local_update_info_file.unlink() - with open(_version_file, "w", encoding="utf8") as f: - f.write(f"__version__: {latest_version}") - os.system(f"poetry run pip install -r {(Path() / 'pyproject.toml').absolute()}") - return error - - -# 获取最新版本号 -async def get_latest_version_data() -> dict: - for _ in range(3): - try: - res = await AsyncHttpx.get(release_url) - if res.status_code == 200: - return res.json() - except TimeoutError: - pass - except Exception as e: - logger.error(f"检查更新真寻获取版本失败 {type(e)}:{e}") - return {} - - -# 逐行检测 -def check_old_lines(lines: List[str], line: str) -> str: - if "=" not in line: - return line - for l in lines: - if "=" in l and l.split("=")[0].strip() == line.split("=")[0].strip(): - return l - return line diff --git a/plugins/coser/__init__.py b/plugins/coser/__init__.py deleted file mode 100755 index edb78e3f..00000000 --- a/plugins/coser/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -import time -from typing import Any, Tuple - -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.params import RegexGroup - -from configs.config import Config -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.manager import withdraw_message_manager -from utils.message_builder import image - -__zx_plugin_name__ = "coser" -__plugin_usage__ = """ -usage: - 三次元也不戳,嘿嘿嘿 - 指令: - ?N连cos/coser - 示例:cos - 示例:5连cos (单次请求张数小于9) -""".strip() -__plugin_des__ = "三次元也不戳,嘿嘿嘿" -__plugin_cmd__ = ["cos/coser"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["cos", "coser", "括丝", "COS", "Cos", "cOS", "coS"], -} -__plugin_configs__ = { - "WITHDRAW_COS_MESSAGE": { - "value": (0, 1), - "help": "自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", - "default_value": (0, 1), - "type": Tuple[int, int], - }, -} - -coser = on_regex(r"^(\d)?连?(cos|COS|coser|括丝)$", priority=5, block=True) - -# 纯cos,较慢:https://picture.yinux.workers.dev -# 比较杂,有福利姬,较快:https://api.jrsgslb.cn/cos/url.php?return=img -url = "https://picture.yinux.workers.dev" - - -@coser.handle() -async def _(event: MessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - num = reg_group[0] or 1 - for _ in range(int(num)): - path = TEMP_PATH / f"cos_cc{int(time.time())}.jpeg" - try: - await AsyncHttpx.download_file(url, path) - msg_id = await coser.send(image(path)) - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("coser", "WITHDRAW_COS_MESSAGE"), - ) - logger.info( - f"发送cos", "cos", event.user_id, getattr(event, "group_id", None) - ) - except Exception as e: - await coser.send("你cos给我看!") - logger.error( - f"coser错误", "cos", event.user_id, getattr(event, "group_id", None), e=e - ) diff --git a/plugins/dialogue/__init__.py b/plugins/dialogue/__init__.py deleted file mode 100755 index e7ccb9b5..00000000 --- a/plugins/dialogue/__init__.py +++ /dev/null @@ -1,167 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, Message, GroupMessageEvent -from nonebot.permission import SUPERUSER -from utils.utils import is_number, get_message_img -from utils.message_builder import image -from utils.message_builder import text as _text -from services.log import logger -from utils.message_builder import at -from nonebot.params import CommandArg - - -__zx_plugin_name__ = "联系管理员" -__plugin_usage__ = """ -usage: - 有什么话想对管理员说嘛? - 指令: - [滴滴滴]/滴滴滴- ?[文本] ?[图片] - 示例:滴滴滴- 我喜欢你 -""".strip() -__plugin_superuser_usage__ = """ -superuser usage: - 管理员对消息的回复 - 指令[以下qq与group均为乱打]: - /t: 查看当前存储的消息 - /t [qq] [group] [文本]: 在group回复指定用户 - /t [qq] [文本]: 私聊用户 - /t -1 [group] [文本]: 在group内发送消息 - /t [id] [文本]: 回复指定id的对话,id在 /t 中获取 - 示例:/t 73747222 32848432 你好啊 - 示例:/t 73747222 你好不好 - 示例:/t -1 32848432 我不太好 - 示例:/t 0 我收到你的话了 -""" -__plugin_des__ = "跨越空间与时间跟管理员对话" -__plugin_cmd__ = [ - "滴滴滴-/[滴滴滴] ?[文本] ?[图片]", - "/t [_superuser]", - "t [qq] [group] [文本] [_superuser]", - "/t [qq] [文本] [_superuser]", - "/t -1 [group] [_superuser]", - "/t [id] [文本] [_superuser]", -] -__plugin_type__ = ("联系管理员",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["滴滴滴-", "滴滴滴"], -} - -dialogue_data = {} - - -dialogue = on_command("[滴滴滴]", aliases={"滴滴滴-"}, priority=5, block=True) -reply = on_command("/t", priority=1, permission=SUPERUSER, block=True) - - -@dialogue.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - text = arg.extract_plain_text().strip() - img_msg = _text("") - for img in get_message_img(event.json()): - img_msg += image(img) - if not text and not img_msg: - await dialogue.send("请发送[滴滴滴]+您要说的内容~", at_sender=True) - else: - group_id = 0 - group_name = "None" - nickname = event.sender.nickname - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - group_name = (await bot.get_group_info(group_id=event.group_id))[ - "group_name" - ] - nickname = event.sender.card or event.sender.nickname - for coffee in bot.config.superusers: - await bot.send_private_msg( - user_id=int(coffee), - message=_text( - f"*****一份交流报告*****\n" - f"昵称:{nickname}({event.user_id})\n" - f"群聊:{group_name}({group_id})\n" - f"消息:{text}" - ) - + img_msg, - ) - await dialogue.send( - _text(f"您的话已发送至管理员!\n======\n{text}") + img_msg, at_sender=True - ) - nickname = event.sender.nickname if event.sender.nickname else event.sender.card - dialogue_data[len(dialogue_data)] = { - "nickname": nickname, - "user_id": event.user_id, - "group_id": group_id, - "group_name": group_name, - "msg": _text(text) + img_msg, - } - # print(dialogue_data) - logger.info(f"Q{event.user_id}@群{group_id} 联系管理员:text:{text}") - - -@reply.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - result = "*****待回复消息总览*****\n" - for key in dialogue_data.keys(): - result += ( - f"id:{key}\n" - f'\t昵称:{dialogue_data[key]["nickname"]}({dialogue_data[key]["user_id"]})\n' - f'\t群群:{dialogue_data[key]["group_name"]}({dialogue_data[key]["group_id"]})\n' - f'\t消息:{dialogue_data[key]["msg"]}' - f"\n--------------------\n" - ) - await reply.finish(Message(result[:-1])) - msg = msg.split() - text = "" - group_id = 0 - user_id = -1 - if is_number(msg[0]): - if len(msg[0]) < 3: - msg[0] = int(msg[0]) - if msg[0] >= 0: - id_ = msg[0] - user_id = dialogue_data[id_]["user_id"] - group_id = dialogue_data[id_]["group_id"] - text = " ".join(msg[1:]) - dialogue_data.pop(id_) - else: - user_id = 0 - if is_number(msg[1]): - group_id = int(msg[1]) - text = " ".join(msg[2:]) - else: - await reply.finish("群号错误...", at_sender=True) - else: - user_id = int(msg[0]) - if is_number(msg[1]) and len(msg[1]) > 5: - group_id = int(msg[1]) - text = " ".join(msg[2:]) - else: - group_id = 0 - text = " ".join(msg[1:]) - else: - await reply.finish("第一参数,请输入数字.....", at_sender=True) - for img in get_message_img(event.json()): - text += image(img) - if group_id: - if user_id: - await bot.send_group_msg( - group_id=group_id, message=at(user_id) + "\n管理员回复\n=======\n" + text - ) - else: - await bot.send_group_msg(group_id=group_id, message=text) - await reply.finish("消息发送成功...", at_sender=True) - else: - if user_id in [qq["user_id"] for qq in await bot.get_friend_list()]: - await bot.send_private_msg( - user_id=user_id, message="管理员回复\n=======\n" + text - ) - await reply.finish("发送成功", at_sender=True) - else: - await reply.send( - f"对象不是{list(bot.config.nickname)[0]}的好友...", at_sender=True - ) diff --git a/plugins/epic/__init__.py b/plugins/epic/__init__.py deleted file mode 100755 index e201a123..00000000 --- a/plugins/epic/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent -from nonebot.typing import T_State - -from configs.config import Config -from services.log import logger -from utils.manager import group_manager -from utils.utils import get_bot, scheduler - -from .data_source import get_epic_free - -__zx_plugin_name__ = "epic免费游戏" -__plugin_usage__ = """ -usage: - 可以不玩,不能没有,每日白嫖 - 指令: - epic -""".strip() -__plugin_des__ = "可以不玩,不能没有,每日白嫖" -__plugin_cmd__ = ["epic"] -__plugin_version__ = 0.1 -__plugin_author__ = "AkashiCoin" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["epic"], -} -__plugin_task__ = {"epic_free_game": "epic免费游戏"} -Config.add_plugin_config( - "_task", - "DEFAULT_EPIC_FREE_GAME", - True, - help_="被动 epic免费游戏 进群默认开关状态", - default_value=True, - type=bool, -) - -epic = on_regex("^epic$", priority=5, block=True) - - -@epic.handle() -async def handle(bot: Bot, event: MessageEvent, state: T_State): - Type_Event = "Private" - if isinstance(event, GroupMessageEvent): - Type_Event = "Group" - msg_list, code = await get_epic_free(bot, Type_Event) - if code == 404: - await epic.send(msg_list) - elif isinstance(event, GroupMessageEvent): - await bot.send_group_forward_msg(group_id=event.group_id, messages=msg_list) - else: - for msg in msg_list: - await bot.send_private_msg(user_id=event.user_id, message=msg) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 获取epic免费游戏" - ) - - -# epic免费游戏 -@scheduler.scheduled_job( - "cron", - hour=12, - minute=1, -) -async def _(): - bot = get_bot() - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - msg_list, code = await get_epic_free(bot, "Group") - for g in gl: - if group_manager.check_task_status("epic_free_game", str(g)): - try: - if msg_list and code == 200: - await bot.send_group_forward_msg(group_id=g, messages=msg_list) - else: - await bot.send_group_msg(group_id=g) - except Exception as e: - logger.error(f"GROUP {g} epic免费游戏推送错误 {type(e)}: {e}") diff --git a/plugins/fake_msg.py b/plugins/fake_msg.py deleted file mode 100755 index 44d16314..00000000 --- a/plugins/fake_msg.py +++ /dev/null @@ -1,57 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from nonebot import on_command -from utils.utils import get_message_img -from utils.message_builder import share -from services.log import logger - - -__zx_plugin_name__ = "构造分享消息" -__plugin_usage__ = """ -usage: - 自定义的分享消息构造 - 指令: - 假消息 [网址] [标题] ?[内容] ?[图片] - 示例:假消息 www.4399.com 我喜欢萝莉 为什么我喜欢... [图片] -""".strip() -__plugin_des__ = "自定义的分享消息构造" -__plugin_cmd__ = ["假消息 [网址] [标题] ?[内容] ?[图片]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["假消息"], -} - - -fake_msg = on_command("假消息", priority=5, block=True) - - -@fake_msg.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - img = get_message_img(event.json()) - if len(msg) > 1: - if len(msg) == 2: - url = msg[0] - title = msg[1] - content = "" - else: - url = msg[0] - title = msg[1] - content = msg[2] - if img: - img = img[0] - else: - img = "" - if "http" not in url: - url = "http://" + url - await fake_msg.send(share(url, title, content, img)) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 构造假消息 url {url}, title {title}, content {content}" - ) - else: - await fake_msg.finish("消息格式错误:\n网址 标题 内容(可省略) 图片(可省略)") diff --git a/plugins/fudu.py b/plugins/fudu.py deleted file mode 100755 index 972f5f24..00000000 --- a/plugins/fudu.py +++ /dev/null @@ -1,138 +0,0 @@ -import random - -from nonebot import on_message -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from configs.config import NICKNAME, Config -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import get_img_hash -from utils.message_builder import image -from utils.utils import get_message_img, get_message_text - -__zx_plugin_name__ = "复读" -__plugin_usage__ = """ -usage: - 重复3次相同的消息时会复读 -""".strip() -__plugin_des__ = "群友的本质是什么?是复读机哒!" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"fudu": "复读"} -__plugin_configs__ = { - "FUDU_PROBABILITY": { - "value": 0.7, - "help": "复读概率", - "default_value": 0.7, - "type": float, - } -} -Config.add_plugin_config( - "_task", "DEFAULT_FUDU", True, help_="被动 复读 进群默认开关状态", default_value=True, type=bool -) - - -class Fudu: - def __init__(self): - self.data = {} - - def append(self, key, content): - self._create(key) - self.data[key]["data"].append(content) - - def clear(self, key): - self._create(key) - self.data[key]["data"] = [] - self.data[key]["is_repeater"] = False - - def size(self, key) -> int: - self._create(key) - return len(self.data[key]["data"]) - - def check(self, key, content) -> bool: - self._create(key) - return self.data[key]["data"][0] == content - - def get(self, key): - self._create(key) - return self.data[key]["data"][0] - - def is_repeater(self, key): - self._create(key) - return self.data[key]["is_repeater"] - - def set_repeater(self, key): - self._create(key) - self.data[key]["is_repeater"] = True - - def _create(self, key): - if self.data.get(key) is None: - self.data[key] = {"is_repeater": False, "data": []} - - -_fudu_list = Fudu() - - -fudu = on_message(permission=GROUP, priority=999) - - -@fudu.handle() -async def _(event: GroupMessageEvent): - if event.is_tome(): - return - if msg := get_message_text(event.json()): - if msg.startswith(f"@可爱的{NICKNAME}"): - await fudu.finish("复制粘贴的虚空艾特?", at_sender=True) - img = get_message_img(event.json()) - msg = get_message_text(event.json()) - if not img and not msg: - return - if img: - img_hash = await get_fudu_img_hash(img[0], event.group_id) - else: - img_hash = "" - add_msg = msg + "|-|" + img_hash - if _fudu_list.size(event.group_id) == 0: - _fudu_list.append(event.group_id, add_msg) - elif _fudu_list.check(event.group_id, add_msg): - _fudu_list.append(event.group_id, add_msg) - else: - _fudu_list.clear(event.group_id) - _fudu_list.append(event.group_id, add_msg) - if _fudu_list.size(event.group_id) > 2: - if random.random() < Config.get_config( - "fudu", "FUDU_PROBABILITY" - ) and not _fudu_list.is_repeater(event.group_id): - if random.random() < 0.2: - if msg.endswith("打断施法!"): - await fudu.finish("[[_task|fudu]]打断" + msg) - else: - await fudu.finish("[[_task|fudu]]打断施法!") - _fudu_list.set_repeater(event.group_id) - if img and msg: - rst = msg + image(TEMP_PATH / f"compare_{event.group_id}_img.jpg") - elif img: - rst = image(TEMP_PATH / f"compare_{event.group_id}_img.jpg") - elif msg: - rst = msg - else: - rst = "" - if rst: - await fudu.finish("[[_task|fudu]]" + rst) - - -async def get_fudu_img_hash(url, group_id): - try: - if await AsyncHttpx.download_file( - url, TEMP_PATH / f"compare_{group_id}_img.jpg" - ): - img_hash = get_img_hash(TEMP_PATH / f"compare_{group_id}_img.jpg") - return str(img_hash) - else: - logger.warning(f"复读下载图片失败...") - except Exception as e: - logger.warning(f"复读读取图片Hash出错 {type(e)}:{e}") - return "" diff --git a/plugins/genshin/almanac/__init__.py b/plugins/genshin/almanac/__init__.py deleted file mode 100755 index ecbe8e75..00000000 --- a/plugins/genshin/almanac/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.manager import group_manager -from utils.message_builder import image -from utils.utils import get_bot, scheduler - -from ._data_source import build_alc_image - -__zx_plugin_name__ = "原神老黄历" -__plugin_usage__ = """ -usage: - 有时候也该迷信一回!特别是运气方面 - 指令: - 原神黄历 -""".strip() -__plugin_des__ = "有时候也该迷信一回!特别是运气方面" -__plugin_cmd__ = ["原神黄历"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神黄历", "原神老黄历"], -} -__plugin_task__ = {"genshin_alc": "原神黄历提醒"} - -Config.add_plugin_config( - "_task", - "DEFAULT_GENSHIN_ALC", - True, - help_="被动 原神黄历提醒 进群默认开关状态", - default_value=True, - type=bool, -) - -almanac = on_command("原神黄历", priority=5, block=True) - - -@almanac.handle() -async def _(event: MessageEvent): - await almanac.send(image(b64=await build_alc_image())) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送查看原神黄历" - ) - - -@scheduler.scheduled_job( - "cron", - hour=10, - minute=25, -) -async def _(): - # 每日提醒 - bot = get_bot() - if bot: - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - alc_img = image(b64=await build_alc_image()) - if alc_img: - mes = "[[_task|genshin_alc]]" + alc_img - for gid in gl: - if group_manager.check_group_task_status(gid, "genshin_alc"): - await bot.send_group_msg(group_id=int(gid), message=mes) diff --git a/plugins/genshin/almanac/_data_source.py b/plugins/genshin/almanac/_data_source.py deleted file mode 100644 index 07538d7e..00000000 --- a/plugins/genshin/almanac/_data_source.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -import random -from dataclasses import dataclass -from datetime import datetime -from typing import List, Tuple, Union - -import ujson as json -from configs.path_config import DATA_PATH, IMAGE_PATH -from utils.image_utils import BuildImage - -CONFIG_PATH = DATA_PATH / "genshin_alc" / "config.json" - -ALC_PATH = IMAGE_PATH / "genshin" / "alc" - -ALC_PATH.mkdir(exist_ok=True, parents=True) - -BACKGROUND_PATH = ALC_PATH / "back.png" - -chinese = { - "0": "十", - "1": "一", - "2": "二", - "3": "三", - "4": "四", - "5": "五", - "6": "六", - "7": "七", - "8": "八", - "9": "九", -} - - -@dataclass -class Fortune: - title: str - desc: str - - -def random_fortune() -> Tuple[List[Fortune], List[Fortune]]: - """ - 说明: - 随机运势 - """ - data = json.load(CONFIG_PATH.open("r", encoding="utf8")) - fortune_data = {} - good_fortune = [] - bad_fortune = [] - while len(fortune_data) < 6: - r = random.choice(list(data.keys())) - if r not in fortune_data: - fortune_data[r] = data[r] - for i, k in enumerate(fortune_data): - if i < 3: - good_fortune.append( - Fortune(title=k, desc=random.choice(fortune_data[k]["buff"])) - ) - else: - bad_fortune.append( - Fortune(title=k, desc=random.choice(fortune_data[k]["debuff"])) - ) - return good_fortune, bad_fortune - - -def int2cn(v: Union[str, int]): - """ - 说明: - 数字转中文 - 参数: - :param v: str - """ - return "".join([chinese[x] for x in str(v)]) - - -async def build_alc_image() -> str: - """ - 说明: - 构造今日运势图片 - """ - for file in os.listdir(ALC_PATH): - if file not in ["back.png", f"{datetime.now().date()}.png"]: - (ALC_PATH / file).unlink() - path = ALC_PATH / f"{datetime.now().date()}.png" - if path.exists(): - return BuildImage(0, 0, background=path).pic2bs4() - good_fortune, bad_fortune = random_fortune() - background = BuildImage( - 0, 0, background=BACKGROUND_PATH, font="HYWenHei-85W.ttf", font_size=30 - ) - now = datetime.now() - await background.atext((78, 145), str(now.year), fill="#8d7650ff") - month = str(now.month) - month_w = 358 - if now.month < 10: - month_w = 373 - elif now.month != 10: - month = "0" + month[-1] - await background.atext((month_w, 145), f"{int2cn(month)}月", fill="#8d7650ff") - day = str(now.day) - if now.day > 10 and day[-1] != "0": - day = day[0] + "0" + day[-1] - day_str = f"{int2cn(day)}日" - day_w = 193 - if (n := len(day_str)) == 3: - day_w = 207 - elif n == 2: - day_w = 228 - await background.atext( - (day_w, 145), f"{int2cn(day)}日", fill="#f7f8f2ff", font_size=35 - ) - fortune_h = 230 - for fortune in good_fortune: - await background.atext( - (150, fortune_h), fortune.title, fill="#756141ff", font_size=25 - ) - await background.atext( - (150, fortune_h + 28), fortune.desc, fill="#b5b3acff", font_size=19 - ) - fortune_h += 55 - fortune_h += 4 - for fortune in bad_fortune: - await background.atext( - (150, fortune_h), fortune.title, fill="#756141ff", font_size=25 - ) - await background.atext( - (150, fortune_h + 28), fortune.desc, fill="#b5b3acff", font_size=19 - ) - fortune_h += 55 - await background.asave(path) - return background.pic2bs4() diff --git a/plugins/genshin/material_remind/__init__.py b/plugins/genshin/material_remind/__init__.py deleted file mode 100755 index 795f8911..00000000 --- a/plugins/genshin/material_remind/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -import asyncio -import os -import time -from datetime import datetime, timedelta -from typing import List - -import nonebot -from nonebot import Driver, on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.permission import SUPERUSER - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.browser import get_browser -from utils.image_utils import BuildImage -from utils.message_builder import image - -__zx_plugin_name__ = "今日素材" -__plugin_usage__ = """ -usage: - 看看原神今天要刷什么 - 指令: - 今日素材/今天素材 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 更新原神今日素材 - 指令: - 更新原神今日素材 -""".strip() -__plugin_des__ = "看看原神今天要刷什么" -__plugin_cmd__ = ["今日素材/今天素材", "更新原神今日素材 [_superuser]"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["今日素材", "今天素材"], -} - -driver: Driver = nonebot.get_driver() - -material = on_command("今日素材", aliases={"今日材料", "今天素材", "今天材料"}, priority=5, block=True) - -super_cmd = on_command("更新原神今日素材", permission=SUPERUSER, priority=1, block=True) - - -@material.handle() -async def _(event: MessageEvent): - if time.strftime("%w") == "0": - await material.send("今天是周日,所有材料副本都开放了。") - return - file_name = str((datetime.now() - timedelta(hours=4)).date()) - if not (IMAGE_PATH / "genshin" / "material" / f"{file_name}.png").exists(): - await update_image() - await material.send( - Message( - image(IMAGE_PATH / "genshin" / "material" / f"{file_name}.png") - + "\n※ 每日素材数据来源于米游社" - ) - ) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送查看今日素材" - ) - - -@super_cmd.handle() -async def _(): - if await update_image(): - await super_cmd.send("更新成功...") - logger.info(f"更新每日天赋素材成功...") - else: - await super_cmd.send(f"更新失败...") - - -async def update_image(): - page = None - try: - if not os.path.exists(f"{IMAGE_PATH}/genshin/material"): - os.mkdir(f"{IMAGE_PATH}/genshin/material") - for file in os.listdir(f"{IMAGE_PATH}/genshin/material"): - os.remove(f"{IMAGE_PATH}/genshin/material/{file}") - browser = get_browser() - if not browser: - logger.warning("获取 browser 失败,请部署至 linux 环境....") - return False - # url = "https://genshin.pub/daily" - url = "https://bbs.mihoyo.com/ys/obc/channel/map/193" - page = await browser.new_page(viewport={"width": 860, "height": 3000}) - await page.goto(url) - await page.wait_for_timeout(3000) - file_name = str((datetime.now() - timedelta(hours=4)).date()) - # background_img.save(f"{IMAGE_PATH}/genshin/material/{file_name}.png") - await page.locator( - '//*[@id="__layout"]/div/div[2]/div[2]/div/div[1]/div[2]/div/div' - ).screenshot(path=f"{IMAGE_PATH}/genshin/material/{file_name}.png") - await page.close() - return True - except Exception as e: - logger.error(f"原神每日素材更新出错... {type(e)}: {e}") - if page: - await page.close() - return False diff --git a/plugins/genshin/query_resource_points/__init__.py b/plugins/genshin/query_resource_points/__init__.py deleted file mode 100755 index b17edf72..00000000 --- a/plugins/genshin/query_resource_points/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -from nonebot import on_command, on_regex -from .query_resource import get_resource_type_list, query_resource, init, check_resource_exists -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, Message -from utils.utils import scheduler -from services.log import logger -from configs.config import NICKNAME -from nonebot.permission import SUPERUSER -from nonebot.params import CommandArg -import re - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "原神资源查询" -__plugin_usage__ = """ -usage: - 不需要打开网页,就能帮你生成资源图片 - 指令: - 原神资源查询 [资源名称] - 原神资源列表 - [资源名称]在哪 - 哪有[资源名称] -""".strip() -__plugin_superuser_usage__ = """ -usage: - 更新原神资源信息 - 指令: - 更新原神资源信息 -""".strip() -__plugin_des__ = "原神大地图资源速速查看" -__plugin_cmd__ = ["原神资源查询 [资源名称]", "原神资源列表", "[资源名称]在哪/哪有[资源名称]", "更新原神资源信息 [_superuser]"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神资源查询", "原神资源列表"], -} -__plugin_block_limit__ = { - "rst": "您有资源正在查询!" -} - -qr = on_command("原神资源查询", aliases={"原神资源查找"}, priority=5, block=True) -qr_lst = on_command("原神资源列表", priority=5, block=True) -rex_qr = on_regex(".*?(在哪|在哪里|哪有|哪里有).*?", priority=5, block=True) -update_info = on_regex("^更新原神资源信息$", permission=SUPERUSER, priority=1, block=True) - - -@qr.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - resource_name = arg.extract_plain_text().strip() - if check_resource_exists(resource_name): - await qr.send("正在生成位置....") - resource = await query_resource(resource_name) - await qr.send(Message(resource), at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询原神材料:" + resource_name - ) - else: - await qr.send(f"未查找到 {resource_name} 资源,可通过 “原神资源列表” 获取全部资源名称..") - - -@rex_qr.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if "在哪" in msg: - rs = re.search("(.*)在哪.*?", msg) - resource_name = rs.group(1) if rs else "" - else: - rs = re.search(".*?(哪有|哪里有)(.*)", msg) - resource_name = rs.group(2) if rs else "" - if check_resource_exists(resource_name): - await qr.send("正在生成位置....") - resource = await query_resource(resource_name) - if resource: - await rex_qr.send(Message(resource), at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询原神材料:" + resource_name - ) - - -@qr_lst.handle() -async def _(bot: Bot, event: MessageEvent): - txt = get_resource_type_list() - txt_list = txt.split("\n") - if isinstance(event, GroupMessageEvent): - mes_list = [] - for txt in txt_list: - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": txt, - }, - } - mes_list.append(data) - await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) - else: - rst = "" - for i in range(len(txt_list)): - rst += txt_list[i] + "\n" - if i % 5 == 0: - if rst: - await qr_lst.send(rst) - rst = "" - - -@update_info.handle() -async def _(): - await init(True) - await update_info.send("更新原神资源信息完成...") - - -@scheduler.scheduled_job( - "cron", - hour=5, - minute=1, -) -async def _(): - try: - await init() - logger.info(f"每日更新原神材料信息成功!") - except Exception as e: - logger.error(f"每日更新原神材料信息错误:{e}") diff --git a/plugins/genshin/query_resource_points/map.py b/plugins/genshin/query_resource_points/map.py deleted file mode 100755 index f974f812..00000000 --- a/plugins/genshin/query_resource_points/map.py +++ /dev/null @@ -1,265 +0,0 @@ -from configs.path_config import IMAGE_PATH, TEXT_PATH, TEMP_PATH -from utils.image_utils import BuildImage -from typing import Tuple, List -from math import sqrt, pow -import random - -try: - import ujson as json -except ModuleNotFoundError: - import json - -icon_path = IMAGE_PATH / "genshin" / "genshin_icon" -map_path = IMAGE_PATH / "genshin" / "map" / "map.png" -resource_label_file = TEXT_PATH / "genshin" / "resource_label_file.json" -resource_point_file = TEXT_PATH / "genshin" / "resource_point_file.json" - - -class Map: - """ - 原神资源生成类 - """ - - def __init__( - self, - resource_name: str, - center_point: Tuple[int, int], - deviation: Tuple[int, int] = (25, 51), - padding: int = 100, - planning_route: bool = False, - ratio: float = 1, - ): - """ - 参数: - :param resource_name: 资源名称 - :param center_point: 中心点 - :param deviation: 坐标误差 - :param padding: 截图外边距 - :param planning_route: 是否规划最佳线路 - :param ratio: 压缩比率 - """ - self.map = BuildImage(0, 0, background=map_path) - self.resource_name = resource_name - self.center_x = center_point[0] - self.center_y = center_point[1] - self.deviation = deviation - self.padding = int(padding * ratio) - self.planning_route = planning_route - self.ratio = ratio - - self.deviation = ( - int(self.deviation[0] * ratio), - int(self.deviation[1] * ratio), - ) - - data = json.load(open(resource_label_file, "r", encoding="utf8")) - # 资源 id - self.resource_id = [ - data[x]["id"] - for x in data - if x != "CENTER_POINT" and data[x]["name"] == resource_name - ][0] - # 传送锚点 id - self.teleport_anchor_id = [ - data[x]["id"] - for x in data - if x != "CENTER_POINT" and data[x]["name"] == "传送锚点" - ][0] - # 神像 id - self.teleport_god_id = [ - data[x]["id"] - for x in data - if x != "CENTER_POINT" and data[x]["name"] == "七天神像" - ][0] - # 资源坐标 - data = json.load(open(resource_point_file, "r", encoding="utf8")) - self.resource_point = [ - Resources( - int((self.center_x + data[x]["x_pos"]) * ratio), - int((self.center_y + data[x]["y_pos"]) * ratio), - ) - for x in data - if x != "CENTER_POINT" and data[x]["label_id"] == self.resource_id - ] - # 传送锚点坐标 - self.teleport_anchor_point = [ - Resources( - int((self.center_x + data[x]["x_pos"]) * ratio), - int((self.center_y + data[x]["y_pos"]) * ratio), - ) - for x in data - if x != "CENTER_POINT" and data[x]["label_id"] == self.teleport_anchor_id - ] - # 神像坐标 - self.teleport_god_point = [ - Resources( - int((self.center_x + data[x]["x_pos"]) * ratio), - int((self.center_y + data[x]["y_pos"]) * ratio), - ) - for x in data - if x != "CENTER_POINT" and data[x]["label_id"] == self.teleport_god_id - ] - - # 将地图上生成资源图标 - def generate_resource_icon_in_map(self) -> int: - x_list = [x.x for x in self.resource_point] - y_list = [x.y for x in self.resource_point] - min_width = min(x_list) - self.padding - max_width = max(x_list) + self.padding - min_height = min(y_list) - self.padding - max_height = max(y_list) + self.padding - self._generate_transfer_icon((min_width, min_height, max_width, max_height)) - for res in self.resource_point: - icon = self._get_icon_image(self.resource_id) - self.map.paste( - icon, (res.x - self.deviation[0], res.y - self.deviation[1]), True - ) - if self.planning_route: - self._generate_best_route() - self.map.crop((min_width, min_height, max_width, max_height)) - rand = random.randint(1, 10000) - self.map.save(f"{TEMP_PATH}/genshin_map_{rand}.png") - return rand - - # 资源数量 - def get_resource_count(self) -> int: - return len(self.resource_point) - - # 生成传送锚点和神像 - def _generate_transfer_icon(self, box: Tuple[int, int, int, int]): - min_width, min_height, max_width, max_height = box - for resources in [self.teleport_anchor_point, self.teleport_god_point]: - id_ = ( - self.teleport_anchor_id - if resources == self.teleport_anchor_point - else self.teleport_god_id - ) - for res in resources: - if min_width < res.x < max_width and min_height < res.y < max_height: - icon = self._get_icon_image(id_) - self.map.paste( - icon, - (res.x - self.deviation[0], res.y - self.deviation[1]), - True, - ) - - # 生成最优路线(说是最优其实就是直线最短) - def _generate_best_route(self): - line_points = [] - teleport_list = self.teleport_anchor_point + self.teleport_god_point - for teleport in teleport_list: - current_res, res_min_distance = teleport.get_resource_distance(self.resource_point) - current_teleport, teleport_min_distance = current_res.get_resource_distance(teleport_list) - if current_teleport == teleport: - self.map.line( - (current_teleport.x, current_teleport.y, current_res.x, current_res.y), (255, 0, 0), width=1 - ) - is_used_res_points = [] - for res in self.resource_point: - if res in is_used_res_points: - continue - current_teleport, teleport_min_distance = res.get_resource_distance(teleport_list) - current_res, res_min_distance = res.get_resource_distance(self.resource_point) - if teleport_min_distance < res_min_distance: - self.map.line( - (current_teleport.x, current_teleport.y, res.x, res.y), (255, 0, 0), width=1 - ) - else: - is_used_res_points.append(current_res) - self.map.line( - (current_res.x, current_res.y, res.x, res.y), (255, 0, 0), width=1 - ) - res_cp = self.resource_point[:] - res_cp.remove(current_res) - # for _ in res_cp: - current_teleport_, teleport_min_distance = res.get_resource_distance(teleport_list) - current_res, res_min_distance = res.get_resource_distance(res_cp) - if teleport_min_distance < res_min_distance: - self.map.line( - (current_teleport.x, current_teleport.y, res.x, res.y), (255, 0, 0), width=1 - ) - else: - self.map.line( - (current_res.x, current_res.y, res.x, res.y), (255, 0, 0), width=1 - ) - is_used_res_points.append(current_res) - is_used_res_points.append(res) - - # resources_route = [] - # # 先连上最近的资源路径 - # for res in self.resource_point: - # # 拿到最近的资源 - # current_res, _ = res.get_resource_distance( - # self.resource_point - # + self.teleport_anchor_point - # + self.teleport_god_point - # ) - # self.map.line( - # (current_res.x, current_res.y, res.x, res.y), (255, 0, 0), width=1 - # ) - # resources_route.append((current_res, res)) - # teleport_list = self.teleport_anchor_point + self.teleport_god_point - # for res1, res2 in resources_route: - # point_list = [x for x in resources_route if res1 in x or res2 in x] - # if not list(set(point_list).intersection(set(teleport_list))): - # if res1 not in teleport_list and res2 not in teleport_list: - # # while True: - # # tmp = [x for x in point_list] - # # break - # teleport1, distance1 = res1.get_resource_distance(teleport_list) - # teleport2, distance2 = res2.get_resource_distance(teleport_list) - # if distance1 > distance2: - # self.map.line( - # (teleport1.x, teleport1.y, res1.x, res1.y), - # (255, 0, 0), - # width=1, - # ) - # else: - # self.map.line( - # (teleport2.x, teleport2.y, res2.x, res2.y), - # (255, 0, 0), - # width=1, - # ) - - # self.map.line(xy, (255, 0, 0), width=3) - - # 获取资源图标 - def _get_icon_image(self, id_: int) -> "BuildImage": - icon = icon_path / f"{id_}.png" - if icon.exists(): - return BuildImage( - int(50 * self.ratio), int(50 * self.ratio), background=icon - ) - return BuildImage( - int(50 * self.ratio), - int(50 * self.ratio), - background=f"{icon_path}/box.png", - ) - - # def _get_shortest_path(self, res: 'Resources', res_2: 'Resources'): - - -# 资源类 -class Resources: - def __init__(self, x: int, y: int): - self.x = x - self.y = y - - def get_distance(self, x: int, y: int): - return int(sqrt(pow(abs(self.x - x), 2) + pow(abs(self.y - y), 2))) - - # 拿到资源在该列表中的最短路径 - def get_resource_distance(self, resources: List["Resources"]) -> "Resources, int": - current_res = None - min_distance = 999999 - for res in resources: - distance = self.get_distance(res.x, res.y) - if distance < min_distance and res != self: - current_res = res - min_distance = distance - return current_res, min_distance - - - - - diff --git a/plugins/genshin/query_resource_points/query_resource.py b/plugins/genshin/query_resource_points/query_resource.py deleted file mode 100755 index 53c2beaf..00000000 --- a/plugins/genshin/query_resource_points/query_resource.py +++ /dev/null @@ -1,272 +0,0 @@ -from pathlib import Path -from typing import Tuple, Optional, List -from configs.path_config import IMAGE_PATH, TEXT_PATH, TEMP_PATH -from utils.message_builder import image -from services.log import logger -from utils.image_utils import BuildImage -from asyncio.exceptions import TimeoutError -from asyncio import Semaphore -from utils.image_utils import is_valid -from utils.http_utils import AsyncHttpx -from httpx import ConnectTimeout -from .map import Map -import asyncio -import nonebot -import os - -try: - import ujson as json -except ModuleNotFoundError: - import json - -driver: nonebot.Driver = nonebot.get_driver() - -LABEL_URL = "https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/map/label/tree?app_sn=ys_obc" -POINT_LIST_URL = "https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/map/point/list?map_id=2&app_sn=ys_obc" -MAP_URL = "https://api-static.mihoyo.com/common/map_user/ys_obc/v1/map/info?map_id=2&app_sn=ys_obc&lang=zh-cn" - -icon_path = IMAGE_PATH / "genshin" / "genshin_icon" -map_path = IMAGE_PATH / "genshin" / "map" -resource_label_file = TEXT_PATH / "genshin" / "resource_label_file.json" -resource_point_file = TEXT_PATH / "genshin" / "resource_point_file.json" -resource_type_file = TEXT_PATH / "genshin" / "resource_type_file.json" - -# 地图中心坐标 -CENTER_POINT: Optional[Tuple[int, int]] = None - -resource_name_list: List[str] = [] - -MAP_RATIO = 0.5 - - -# 查找资源 -async def query_resource(resource_name: str) -> str: - global CENTER_POINT - planning_route: bool = False - if resource_name and resource_name[-2:] in ["路径", "路线"]: - resource_name = resource_name[:-2].strip() - planning_route = True - if not resource_name or resource_name not in resource_name_list: - # return f"未查找到 {resource_name} 资源,可通过 “原神资源列表” 获取全部资源名称.." - return "" - map_ = Map( - resource_name, CENTER_POINT, planning_route=planning_route, ratio=MAP_RATIO - ) - count = map_.get_resource_count() - rand = await asyncio.get_event_loop().run_in_executor( - None, map_.generate_resource_icon_in_map - ) - return ( - f"{image(TEMP_PATH / f'genshin_map_{rand}.png')}" - f"\n\n※ {resource_name} 一共找到 {count} 个位置点\n※ 数据来源于米游社wiki" - ) - - -# 原神资源列表 -def get_resource_type_list(): - with open(resource_type_file, "r", encoding="utf8") as f: - data = json.load(f) - temp = {} - for id_ in data.keys(): - temp[data[id_]["name"]] = [] - for x in data[id_]["children"]: - temp[data[id_]["name"]].append(x["name"]) - - mes = "当前资源列表如下:\n" - - for resource_type in temp.keys(): - mes += f"{resource_type}:{','.join(temp[resource_type])}\n" - return mes - - -def check_resource_exists(resource: str) -> bool: - """ - 检查资源是否存在 - :param resource: 资源名称 - """ - resource = resource.replace("路径", "").replace("路线", "") - return resource in resource_name_list - - -@driver.on_startup -async def init(flag: bool = False): - global CENTER_POINT, resource_name_list - try: - semaphore = asyncio.Semaphore(10) - await download_map_init(semaphore, flag) - await download_resource_data(semaphore) - await download_resource_type() - if not CENTER_POINT: - if resource_label_file.exists(): - CENTER_POINT = json.load( - open(resource_label_file, "r", encoding="utf8") - )["CENTER_POINT"] - if resource_label_file.exists(): - with open(resource_type_file, "r", encoding="utf8") as f: - data = json.load(f) - for id_ in data: - for x in data[id_]["children"]: - resource_name_list.append(x["name"]) - except TimeoutError: - logger.warning("原神资源查询信息初始化超时....") - - -# 图标及位置资源 -async def download_resource_data(semaphore: Semaphore): - icon_path.mkdir(parents=True, exist_ok=True) - resource_label_file.parent.mkdir(parents=True, exist_ok=True) - try: - response = await AsyncHttpx.get(POINT_LIST_URL, timeout=10) - if response.status_code == 200: - data = response.json() - if data["message"] == "OK": - data = data["data"] - for lst in ["label_list", "point_list"]: - resource_data = {"CENTER_POINT": CENTER_POINT} - tasks = [] - file = ( - resource_label_file - if lst == "label_list" - else resource_point_file - ) - for x in data[lst]: - id_ = x["id"] - if lst == "label_list": - img_url = x["icon"] - tasks.append( - asyncio.ensure_future( - download_image( - img_url, - icon_path / f"{id_}.png", - semaphore, - True, - ) - ) - ) - resource_data[id_] = x - await asyncio.gather(*tasks) - with open(file, "w", encoding="utf8") as f: - json.dump(resource_data, f, ensure_ascii=False, indent=4) - else: - logger.warning(f'获取原神资源失败 msg: {data["message"]}') - else: - logger.warning(f"获取原神资源失败 code:{response.status_code}") - except (TimeoutError, ConnectTimeout): - logger.warning("获取原神资源数据超时...已再次尝试...") - await download_resource_data(semaphore) - except Exception as e: - logger.error(f"获取原神资源数据未知错误 {type(e)}:{e}") - - -# 下载原神地图并拼图 -async def download_map_init(semaphore: Semaphore, flag: bool = False): - global CENTER_POINT, MAP_RATIO - map_path.mkdir(exist_ok=True, parents=True) - _map = map_path / "map.png" - if _map.exists() and os.path.getsize(_map) > 1024 * 1024 * 30: - _map.unlink() - try: - response = await AsyncHttpx.get(MAP_URL, timeout=10) - if response.status_code == 200: - data = response.json() - if data["message"] == "OK": - data = json.loads(data["data"]["info"]["detail"]) - CENTER_POINT = (data["origin"][0], data["origin"][1]) - if not _map.exists() or flag: - data = data["slices"] - idx = 0 - w_len = len(data[0]) - h_len = len(data) - for _map_data in data: - for _map in _map_data: - map_url = _map["url"] - await download_image( - map_url, - map_path / f"{idx}.png", - semaphore, - force_flag=flag, - ) - BuildImage( - 0, 0, background=f"{map_path}/{idx}.png", ratio=MAP_RATIO - ).save() - idx += 1 - w, h = BuildImage(0, 0, background=f"{map_path}/0.png").size - map_file = BuildImage(w * w_len, h * h_len, w, h, ratio=MAP_RATIO) - for i in range(idx): - img = BuildImage(0, 0, background=f"{map_path}/{i}.png") - await map_file.apaste(img) - map_file.save(f"{map_path}/map.png") - else: - logger.warning(f'获取原神地图失败 msg: {data["message"]}') - else: - logger.warning(f"获取原神地图失败 code:{response.status_code}") - except (TimeoutError, ConnectTimeout): - logger.warning("下载原神地图数据超时....") - except Exception as e: - logger.error(f"下载原神地图数据失败 {type(e)}:{e}") - - -# 下载资源类型数据 -async def download_resource_type(): - resource_type_file.parent.mkdir(parents=True, exist_ok=True) - try: - response = await AsyncHttpx.get(LABEL_URL, timeout=10) - if response.status_code == 200: - data = response.json() - if data["message"] == "OK": - data = data["data"]["tree"] - resource_data = {} - for x in data: - id_ = x["id"] - resource_data[id_] = x - with open(resource_type_file, "w", encoding="utf8") as f: - json.dump(resource_data, f, ensure_ascii=False, indent=4) - logger.info(f"更新原神资源类型成功...") - else: - logger.warning(f'获取原神资源类型失败 msg: {data["message"]}') - else: - logger.warning(f"获取原神资源类型失败 code:{response.status_code}") - except (TimeoutError, ConnectTimeout): - logger.warning("下载原神资源类型数据超时....") - except Exception as e: - logger.error(f"载原神资源类型数据错误 {type(e)}:{e}") - - -# 初始化资源图标 -def gen_icon(icon: Path): - A = BuildImage(0, 0, background=f"{icon_path}/box.png") - B = BuildImage(0, 0, background=f"{icon_path}/box_alpha.png") - icon_img = BuildImage(115, 115, background=icon) - icon_img.circle() - B.paste(icon_img, (17, 10), True) - B.paste(A, alpha=True) - B.save(icon) - logger.info(f"生成图片成功 file:{str(icon)}") - - -# 下载图片 -async def download_image( - img_url: str, - path: Path, - semaphore: Semaphore, - gen_flag: bool = False, - force_flag: bool = False, -): - async with semaphore: - try: - if not path.exists() or not is_valid(path) or force_flag: - if await AsyncHttpx.download_file(img_url, path, timeout=10): - logger.info(f"下载原神资源图标:{img_url}") - if gen_flag: - gen_icon(path) - else: - logger.info(f"下载原神资源图标:{img_url} 失败,等待下次更新...") - except Exception as e: - logger.warning(f"原神图片错误..已删除,等待下次更新... file: {path} {type(e)}:{e}") - if os.path.exists(path): - os.remove(path) - - -# -# def _get_point_ratio(): -# diff --git a/plugins/genshin/query_user/__init__.py b/plugins/genshin/query_user/__init__.py deleted file mode 100644 index 49b8a858..00000000 --- a/plugins/genshin/query_user/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from pathlib import Path - -from configs.config import Config -import nonebot - - -Config.add_plugin_config( - "genshin", - "mhyVersion", - "2.11.1" -) - -Config.add_plugin_config( - "genshin", - "salt", - "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs" -) - -Config.add_plugin_config( - "genshin", - "n", - "h8w582wxwgqvahcdkpvdhbh2w9casgfl" -) - -Config.add_plugin_config( - "genshin", - "client_type", - "5" -) - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) - - - - diff --git a/plugins/genshin/query_user/_models/__init__.py b/plugins/genshin/query_user/_models/__init__.py deleted file mode 100644 index dacb029e..00000000 --- a/plugins/genshin/query_user/_models/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -import random -from datetime import datetime, timedelta -from typing import Optional - -import pytz -from tortoise import fields -from tortoise.contrib.postgres.functions import Random - -from services.db_context import Model - - -class Genshin(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - uid = fields.BigIntField() - """uid""" - mys_id: int = fields.BigIntField(null=True) - """米游社id""" - cookie: str = fields.TextField(default="") - """米游社cookie""" - auto_sign = fields.BooleanField(default=False) - """是否自动签到""" - today_query_uid = fields.TextField(default="") - """cookie今日查询uid""" - auto_sign_time = fields.DatetimeField(null=True) - """签到日期时间""" - resin_remind = fields.BooleanField(default=False) - """树脂提醒""" - resin_recovery_time = fields.DatetimeField(null=True) - """满树脂提醒日期""" - bind_group: int = fields.BigIntField(null=True) - """发送提示 绑定群聊""" - login_ticket = fields.TextField(default="") - """login_ticket""" - stuid: str = fields.TextField(default="") - """stuid""" - stoken: str = fields.TextField(default="") - """stoken""" - - class Meta: - table = "genshin" - table_description = "原神数据表" - unique_together = ("user_id", "uid") - - @classmethod - async def random_sign_time(cls, uid: int) -> Optional[datetime]: - """ - 说明: - 随机签到时间 - 说明: - :param uid: uid - """ - user = await cls.get_or_none(uid=uid) - if user and user.cookie: - if user.auto_sign_time and user.auto_sign_time.astimezone( - pytz.timezone("Asia/Shanghai") - ) - timedelta(seconds=2) >= datetime.now(pytz.timezone("Asia/Shanghai")): - return user.auto_sign_time.astimezone(pytz.timezone("Asia/Shanghai")) - hours = int(str(datetime.now()).split()[1].split(":")[0]) - minutes = int(str(datetime.now()).split()[1].split(":")[1]) - date = ( - datetime.now() - + timedelta(days=1) - - timedelta(hours=hours) - - timedelta(minutes=minutes - 1) - ) - random_hours = random.randint(0, 22) - random_minutes = random.randint(1, 59) - date += timedelta(hours=random_hours) + timedelta(minutes=random_minutes) - user.auto_sign_time = date - await user.save(update_fields=["auto_sign_time"]) - return date - return None - - @classmethod - async def random_cookie(cls, uid: int) -> Optional[str]: - """ - 说明: - 随机获取查询角色信息cookie - 参数: - :param uid: 原神uid - """ - # 查找用户今日是否已经查找过,防止重复 - user = await cls.get_or_none(today_query_uid__contains=str(uid)) - if user: - return user.cookie - for user in await cls.filter(cookie__not="").annotate(rand=Random()).all(): - if not user.today_query_uid or len(user.today_query_uid[:-1].split()) < 30: - user.today_query_uid = user.today_query_uid + f"{uid} " - await user.save(update_fields=["today_query_uid"]) - return user.cookie - return None - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE genshin ADD auto_sign_time timestamp with time zone;", - "ALTER TABLE genshin ADD resin_remind boolean DEFAULT False;", - "ALTER TABLE genshin ADD resin_recovery_time timestamp with time zone;", - "ALTER TABLE genshin ADD login_ticket VARCHAR(255) DEFAULT '';", - "ALTER TABLE genshin ADD stuid VARCHAR(255) DEFAULT '';", - "ALTER TABLE genshin ADD stoken VARCHAR(255) DEFAULT '';", - "ALTER TABLE genshin RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE genshin ALTER COLUMN user_id TYPE character varying(255);", - ] diff --git a/plugins/genshin/query_user/_utils/__init__.py b/plugins/genshin/query_user/_utils/__init__.py deleted file mode 100644 index 8dddb2c9..00000000 --- a/plugins/genshin/query_user/_utils/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -import hashlib -import json -import random -import string -import time - -from configs.config import Config - - -def _md5(text): - md5 = hashlib.md5() - md5.update(text.encode()) - return md5.hexdigest() - - -def get_old_ds() -> str: - n = Config.get_config("genshin", "n") - i = str(int(time.time())) - r = "".join(random.sample(string.ascii_lowercase + string.digits, 6)) - c = _md5("salt=" + n + "&t=" + i + "&r=" + r) - return i + "," + r + "," + c - - -def get_ds(q: str = "", b: dict = None) -> str: - if b: - br = json.dumps(b) - else: - br = "" - s = Config.get_config("genshin", "salt") - t = str(int(time.time())) - r = str(random.randint(100000, 200000)) - c = _md5("salt=" + s + "&t=" + t + "&r=" + r + "&b=" + br + "&q=" + q) - return t + "," + r + "," + c - - -def random_hex(length: int) -> str: - result = hex(random.randint(0, 16**length)).replace("0x", "").upper() - if len(result) < length: - result = "0" * (length - len(result)) + result - return result - - -element_mastery = { - "anemo": "风", - "pyro": "火", - "geo": "岩", - "electro": "雷", - "cryo": "冰", - "hydro": "水", - "dendro": "草", - "none": "无", -} diff --git a/plugins/genshin/query_user/bind/__init__.py b/plugins/genshin/query_user/bind/__init__.py deleted file mode 100644 index 86da4281..00000000 --- a/plugins/genshin/query_user/bind/__init__.py +++ /dev/null @@ -1,153 +0,0 @@ -import json -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import Command, CommandArg - -from services.log import logger -from utils.depends import OneCommand -from utils.http_utils import AsyncHttpx -from utils.utils import is_number - -from .._models import Genshin - -__zx_plugin_name__ = "原神绑定" -__plugin_usage__ = """ -usage: - 绑定原神uid等数据,cookie极为重要,请谨慎绑定 - ** 如果对拥有者不熟悉,并不建议添加cookie ** - 该项目只会对cookie用于”米游社签到“,“原神玩家查询”,“原神便笺查询” - 指令: - 原神绑定uid [uid] - 原神绑定米游社id [mys_id] - 原神绑定cookie [cookie] # 该绑定请私聊 - 原神解绑 - 示例:原神绑定uid 92342233 - 如果不明白怎么获取cookie请输入“原神绑定cookie”。 -""".strip() -__plugin_des__ = "绑定自己的原神uid等" -__plugin_cmd__ = ["原神绑定uid [uid]", "原神绑定米游社id [mys_id]", "原神绑定cookie [cookie]", "原神解绑"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神绑定"], -} - -bind = on_command( - "原神绑定uid", aliases={"原神绑定米游社id", "原神绑定cookie"}, priority=5, block=True -) - -unbind = on_command("原神解绑", priority=5, block=True) - -web_Api = "https://api-takumi.mihoyo.com" -bbs_Cookie_url = "https://webapi.account.mihoyo.com/Api/cookie_accountinfo_by_loginticket?login_ticket={}" -bbs_Cookie_url2 = ( - web_Api - + "/auth/api/getMultiTokenByLoginTicket?login_ticket={}&token_types=3&uid={}" -) - - -@bind.handle() -async def _(event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if cmd in ["原神绑定uid", "原神绑定米游社id"]: - if not is_number(msg): - await bind.finish("uid/id必须为纯数字!", at_senders=True) - msg = int(msg) - if cmd == "原神绑定uid": - if user: - await bind.finish(f"您已绑定过uid:{user.uid},如果希望更换uid,请先发送原神解绑") - if await Genshin.get_or_none(user_id=str(event.user_id), uid=msg): - await bind.finish("添加失败,该uid可能已存在...") - user = await Genshin.create(user_id=str(event.user_id), uid=msg) - _x = f"已成功添加原神uid:{msg}" - elif cmd == "原神绑定米游社id": - if not user: - await bind.finish("请先绑定原神uid..") - user.mys_id = int(msg) - _x = f"已成功为uid:{user.uid} 设置米游社id:{msg}" - else: - if not msg: - await bind.finish( - """私聊发送!! - 1.以无痕模式打开浏览器(Edge请新建InPrivate窗口) - 2.打开http://bbs.mihoyo.com/ys/ 并登陆 - 3.登陆后打开http://user.mihoyo.com/进行登陆 - 4.按下F12,打开控制台,输入以下命令: - var cookie=document.cookie;var ask=confirm('Cookie:'+cookie+'\\n\\nDo you want to copy the cookie to the clipboard?');if(ask==true){copy(cookie);msg=cookie}else{msg='Cancel'} - 5.私聊发送:原神绑定cookie 刚刚复制的cookie""" - ) - if isinstance(event, GroupMessageEvent): - await bind.finish("请立即撤回你的消息并私聊发送!") - if not user: - await bind.finish("请先绑定原神uid..") - if msg.startswith('"') or msg.startswith("'"): - msg = msg[1:] - if msg.endswith('"') or msg.endswith("'"): - msg = msg[:-1] - cookie = msg - # 用: 代替=, ,代替; - cookie = '{"' + cookie.replace("=", '": "').replace("; ", '","') + '"}' - # print(cookie) - cookie_json = json.loads(cookie) - # print(cookie_json) - if "login_ticket" not in cookie_json: - await bind.finish("请发送正确完整的cookie!") - user.cookie = str(msg) - login_ticket = cookie_json["login_ticket"] - # try: - res = await AsyncHttpx.get(url=bbs_Cookie_url.format(login_ticket)) - res.encoding = "utf-8" - data = json.loads(res.text) - # print(data) - if "成功" in data["data"]["msg"]: - stuid = str(data["data"]["cookie_info"]["account_id"]) - res = await AsyncHttpx.get(url=bbs_Cookie_url2.format(login_ticket, stuid)) - res.encoding = "utf-8" - data = json.loads(res.text) - stoken = data["data"]["list"][0]["token"] - # await Genshin.set_cookie(uid, cookie) - user.stoken = stoken - user.stuid = stuid - user.login_ticket = login_ticket - # except Exception as e: - # await bind.finish("获取登陆信息失败,请检查cookie是否正确或更新cookie") - elif data["data"]["msg"] == "登录信息已失效,请重新登录": - await bind.finish("登录信息失效,请重新获取最新cookie进行绑定") - _x = f"已成功为uid:{user.uid} 设置cookie" - if isinstance(event, GroupMessageEvent): - user.bind_group = event.group_id - if user: - await user.save( - update_fields=[ - "mys_id", - "cookie", - "stoken", - "stuid", - "login_ticket", - "bind_group", - ] - ) - await bind.send(_x) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" {cmd}:{msg}" - ) - - -@unbind.handle() -async def _(event: MessageEvent): - await Genshin.filter(user_id=str(event.user_id)).delete() - await unbind.send("用户数据删除成功...") - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f"原神解绑" - ) diff --git a/plugins/genshin/query_user/genshin_sign/__init__.py b/plugins/genshin/query_user/genshin_sign/__init__.py deleted file mode 100644 index 732f0f44..00000000 --- a/plugins/genshin/query_user/genshin_sign/__init__.py +++ /dev/null @@ -1,121 +0,0 @@ -from typing import Tuple - -from apscheduler.jobstores.base import JobLookupError -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.params import Command - -from services.log import logger -from utils.depends import OneCommand - -from .._models import Genshin -from ..mihoyobbs_sign import mihoyobbs_sign -from .data_source import genshin_sign, get_sign_reward_list -from .init_task import _sign, add_job, scheduler - -__zx_plugin_name__ = "原神自动签到" -__plugin_usage__ = """ -usage: - 米游社原神签到,需要uid以及cookie - 且在第二天自动排序签到时间 - # 不听,就要手动签到!(使用命令 “原神我硬签 - 指令: - 开/关原神自动签到 - 原神我硬签 -""".strip() -__plugin_des__ = "原神懒人签到" -__plugin_cmd__ = ["开启/关闭原神自动签到", "原神我硬签", "查看我的cookie"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神签到"], -} - - -genshin_matcher = on_command( - "开原神自动签到", aliases={"关原神自动签到", "原神我硬签", "查看我的cookie"}, priority=5, block=True -) - - -@genshin_matcher.handle() -async def _(event: MessageEvent, cmd: str = OneCommand()): - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not user: - await genshin_matcher.finish("请先绑定user.uid...") - if cmd == "查看我的cookie": - if isinstance(event, GroupMessageEvent): - await genshin_matcher.finish("请私聊查看您的cookie!") - await genshin_matcher.finish("您的cookie为" + user.cookie) - if not user.uid or not user.cookie: - await genshin_matcher.finish("请先绑定user.uid和cookie!") - # if "account_id" not in await Genshin.get_user_cookie(user.uid, True): - # await genshin_matcher.finish("请更新cookie!") - if cmd == "原神我硬签": - try: - await genshin_matcher.send("正在进行签到...", at_sender=True) - msg = await genshin_sign(user.uid) - return_data = await mihoyobbs_sign(event.user_id) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) UID:{user.uid} 原神签到" - ) - logger.info(msg) - # 硬签,移除定时任务 - try: - for i in range(3): - scheduler.remove_job( - f"genshin_auto_sign_{user.uid}_{event.user_id}_{i}", - ) - except JobLookupError: - pass - if user.auto_sign: - user.auto_sign_time = None - next_date = await Genshin.random_sign_time(user.uid) - add_job(event.user_id, user.uid, next_date) - msg += f"\n{return_data}\n因开启自动签到\n下一次签到时间为:{next_date.replace(microsecond=0)}" - except Exception as e: - msg = "原神签到失败..请尝试检查cookie或报告至管理员!" - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) UID:{user.uid} 原神签到发生错误 " - f"{type(e)}:{e}" - ) - msg = msg or "请检查cookie是否更新!" - await genshin_matcher.send(msg, at_sender=True) - else: - for i in range(3): - try: - scheduler.remove_job( - f"genshin_auto_sign_{user.uid}_{event.user_id}_{i}" - ) - except JobLookupError: - pass - if cmd[0] == "开": - next_date = await Genshin.random_sign_time(user.uid) - user.auto_sign = True - user.auto_sign_time = next_date - add_job(event.user_id, user.uid, next_date) - await genshin_matcher.send( - f"已开启原神自动签到!\n下一次签到时间为:{next_date.replace(microsecond=0)}", - at_sender=True, - ) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 开启原神自动签到" - ) - else: - user.auto_sign = False - user.auto_sign_time = None - await genshin_matcher.send(f"已关闭原神自动签到!", at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 关闭原神自动签到" - ) - if user: - await user.save(update_fields=["auto_sign_time", "auto_sign"]) diff --git a/plugins/genshin/query_user/genshin_sign/data_source.py b/plugins/genshin/query_user/genshin_sign/data_source.py deleted file mode 100644 index 1dbaac4c..00000000 --- a/plugins/genshin/query_user/genshin_sign/data_source.py +++ /dev/null @@ -1,153 +0,0 @@ -import hashlib -import random -import string -import time -import uuid -from typing import Dict, Optional - -from configs.config import Config -from services.log import logger -from utils.http_utils import AsyncHttpx - -from .._models import Genshin -from ..mihoyobbs_sign.setting import * - - -async def genshin_sign(uid: int) -> Optional[str]: - """ - 原神签到信息 - :param uid: uid - """ - data = await _sign(uid) - if not data: - return "签到失败..." - status = data["message"] - if status == "OK": - try: - sign_info = await _get_sign_info(uid) - if sign_info: - sign_info = sign_info["data"] - sign_list = await get_sign_reward_list() - get_reward = sign_list["data"]["awards"][ - int(sign_info["total_sign_day"]) - 1 - ]["name"] - reward_num = sign_list["data"]["awards"][ - int(sign_info["total_sign_day"]) - 1 - ]["cnt"] - get_im = f"本次签到获得:{get_reward}x{reward_num}" - logger.info("get_im:" + get_im + "\nsign_info:" + str(sign_info)) - if status == "OK" and sign_info["is_sign"]: - return f"原神签到成功!\n{get_im}\n本月漏签次数:{sign_info['sign_cnt_missed']}" - except Exception as e: - logger.error(f"原神签到发生错误 UID:{str(data)}") - return f"原神签到发生错误: {str(data)}" - else: - return status - if data["data"]["risk_code"] == 375: - return "原神签到失败\n账号可能被风控,请前往米游社手动签到!" - return str(data) - - -# 获取请求Header里的DS 当web为true则生成网页端的DS -def get_ds(web: bool) -> str: - if web: - n = mihoyobbs_Salt_web - else: - n = mihoyobbs_Salt - i = str(timestamp()) - r = random_text(6) - c = md5("salt=" + n + "&t=" + i + "&r=" + r) - return f"{i},{r},{c}" - - -# 时间戳 -def timestamp() -> int: - return int(time.time()) - - -def random_text(num: int) -> str: - return "".join(random.sample(string.ascii_lowercase + string.digits, num)) - - -def md5(text: str) -> str: - md5 = hashlib.md5() - md5.update(text.encode()) - return md5.hexdigest() - - -# 生成一个device id -def get_device_id(cookie) -> str: - return str(uuid.uuid3(uuid.NAMESPACE_URL, cookie)).replace("-", "").upper() - - -async def _sign(uid: int, server_id: str = "cn_gf01") -> Optional[Dict[str, str]]: - """ - 米游社签到 - :param uid: uid - :param server_id: 服务器id - """ - if str(uid)[0] == "5": - server_id = "cn_qd01" - try: - if user := await Genshin.get_or_none(uid=uid): - headers["DS"] = get_ds(web=True) - headers["Referer"] = ( - "https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?bbs_auth_required=true" - f"&act_id={genshin_Act_id}&utm_source=bbs&utm_medium=mys&utm_campaign=icon" - ) - headers["Cookie"] = user.cookie - headers["x-rpc-device_id"] = get_device_id(user.cookie) - req = await AsyncHttpx.post( - url=genshin_Signurl, - headers=headers, - json={"act_id": genshin_Act_id, "uid": uid, "region": server_id}, - ) - return req.json() - except Exception as e: - logger.error(f"米游社签到发生错误 UID:{uid} {type(e)}:{e}") - return None - - -async def get_sign_reward_list(): - """ - 获取签到奖励列表 - """ - try: - req = await AsyncHttpx.get( - url="https://api-takumi.mihoyo.com/event/bbs_sign_reward/home?act_id=e202009291139501", - headers={ - "x-rpc-app_version": str(Config.get_config("genshin", "mhyVersion")), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": str(Config.get_config("genshin", "client_type")), - "Referer": "https://webstatic.mihoyo.com/", - }, - ) - return req.json() - except Exception as e: - logger.error(f"获取签到奖励列表发生错误 {type(e)}:{e}") - return None - - -async def _get_sign_info(uid: int, server_id: str = "cn_gf01"): - if str(uid)[0] == "5": - server_id = "cn_qd01" - try: - if user := await Genshin.get_or_none(uid=uid): - req = await AsyncHttpx.get( - url=f"https://api-takumi.mihoyo.com/event/bbs_sign_reward/info?act_id=e202009291139501®ion={server_id}&uid={uid}", - headers={ - "x-rpc-app_version": str( - Config.get_config("genshin", "mhyVersion") - ), - "Cookie": user.cookie, - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": str( - Config.get_config("genshin", "client_type") - ), - "Referer": "https://webstatic.mihoyo.com/", - }, - ) - return req.json() - except Exception as e: - logger.error(f"获取签到信息发生错误 UID:{uid} {type(e)}:{e}") - return None diff --git a/plugins/genshin/query_user/genshin_sign/init_task.py b/plugins/genshin/query_user/genshin_sign/init_task.py deleted file mode 100644 index 828727e1..00000000 --- a/plugins/genshin/query_user/genshin_sign/init_task.py +++ /dev/null @@ -1,124 +0,0 @@ -import random -from datetime import datetime, timedelta - -import nonebot -import pytz -from apscheduler.jobstores.base import ConflictingIdError -from nonebot import Driver - -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.message_builder import at -from utils.utils import get_bot, scheduler - -from .._models import Genshin -from ..mihoyobbs_sign import mihoyobbs_sign -from .data_source import genshin_sign - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - """ - 启动时分配定时任务 - """ - g_list = await Genshin.filter(auto_sign=True).all() - for u in g_list: - if u.auto_sign_time: - if date := await Genshin.random_sign_time(u.uid): - scheduler.add_job( - _sign, - "date", - run_date=date.replace(microsecond=0), - id=f"genshin_auto_sign_{u.uid}_{u.user_id}_0", - args=[u.user_id, u.uid, 0], - ) - logger.info( - f"genshin_sign add_job:USER:{u.user_id} UID:{u.uid} " - f"{date} 原神自动签到" - ) - - -def add_job(user_id: int, uid: int, date: datetime): - try: - scheduler.add_job( - _sign, - "date", - run_date=date.replace(microsecond=0), - id=f"genshin_auto_sign_{uid}_{user_id}_0", - args=[user_id, uid, 0], - ) - logger.debug(f"genshin_sign add_job:{date.replace(microsecond=0)} 原神自动签到") - except ConflictingIdError: - pass - - -async def _sign(user_id: int, uid: int, count: int): - """ - 执行签到任务 - :param user_id: 用户id - :param uid: uid - :param count: 执行次数 - """ - try: - return_data = await mihoyobbs_sign(user_id) - except Exception as e: - logger.error(f"mihoyobbs_sign error:{e}") - return_data = "米游社签到失败,请尝试发送'米游社签到'进行手动签到" - if count < 3: - try: - msg = await genshin_sign(uid) - next_time = await Genshin.random_sign_time(uid) - msg += f"\n下一次签到时间为:{next_time.replace(microsecond=0)}" - logger.info(f"USER:{user_id} UID:{uid} 原神自动签到任务发生成功...") - try: - scheduler.add_job( - _sign, - "date", - run_date=next_time.replace(microsecond=0), - id=f"genshin_auto_sign_{uid}_{user_id}_0", - args=[user_id, uid, 0], - ) - except ConflictingIdError: - msg += "\n定时任务设定失败..." - except Exception as e: - logger.error(f"USER:{user_id} UID:{uid} 原神自动签到任务发生错误 {type(e)}:{e}") - msg = None - if not msg: - now = datetime.now(pytz.timezone("Asia/Shanghai")) - if now.hour < 23: - random_hours = random.randint(1, 23 - now.hour) - next_time = now + timedelta(hours=random_hours) - scheduler.add_job( - _sign, - "date", - run_date=next_time.replace(microsecond=0), - id=f"genshin_auto_sign_{uid}_{user_id}_{count}", - args=[user_id, uid, count + 1], - ) - msg = ( - f"{now.replace(microsecond=0)} 原神" - f"签到失败,将在 {next_time.replace(microsecond=0)} 时重试!" - ) - else: - msg = "今日原神签到失败,请手动签到..." - logger.debug(f"USER:{user_id} UID:{uid} 原神今日签到失败...") - else: - msg = "今日原神自动签到重试次数已达到3次,请手动签到。" - logger.debug(f"USER:{user_id} UID:{uid} 原神今日签到失败次数打到 3 次...") - bot = get_bot() - if bot: - if user_id in [x["user_id"] for x in await bot.get_friend_list()]: - await bot.send_private_msg(user_id=user_id, message=return_data) - await bot.send_private_msg(user_id=user_id, message=msg) - else: - if user := await Genshin.get_or_none(uid=uid): - group_id = user.bind_group - if not group_id: - if group_list := await GroupInfoUser.get_user_all_group(user_id): - group_id = group_list[0] - if group_id: - await bot.send_group_msg( - group_id=group_id, message=at(user_id) + msg - ) diff --git a/plugins/genshin/query_user/mihoyobbs_sign/__init__.py b/plugins/genshin/query_user/mihoyobbs_sign/__init__.py deleted file mode 100644 index 5bd3267c..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.params import Command - -from services.log import logger - -# from .init_task import add_job, scheduler, _sign -# from apscheduler.jobstores.base import JobLookupError -from .._models import Genshin -from .mihoyobbs import * - -__zx_plugin_name__ = "米游社自动签到" -__plugin_usage__ = """ -usage: - 发送'米游社签到'或绑定原神自动签到 - 即可手动/自动进行米游社签到 - (若启用了原神自动签到会在签到原神同时完成米游币领取) - --> 每天白嫖90-110米游币不香吗 - 注:需要重新绑定原神cookie!!! - 遇到问题请提issue或@作者 -""".strip() -__plugin_des__ = "米游社自动签到任务" -__plugin_cmd__ = ["米游社签到", "米游社我硬签"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HDU_Nbsp" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["米游社签到"], -} - -mihoyobbs_matcher = on_command("米游社签到", aliases={"米游社我硬签"}, priority=5, block=True) - - -@mihoyobbs_matcher.handle() -async def _(event: MessageEvent, cmd: Tuple[str, ...] = Command()): - await mihoyobbs_matcher.send("提交米游社签到申请", at_sender=True) - return_data = await mihoyobbs_sign(event.user_id) - if return_data: - await mihoyobbs_matcher.finish(return_data, at_sender=True) - else: - await mihoyobbs_matcher.finish("米游社签到失败,请查看控制台输出", at_sender=True) - - -async def mihoyobbs_sign(user_id): - user = await Genshin.get_or_none(user_id=str(user_id)) - if not user or not user.uid or not user.cookie: - await mihoyobbs_matcher.finish("请先绑定uid和cookie!", at_sender=True) - bbs = mihoyobbs.Mihoyobbs(stuid=user.stuid, stoken=user.stoken, cookie=user.cookie) - await bbs.init() - return_data = "" - if ( - bbs.Task_do["bbs_Sign"] - and bbs.Task_do["bbs_Read_posts"] - and bbs.Task_do["bbs_Like_posts"] - and bbs.Task_do["bbs_Share"] - ): - return_data += ( - f"今天的米游社签到任务已经全部完成了!\n" - f"一共获得{mihoyobbs.today_have_get_coins}个米游币\n目前有{mihoyobbs.Have_coins}个米游币" - ) - logger.info( - f"今天已经全部完成了!一共获得{mihoyobbs.today_have_get_coins}个米游币,目前有{mihoyobbs.Have_coins}个米游币" - ) - else: - i = 0 - # print("开始签到") - # print(mihoyobbs.today_have_get_coins) - while mihoyobbs.today_get_coins != 0 and i < 3: - # if i > 0: - await bbs.refresh_list() - await bbs.signing() - await bbs.read_posts() - await bbs.like_posts() - await bbs.share_post() - await bbs.get_tasks_list() - i += 1 - return_data += ( - "\n" + f"今天已经获得{mihoyobbs.today_have_get_coins}个米游币\n" - f"还能获得{mihoyobbs.today_get_coins}个米游币\n目前有{mihoyobbs.Have_coins}个米游币" - ) - logger.info( - f"今天已经获得{mihoyobbs.today_have_get_coins}个米游币," - f"还能获得{mihoyobbs.today_get_coins}个米游币,目前有{mihoyobbs.Have_coins}个米游币" - ) - return return_data diff --git a/plugins/genshin/query_user/mihoyobbs_sign/error.py b/plugins/genshin/query_user/mihoyobbs_sign/error.py deleted file mode 100644 index b5e6ff00..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/error.py +++ /dev/null @@ -1,6 +0,0 @@ -class CookieError(Exception): - def __init__(self, info): - self.info = info - - def __str__(self): - return repr(self.info) diff --git a/plugins/genshin/query_user/mihoyobbs_sign/mihoyobbs.py b/plugins/genshin/query_user/mihoyobbs_sign/mihoyobbs.py deleted file mode 100644 index e8d67a8f..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/mihoyobbs.py +++ /dev/null @@ -1,193 +0,0 @@ -from services.log import logger -from .error import CookieError -from utils.http_utils import AsyncHttpx -from .setting import * -from .tools import * -import json - -today_get_coins = 0 -today_have_get_coins = 0 # 这个变量以后可能会用上,先留着了 -Have_coins = 0 - - -class Mihoyobbs: - def __init__(self, stuid: str, stoken: str, cookie: str) -> None: - self.postsList = None - self.headers = { - "DS": get_ds(web=False), - "cookie": f'stuid={stuid};stoken={stoken}', - "x-rpc-client_type": mihoyobbs_Client_type, - "x-rpc-app_version": mihoyobbs_Version, - "x-rpc-sys_version": "6.0.1", - "x-rpc-channel": "miyousheluodi", - "x-rpc-device_id": get_device_id(cookie=cookie), - "x-rpc-device_name": random_text(random.randint(1, 10)), - "x-rpc-device_model": "Mi 10", - "Referer": "https://app.mihoyo.com", - "Host": "bbs-api.mihoyo.com", - "User-Agent": "okhttp/4.8.0" - } - self.Task_do = { - "bbs_Sign": False, - "bbs_Read_posts": False, - "bbs_Read_posts_num": 3, - "bbs_Like_posts": False, - "bbs_Like_posts_num": 5, - "bbs_Share": False - } - - async def init(self): - await self.get_tasks_list() - # 如果这三个任务都做了就没必要获取帖子了 - if self.Task_do["bbs_Read_posts"] and self.Task_do["bbs_Like_posts"] and self.Task_do["bbs_Share"]: - pass - else: - self.postsList = await self.get_list() - - async def refresh_list(self) -> None: - self.postsList = await self.get_list() - - # 获取任务列表,用来判断做了哪些任务 - async def get_tasks_list(self): - global today_get_coins - global today_have_get_coins - global Have_coins - logger.info("正在获取任务列表") - req = await AsyncHttpx.get(url=bbs_Tasks_list, headers=self.headers) - data = req.json() - if "err" in data["message"] or data["retcode"] == -100: - logger.error("获取任务列表失败,你的cookie可能已过期,请重新设置cookie。") - raise CookieError('Cookie expires') - else: - today_get_coins = data["data"]["can_get_points"] - today_have_get_coins = data["data"]["already_received_points"] - Have_coins = data["data"]["total_points"] - # 如果当日可获取米游币数量为0直接判断全部任务都完成了 - if today_get_coins == 0: - self.Task_do["bbs_Sign"] = True - self.Task_do["bbs_Read_posts"] = True - self.Task_do["bbs_Like_posts"] = True - self.Task_do["bbs_Share"] = True - else: - # 如果第0个大于或等于62则直接判定任务没做 - if data["data"]["states"][0]["mission_id"] >= 62: - logger.info(f"今天可以获得{today_get_coins}个米游币") - pass - else: - logger.info(f"还有任务未完成,今天还能获得{today_get_coins}米游币") - for i in data["data"]["states"]: - # 58是讨论区签到 - if i["mission_id"] == 58: - if i["is_get_award"]: - self.Task_do["bbs_Sign"] = True - # 59是看帖子 - elif i["mission_id"] == 59: - if i["is_get_award"]: - self.Task_do["bbs_Read_posts"] = True - else: - self.Task_do["bbs_Read_posts_num"] -= i["happened_times"] - # 60是给帖子点赞 - elif i["mission_id"] == 60: - if i["is_get_award"]: - self.Task_do["bbs_Like_posts"] = True - else: - self.Task_do["bbs_Like_posts_num"] -= i["happened_times"] - # 61是分享帖子 - elif i["mission_id"] == 61: - if i["is_get_award"]: - self.Task_do["bbs_Share"] = True - # 分享帖子,是最后一个任务,到这里了下面都是一次性任务,直接跳出循环 - break - - # 获取要帖子列表 - async def get_list(self) -> list: - temp_list = [] - logger.info("正在获取帖子列表......") - req = await AsyncHttpx.get(url=bbs_List_url.format(mihoyobbs_List_Use[0]["forumId"]), - headers=self.headers) - data = req.json()["data"]["list"] - for n in range(5): - r_l = random.choice(data) - while r_l["post"]["subject"] in str(temp_list): - r_l = random.choice(data) - temp_list.append([r_l["post"]["post_id"], r_l["post"]["subject"]]) - # temp_list.append([data["data"]["list"][n]["post"]["post_id"], data["data"]["list"][n]["post"]["subject"]]) - - logger.info("已获取{}个帖子".format(len(temp_list))) - return temp_list - - # 进行签到操作 - async def signing(self): - if self.Task_do["bbs_Sign"]: - logger.info("讨论区任务已经完成过了~") - else: - logger.info("正在签到......") - header = {} - header.update(self.headers) - for i in mihoyobbs_List_Use: - header["DS"] = get_ds2("", json.dumps({"gids": i["id"]})) - req = await AsyncHttpx.post(url=bbs_Sign_url, json={"gids": i["id"]}, headers=header) - data = req.json() - if "err" not in data["message"]: - logger.info(str(i["name"] + data["message"])) - time.sleep(random.randint(2, 8)) - else: - logger.error("签到失败,你的cookie可能已过期,请重新设置cookie。") - raise CookieError('Cookie expires') - - # 看帖子 - async def read_posts(self): - if self.Task_do["bbs_Read_posts"]: - logger.info("看帖任务已经完成过了~") - else: - logger.info("正在看帖......") - for i in range(self.Task_do["bbs_Read_posts_num"]): - req = await AsyncHttpx.get(url=bbs_Detail_url.format(self.postsList[i][0]), headers=self.headers) - data = req.json() - if data["message"] == "OK": - logger.debug("看帖:{} 成功".format(self.postsList[i][1])) - time.sleep(random.randint(2, 8)) - - # 点赞 - async def like_posts(self): - if self.Task_do["bbs_Like_posts"]: - logger.info("点赞任务已经完成过了~") - else: - logger.info("正在点赞......") - for i in range(self.Task_do["bbs_Like_posts_num"]): - req = await AsyncHttpx.post(url=bbs_Like_url, headers=self.headers, - json={"post_id": self.postsList[i][0], "is_cancel": False}) - data = req.json() - if data["message"] == "OK": - logger.debug("点赞:{} 成功".format(self.postsList[i][1])) - # 判断取消点赞是否打开 - # if config.config["mihoyobbs"]["un_like"] : - # time.sleep(random.randint(2, 8)) - # req = httpx.post(url=bbs_Like_url, headers=self.headers, - # json={"post_id": self.postsList[i][0], "is_cancel": True}) - # data = req.json() - # if data["message"] == "OK": - # logger.debug("取消点赞:{} 成功".format(self.postsList[i][1])) - time.sleep(random.randint(2, 8)) - - # 分享操作 - - async def share_post(self): - if self.Task_do["bbs_Share"]: - logger.info("分享任务已经完成过了~") - else: - logger.info("正在执行分享任务......") - for i in range(3): - req = await AsyncHttpx.get(url=bbs_Share_url.format(self.postsList[0][0]), headers=self.headers) - data = req.json() - if data["message"] == "OK": - logger.debug("分享:{} 成功".format(self.postsList[0][1])) - logger.info("分享任务执行成功......") - break - else: - logger.debug(f"分享任务执行失败,正在执行第{i + 2}次,共3次") - time.sleep(random.randint(2, 8)) - time.sleep(random.randint(2, 8)) - - - diff --git a/plugins/genshin/query_user/mihoyobbs_sign/setting.py b/plugins/genshin/query_user/mihoyobbs_sign/setting.py deleted file mode 100644 index e6b7e538..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/setting.py +++ /dev/null @@ -1,124 +0,0 @@ -# 米游社的Salt -mihoyobbs_Salt = "z8DRIUjNDT7IT5IZXvrUAxyupA1peND9" -mihoyobbs_Salt2 = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v" -mihoyobbs_Salt_web = "9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7" -# 米游社的版本 -mihoyobbs_Version = "2.34.1" # Slat和Version相互对应 -# 米游社的客户端类型 -mihoyobbs_Client_type = "2" # 1为ios 2为安卓 -mihoyobbs_Client_type_web = "5" # 4为pc web 5为mobile web -# 米游社的分区列表 -mihoyobbs_List = [{ - "id": "1", - "forumId": "1", - "name": "崩坏3", - "url": "https://bbs.mihoyo.com/bh3/" -}, { - "id": "2", - "forumId": "26", - "name": "原神", - "url": "https://bbs.mihoyo.com/ys/" -}, { - "id": "3", - "forumId": "30", - "name": "崩坏2", - "url": "https://bbs.mihoyo.com/bh2/" -}, { - "id": "4", - "forumId": "37", - "name": "未定事件簿", - "url": "https://bbs.mihoyo.com/wd/" -}, { - "id": "5", - "forumId": "34", - "name": "大别野", - "url": "https://bbs.mihoyo.com/dby/" -}, { - "id": "6", - "forumId": "52", - "name": "崩坏:星穹铁道", - "url": "https://bbs.mihoyo.com/sr/" -}, { - "id": "8", - "forumId": "57", - "name": "绝区零", - "url": "https://bbs.mihoyo.com/zzz/" -}] - -game_id2name = { - "bh2_cn": "崩坏2", - "bh3_cn": "崩坏3", - "nxx_cn": "未定事件簿", - "hk4e_cn": "原神", -} -# Config Load之后run里面进行列表的选择 -mihoyobbs_List_Use = [{ - "id": "2", - "forumId": "26", - "name": "原神", - "url": "https://bbs.mihoyo.com/ys/" -}, - # 不玩原神可以把签到讨论区换为大别墅 - # { - # "id": "5", - # "forumId": "34", - # "name": "大别野", - # "url": "https://bbs.mihoyo.com/dby/" - # } -] - -# 游戏签到的请求头 -headers = { - 'Accept': 'application/json, text/plain, */*', - 'DS': "", - 'Origin': 'https://webstatic.mihoyo.com', - 'x-rpc-app_version': mihoyobbs_Version, - 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; Unspecified Device) AppleWebKit/537.36 (KHTML, like Gecko) ' - f'Version/4.0 Chrome/103.0.5060.129 Mobile Safari/537.36 miHoYoBBS/{mihoyobbs_Version}', - 'x-rpc-client_type': mihoyobbs_Client_type_web, - 'Referer': '', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'zh-CN,en-US;q=0.8', - 'X-Requested-With': 'com.mihoyo.hyperion', - "Cookie": "", - 'x-rpc-device_id': "" -} - -# 通用设置 -bbs_Api = "https://bbs-api.mihoyo.com" -web_Api = "https://api-takumi.mihoyo.com" -account_Info_url = web_Api + "/binding/api/getUserGameRolesByCookie?game_biz=" - -# 米游社的API列表 -bbs_Cookie_url = "https://webapi.account.mihoyo.com/Api/cookie_accountinfo_by_loginticket?login_ticket={}" -bbs_Cookie_url2 = web_Api + "/auth/api/getMultiTokenByLoginTicket?login_ticket={}&token_types=3&uid={}" -bbs_Tasks_list = bbs_Api + "/apihub/sapi/getUserMissionsState" # 获取任务列表 -bbs_Sign_url = bbs_Api + "/apihub/app/api/signIn" # post -bbs_List_url = bbs_Api + "/post/api/getForumPostList?forum_id={}&is_good=false&is_hot=false&page_size=20&sort_type=1" -bbs_Detail_url = bbs_Api + "/post/api/getPostFull?post_id={}" -bbs_Share_url = bbs_Api + "/apihub/api/getShareConf?entity_id={}&entity_type=1" -bbs_Like_url = bbs_Api + "/apihub/sapi/upvotePost" # post json - -# 崩坏2自动签到相关的相关设置 -honkai2_Act_id = "e202203291431091" -honkai2_checkin_rewards = f'{web_Api}/event/luna/home?lang=zh-cn&act_id={honkai2_Act_id}' -honkai2_Is_signurl = web_Api + "/event/luna/info?lang=zh-cn&act_id={}®ion={}&uid={}" -honkai2_Sign_url = web_Api + "/event/luna/sign" - -# 崩坏3自动签到相关的设置 -honkai3rd_Act_id = "e202207181446311" -honkai3rd_checkin_rewards = f'{web_Api}/event/luna/home?lang=zh-cn&act_id={honkai3rd_Act_id}' -honkai3rd_Is_signurl = web_Api + "/event/luna/info?lang=zh-cn&act_id={}®ion={}&uid={}" -honkai3rd_Sign_url = web_Api + "/event/luna/sign" - -# 未定事件簿自动签到相关设置 -tearsofthemis_Act_id = "e202202251749321" -tearsofthemis_checkin_rewards = f'{web_Api}/event/luna/home?lang=zh-cn&act_id={tearsofthemis_Act_id}' -tearsofthemis_Is_signurl = honkai2_Is_signurl -tearsofthemis_Sign_url = honkai2_Sign_url # 和二崩完全一致 - -# 原神自动签到相关的设置 -genshin_Act_id = "e202009291139501" -genshin_checkin_rewards = f'{web_Api}/event/bbs_sign_reward/home?act_id={genshin_Act_id}' -genshin_Is_signurl = web_Api + "/event/bbs_sign_reward/info?act_id={}®ion={}&uid={}" -genshin_Signurl = web_Api + "/event/bbs_sign_reward/sign" diff --git a/plugins/genshin/query_user/mihoyobbs_sign/tools.py b/plugins/genshin/query_user/mihoyobbs_sign/tools.py deleted file mode 100644 index 2552330e..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/tools.py +++ /dev/null @@ -1,65 +0,0 @@ -import uuid -import time -import random -import string -import hashlib -from .setting import * - - -# md5计算 -def md5(text: str) -> str: - md5 = hashlib.md5() - md5.update(text.encode()) - return md5.hexdigest() - - -# 随机文本 -def random_text(num: int) -> str: - return ''.join(random.sample(string.ascii_lowercase + string.digits, num)) - - -# 时间戳 -def timestamp() -> int: - return int(time.time()) - - -# 获取请求Header里的DS 当web为true则生成网页端的DS -def get_ds(web: bool) -> str: - if web: - n = mihoyobbs_Salt_web - else: - n = mihoyobbs_Salt - i = str(timestamp()) - r = random_text(6) - c = md5("salt=" + n + "&t=" + i + "&r=" + r) - return f"{i},{r},{c}" - - -# 获取请求Header里的DS(版本2) 这个版本ds之前见到都是查询接口里的 -def get_ds2(q: str, b: str) -> str: - n = mihoyobbs_Salt2 - i = str(timestamp()) - r = str(random.randint(100001, 200000)) - add = f'&b={b}&q={q}' - c = md5("salt=" + n + "&t=" + i + "&r=" + r + add) - return f"{i},{r},{c}" - - -# 生成一个device id -def get_device_id(cookie) -> str: - return str(uuid.uuid3(uuid.NAMESPACE_URL, cookie)) - - -# 获取签到的奖励名称 -def get_item(raw_data: dict) -> str: - temp_name = raw_data["name"] - temp_cnt = raw_data["cnt"] - return f"{temp_name}x{temp_cnt}" - - -# 获取明天早晨0点的时间戳 -def next_day() -> int: - now_time = int(time.time()) - next_day_time = now_time - now_time % 86400 + time.timezone + 86400 - return next_day_time - diff --git a/plugins/genshin/query_user/query_memo/__init__.py b/plugins/genshin/query_user/query_memo/__init__.py deleted file mode 100644 index 090c2a7f..00000000 --- a/plugins/genshin/query_user/query_memo/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent - -from services.log import logger - -from .._models import Genshin -from .data_source import get_memo, get_user_memo - -__zx_plugin_name__ = "原神便笺查询" -__plugin_usage__ = """ -usage: - 通过指定cookie和uid查询事实数据 - 指令: - 原神便笺查询/yss - 示例:原神便笺查询 92342233 -""".strip() -__plugin_des__ = "不能浪费丝毫体力" -__plugin_cmd__ = ["原神便笺查询/yss"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神便笺查询"], -} -__plugin_block_limit__ = {} - - -query_memo_matcher = on_command( - "原神便签查询", aliases={"原神便笺查询", "yss"}, priority=5, block=True -) - - -@query_memo_matcher.handle() -async def _(event: MessageEvent): - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not user or not user.uid or not user.cookie: - await query_memo_matcher.finish("请先绑定uid和cookie!") - if isinstance(event, GroupMessageEvent): - uname = event.sender.card or event.sender.nickname - else: - uname = event.sender.nickname - data = await get_user_memo(event.user_id, user.uid, uname) - if data: - await query_memo_matcher.send(data) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) " - f"使用原神便笺查询 uid:{user.uid}" - ) - else: - await query_memo_matcher.send("未查询到数据...") diff --git a/plugins/genshin/query_user/query_memo/data_source.py b/plugins/genshin/query_user/query_memo/data_source.py deleted file mode 100644 index f7fe592f..00000000 --- a/plugins/genshin/query_user/query_memo/data_source.py +++ /dev/null @@ -1,280 +0,0 @@ -import asyncio -from asyncio.exceptions import TimeoutError -from io import BytesIO -from typing import Optional, Tuple, Union - -import nonebot -from nonebot import Driver -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import get_user_avatar - -from .._models import Genshin -from .._utils import get_ds - -driver: Driver = nonebot.get_driver() - - -memo_path = IMAGE_PATH / "genshin" / "genshin_memo" -memo_path.mkdir(exist_ok=True, parents=True) - - -@driver.on_startup -async def _(): - for name, url in zip( - ["resin.png", "task.png", "resin_discount.png", "chengehu.png", "zhibian.png"], - [ - "https://upload-bbs.mihoyo.com/upload/2021/09/29/8819732/54266243c7d15ba31690c8f5d63cc3c6_71491376413333325" - "20.png?x-oss-process=image//resize,s_600/quality,q_80/auto-orient,0/interlace,1/format,png", - "https://patchwiki.biligame.com/images/ys/thumb/c/cc/6k6kuj1kte6m1n7hexqfrn92z6h4yhh.png/60px-委托任务logo.png", - "https://patchwiki.biligame.com/images/ys/d/d9/t1hv6wpucbwucgkhjntmzroh90nmcdv.png", - "https://s3.bmp.ovh/imgs/2022/08/21/3a3b2e6c22e305ff.png", - "https://s3.bmp.ovh/imgs/2022/08/21/c2d7ace21e1d46cf.png", - ], - ): - file = memo_path / name - if not file.exists(): - await AsyncHttpx.download_file(url, file) - logger.info(f"已下载原神便签资源 -> {file}...") - - -async def get_user_memo( - user_id: int, uid: int, uname: str -) -> Optional[Union[str, MessageSegment]]: - uid = str(uid) - if uid[0] in ["1", "2"]: - server_id = "cn_gf01" - elif uid[0] == "5": - server_id = "cn_qd01" - else: - return None - return await parse_data_and_draw(user_id, uid, server_id, uname) - - -async def get_memo(uid: str, server_id: str) -> Tuple[Union[str, dict], int]: - try: - req = await AsyncHttpx.get( - url=f"https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/dailyNote?server={server_id}&role_id={uid}", - headers={ - "DS": get_ds(f"role_id={uid}&server={server_id}"), - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": Config.get_config("genshin", "client_type"), - "Referer": "https://webstatic.mihoyo.com/", - "Cookie": await Genshin.random_cookie(uid), - }, - ) - data = req.json() - if data["message"] == "OK": - return data["data"], 200 - return data["message"], 999 - except TimeoutError: - return "访问超时,请稍后再试", 997 - except Exception as e: - logger.error(f"便签查询获取失败未知错误 {e}:{e}") - return "发生了一些错误,请稍后再试", 998 - - -def create_border( - image_name: str, content: str, notice_text: str, value: str -) -> BuildImage: - border = BuildImage(500, 75, color="#E0D9D1", font="HYWenHei-85W.ttf", font_size=20) - text_bk = BuildImage( - 350, 75, color="#F5F1EB", font_size=23, font="HYWenHei-85W.ttf" - ) - _x = 70 if image_name == "resin.png" else 50 - _px = 10 if image_name == "resin.png" else 20 - text_bk.paste( - BuildImage(_x, _x, background=memo_path / image_name), - (_px, 0), - True, - center_type="by_height", - ) - text_bk.text((87, 15), content) - text_bk.paste( - BuildImage( - 0, - 0, - plain_text=notice_text, - font_color=(203, 189, 175), - font="HYWenHei-85W.ttf", - font_size=17, - ), - (87, 45), - True, - ) - font_width, _ = border.getsize(value) - border.text((350 + 76 - int(font_width / 2), 0), value, center_type="by_height") - border.paste(text_bk, (2, 0), center_type="by_height") - return border - - -async def parse_data_and_draw( - user_id: int, uid: str, server_id: str, uname: str -) -> Union[str, MessageSegment]: - data, code = await get_memo(uid, server_id) - if code != 200: - return data - user_avatar = BytesIO(await get_user_avatar(user_id)) - for x in data["expeditions"]: - file_name = x["avatar_side_icon"].split("_")[-1] - role_avatar = memo_path / "role_avatar" / file_name - if not role_avatar.exists(): - await AsyncHttpx.download_file(x["avatar_side_icon"], role_avatar) - return await asyncio.get_event_loop().run_in_executor( - None, _parse_data_and_draw, data, user_avatar, uid, uname - ) - - -def _parse_data_and_draw( - data: dict, user_avatar: BytesIO, uid: int, uname: str -) -> Union[str, MessageSegment]: - current_resin = data["current_resin"] # 当前树脂 - max_resin = data["max_resin"] # 最大树脂 - resin_recovery_time = data["resin_recovery_time"] # 树脂全部回复时间 - finished_task_num = data["finished_task_num"] # 完成的每日任务 - total_task_num = data["total_task_num"] # 每日任务总数 - remain_resin_discount_num = data["remain_resin_discount_num"] # 值得铭记的强敌总数 - resin_discount_num_limit = data["resin_discount_num_limit"] # 剩余值得铭记的强敌 - current_expedition_num = data["current_expedition_num"] # 当前挖矿人数 - max_expedition_num = data["max_expedition_num"] # 每日挖矿最大人数 - expeditions = data["expeditions"] # 挖矿详情 - current_coin = data["current_home_coin"] # 当前宝钱 - max_coin = data["max_home_coin"] # 最大宝钱 - coin_recovery_time = data["home_coin_recovery_time"] # 宝钱全部回复时间 - transformer_available = data["transformer"]["obtained"] # 参量质变仪可获取 - transformer_state = data["transformer"]["recovery_time"]["reached"] # 参量质变仪状态 - transformer_recovery_time = data["transformer"]["recovery_time"]["Day"] # 参量质变仪回复时间 - transformer_recovery_hour = data["transformer"]["recovery_time"][ - "Hour" - ] # 参量质变仪回复时间 - coin_minute, coin_second = divmod(int(coin_recovery_time), 60) - coin_hour, coin_minute = divmod(coin_minute, 60) - # print(data) - minute, second = divmod(int(resin_recovery_time), 60) - hour, minute = divmod(minute, 60) - - A = BuildImage(1030, 570, color="#f1e9e1", font_size=15, font="HYWenHei-85W.ttf") - A.text((10, 15), "原神便笺 | Create By ZhenXun", (198, 186, 177)) - ava = BuildImage(100, 100, background=user_avatar) - ava.circle() - A.paste(ava, (40, 40), True) - A.paste( - BuildImage(0, 0, plain_text=uname, font_size=20, font="HYWenHei-85W.ttf"), - (160, 62), - True, - ) - A.paste( - BuildImage( - 0, - 0, - plain_text=f"UID:{uid}", - font_size=15, - font="HYWenHei-85W.ttf", - font_color=(21, 167, 89), - ), - (160, 92), - True, - ) - border = create_border( - "resin.png", - "原粹树脂", - "将在{:0>2d}:{:0>2d}:{:0>2d}秒后全部恢复".format(hour, minute, second), - f"{current_resin}/{max_resin}", - ) - - A.paste(border, (10, 155)) - border = create_border( - "task.png", - "每日委托", - "今日委托已全部完成" if finished_task_num == total_task_num else "今日委托完成数量不足", - f"{finished_task_num}/{total_task_num}", - ) - A.paste(border, (10, 235)) - border = create_border( - "resin_discount.png", - "值得铭记的强敌", - "本周剩余消耗减半次数", - f"{remain_resin_discount_num}/{resin_discount_num_limit}", - ) - A.paste(border, (10, 315)) - border = create_border( - "chengehu.png", - "洞天财翁-洞天宝钱", - "洞天财翁已达到存储上限" - if current_coin == max_coin - else f"{coin_hour}小时{coin_minute}分钟后存满", - f"{current_coin}/{max_coin}", - ) - A.paste(border, (10, 395)) - border = create_border( - "zhibian.png", - "参量质变仪", - "不存在" - if not transformer_available - else "已准备完成 " - if transformer_state - else f"{transformer_recovery_hour}小时后可使用" - if not transformer_recovery_time - else f"{transformer_recovery_time}天后可使用", - "不存在" if not transformer_available else "可使用" if transformer_state else "冷却中", - ) - A.paste(border, (10, 475)) - - expeditions_border = BuildImage( - 470, 510, color="#E0D9D1", font="HYWenHei-85W.ttf", font_size=20 - ) - expeditions_text = BuildImage( - 466, 506, color="#F5F1EB", font_size=23, font="HYWenHei-85W.ttf" - ) - expeditions_text.text( - (5, 5), f"探索派遣限制{current_expedition_num}/{max_expedition_num}", (100, 100, 98) - ) - h = 45 - for x in expeditions: - _bk = BuildImage( - 400, 82, color="#ECE3D8", font="HYWenHei-85W.ttf", font_size=21 - ) - file_name = x["avatar_side_icon"].split("_")[-1] - role_avatar = memo_path / "role_avatar" / file_name - _ava_img = BuildImage(75, 75, background=role_avatar) - # _ava_img.circle() - if x["status"] == "Finished": - msg = "探索完成" - font_color = (146, 188, 63) - _circle_color = (146, 188, 63) - else: - minute, second = divmod(int(x["remained_time"]), 60) - hour, minute = divmod(minute, 60) - font_color = (193, 180, 167) - msg = "还剩{:0>2d}小时{:0>2d}分钟{:0>2d}秒".format(hour, minute, second) - _circle_color = "#DE9C58" - - _circle_bk = BuildImage(60, 60) - _circle_bk.circle() - a_circle = BuildImage(55, 55, color=_circle_color) - a_circle.circle() - b_circle = BuildImage(47, 47) - b_circle.circle() - a_circle.paste(b_circle, (4, 4), True) - _circle_bk.paste(a_circle, (4, 4), True) - - _bk.paste(_circle_bk, (25, 0), True, "by_height") - _bk.paste(_ava_img, (19, -13), True) - _bk.text((100, 0), msg, font_color, "by_height") - _bk.circle_corner(20) - - expeditions_text.paste(_bk, (25, h), True) - h += 75 + 16 - - expeditions_border.paste(expeditions_text, center_type="center") - - A.paste(expeditions_border, (550, 45)) - - return image(b64=A.pic2bs4()) diff --git a/plugins/genshin/query_user/query_role/__init__.py b/plugins/genshin/query_user/query_role/__init__.py deleted file mode 100644 index c203439f..00000000 --- a/plugins/genshin/query_user/query_role/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -from httpx import ConnectError -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg - -from services.log import logger -from utils.utils import is_number - -from .._models import Genshin -from .data_source import query_role_data - -__zx_plugin_name__ = "原神玩家查询" -__plugin_usage__ = """ -usage: - 通过uid查询原神玩家信息 - 指令: - 原神玩家查询/ys ?[uid] - 示例:原神玩家查询 92342233 -""".strip() -__plugin_des__ = "请问你们有几个肝?" -__plugin_cmd__ = ["原神玩家查询/ys"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神玩家查询"], -} -__plugin_block_limit__ = {} - - -query_role_info_matcher = on_command( - "原神玩家查询", aliases={"原神玩家查找", "ys"}, priority=5, block=True -) - - -@query_role_info_matcher.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - if not is_number(msg): - await query_role_info_matcher.finish("查询uid必须为数字!") - msg = int(msg) - uid = None - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not msg and user: - uid = user.uid - else: - uid = msg - if not uid: # or not await Genshin.get_user_cookie(uid): - await query_role_info_matcher.finish("请先绑定uid和cookie!") - nickname = event.sender.card or event.sender.nickname - mys_id = user.mys_id if user else None - try: - data = await query_role_data(event.user_id, uid, mys_id, nickname) - if data: - await query_role_info_matcher.send(data) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 使用原神玩家查询 uid:{uid}" - ) - else: - await query_role_info_matcher.send("查询失败..") - except ConnectError: - await query_role_info_matcher.send("网络出小差啦~") diff --git a/plugins/genshin/query_user/query_role/data_source.py b/plugins/genshin/query_user/query_role/data_source.py deleted file mode 100644 index 58f0ffe9..00000000 --- a/plugins/genshin/query_user/query_role/data_source.py +++ /dev/null @@ -1,254 +0,0 @@ -from typing import Dict, List, Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import Config -from services.log import logger -from utils.http_utils import AsyncHttpx - -from .._models import Genshin -from .._utils import element_mastery, get_ds -from .draw_image import get_genshin_image, init_image - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -async def query_role_data( - user_id: int, uid: int, mys_id: Optional[str] = None, nickname: Optional[str] = None -) -> Optional[Union[MessageSegment, str]]: - uid = str(uid) - if uid[0] == "1" or uid[0] == "2": - server_id = "cn_gf01" - elif uid[0] == "5": - server_id = "cn_qd01" - else: - return None - return await get_image(user_id, uid, server_id, mys_id, nickname) - - -async def get_image( - user_id: int, - uid: str, - server_id: str, - mys_id: Optional[str] = None, - nickname: Optional[str] = None, -) -> Optional[Union[MessageSegment, str]]: - """ - 生成图片 - :param user_id:用户qq - :param uid: 用户uid - :param server_id: 服务器 - :param mys_id: 米游社id - :param nickname: QQ昵称 - :return: - """ - data, code = await get_info(uid, server_id) - if code != 200: - return data - if data: - char_data_list, role_data, world_data_dict, home_data_list = parsed_data(data) - mys_data = await get_mys_data(uid, mys_id) - if mys_data: - nickname = None - if char_data_list: - char_detailed_data = await get_character( - uid, [x["id"] for x in char_data_list], server_id - ) - _x = {} - if char_detailed_data: - for char in char_detailed_data["avatars"]: - _x[char["name"]] = { - "weapon": char["weapon"]["name"], - "weapon_image": char["weapon"]["icon"], - "level": char["weapon"]["level"], - "affix_level": char["weapon"]["affix_level"], - } - - await init_image(world_data_dict, char_data_list, _x, home_data_list) - return await get_genshin_image( - user_id, - uid, - char_data_list, - role_data, - world_data_dict, - home_data_list, - _x, - mys_data, - nickname, - ) - return "未找到用户数据..." - - -# Github-@lulu666lulu https://github.com/Azure99/GenshinPlayerQuery/issues/20 -""" -{body:"",query:{"action_ticket": undefined, "game_biz": "hk4e_cn”}} -对应 https://api-takumi.mihoyo.com/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn //查询米哈游账号下绑定的游戏(game_biz可留空) -{body:"",query:{"uid": 12345(被查询账号米哈游uid)}} -对应 https://api-takumi.mihoyo.com/game_record/app/card/wapi/getGameRecordCard?uid= -{body:"",query:{'role_id': '查询账号的uid(游戏里的)' ,'server': '游戏服务器'}} -对应 https://api-takumi.mihoyo.com/game_record/app/genshin/api/index?server= server信息 &role_id= 游戏uid -{body:"",query:{'role_id': '查询账号的uid(游戏里的)' , 'schedule_type': 1(我这边只看到出现过1和2), 'server': 'cn_gf01'}} -对应 https://api-takumi.mihoyo.com/game_record/app/genshin/api/spiralAbyss?schedule_type=1&server= server信息 &role_id= 游戏uid -{body:"",query:{game_id: 2(目前我知道有崩坏3是1原神是2)}} -对应 https://api-takumi.mihoyo.com/game_record/app/card/wapi/getAnnouncement?game_id= 这个是公告api -b=body q=query -其中b只在post的时候有内容,q只在get的时候有内容 -""" - - -async def get_info(uid_: str, server_id: str) -> Tuple[Optional[Union[dict, str]], int]: - # try: - req = await AsyncHttpx.get( - url=f"https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/index?server={server_id}&role_id={uid_}", - headers={ - "Accept": "application/json, text/plain, */*", - "DS": get_ds(f"role_id={uid_}&server={server_id}"), - "Origin": "https://webstatic.mihoyo.com", - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (Linux; Android 9; Unspecified Device) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36 miHoYoBBS/2.2.0", - "x-rpc-client_type": Config.get_config("genshin", "client_type"), - "Referer": "https://webstatic.mihoyo.com/app/community-game-records/index.html?v=6", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "zh-CN,en-US;q=0.8", - "X-Requested-With": "com.mihoyo.hyperion", - "Cookie": await Genshin.random_cookie(uid_), - }, - ) - data = req.json() - if data["message"] == "OK": - return data["data"], 200 - return data["message"], 999 - # except Exception as e: - # logger.error(f"访问失败,请重试! {type(e)}: {e}") - return None, -1 - - -async def get_character( - uid: str, character_ids: List[str], server_id="cn_gf01" -) -> Optional[dict]: - # try: - req = await AsyncHttpx.post( - url="https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/character", - headers={ - "Accept": "application/json, text/plain, */*", - "DS": get_ds( - "", - { - "character_ids": character_ids, - "role_id": uid, - "server": server_id, - }, - ), - "Origin": "https://webstatic.mihoyo.com", - "Cookie": await Genshin.random_cookie(uid), - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": "5", - "Referer": "https://webstatic.mihoyo.com/", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "zh-CN,en-US;q=0.8", - "X-Requested-With": "com.mihoyo.hyperion", - }, - json={"character_ids": character_ids, "role_id": uid, "server": server_id}, - ) - data = req.json() - if data["message"] == "OK": - return data["data"] - # except Exception as e: - # logger.error(f"访问失败,请重试! {type(e)}: {e}") - return None - - -def parsed_data( - data: dict, -) -> "Optional[List[Dict[str, str]]], Dict[str, str], Optional[List[Dict[str, str]]], Optional[List[Dict[str, str]]]": - """ - 解析数据 - :param data: 数据 - """ - char_data_list = [] - for char in data["avatars"]: - _x = { - "id": char["id"], - "image": char["image"], - "name": char["name"], - "element": element_mastery[char["element"].lower()], - "fetter": char["fetter"], - "level": char["level"], - "rarity": char["rarity"], - "actived_constellation_num": char["actived_constellation_num"], - } - char_data_list.append(_x) - role_data = { - "active_day_number": data["stats"]["active_day_number"], # 活跃天数 - "achievement_number": data["stats"]["achievement_number"], # 达成成就数量 - # "win_rate": data["stats"]["win_rate"], - "anemoculus_number": data["stats"]["anemoculus_number"], # 风神瞳已收集 - "geoculus_number": data["stats"]["geoculus_number"], # 岩神瞳已收集 - "avatar_number": data["stats"]["avatar_number"], # 获得角色数量 - "way_point_number": data["stats"]["way_point_number"], # 传送点已解锁 - "domain_number": data["stats"]["domain_number"], # 秘境解锁数量 - "spiral_abyss": data["stats"]["spiral_abyss"], # 深渊当期进度 - "precious_chest_number": data["stats"]["precious_chest_number"], # 珍贵宝箱 - "luxurious_chest_number": data["stats"]["luxurious_chest_number"], # 华丽宝箱 - "exquisite_chest_number": data["stats"]["exquisite_chest_number"], # 精致宝箱 - "magic_chest_number": data["stats"]["magic_chest_number"], # 奇馈宝箱 - "common_chest_number": data["stats"]["common_chest_number"], # 普通宝箱 - "electroculus_number": data["stats"]["electroculus_number"], # 雷神瞳已收集 - "dendroculus_number": data["stats"]["dendroculus_number"], # 草神瞳已收集 - } - world_data_dict = {} - for world in data["world_explorations"]: - _x = { - "level": world["level"], # 声望等级 - "exploration_percentage": world["exploration_percentage"], # 探索进度 - "image": world["icon"], - "name": world["name"], - "offerings": world["offerings"], - "icon": world["icon"], - } - world_data_dict[world["name"]] = _x - home_data_list = [] - for home in data["homes"]: - _x = { - "level": home["level"], # 最大信任等级 - "visit_num": home["visit_num"], # 最高历史访客数 - "comfort_num": home["comfort_num"], # 最高洞天仙力 - "item_num": home["item_num"], # 已获得摆件数量 - "name": home["name"], - "icon": home["icon"], - "comfort_level_name": home["comfort_level_name"], - "comfort_level_icon": home["comfort_level_icon"], - } - home_data_list.append(_x) - return char_data_list, role_data, world_data_dict, home_data_list - - -async def get_mys_data(uid: str, mys_id: Optional[str]) -> Optional[List[Dict]]: - """ - 获取用户米游社数据 - :param uid: 原神uid - :param mys_id: 米游社id - """ - if mys_id: - # try: - req = await AsyncHttpx.get( - url=f"https://api-takumi-record.mihoyo.com/game_record/card/wapi/getGameRecordCard?uid={mys_id}", - headers={ - "DS": get_ds(f"uid={mys_id}"), - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": "5", - "Referer": "https://webstatic.mihoyo.com/", - "Cookie": await Genshin.random_cookie(uid), - }, - ) - data = req.json() - if data["message"] == "OK": - return data["data"]["list"] - # except Exception as e: - # logger.error(f"访问失败,请重试! {type(e)}: {e}") - return None diff --git a/plugins/genshin/query_user/query_role/draw_image.py b/plugins/genshin/query_user/query_role/draw_image.py deleted file mode 100644 index fbb2eb5d..00000000 --- a/plugins/genshin/query_user/query_role/draw_image.py +++ /dev/null @@ -1,595 +0,0 @@ -from configs.path_config import IMAGE_PATH, TEMP_PATH -from utils.image_utils import BuildImage -from typing import List, Dict, Optional -from utils.message_builder import image -from nonebot.adapters.onebot.v11 import MessageSegment -from utils.http_utils import AsyncHttpx -from utils.utils import get_user_avatar -from io import BytesIO -import random -import asyncio -import os - - -image_path = IMAGE_PATH / "genshin" / "genshin_card" - - -async def get_genshin_image( - user_id: int, - uid: str, - char_data_list: List[Dict], - role_data: Dict, - world_data_dict: Dict, - home_data_list: List[Dict], - char_detailed_dict: dict = None, - mys_data: Optional[List[Dict]] = None, - nickname: Optional[str] = None, -) -> MessageSegment: - """ - 生成图片数据 - :param user_id:用户qq - :param uid: 原神uid - :param char_data_list: 角色列表 - :param role_data: 玩家数据 - :param world_data_dict: 国家数据字典 - :param home_data_list: 家园列表 - :param char_detailed_dict: 角色武器字典 - :param mys_data: 用户米游社数据 - :param nickname: 用户昵称 - """ - user_ava = BytesIO(await get_user_avatar(user_id)) - return await asyncio.get_event_loop().run_in_executor( - None, - _get_genshin_image, - uid, - char_data_list, - role_data, - world_data_dict, - home_data_list, - char_detailed_dict, - mys_data, - nickname, - user_ava, - ) - - -def _get_genshin_image( - uid: str, - char_data_list: List[Dict], - role_data: Dict, - world_data_dict: Dict, - home_data_list: List[Dict], - char_detailed_dict: dict = None, - mys_data: Optional[Dict] = None, - nickname: Optional[str] = None, - user_ava: Optional[BytesIO] = None, -) -> MessageSegment: - """ - 生成图片数据 - :param uid: 原神uid - :param char_data_list: 角色列表 - :param role_data: 玩家数据 - :param world_data_dict: 国家数据字典 - :param home_data_list: 家园列表 - :param char_detailed_dict: 角色武器字典 - :param mys_data: 用户米游社数据 - :param nickname: 用户昵称 - :param user_ava:用户头像 - """ - user_image = get_user_data_image(uid, role_data, mys_data, nickname, user_ava) - home_image = get_home_data_image(home_data_list) - country_image = get_country_data_image(world_data_dict) - char_image = get_char_data_image(char_data_list, char_detailed_dict) - top_bk = BuildImage(user_image.w, user_image.h + max([home_image.h, country_image.h]) + 100, color="#F9F6F2") - top_bk.paste(user_image, alpha=True) - top_bk.paste(home_image, (0, user_image.h + 50), alpha=True) - top_bk.paste(country_image, (home_image.w + 100, user_image.h + 50), alpha=True) - bar = BuildImage(1600, 200, font_size=50, color="#F9F6F2", font="HYWenHei-85W.ttf") - bar.text((50, 10), "角色背包", (104, 103, 101)) - bar.line((50, 90, 1550, 90), (227, 219, 209), width=10) - - foot = BuildImage(1700, 87, background=image_path / "head.png") - head = BuildImage(1700, 87, background=image_path / "head.png") - head.rotate(180) - middle = BuildImage( - 1700, top_bk.h + bar.h + char_image.h, background=image_path / "middle.png" - ) - A = BuildImage(middle.w, middle.h + foot.h + head.h) - A.paste(head, (-5, 0), True) - A.paste(middle, (0, head.h), True) - A.paste(foot, (0, head.h + middle.h), True) - A.crop((0, 0, A.w - 5, A.h)) - if A.h - top_bk.h - bar.h - char_image.h > 200: - _h = A.h - top_bk.h - bar.h - char_image.h - 200 - A.crop((0, 0, A.w, A.h - _h)) - A.paste(foot, (0, A.h - 87)) - A.paste(top_bk, (0, 100), center_type="by_width") - A.paste(bar, (50, top_bk.h + 80)) - A.paste(char_image, (0, top_bk.h + bar.h + 10), center_type="by_width") - rand = random.randint(1, 10000) - A.resize(0.8) - A.save(TEMP_PATH / f"genshin_user_card_{rand}.png") - return image(TEMP_PATH / f"genshin_user_card_{rand}.png") - - -def get_user_data_image( - uid: str, - role_data: Dict, - mys_data: Optional[Dict] = None, - nickname: Optional[str] = None, - user_ava: Optional[BytesIO] = None, -) -> BuildImage: - """ - 画出玩家基本数据 - :param uid: 原神uid - :param role_data: 玩家数据 - :param mys_data: 玩家米游社数据 - :param nickname: 用户昵称 - :param user_ava:用户头像 - """ - if mys_data: - nickname = [x["nickname"] for x in mys_data if x["game_id"] == 2][0] - region = BuildImage(1440, 560, color="#E3DBD1", font="HYWenHei-85W.ttf") - region.circle_corner(30) - uname_img = BuildImage( - 0, - 0, - plain_text=nickname, - font_size=40, - color=(255, 255, 255, 0), - font="HYWenHei-85W.ttf", - ) - uid_img = BuildImage( - 0, - 0, - plain_text=f"UID: {uid}", - font_size=25, - color=(255, 255, 255, 0), - font="HYWenHei-85W.ttf", - font_color=(21, 167, 89), - ) - ava_bk = BuildImage(270, 270, background=image_path / "cover.png") - # 用户头像 - if user_ava: - ava_img = BuildImage(200, 200, background=user_ava) - ava_img.circle() - ava_bk.paste(ava_img, alpha=True, center_type="center") - else: - ava_img = BuildImage( - 245, - 245, - background=image_path - / "chars_ava" - / random.choice(os.listdir(image_path / "chars_ava")), - ) - ava_bk.paste(ava_img, (12, 16), alpha=True) - region.paste(uname_img, (int(170 + uid_img.w / 2 - uname_img.w / 2), 365), True) - region.paste(uid_img, (170, 415), True) - region.paste(ava_bk, (int(550 / 2 - ava_bk.w / 2), 100), True) - data_img = BuildImage( - 800, 510, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - _height = 0 - keys = [ - ["活跃天数", "成就达成", "获得角色", "解锁传送"], - ["风神瞳", "岩神瞳", "雷神瞳", "草神瞳"], - ["解锁秘境", "深境螺旋", "华丽宝箱", "珍贵宝箱"], - ["精致宝箱", "普通宝箱", "奇馈宝箱",], - ] - values = [ - [ - role_data["active_day_number"], - role_data["achievement_number"], - role_data["avatar_number"], - role_data["way_point_number"], - ], - [ - role_data["anemoculus_number"], - role_data["geoculus_number"], - role_data["electroculus_number"], - role_data["dendroculus_number"], - ], - [ - role_data["domain_number"], - role_data["spiral_abyss"], - role_data["luxurious_chest_number"], - role_data["precious_chest_number"], - ], - [ - role_data["exquisite_chest_number"], - role_data["common_chest_number"], - role_data["magic_chest_number"], - ], - ] - for key, value in zip(keys, values): - _tmp_data_img = BuildImage( - 800, 200, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - _width = 10 - for k, v in zip(key, value): - t_ = BuildImage( - 0, - 0, - plain_text=k, - color=(255, 255, 255, 0), - font_color=(138, 143, 143), - font="HYWenHei-85W.ttf", - font_size=30, - ) - tmp_ = BuildImage( - t_.w, t_.h + 70, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - tmp_.text((0, 0), str(v), center_type="by_width") - tmp_.paste(t_, (0, 50), True, "by_width") - _tmp_data_img.paste(tmp_, ((_width + 15) if keys.index(key) == 1 else _width, 0)) - _width += 200 - data_img.paste(_tmp_data_img, (0, _height)) - _height += _tmp_data_img.h - 70 - region.paste(data_img, (510, 50)) - return region - - -def get_home_data_image(home_data_list: List[Dict]) -> BuildImage: - """ - 画出家园数据 - :param home_data_list: 家园列表 - """ - homes = os.listdir(image_path / "homes") - homes.remove("lock.png") - homes.sort() - h = 130 + 340 * len(homes) - region = BuildImage( - 550, h, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - try: - region.text( - (0, 30), f'尘歌壶 Lv.{home_data_list[0]["level"]}', center_type="by_width" - ) - region.text( - (0, region.h - 70), f'仙力: {home_data_list[0]["comfort_num"]}', center_type="by_width" - ) - except (IndexError, KeyError): - region.text((0, 30), f"尘歌壶 Lv.0", center_type="by_width") - region.text((0, region.h - 70), f"仙力: 0", center_type="by_width") - region.circle_corner(30) - height = 100 - unlock_home = [x["name"] for x in home_data_list] - for i, file in enumerate(homes): - home_img = image_path / "homes" / file - x = BuildImage(500, 250, background=home_img) - if file.split(".")[0] not in unlock_home: - black_img = BuildImage(500, 250, color="black") - lock_img = BuildImage(0, 0, background=image_path / "homes" / "lock.png") - black_img.circle_corner(50) - black_img.transparent(1) - black_img.paste(lock_img, alpha=True, center_type="center") - x.paste(black_img, alpha=True) - else: - black_img = BuildImage( - 500, 150, color="black", font="HYWenHei-85W.ttf", font_size=40 - ) - black_img.text((55, 55), file.split(".")[0], fill=(226, 211, 146)) - black_img.transparent(1) - text_img = BuildImage( - 0, - 0, - plain_text="洞天等级", - font="HYWenHei-85W.ttf", - font_color=(203, 200, 184), - font_size=35, - color=(255, 255, 255, 0), - ) - level_img = BuildImage( - 0, - 0, - plain_text=f'{home_data_list[0]["comfort_level_name"]}', - font="HYWenHei-85W.ttf", - font_color=(211, 213, 207), - font_size=30, - color=(255, 255, 255, 0), - ) - black_img.paste(text_img, (270, 25), True) - black_img.paste(level_img, (278, 85), True) - x.paste(black_img, alpha=True, center_type="center") - x.circle_corner(50) - region.paste(x, (0, height), True, "by_width") - height += 340 - return region - - -def get_country_data_image(world_data_dict: Dict) -> BuildImage: - """ - 画出国家探索供奉等图像 - :param world_data_dict: 国家数据字典 - """ - # 层岩巨渊 和 地下矿区 算一个 - region = BuildImage(790, 267 * ((len(world_data_dict) - 1) if world_data_dict.get("层岩巨渊·地下矿区") else len(world_data_dict)), color="#F9F6F2") - height = 0 - for country in ["蒙德", "龙脊雪山", "璃月", "层岩巨渊", "稻妻", "渊下宫", "须弥"]: - if not world_data_dict.get(country): - continue - x = BuildImage(790, 250, color="#3A4467") - logo = BuildImage(180, 180, background=image_path / "logo" / f"{country}.png") - tmp_bk = BuildImage(770, 230, color="#606779") - tmp_bk.circle_corner(10) - content_bk = BuildImage( - 755, 215, color="#3A4467", font_size=40, font="HYWenHei-85W.ttf" - ) - content_bk.paste(logo, (50, 0), True, "by_height") - if country in ["蒙德", "璃月"]: - content_bk.text((300, 40), "蒙德探索" if country == "蒙德" else "璃月探索", fill=(239, 211, 114)) - content_bk.text( - (500, 40), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 120), "蒙德声望" if country == "蒙德" else "璃月声望", fill=(239, 211, 114)) - content_bk.text( - (500, 120), - f"Lv.{world_data_dict[country]['level']}", - fill=(255, 255, 255), - ) - elif country in ["层岩巨渊"]: - content_bk.text((300, 20), "层岩巨渊探索", fill=(239, 211, 114)) - content_bk.text( - (570, 20), - f"{world_data_dict['层岩巨渊']['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - if world_data_dict.get('层岩巨渊·地下矿区'): - content_bk.text((300, 85), "地下矿区探索", fill=(239, 211, 114)) - content_bk.text( - (570, 85), - f"{world_data_dict['层岩巨渊·地下矿区']['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 150), "流明石触媒", fill=(239, 211, 114)) - content_bk.text( - (570, 150), - f"LV.{world_data_dict['层岩巨渊·地下矿区']['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - elif country in ["龙脊雪山"]: - content_bk.text((300, 40), "雪山探索", fill=(239, 211, 114)) - content_bk.text( - (500, 40), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 120), "忍冬之树", fill=(239, 211, 114)) - content_bk.text( - (500, 120), - f"Lv.{world_data_dict[country]['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - elif country in ["稻妻"]: - content_bk.text((300, 20), "稻妻探索", fill=(239, 211, 114)) - content_bk.text( - (500, 20), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 85), "稻妻声望", fill=(239, 211, 114)) - content_bk.text( - (500, 85), - f"Lv.{world_data_dict[country]['level']}", - fill=(255, 255, 255), - ) - content_bk.text((300, 150), "神樱眷顾", fill=(239, 211, 114)) - content_bk.text( - (500, 150), - f"Lv.{world_data_dict[country]['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - elif country in ["渊下宫"]: - content_bk.text((300, 0), "渊下宫探索", fill=(239, 211, 114), center_type="by_height") - content_bk.text( - (530, 20), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - center_type="by_height", - ) - elif country in ["须弥"]: - content_bk.text((300, 20), "须弥探索", fill=(239, 211, 114)) - content_bk.text( - (500, 20), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 85), "须弥声望", fill=(239, 211, 114)) - content_bk.text( - (500, 85), - f"Lv.{world_data_dict[country]['level']}", - fill=(255, 255, 255), - ) - content_bk.text((300, 150), "梦之树", fill=(239, 211, 114)) - content_bk.text( - (500, 150), - f"Lv.{world_data_dict[country]['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - - x.paste(tmp_bk, alpha=True, center_type="center") - x.paste(content_bk, alpha=True, center_type="center") - x.circle_corner(20) - region.paste(x, (0, height), center_type="by_width") - height += 267 - return region - - -def get_char_data_image( - char_data_list: List[Dict], char_detailed_dict: dict -) -> "BuildImage, int": - """ - 画出角色列表 - :param char_data_list: 角色列表 - :param char_detailed_dict: 角色武器 - """ - lens = len(char_data_list) / 7 if len(char_data_list) % 7 == 0 else len(char_data_list) / 7 + 1 - x = 500 - _h = int(x * lens) - region = BuildImage( - 1600, - _h, - color="#F9F6F2", - ) - width = 120 - height = 0 - idx = 0 - for char in char_data_list: - if width + 230 > 1550: - width = 120 - height += 420 - idx += 1 - char_img = image_path / "chars" / f'{char["name"]}.png' - char_bk = BuildImage( - 270, - 500, - background=image_path / "element.png", - font="HYWenHei-85W.ttf", - font_size=35, - ) - char_img = BuildImage(0, 0, background=char_img) - actived_constellation_num = BuildImage( - 0, - 0, - plain_text=f"命之座: {char['actived_constellation_num']}层", - font="HYWenHei-85W.ttf", - font_size=25, - color=(255, 255, 255, 0), - ) - level = BuildImage( - 0, - 0, - plain_text=f"Lv.{char['level']}", - font="HYWenHei-85W.ttf", - font_size=30, - color=(255, 255, 255, 0), - font_color=(21, 167, 89), - ) - love_log = BuildImage( - 0, - 0, - plain_text="♥", - font="HWZhongSong.ttf", - font_size=40, - color=(255, 255, 255, 0), - font_color=(232, 31, 168), - ) - fetter = BuildImage( - 0, - 0, - plain_text=f'{char["fetter"]}', - font="HYWenHei-85W.ttf", - font_size=30, - color=(255, 255, 255, 0), - font_color=(232, 31, 168), - ) - if char_detailed_dict.get(char["name"]): - weapon = BuildImage( - 100, - 100, - background=image_path - / "weapons" - / f'{char_detailed_dict[char["name"]]["weapon"]}.png', - ) - weapon_name = BuildImage( - 0, - 0, - plain_text=f"{char_detailed_dict[char['name']]['weapon']}", - font="HYWenHei-85W.ttf", - font_size=25, - color=(255, 255, 255, 0), - ) - weapon_affix_level = BuildImage( - 0, - 0, - plain_text=f"精炼: {char_detailed_dict[char['name']]['affix_level']}", - font="HYWenHei-85W.ttf", - font_size=20, - color=(255, 255, 255, 0), - ) - weapon_level = BuildImage( - 0, - 0, - plain_text=f"Lv.{char_detailed_dict[char['name']]['level']}", - font="HYWenHei-85W.ttf", - font_size=25, - color=(255, 255, 255, 0), - font_color=(21, 167, 89), - ) - char_bk.paste(weapon, (20, 380), True) - char_bk.paste( - weapon_name, - (100 + int((char_bk.w - 22 - weapon.w - weapon_name.w) / 2 - 10), 390), - True, - ) - char_bk.paste( - weapon_affix_level, - ( - ( - 100 - + int( - (char_bk.w - 10 - weapon.w - weapon_affix_level.w) / 2 - 10 - ), - 420, - ) - ), - True, - ) - char_bk.paste( - weapon_level, - ( - ( - 100 - + int((char_bk.w - 10 - weapon.w - weapon_level.w) / 2 - 10), - 450, - ) - ), - True, - ) - char_bk.paste(char_img, (0, 5), alpha=True, center_type="by_width") - char_bk.text((0, 270), char["name"], center_type="by_width") - char_bk.paste(actived_constellation_num, (0, 310), True, "by_width") - char_bk.paste(level, (60, 340), True) - char_bk.paste(love_log, (155, 330), True) - char_bk.paste(fetter, (180, 340), True) - char_bk.resize(0.8) - region.paste(char_bk, (width, height), True) - width += 230 - region.crop((0, 0, region.w, height + 430)) - return region - - -async def init_image(world_data_dict: Dict[str, Dict[str, str]], char_data_list: List[Dict[str, str]], char_detailed_dict: dict, home_data_list: List[Dict]): - """ - 下载头像 - :param world_data_dict: 地图标志 - :param char_data_list: 角色列表 - :param char_detailed_dict: 角色武器 - :param home_data_list: 家园列表 - """ - for world in world_data_dict: - file = image_path / "logo" / f'{world_data_dict[world]["name"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file(world_data_dict[world]["icon"], file) - for char in char_data_list: - file = image_path / "chars" / f'{char["name"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file(char["image"], file) - for char in char_detailed_dict.keys(): - file = image_path / "weapons" / f'{char_detailed_dict[char]["weapon"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file( - char_detailed_dict[char]["weapon_image"], file - ) - for home in home_data_list: - file = image_path / "homes" / f'{home["name"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file( - home["icon"], file - ) diff --git a/plugins/genshin/query_user/reset_today_query_user_data/__init__.py b/plugins/genshin/query_user/reset_today_query_user_data/__init__.py deleted file mode 100644 index 667ba90a..00000000 --- a/plugins/genshin/query_user/reset_today_query_user_data/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from services.log import logger -from utils.utils import scheduler - -from .._models import Genshin - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=1, -) -async def _(): - try: - await Genshin.all().update(today_query_uid="") - logger.warning(f"重置原神查询记录成功..") - except Exception as e: - logger.error(f"重置原神查询记录失败. {type(e)}:{e}") - diff --git a/plugins/genshin/query_user/resin_remind/__init__.py b/plugins/genshin/query_user/resin_remind/__init__.py deleted file mode 100644 index 7d1400a8..00000000 --- a/plugins/genshin/query_user/resin_remind/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import Tuple - -from apscheduler.jobstores.base import JobLookupError -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.params import Command - -from services.log import logger -from utils.depends import OneCommand - -from .._models import Genshin -from .init_task import add_job, scheduler - -__zx_plugin_name__ = "原神树脂提醒" -__plugin_usage__ = """ -usage: - 即将满树脂的提醒 - 会在 120-140 140-160 160 以及溢出指定部分时提醒, - 共提醒3-4次 - 指令: - 开原神树脂提醒 - 关原神树脂提醒 -""".strip() -__plugin_des__ = "时时刻刻警醒你!" -__plugin_cmd__ = ["开原神树脂提醒", "关原神树脂提醒"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神树脂提醒", "关原神树脂提醒", "开原神树脂提醒"], -} -__plugin_configs__ = { - "AUTO_CLOSE_QUERY_FAIL_RESIN_REMIND": { - "value": True, - "help": "当请求连续三次失败时,关闭用户的树脂提醒", - "default_value": True, - "type": bool, - }, - "CUSTOM_RESIN_OVERFLOW_REMIND": { - "value": 20, - "help": "自定义树脂溢出指定数量时的提醒,空值是为关闭", - "default_value": None, - "type": int, - }, -} - -resin_remind = on_command("开原神树脂提醒", aliases={"关原神树脂提醒"}, priority=5, block=True) - - -@resin_remind.handle() -async def _(event: MessageEvent, cmd: str = OneCommand()): - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not user or not user.uid or not user.cookie: - await resin_remind.finish("请先绑定uid和cookie!") - try: - scheduler.remove_job(f"genshin_resin_remind_{user.uid}_{event.user_id}") - except JobLookupError: - pass - if cmd[0] == "开": - if user.resin_remind: - await resin_remind.finish("原神树脂提醒已经是开启状态,请勿重复开启!", at_sender=True) - user.resin_remind = True - add_job(event.user_id, user.uid) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 开启原神体力提醒" - ) - await resin_remind.send("开启原神树脂提醒成功!", at_sender=True) - else: - if not user.resin_remind: - await resin_remind.finish("原神树脂提醒已经是开启状态,请勿重复开启!", at_sender=True) - user.resin_remind = False - user.resin_recovery_time = None - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 关闭原神体力提醒" - ) - await resin_remind.send("已关闭原神树脂提醒..", at_sender=True) - if user: - await user.save(update_fields=["resin_remind", "resin_recovery_time"]) diff --git a/plugins/genshin/query_user/resin_remind/init_task.py b/plugins/genshin/query_user/resin_remind/init_task.py deleted file mode 100644 index fefabc17..00000000 --- a/plugins/genshin/query_user/resin_remind/init_task.py +++ /dev/null @@ -1,217 +0,0 @@ -import random -from datetime import datetime, timedelta - -import nonebot -import pytz -from apscheduler.jobstores.base import ConflictingIdError, JobLookupError -from nonebot import Driver -from nonebot.adapters.onebot.v11 import ActionFailed -from nonebot.plugin import require - -from configs.config import Config -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.message_builder import at -from utils.utils import get_bot, scheduler - -from .._models import Genshin - -driver: Driver = nonebot.get_driver() - - -require("query_memo") - -from ..query_memo import get_memo - -global_map = {} - - -class UserManager: - def __init__(self, max_error_count: int = 3): - self._data = [] - self._overflow_data = [] - self._error_count = {} - self.max_error_count = max_error_count - - def append(self, o: str): - if o not in self._data: - self._data.append(o) - - def remove(self, o: str): - if o in self._data: - self._data.remove(o) - - def exists(self, o: str): - return o in self._data - - def add_error_count(self, uid: str): - if uid in self._error_count.keys(): - self._error_count[uid] += 1 - else: - self._error_count[uid] = 1 - - def check(self, uid: str) -> bool: - if uid in self._error_count.keys(): - return self._error_count[uid] == self.max_error_count - return False - - def remove_error_count(self, uid): - if uid in self._error_count.keys(): - del self._error_count[uid] - - def add_overflow(self, uid: str): - if uid not in self._overflow_data: - self._overflow_data.append(uid) - - def remove_overflow(self, uid: str): - if uid in self._overflow_data: - self._overflow_data.remove(uid) - - def is_overflow(self, uid: str) -> bool: - return uid in self._overflow_data - - -user_manager = UserManager() - - -@driver.on_startup -async def _(): - """ - 启动时分配定时任务 - """ - g_list = await Genshin.filter(resin_remind=True).all() - update_list = [] - date = datetime.now(pytz.timezone("Asia/Shanghai")) + timedelta(seconds=30) - for u in g_list: - if u.resin_remind: - if u.resin_recovery_time: - if u.resin_recovery_time and u.resin_recovery_time > datetime.now( - pytz.timezone("Asia/Shanghai") - ): - add_job(u.user_id, u.uid) - logger.info( - f"genshin_resin_remind add_job:USER:{u.user_id} UID:{u.uid}启动原神树脂提醒 " - ) - else: - u.resin_recovery_time = None # type: ignore - update_list.append(u) - add_job(u.user_id, u.uid) - logger.info( - f"genshin_resin_remind add_job CHECK:USER:{u.user_id} UID:{u.uid}启动原神树脂提醒 " - ) - else: - add_job(u.user_id, u.uid) - logger.info( - f"genshin_resin_remind add_job CHECK:USER:{u.user_id} UID:{u.uid}启动原神树脂提醒 " - ) - if update_list: - await Genshin.bulk_update(update_list, ["resin_recovery_time"]) - - -def add_job(user_id: str, uid: int): - # 移除 - try: - scheduler.remove_job(f"genshin_resin_remind_{uid}_{user_id}") - except JobLookupError: - pass - date = datetime.now(pytz.timezone("Asia/Shanghai")) + timedelta(seconds=30) - try: - scheduler.add_job( - _remind, - "date", - run_date=date.replace(microsecond=0), - id=f"genshin_resin_remind_{uid}_{user_id}", - args=[user_id, uid], - ) - except ConflictingIdError: - pass - - -async def _remind(user_id: int, uid: str): - user = await Genshin.get_or_none(user_id=str(user_id), uid=int(uid)) - uid = str(uid) - if uid[0] in ["1", "2"]: - server_id = "cn_gf01" - elif uid[0] == "5": - server_id = "cn_qd01" - else: - return - data, code = await get_memo(uid, server_id) - now = datetime.now(pytz.timezone("Asia/Shanghai")) - next_time = None - if code == 200: - current_resin = int(data["current_resin"]) # 当前树脂 - max_resin = int(data["max_resin"]) # 最大树脂 - msg = f"你的已经存了 {current_resin} 个树脂了!不要忘记刷掉!" - # resin_recovery_time = data["resin_recovery_time"] # 树脂全部回复时间 - if current_resin < max_resin: - user_manager.remove(uid) - user_manager.remove_overflow(uid) - if current_resin < max_resin - 40: - next_time = now + timedelta(minutes=(max_resin - 40 - current_resin) * 8) - elif max_resin - 40 <= current_resin < max_resin - 20: - next_time = now + timedelta(minutes=(max_resin - 20 - current_resin) * 8) - elif max_resin - 20 <= current_resin < max_resin: - next_time = now + timedelta(minutes=(max_resin - current_resin) * 8) - elif current_resin == max_resin: - custom_overflow_resin = Config.get_config( - "resin_remind", "CUSTOM_RESIN_OVERFLOW_REMIND" - ) - if user_manager.is_overflow(uid) and custom_overflow_resin: - next_time = now + timedelta(minutes=custom_overflow_resin * 8) - user_manager.add_overflow(uid) - user_manager.remove(uid) - msg = f"你的树脂都溢出 {custom_overflow_resin} 个了!浪费可耻!" - else: - next_time = now + timedelta(minutes=40 * 8 + random.randint(5, 50)) - - if not user_manager.exists(uid) and current_resin >= max_resin - 40: - if current_resin == max_resin: - user_manager.append(uid) - bot = get_bot() - if bot: - if user_id in [x["user_id"] for x in await bot.get_friend_list()]: - await bot.send_private_msg( - user_id=user_id, - message=msg, - ) - else: - if user: - group_id = user.bind_group - if not group_id: - if group_list := await GroupInfoUser.get_user_all_group( - user_id - ): - group_id = group_list[0] - try: - await bot.send_group_msg( - group_id=group_id, message=at(user_id) + msg - ) - except ActionFailed as e: - logger.error(f"树脂提醒推送发生错误 {type(e)}:{e}") - - if not next_time: - if user_manager.check(uid) and Config.get_config( - "resin_remind", "AUTO_CLOSE_QUERY_FAIL_RESIN_REMIND" - ): - if user: - user.resin_remind = False - user.resin_recovery_time = None - await user.save(update_fields=["resin_recovery_time", "resin_remind"]) - next_time = now + timedelta(minutes=(20 + random.randint(5, 20)) * 8) - user_manager.add_error_count(uid) - else: - user_manager.remove_error_count(uid) - if user: - user.resin_recovery_time = next_time - await user.save(update_fields=["resin_recovery_time", "resin_remind"]) - scheduler.add_job( - _remind, - "date", - run_date=next_time, - id=f"genshin_resin_remind_{uid}_{user_id}", - args=[user_id, uid], - ) - logger.info( - f"genshin_resin_remind add_job:USER:{user_id} UID:{uid} " f"{next_time} 原神树脂提醒" - ) diff --git a/plugins/gold_redbag/__init__.py b/plugins/gold_redbag/__init__.py deleted file mode 100755 index 1cf9733c..00000000 --- a/plugins/gold_redbag/__init__.py +++ /dev/null @@ -1,373 +0,0 @@ -import random -import re -import time -from datetime import datetime, timedelta -from typing import Dict, List, Optional - -from apscheduler.jobstores.base import JobLookupError -from nonebot import on_command, on_notice -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - GroupMessageEvent, - Message, - PokeNotifyEvent, -) -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.matcher import Matcher -from nonebot.message import IgnoredException, run_preprocessor -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.depends import AtList, GetConfig -from utils.message_builder import at, image -from utils.utils import is_number, scheduler - -from .config import FESTIVE_KEY, GroupRedBag, RedBag -from .data_source import ( - build_open_result_image, - check_gold, - end_festive_red_bag, - random_red_bag_background, -) - -__zx_plugin_name__ = "金币红包" -__plugin_usage__ = """ -usage: - 在群内发送指定金额的红包,拼手气项目 - 指令: - 塞红包 [金币数] ?[红包数=5] ?[at指定人]: 塞入红包 - 开/抢/*戳一戳*: 打开红包 - 退回: 退回未开完的红包,必须在一分钟后使用 - 示例:塞红包 1000 - 示例:塞红包 1000 10 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 节日全群红包指令 - 指令: - 节日红包 [金额] [数量] ?[祝福语] ?[指定群] -""".strip() -__plugin_des__ = "运气项目又来了" -__plugin_cmd__ = [ - "塞红包 [金币数] ?[红包数=5] ?[at指定人]", - "开/抢", - "退回", - "节日红包 [金额] [数量] ?[祝福语] ?[指定群] [_superuser]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["金币红包", "塞红包"], -} -__plugin_cd_limit__ = {"rst": "急什么急什么,待会再发!"} -__plugin_configs__ = { - "DEFAULT_TIMEOUT": { - "value": 600, - "help": "普通红包默认超时时间", - "default_value": 600, - "type": int, - }, - "DEFAULT_INTERVAL": { - "value": 60, - "help": "用户发送普通红包最小间隔时间", - "default_value": 60, - "type": int, - }, - "RANK_NUM": { - "value": 10, - "help": "结算排行显示前N位", - "default_value": 10, - "type": int, - }, -} -# __plugin_resources__ = {"prts": IMAGE_PATH} - - -async def rule(event: GroupMessageEvent) -> bool: - return check_on_gold_red(event) - - -gold_red_bag = on_command( - "塞红包", aliases={"金币红包"}, priority=5, block=True, permission=GROUP -) - -open_ = on_command( - "开", aliases={"抢"}, priority=5, block=True, permission=GROUP, rule=rule -) - -poke_ = on_notice(priority=6, block=False) - -return_ = on_command("退回", aliases={"退还"}, priority=5, block=True, permission=GROUP) - -festive_redbag = on_command( - "节日红包", priority=5, block=True, permission=SUPERUSER, rule=to_me() -) - -GROUP_DATA: Dict[int, GroupRedBag] = {} - -PATTERN = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~,。;‘、""" - - -# 阻断其他poke -# @run_preprocessor -# async def _( -# matcher: Matcher, -# event: PokeNotifyEvent, -# ): -# try: -# if matcher.type == "notice" and event.self_id == event.target_id: -# flag = check_on_gold_red(event) -# if flag: -# if matcher.plugin_name == "poke": -# raise IgnoredException("目前正在抢红包...") -# else: -# if matcher.plugin_name == "gold_red_bag": -# raise IgnoredException("目前没有红包...") -# except AttributeError: -# pass - - -@gold_red_bag.handle() -async def _( - bot: Bot, - event: GroupMessageEvent, - arg: Message = CommandArg(), - at_list: List[int] = AtList(), - default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), -): - group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(event.group_id) - if not group_red_bag: - group_red_bag = GroupRedBag(event.group_id) - GROUP_DATA[event.group_id] = group_red_bag - # 剩余过期时间 - time_remaining = group_red_bag.check_timeout(event.user_id) - if time_remaining != -1: - # 判断用户红包是否存在且是否过时覆盖 - if user_red_bag := group_red_bag.get_user_red_bag(event.user_id): - now = time.time() - if now < user_red_bag.start_time + default_interval: - await gold_red_bag.finish( - f"你的红包还没消化完捏...还剩下 {user_red_bag.num - len(user_red_bag.open_user)} 个! 请等待红包领取完毕..." - f"(或等待{time_remaining}秒红包cd)" - ) - msg = arg.extract_plain_text().strip().split() - if not msg: - await gold_red_bag.finish("不塞钱发什么红包!") - amount = msg[0] - if len(msg) == 1: - flag, tip = await check_gold(str(event.user_id), str(event.group_id), amount) - if not flag: - await gold_red_bag.finish(tip, at_sender=True) - num = 5 - else: - num = msg[1] - if not is_number(num) or int(num) < 1: - await gold_red_bag.finish("红包个数给我输正确啊!", at_sender=True) - flag, tip = await check_gold(str(event.user_id), str(event.group_id), amount) - if not flag: - await gold_red_bag.finish(tip, at_sender=True) - group_member_num = (await bot.get_group_info(group_id=event.group_id))[ - "member_count" - ] - num = int(num) - if num > group_member_num: - await gold_red_bag.send("你发的红包数量也太多了,已经为你修改成与本群人数相同的红包数量...") - num = group_member_num - nickname = event.sender.card or event.sender.nickname - await group_red_bag.add_red_bag( - f"{nickname}的红包", - int(amount), - 1 if at_list else num, - nickname or "", - str(event.user_id), - assigner=str(at_list[0]) if at_list else None, - ) - await gold_red_bag.send( - f"{nickname}发起了金币红包\n金额: {amount}\n数量: {num}\n" - + image(await random_red_bag_background(event.user_id)) - ) - logger.info(f"塞入 {num} 个红包,共 {amount} 金币", "金币红包", event.user_id, event.group_id) - - -@open_.handle() -async def _( - event: GroupMessageEvent, - arg: Message = CommandArg(), - rank_num: int = GetConfig(config="RANK_NUM"), -): - if msg := arg.extract_plain_text().strip(): - msg = re.sub(PATTERN, "", msg) - if "红包" not in msg: - return - group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(event.group_id) - if group_red_bag: - open_data, settlement_list = await group_red_bag.open(event.user_id) - send_msg = "" - for _, item in open_data.items(): - amount, red_bag = item - result_image = await build_open_result_image(red_bag, event.user_id, amount) - send_msg += ( - f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n" - + image(result_image) - + "\n" - ) - logger.info( - f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", - "开红包", - event.user_id, - event.group_id, - ) - send_msg = send_msg[:-1] if send_msg else "没有红包给你开!" - await open_.send(send_msg, at_sender=True) - if settlement_list: - for red_bag in settlement_list: - await open_.send( - f"{red_bag.name}已结算\n" - + image(await red_bag.build_amount_rank(rank_num)) - ) - - -# @poke_.handle() -# async def _poke_(event: PokeNotifyEvent): -# group_id = getattr(event, "group_id", None) -# if event.self_id == event.target_id and group_id: -# is_open = check_on_gold_red(event) -# if not is_open: -# return -# group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(group_id) -# if group_red_bag: -# open_data, settlement_list = await group_red_bag.open(event.user_id) -# send_msg = "" -# for _, item in open_data.items(): -# amount, red_bag = item -# result_image = await build_open_result_image( -# red_bag, event.user_id, amount -# ) -# send_msg += ( -# f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n" -# + image(result_image) -# + "\n" -# ) -# logger.info( -# f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", -# "开红包", -# event.user_id, -# event.group_id, -# ) -# if send_msg: -# await open_.send(send_msg, at_sender=True) -# if settlement_list: -# for red_bag in settlement_list: -# await open_.send( -# f"{red_bag.name}已结算\n" -# + image(await red_bag.build_amount_rank()) -# ) - - -@return_.handle() -async def _( - event: GroupMessageEvent, - default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), - rank_num: int = GetConfig(config="RANK_NUM"), -): - group_red_bag: GroupRedBag = GROUP_DATA[event.group_id] - if group_red_bag: - if user_red_bag := group_red_bag.get_user_red_bag(event.user_id): - now = time.time() - if now - user_red_bag.start_time < default_interval: - await return_.finish( - f"你的红包还没有过时, 在 {int(default_interval - now + user_red_bag.start_time)} " - f"秒后可以退回...", - at_sender=True, - ) - user_red_bag = group_red_bag.get_user_red_bag(event.user_id) - if user_red_bag and ( - return_amount := await group_red_bag.settlement(event.user_id) - ): - logger.info( - f"退回了红包 {return_amount} 金币", "红包退回", event.user_id, event.group_id - ) - await return_.send( - f"已成功退还了 " - f"{return_amount} 金币\n" - + image(await user_red_bag.build_amount_rank(rank_num)), - at_sender=True, - ) - await return_.send("目前没有红包可以退回...", at_sender=True) - - -@festive_redbag.handle() -async def _(bot: Bot, arg: Message = CommandArg()): - global redbag_data - msg = arg.extract_plain_text().strip() - if msg: - msg = msg.split() - amount = 0 - num = 0 - greetings = "恭喜发财 大吉大利" - gl = [] - if (lens := len(msg)) < 2: - await festive_redbag.finish("参数不足,格式:节日红包 [金额] [数量] [祝福语](可省) [指定群](可省)") - if lens > 1: - if not is_number(msg[0]): - await festive_redbag.finish("金额必须要是数字!", at_sender=True) - amount = int(msg[0]) - if not is_number(msg[1]): - await festive_redbag.finish("数量必须要是数字!", at_sender=True) - num = int(msg[1]) - if lens > 2: - greetings = msg[2] - if lens > 3: - for i in range(3, lens): - if not is_number(msg[i]): - await festive_redbag.finish("指定的群号必须要是数字啊!", at_sender=True) - gl.append(int(msg[i])) - if not gl: - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - for g in gl: - group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(g) - if not group_red_bag: - group_red_bag = GroupRedBag(g) - GROUP_DATA[g] = group_red_bag - try: - scheduler.remove_job(f"{FESTIVE_KEY}_{g}") - await end_festive_red_bag(bot, group_red_bag) - except JobLookupError: - pass - await group_red_bag.add_red_bag( - f"{NICKNAME}的红包", int(amount), num, NICKNAME, FESTIVE_KEY, True - ) - scheduler.add_job( - end_festive_red_bag, - "date", - # run_date=(datetime.now() + timedelta(hours=24)).replace(microsecond=0), - run_date=(datetime.now() + timedelta(seconds=30)).replace( - microsecond=0 - ), - id=f"{FESTIVE_KEY}_{g}", - args=[bot, group_red_bag], - ) - try: - await bot.send_group_msg( - group_id=g, - message=f"{NICKNAME}发起了金币红包\n金额:{amount}\n数量:{num}\n" - + image(await random_red_bag_background(bot.self_id, greetings)), - ) - logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g) - except ActionFailed: - logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g) - - -def check_on_gold_red(event) -> bool: - if group_red_bag := GROUP_DATA.get(event.group_id): - return group_red_bag.check_open(event.user_id) - return False diff --git a/plugins/gold_redbag/data_source.py b/plugins/gold_redbag/data_source.py deleted file mode 100755 index e2e02bc7..00000000 --- a/plugins/gold_redbag/data_source.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import random -from io import BytesIO -from typing import Tuple, Union - -from nonebot.adapters.onebot.v11 import Bot - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH -from models.bag_user import BagUser -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import get_user_avatar, is_number - -from .config import FESTIVE_KEY, GroupRedBag, RedBag - - -async def end_festive_red_bag(bot: Bot, group_red_bag: GroupRedBag): - """结算节日红包 - - 参数: - bot: Bot - group_red_bag: GroupRedBag - """ - if festive_red_bag := group_red_bag.festive_red_bag_expire(): - rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10 - rank_image = await festive_red_bag.build_amount_rank(rank_num) - message = ( - f"{NICKNAME}的节日红包过时了,一共开启了 " - f"{len(festive_red_bag.open_user)}" - f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n" + image(rank_image) - ) - await bot.send_group_msg(group_id=int(group_red_bag.group_id), message=message) - - -async def check_gold( - user_id: str, group_id: str, amount: Union[str, int] -) -> Tuple[bool, str]: - """检查金币数量是否合法 - - 参数: - user_id: 用户id - group_id: 群聊id - amount: 金币数量 - - 返回: - Tuple[bool, str]: 是否合法以及提示语 - """ - if is_number(amount): - amount = int(amount) - user_gold = await BagUser.get_gold(user_id, group_id) - if amount < 1: - return False, "小气鬼,要别人倒贴金币给你嘛!" - if user_gold < amount: - return False, "没有金币的话请不要发红包..." - return True, "" - else: - return False, "给我好好的输入红包里金币的数量啊喂!" - - -async def random_red_bag_background( - user_id: Union[str, int], msg="恭喜发财 大吉大利" -) -> BuildImage: - """构造发送红包图片 - - 参数: - user_id: 用户id - msg: 红包消息. - - 异常: - ValueError: 图片背景列表为空 - - 返回: - BuildImage: 构造后的图片 - """ - background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_2") - if not background_list: - raise ValueError("prts/redbag_1 背景图列表为空...") - random_redbag = random.choice(background_list) - redbag = BuildImage( - 0, 0, font_size=38, background=IMAGE_PATH / "prts" / "redbag_2" / random_redbag - ) - ava_byte = await get_user_avatar(user_id) - ava = None - if ava_byte: - ava = BuildImage(65, 65, background=BytesIO(ava_byte)) - else: - ava = BuildImage(65, 65, color=(0, 0, 0), is_alpha=True) - await ava.acircle() - await redbag.atext( - (int((redbag.size[0] - redbag.getsize(msg)[0]) / 2), 210), msg, (240, 218, 164) - ) - await redbag.apaste(ava, (int((redbag.size[0] - ava.size[0]) / 2), 130), True) - return redbag - - -async def build_open_result_image( - red_bag: RedBag, user_id: Union[int, str], amount: int -) -> BuildImage: - """构造红包开启图片 - - 参数: - red_bag: RedBag - user_id: 开启红包用户id - amount: 开启红包获取的金额 - - 异常: - ValueError: 图片背景列表为空 - - 返回: - BuildImage: 构造后的图片 - """ - background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_1") - if not background_list: - raise ValueError("prts/redbag_1 背景图列表为空...") - random_redbag = random.choice(background_list) - head = BuildImage( - 1000, - 980, - font_size=30, - background=IMAGE_PATH / "prts" / "redbag_1" / random_redbag, - ) - size = BuildImage(0, 0, font_size=50).getsize(red_bag.name) - ava_bk = BuildImage(100 + size[0], 66, is_alpha=True, font_size=50) - - ava_byte = await get_user_avatar(user_id) - ava = None - if ava_byte: - ava = BuildImage(66, 66, is_alpha=True, background=BytesIO(ava_byte)) - else: - ava = BuildImage(66, 66, color=(0, 0, 0), is_alpha=True) - await ava_bk.apaste(ava) - ava_bk.text((100, 7), red_bag.name) - ava_bk_w, ava_bk_h = ava_bk.size - await head.apaste(ava_bk, (int((1000 - ava_bk_w) / 2), 300), alpha=True) - size = BuildImage(0, 0, font_size=150).getsize(amount) - amount_image = BuildImage(size[0], size[1], is_alpha=True, font_size=150) - await amount_image.atext((0, 0), str(amount), fill=(209, 171, 108)) - # 金币中文 - await head.apaste(amount_image, (int((1000 - size[0]) / 2) - 50, 460), alpha=True) - await head.atext( - (int((1000 - size[0]) / 2 + size[0]) - 50, 500 + size[1] - 70), - "金币", - fill=(209, 171, 108), - ) - # 剩余数量和金额 - text = ( - f"已领取" - f"{red_bag.num - len(red_bag.open_user)}" - f"/{red_bag.num}个," - f"共{sum(red_bag.open_user.values())}/{red_bag.amount}金币" - ) - await head.atext((350, 900), text, (198, 198, 198)) - return head diff --git a/plugins/group_welcome_msg.py b/plugins/group_welcome_msg.py deleted file mode 100755 index 27c22c01..00000000 --- a/plugins/group_welcome_msg.py +++ /dev/null @@ -1,53 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP -from configs.path_config import DATA_PATH -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "查看群欢迎消息" -__plugin_usage__ = """ -usage: - 查看当前的群欢迎消息 - 指令: - 查看群欢迎消息 -""".strip() -__plugin_des__ = "查看群欢迎消息" -__plugin_cmd__ = ["查看群欢迎消息"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查看群欢迎消息"], -} - -view_custom_welcome = on_command( - "群欢迎消息", aliases={"查看群欢迎消息", "查看当前群欢迎消息"}, permission=GROUP, priority=5, block=True -) - - -@view_custom_welcome.handle() -async def _(event: GroupMessageEvent): - img = "" - msg = "" - if (DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg").exists(): - img = image(DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg") - custom_welcome_msg_json = ( - DATA_PATH / "custom_welcome_msg" / "custom_welcome_msg.json" - ) - if custom_welcome_msg_json.exists(): - data = json.load(open(custom_welcome_msg_json, "r")) - if data.get(str(event.group_id)): - msg = data[str(event.group_id)] - if msg.find("[at]") != -1: - msg = msg.replace("[at]", "") - if img or msg: - await view_custom_welcome.finish(msg + img, at_sender=True) - else: - await view_custom_welcome.finish("当前还没有自定义群欢迎消息哦", at_sender=True) diff --git a/plugins/image_management/delete_image/__init__.py b/plugins/image_management/delete_image/__init__.py deleted file mode 100755 index 9f7d5d22..00000000 --- a/plugins/image_management/delete_image/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg -from nonebot.rule import to_me -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.message_builder import image -from utils.utils import cn2py, is_number - -__zx_plugin_name__ = "删除图片 [Admin]" -__plugin_usage__ = """ -usage: - 删除图库指定图片 - 指令: - 删除图片 [图库] [id] - 查看图库 - 示例:删除图片 美图 666 -""".strip() -__plugin_des__ = "不好看的图片删掉删掉!" -__plugin_cmd__ = ["删除图片 [图库] [id]", "查看公开图库"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("image_management", "DELETE_IMAGE_LEVEL") -} - - -delete_img = on_command("删除图片", priority=5, rule=to_me(), block=True) - - -_path = IMAGE_PATH / "image_management" - - -@delete_img.handle() -async def _(state: T_State, arg: Message = CommandArg()): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - args = arg.extract_plain_text().strip().split() - if args: - if args[0] in image_dir_list: - state["path"] = args[0] - if len(args) > 1 and is_number(args[1]): - state["id"] = args[1] - - -@delete_img.got("path", prompt="请输入要删除的目标图库?") -@delete_img.got("id", prompt="请输入要删除的图片id?") -async def arg_handle( - event: MessageEvent, - state: T_State, - path_: str = ArgStr("path"), - img_id: str = ArgStr("id"), -): - if path_ in ["取消", "算了"] or img_id in ["取消", "算了"]: - await delete_img.finish("已取消操作...") - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if path_ not in image_dir_list: - await delete_img.reject_arg("path", "此目录不正确,请重新输入目录!") - if not is_number(img_id): - await delete_img.reject_arg("id", "id不正确!请重新输入数字...") - path = _path / cn2py(path_) - if not path.exists() and (path.parent.parent / cn2py(state["path"])).exists(): - path = path.parent.parent / cn2py(state["path"]) - max_id = len(os.listdir(path)) - 1 - if int(img_id) > max_id or int(img_id) < 0: - await delete_img.finish(f"Id超过上下限,上限:{max_id}", at_sender=True) - try: - if (TEMP_PATH / f"{event.user_id}_delete.jpg").exists(): - (TEMP_PATH / f"{event.user_id}_delete.jpg").unlink() - logger.info(f"删除{cn2py(state['path'])}图片 {img_id}.jpg 成功") - except Exception as e: - logger.warning(f"删除图片 delete.jpg 失败 e{e}") - try: - os.rename(path / f"{img_id}.jpg", TEMP_PATH / f"{event.user_id}_delete.jpg") - logger.info(f"移动 {path}/{img_id}.jpg 移动成功") - except Exception as e: - logger.warning(f"{path}/{img_id}.jpg --> 移动失败 e:{e}") - if not os.path.exists(path / f"{img_id}.jpg"): - try: - if int(img_id) != max_id: - os.rename(path / f"{max_id}.jpg", path / f"{img_id}.jpg") - except FileExistsError as e: - logger.error(f"{path}/{max_id}.jpg 替换 {path}/{img_id}.jpg 失败 e:{e}") - logger.info(f"{path}/{max_id}.jpg 替换 {path}/{img_id}.jpg 成功") - logger.info( - f"USER {event.user_id} GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}" - f" -> id: {img_id} 删除成功" - ) - await delete_img.finish( - f"id: {img_id} 删除成功" - + image( - TEMP_PATH / f"{event.user_id}_delete.jpg", - ), - at_sender=True, - ) - await delete_img.finish(f"id: {img_id} 删除失败!") diff --git a/plugins/image_management/move_image/__init__.py b/plugins/image_management/move_image/__init__.py deleted file mode 100755 index d8d9c039..00000000 --- a/plugins/image_management/move_image/__init__.py +++ /dev/null @@ -1,114 +0,0 @@ -from services.log import logger -from nonebot import on_command -from nonebot.rule import to_me -from nonebot.typing import T_State -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from configs.config import Config -from utils.utils import is_number, cn2py -from configs.path_config import IMAGE_PATH -from nonebot.params import CommandArg, ArgStr -import os - -__zx_plugin_name__ = "移动图片 [Admin]" -__plugin_usage__ = """ -usage: - 图库间的图片移动操作 - 指令: - 移动图片 [源图库] [目标图库] [id] - 查看图库 - 示例:移动图片 萝莉 美图 234 -""".strip() -__plugin_des__ = "图库间的图片移动操作" -__plugin_cmd__ = ["移动图片 [源图库] [目标图库] [id]", "查看公开图库"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("image_management", "MOVE_IMAGE_LEVEL") -} - - -move_img = on_command("移动图片", priority=5, rule=to_me(), block=True) - - -_path = IMAGE_PATH / "image_management" - - -@move_img.handle() -async def _(state: T_State, arg: Message = CommandArg()): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - args = arg.extract_plain_text().strip().split() - if args: - if n := len(args): - if args[0] in image_dir_list: - state["source_path"] = args[0] - if n > 1: - if args[1] in image_dir_list: - state["destination_path"] = args[1] - if n > 2 and is_number(args[2]): - state["id"] = args[2] - - -@move_img.got("source_path", prompt="要从哪个图库移出?") -@move_img.got("destination_path", prompt="要移动到哪个图库?") -@move_img.got("id", prompt="要移动的图片id是?") -async def _( - event: MessageEvent, - source_path_: str = ArgStr("source_path"), - destination_path_: str = ArgStr("destination_path"), - img_id: str = ArgStr("id"), -): - if ( - source_path_ in ["取消", "算了"] - or img_id in ["取消", "算了"] - or destination_path_ in ["取消", "算了"] - ): - await move_img.finish("已取消操作...") - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if source_path_ not in image_dir_list: - await move_img.reject_arg("source_path", "移除目录不正确,请重新输入!") - if destination_path_ not in image_dir_list: - await move_img.reject_arg("destination_path", "移入目录不正确,请重新输入!") - if not is_number(img_id): - await move_img.reject_arg("id", "id不正确!请重新输入数字...") - source_path = _path / cn2py(source_path_) - destination_path = _path / cn2py(destination_path_) - if not source_path.exists(): - if (source_path.parent.parent / cn2py(source_path.name)).exists(): - source_path = source_path.parent.parent / cn2py(source_path.name) - if not destination_path.exists(): - if (destination_path.parent.parent / cn2py(destination_path.name)).exists(): - source_path = destination_path.parent.parent / cn2py(destination_path.name) - source_path.mkdir(exist_ok=True, parents=True) - destination_path.mkdir(exist_ok=True, parents=True) - if not len(os.listdir(source_path)): - await move_img.finish(f"{source_path}图库中没有任何图片,移动失败。") - max_id = len(os.listdir(source_path)) - 1 - des_max_id = len(os.listdir(destination_path)) - if int(img_id) > max_id or int(img_id) < 0: - await move_img.finish(f"Id超过上下限,上限:{max_id}", at_sender=True) - try: - move_file = source_path / f"{img_id}.jpg" - move_file.rename(destination_path / f"{des_max_id}.jpg") - logger.info( - f"移动 {source_path}/{img_id}.jpg ---> {destination_path}/{des_max_id} 移动成功" - ) - except Exception as e: - logger.warning( - f"移动 {source_path}/{img_id}.jpg ---> {destination_path}/{des_max_id} 移动失败 e:{e}" - ) - await move_img.finish(f"移动图片id:{img_id} 失败了...", at_sender=True) - if max_id > 0: - try: - rep_file = source_path / f"{max_id}.jpg" - rep_file.rename(source_path / f"{img_id}.jpg") - logger.info(f"{source_path}/{max_id}.jpg 替换 {source_path}/{img_id}.jpg 成功") - except Exception as e: - logger.warning( - f"{source_path}/{max_id}.jpg 替换 {source_path}/{img_id}.jpg 失败 e:{e}" - ) - await move_img.finish(f"替换图片id:{max_id} -> {img_id} 失败了...", at_sender=True) - logger.info( - f"USER {event.user_id} GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'} ->" - f" {source_path} --> {destination_path} (id:{img_id}) 移动图片成功" - ) - await move_img.finish(f"移动图片 id:{img_id} --> id:{des_max_id}成功", at_sender=True) diff --git a/plugins/image_management/send_image/__init__.py b/plugins/image_management/send_image/__init__.py deleted file mode 100755 index d7458939..00000000 --- a/plugins/image_management/send_image/__init__.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -import random -from typing import List - -from nonebot import on_message, on_regex -from nonebot.adapters.onebot.v11 import Message, MessageEvent -from nonebot.params import CommandArg - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.depends import GetConfig -from utils.manager import withdraw_message_manager -from utils.message_builder import image -from utils.utils import FreqLimiter, cn2py, get_message_text, is_number - -from .anti import pix_random_change_file -from .rule import rule - -__zx_plugin_name__ = "本地图库" -__plugin_usage__ = f""" -usage: - 发送指定图库下的随机或指定id图片genshin_memo - 指令: - {Config.get_config("image_management", "IMAGE_DIR_LIST")} ?[id] - 示例:美图 - 示例: 萝莉 2 -""".strip() -__plugin_des__ = "让看看我的私藏,指[图片]" -__plugin_cmd__ = Config.get_config("image_management", "IMAGE_DIR_LIST") -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["发送图片"] + (Config.get_config("image_management", "IMAGE_DIR_LIST") or []), -} -__plugin_resources__ = {"pa": IMAGE_PATH / "pa"} - -Config.add_plugin_config( - "_task", "DEFAULT_PA", True, help_="被动 爬 进群默认开关状态", default_value=True, type=int -) - -_flmt = FreqLimiter(1) - - -send_img = on_message(priority=5, rule=rule, block=True) -pa_reg = on_regex("^(爬|丢人爬|爪巴)$", priority=5, block=True) - - -_path = IMAGE_PATH / "image_management" - - -@send_img.handle() -async def _( - event: MessageEvent, - image_dir_list: List[str] = GetConfig("image_management", "IMAGE_DIR_LIST", []), -): - msg = get_message_text(event.message).split() - gallery = msg[0] - if gallery not in image_dir_list: - return - img_id = None - if len(msg) > 1: - img_id = msg[1] - path = _path / cn2py(gallery) - if gallery in image_dir_list: - if not path.exists() and (path.parent.parent / cn2py(gallery)).exists(): - path = IMAGE_PATH / cn2py(gallery) - else: - path.mkdir(parents=True, exist_ok=True) - length = len(os.listdir(path)) - if not length: - logger.warning(f"图库 {cn2py(gallery)} 为空,调用取消!") - await send_img.finish("该图库中没有图片噢...") - index = img_id if img_id else str(random.randint(0, length - 1)) - if not is_number(index): - return - if int(index) > length - 1 or int(index) < 0: - await send_img.finish(f"超过当前图库的上下限了哦!({length - 1})") - result = image(pix_random_change_file(path / f"{index}.jpg")) - if result: - logger.info( - f"发送{cn2py(gallery)}:" + str(path / f"{index}.jpg"), - "发送图片", - event.user_id, - getattr(event, "group_id", None), - ) - msg_id = await send_img.send( - f"id:{index}" + result - if Config.get_config("image_management", "SHOW_ID") - else "" + result - ) - withdraw_message_manager.withdraw_message( - event, - msg_id, - Config.get_config("image_management", "WITHDRAW_IMAGE_MESSAGE"), - ) - else: - logger.info( - f"发送 {cn2py(gallery)} 不存在", - "发送图片", - event.user_id, - getattr(event, "group_id", None), - ) - await send_img.finish(f"不想给你看Ov|") - - -@pa_reg.handle() -async def _(event: MessageEvent): - if _flmt.check(event.user_id): - _flmt.start_cd(event.user_id) - path = IMAGE_PATH / "pa" - if not path.exists() or not os.listdir(IMAGE_PATH / "pa"): - await pa_reg.finish("该图库中没有图片噢...") - await pa_reg.finish( - image(IMAGE_PATH / "pa" / random.choice(os.listdir(IMAGE_PATH / "pa"))) - ) diff --git a/plugins/image_management/send_image/anti.py b/plugins/image_management/send_image/anti.py deleted file mode 100644 index c3c23b53..00000000 --- a/plugins/image_management/send_image/anti.py +++ /dev/null @@ -1,26 +0,0 @@ -import random -import warnings -from pathlib import Path - -import cv2 -import numpy as np -from PIL import Image - - -def pix_random_change(img): - # Image转cv2 - img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) - img[0, 0, 0] = random.randint(0, 0xFFFFFFF) - # cv2转Image - img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) - return img - - -def pix_random_change_file(path: Path): - # 注意:cv2.imread()不支持路径中文 - str_path = str(path.absolute()) - warnings.filterwarnings("ignore", category=Warning) - img = cv2.imread(str_path) - img[0, 0, 0] = random.randint(0, 0xFFFFFFF) - cv2.imwrite(str_path, img) - return str_path diff --git a/plugins/image_management/send_image/rule.py b/plugins/image_management/send_image/rule.py deleted file mode 100644 index 1ce46829..00000000 --- a/plugins/image_management/send_image/rule.py +++ /dev/null @@ -1,18 +0,0 @@ -from nonebot.adapters.onebot.v11 import Bot, Event -from nonebot.typing import T_State -from utils.utils import get_message_text -from configs.config import Config - - -def rule(bot: Bot, event: Event, state: T_State) -> bool: - """ - 检测文本是否是关闭功能命令 - :param bot: pass - :param event: pass - :param state: pass - """ - msg = get_message_text(event.json()) - for x in Config.get_config("image_management", "IMAGE_DIR_LIST"): - if msg.startswith(x): - return True - return False diff --git a/plugins/image_management/upload_image/__init__.py b/plugins/image_management/upload_image/__init__.py deleted file mode 100755 index 7e7c25e0..00000000 --- a/plugins/image_management/upload_image/__init__.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg -from nonebot.rule import to_me -from nonebot.typing import T_State - -from configs.config import Config -from utils.depends import ImageList -from utils.utils import get_message_img - -from .data_source import upload_image_to_local - -__zx_plugin_name__ = "上传图片 [Admin]" -__plugin_usage__ = """ -usage: - 上传图片至指定图库 - 指令: - 查看图库 - 上传图片 [图库] [图片] - 连续上传图片 [图库] - 示例:上传图片 美图 [图片] - * 连续上传图片可以通过发送 “stop” 表示停止收集发送的图片,可以开始上传 * -""".strip() -__plugin_des__ = "指定图库图片上传" -__plugin_cmd__ = ["上传图片 [图库] [图片]", "连续上传图片 [图库]", "查看公开图库"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("image_management", "UPLOAD_IMAGE_LEVEL") -} - -upload_img = on_command("上传图片", rule=to_me(), priority=5, block=True) - -continuous_upload_img = on_command("连续上传图片", rule=to_me(), priority=5, block=True) - -show_gallery = on_command("查看公开图库", priority=1, block=True) - - -@show_gallery.handle() -async def _(): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") - if not image_dir_list: - await show_gallery.finish("未发现任何图库") - x = "公开图库列表:\n" - for i, e in enumerate(image_dir_list): - x += f"\t{i+1}.{e}\n" - await show_gallery.send(x[:-1]) - - -@upload_img.handle() -async def _( - event: MessageEvent, - state: T_State, - arg: Message = CommandArg(), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") - if not image_dir_list: - await show_gallery.finish("未发现任何图库") - args = arg.extract_plain_text().strip() - if args: - if args in image_dir_list: - state["path"] = args - if img_list: - state["img_list"] = arg - state["dir_list"] = "\n-".join(image_dir_list) - - -@upload_img.got( - "path", - prompt=Message.template("请选择要上传的图库\n-{dir_list}"), -) -@upload_img.got("img_list", prompt="图呢图呢图呢图呢!GKD!") -async def _( - bot: Bot, - event: MessageEvent, - state: T_State, - path: str = ArgStr("path"), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if path not in image_dir_list: - await upload_img.reject_arg("path", "此目录不正确,请重新输入目录!") - if not img_list: - await upload_img.reject_arg("img_list", "图呢图呢图呢图呢!GKD!") - group_id = 0 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - await upload_img.send( - await upload_image_to_local(img_list, path, event.user_id, group_id) - ) - - -@continuous_upload_img.handle() -async def _( - event: MessageEvent, - state: T_State, - arg: Message = CommandArg(), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - path = arg.extract_plain_text().strip() - if path in image_dir_list: - state["path"] = path - state["img_list"] = [] - state["dir_list"] = "\n-".join(image_dir_list) - - -@continuous_upload_img.got("path", prompt=Message.template("请选择要上传的图库\n-{dir_list}")) -@continuous_upload_img.got("img", prompt="图呢图呢图呢图呢!GKD!【发送‘stop’为停止】") -async def _( - event: MessageEvent, - state: T_State, - collect_img_list: List[str] = Arg("img_list"), - path: str = ArgStr("path"), - img: Message = Arg("img"), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if path not in image_dir_list: - await upload_img.reject_arg("path", "此目录不正确,请重新输入目录!") - if not img.extract_plain_text() == "stop": - if img_list: - for i in img_list: - collect_img_list.append(i) - await upload_img.reject_arg("img", "图再来!!【发送‘stop’为停止】") - group_id = 0 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - await continuous_upload_img.send( - await upload_image_to_local(collect_img_list, path, event.user_id, group_id) - ) diff --git a/plugins/image_management/upload_image/data_source.py b/plugins/image_management/upload_image/data_source.py deleted file mode 100755 index b04b3e77..00000000 --- a/plugins/image_management/upload_image/data_source.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -from typing import List - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.utils import cn2py - -_path = IMAGE_PATH / "image_management" - - -async def upload_image_to_local( - img_list: List[str], path_: str, user_id: int, group_id: int = 0 -) -> str: - _path_name = path_ - path = _path / cn2py(path_) - if not path.exists() and (path.parent.parent / cn2py(path_)).exists(): - path = path.parent.parent / cn2py(path_) - path.mkdir(parents=True, exist_ok=True) - img_id = len(os.listdir(path)) - failed_list = [] - success_id = "" - for img_url in img_list: - if await AsyncHttpx.download_file(img_url, path / f"{img_id}.jpg"): - success_id += str(img_id) + "," - img_id += 1 - else: - failed_list.append(img_url) - failed_result = "" - for img in failed_list: - failed_result += str(img) + "\n" - logger.info( - f"上传图片至 {_path_name} 共 {len(img_list)} 张,失败 {len(failed_list)} 张,id={success_id[:-1]}", - "上传图片", - user_id, - group_id, - ) - if failed_list: - return ( - f"这次一共为 {_path_name}库 添加了 {len(img_list) - len(failed_list)} 张图片\n" - f"依次的Id为:{success_id[:-1]}\n上传失败:{failed_result[:-1]}\n{NICKNAME}感谢您对图库的扩充!WW" - ) - else: - return ( - f"这次一共为 {_path_name}库 添加了 {len(img_list)} 张图片\n依次的Id为:" - f"{success_id[:-1]}\n{NICKNAME}感谢您对图库的扩充!WW" - ) diff --git a/plugins/luxun/__init__.py b/plugins/luxun/__init__.py deleted file mode 100755 index 38c2ee85..00000000 --- a/plugins/luxun/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -from configs.path_config import IMAGE_PATH -from nonebot import on_command -from nonebot.typing import T_State -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from utils.message_builder import image -from services.log import logger -from utils.image_utils import BuildImage -from nonebot.params import CommandArg - -__zx_plugin_name__ = "鲁迅说" -__plugin_usage__ = """ -usage: - 鲁迅说了啥? - 指令: - 鲁迅说 [文本] -""".strip() -__plugin_des__ = "鲁迅说他没说过这话!" -__plugin_cmd__ = ["鲁迅说"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["鲁迅说"], -} -__plugin_block_limit__ = { - "rst": "你的鲁迅正在说,等会" -} - -luxun = on_command("鲁迅说过", aliases={"鲁迅说"}, priority=5, block=True) - - -luxun_author = BuildImage(0, 0, plain_text="--鲁迅", font_size=30, font='msyh.ttf', font_color=(255, 255, 255)) - - -@luxun.handle() -async def handle(state: T_State, arg: Message = CommandArg()): - args = arg.extract_plain_text().strip() - if args: - state["content"] = args if args else "烦了,不说了" - - -@luxun.got("content", prompt="你让鲁迅说点啥?") -async def handle_event(event: MessageEvent, state: T_State): - content = state["content"].strip() - if content.startswith(",") or content.startswith(","): - content = content[1:] - A = BuildImage(0, 0, font_size=37, background=f'{IMAGE_PATH}/other/luxun.jpg', font='msyh.ttf') - x = "" - if len(content) > 40: - await luxun.finish('太长了,鲁迅说不完...') - while A.getsize(content)[0] > A.w - 50: - n = int(len(content) / 2) - x += content[:n] + '\n' - content = content[n:] - x += content - if len(x.split('\n')) > 2: - await luxun.finish('太长了,鲁迅说不完...') - A.text((int((480 - A.getsize(x.split("\n")[0])[0]) / 2), 300), x, (255, 255, 255)) - A.paste(luxun_author, (320, 400), True) - await luxun.send(image(b64=A.pic2bs4())) - logger.info( - f"USER {event.user_id} GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 鲁迅说过 {content}" - ) diff --git a/plugins/music/__init__.py b/plugins/music/__init__.py deleted file mode 100644 index 109b215c..00000000 --- a/plugins/music/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -from .music_163 import get_song_id, get_song_info -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from nonebot.typing import T_State -from services.log import logger -from nonebot import on_command -from utils.message_builder import music - - -__zx_plugin_name__ = "点歌" -__plugin_usage__ = """ -usage: - 在线点歌 - 指令: - 点歌 [歌名] -""".strip() -__plugin_des__ = "为你点播了一首曾经的歌" -__plugin_cmd__ = ["点歌 [歌名]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["点歌"], -} - - -music_handler = on_command("点歌", priority=5, block=True) - - -@music_handler.handle() -async def handle_first_receive(state: T_State, arg: Message = CommandArg()): - if args := arg.extract_plain_text().strip(): - state["song_name"] = args - - -@music_handler.got("song_name", prompt="歌名是?") -async def _(bot: Bot, event: MessageEvent, state: T_State): - song = state["song_name"] - song_id = await get_song_id(song) - if not song_id: - await music_handler.finish("没有找到这首歌!", at_sender=True) - await music_handler.send(music("163", song_id)) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 点歌 :{song}" - ) - - - - diff --git a/plugins/music/music_163.py b/plugins/music/music_163.py deleted file mode 100644 index 00f24df2..00000000 --- a/plugins/music/music_163.py +++ /dev/null @@ -1,41 +0,0 @@ -from utils.http_utils import AsyncHttpx -import json - - -headers = {"referer": "http://music.163.com"} -cookies = {"appver": "2.0.2"} - - -async def search_song(song_name: str): - """ - 搜索歌曲 - :param song_name: 歌名 - """ - r = await AsyncHttpx.post( - f"http://music.163.com/api/search/get/", - data={"s": song_name, "limit": 1, "type": 1, "offset": 0}, - ) - if r.status_code != 200: - return None - return json.loads(r.text) - - -async def get_song_id(song_name: str) -> int: - """ """ - r = await search_song(song_name) - try: - return r["result"]["songs"][0]["id"] - except KeyError: - return 0 - - -async def get_song_info(songId: int): - """ - 获取歌曲信息 - """ - r = await AsyncHttpx.post( - f"http://music.163.com/api/song/detail/?id={songId}&ids=%5B{songId}%5D", - ) - if r.status_code != 200: - return None - return json.loads(r.text) diff --git a/plugins/mute.py b/plugins/mute.py deleted file mode 100755 index b4e200e9..00000000 --- a/plugins/mute.py +++ /dev/null @@ -1,188 +0,0 @@ -import time -from io import BytesIO -from typing import Any, Dict, Tuple - -import imagehash -from nonebot import on_command, on_message -from nonebot.adapters.onebot.v11 import ActionFailed, Bot, GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import Command, CommandArg -from PIL import Image - -from configs.config import NICKNAME, Config -from configs.path_config import DATA_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import get_img_hash -from utils.utils import get_message_img, get_message_text, is_number - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -__zx_plugin_name__ = "刷屏禁言 [Admin]" -__plugin_usage__ = f""" -usage: - 刷屏禁言相关操作,需要 {NICKNAME} 有群管理员权限 - 指令: - 设置刷屏检测时间 [秒] - 设置刷屏检测次数 [次数] - 设置刷屏禁言时长 [分钟] - 刷屏检测设置: 查看当前的刷屏检测设置 - * 即 X 秒内发送同样消息 N 次,禁言 M 分钟 * -""".strip() -__plugin_des__ = "刷屏禁言相关操作" -__plugin_cmd__ = ["设置刷屏检测时间 [秒]", "设置刷屏检测次数 [次数]", "设置刷屏禁言时长 [分钟]", "刷屏检测设置"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = {"admin_level": Config.get_config("mute", "MUTE_LEVEL")} -__plugin_configs__ = { - "MUTE_LEVEL [LEVEL]": { - "value": 5, - "help": "更改禁言设置的管理权限", - "default_value": 5, - "type": int, - }, - "MUTE_DEFAULT_COUNT": { - "value": 10, - "help": "刷屏禁言默认检测次数", - "default_value": 10, - "type": int, - }, - "MUTE_DEFAULT_TIME": { - "value": 7, - "help": "刷屏检测默认规定时间", - "default_value": 7, - "type": int, - }, - "MUTE_DEFAULT_DURATION": { - "value": 10, - "help": "刷屏检测默禁言时长(分钟)", - "default_value": 10, - "type": int, - }, -} - - -mute = on_message(priority=1, block=False) -mute_setting = on_command( - "mute_setting", - aliases={"设置刷屏检测时间", "设置刷屏检测次数", "设置刷屏禁言时长", "刷屏检测设置"}, - permission=GROUP, - block=True, - priority=5, -) - - -def get_data() -> Dict[Any, Any]: - try: - with open(DATA_PATH / "group_mute_data.json", "r", encoding="utf8") as f: - data = json.load(f) - except (ValueError, FileNotFoundError): - data = {} - return data - - -def save_data(): - global mute_data - with open(DATA_PATH / "group_mute_data.json", "w", encoding="utf8") as f: - json.dump(mute_data, f, indent=4) - - -async def download_img_and_hash(url) -> str: - return str( - imagehash.average_hash(Image.open(BytesIO((await AsyncHttpx.get(url)).content))) - ) - - -mute_dict = {} -mute_data = get_data() - - -@mute.handle() -async def _(bot: Bot, event: GroupMessageEvent): - group_id = str(event.group_id) - msg = get_message_text(event.json()) - img_list = get_message_img(event.json()) - img_hash = "" - for img in img_list: - img_hash += await download_img_and_hash(img) - msg += img_hash - if not mute_data.get(group_id): - mute_data[group_id] = { - "count": Config.get_config("mute", "MUTE_DEFAULT_COUNT"), - "time": Config.get_config("mute", "MUTE_DEFAULT_TIME"), - "duration": Config.get_config("mute", "MUTE_DEFAULT_DURATION"), - } - if not mute_dict.get(event.user_id): - mute_dict[event.user_id] = {"time": time.time(), "count": 1, "msg": msg} - else: - if msg and msg.find(mute_dict[event.user_id]["msg"]) != -1: - mute_dict[event.user_id]["count"] += 1 - else: - mute_dict[event.user_id]["time"] = time.time() - mute_dict[event.user_id]["count"] = 1 - mute_dict[event.user_id]["msg"] = msg - if time.time() - mute_dict[event.user_id]["time"] > mute_data[group_id]["time"]: - mute_dict[event.user_id]["time"] = time.time() - mute_dict[event.user_id]["count"] = 1 - if ( - mute_dict[event.user_id]["count"] > mute_data[group_id]["count"] - and time.time() - mute_dict[event.user_id]["time"] - < mute_data[group_id]["time"] - ): - try: - if mute_data[group_id]["duration"] != 0: - await bot.set_group_ban( - group_id=event.group_id, - user_id=event.user_id, - duration=mute_data[group_id]["duration"] * 60, - ) - await mute.send(f"检测到恶意刷屏,{NICKNAME}要把你关进小黑屋!", at_sender=True) - mute_dict[event.user_id]["count"] = 0 - logger.info( - f"USER {event.user_id} GROUP {event.group_id} " - f'检测刷屏 被禁言 {mute_data[group_id]["duration"] / 60} 分钟' - ) - except ActionFailed: - pass - - -@mute_setting.handle() -async def _( - event: GroupMessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - global mute_data - group_id = str(event.group_id) - if not mute_data.get(group_id): - mute_data[group_id] = { - "count": Config.get_config("mute", "MUTE_DEFAULT_COUNT"), - "time": Config.get_config("mute", "MUTE_DEFAULT_TIME"), - "duration": Config.get_config("mute", "MUTE_DEFAULT_DURATION"), - } - msg = arg.extract_plain_text().strip() - if cmd[0] == "刷屏检测设置": - await mute_setting.finish( - f'最大次数:{mute_data[group_id]["count"]} 次\n' - f'规定时间:{mute_data[group_id]["time"]} 秒\n' - f'禁言时长:{mute_data[group_id]["duration"]:.2f} 分钟\n' - f"【在规定时间内发送相同消息超过最大次数则禁言\n当禁言时长为0时关闭此功能】" - ) - if not is_number(msg): - await mute.finish("设置的参数必须是数字啊!", at_sender=True) - if cmd[0] == "设置刷屏检测时间": - mute_data[group_id]["time"] = int(msg) - msg += "秒" - if cmd[0] == "设置刷屏检测次数": - mute_data[group_id]["count"] = int(msg) - msg += " 次" - if cmd[0] == "设置刷屏禁言时长": - mute_data[group_id]["duration"] = int(msg) - msg += " 分钟" - await mute_setting.send(f"刷屏检测:{cmd[0]}为 {msg}") - logger.info(f"USER {event.user_id} GROUP {group_id} {cmd[0]}:{msg}") - save_data() diff --git a/plugins/my_info/__init__.py b/plugins/my_info/__init__.py deleted file mode 100644 index 94ea437b..00000000 --- a/plugins/my_info/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import timedelta - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser - -__zx_plugin_name__ = "个人信息权限查看" -__plugin_usage__ = """ -usage: - 个人信息权限查看 - 指令: - 我的信息 - 我的权限 -""".strip() -__plugin_des__ = "我们还记得你和你的权利" -__plugin_cmd__ = ["我的信息", "我的权限"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -get_my_group_info = on_command("我的信息", permission=GROUP, priority=1, block=True) -my_level = on_command("我的权限", permission=GROUP, priority=5, block=True) - - -@get_my_group_info.handle() -async def _(event: GroupMessageEvent): - result = await get_member_info(str(event.user_id), str(event.group_id)) - await get_my_group_info.finish(result) - - -async def get_member_info(user_id: str, group_id: str) -> str: - if user := await GroupInfoUser.get_or_none(user_id=user_id, group_id=group_id): - result = "" - result += "昵称:" + user.user_name + "\n" - result += "加群时间:" + str(user.user_join_time.date()) - return result - else: - return "该群员不在列表中,请更新群成员信息" - - -@my_level.handle() -async def _(event: GroupMessageEvent): - if (level := await LevelUser.get_user_level(event.user_id, event.group_id)) == -1: - await my_level.finish("您目前没有任何权限了,硬要说的话就是0吧~", at_sender=True) - await my_level.finish(f"您目前的权限等级:{level}", at_sender=True) diff --git a/plugins/nbnhhsh.py b/plugins/nbnhhsh.py deleted file mode 100755 index bbbb3f3b..00000000 --- a/plugins/nbnhhsh.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from utils.http_utils import AsyncHttpx -from services.log import logger -import ujson as json - - -__zx_plugin_name__ = "能不能好好说话" -__plugin_usage__ = """ -usage: - 说人话 - 指令: - nbnhhsh [文本] -""".strip() -__plugin_des__ = "能不能好好说话,说人话" -__plugin_cmd__ = ["nbnhhsh [文本]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["能不能好好说话", "nbnhhsh"], -} - -HHSH_GUESS_URL = "https://lab.magiconch.com/api/nbnhhsh/guess" - -nbnhhsh = on_command("nbnhhsh", aliases={"能不能好好说话"}, priority=5, block=True) - - -@nbnhhsh.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - await nbnhhsh.finish("没话说就别说话!") - response = await AsyncHttpx.post( - HHSH_GUESS_URL, - data=json.dumps({"text": msg}), - timeout=5, - headers={"content-type": "application/json"}, - ) - try: - data = response.json() - tmp = "" - rst = "" - for x in data: - trans = "" - if x.get("trans"): - trans = x["trans"][0] - elif x.get("inputting"): - trans = ",".join(x["inputting"]) - tmp += f'{x["name"]} -> {trans}\n' - rst += trans - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送能不能好好说话: {msg} -> {rst}" - ) - await nbnhhsh.send(f"{tmp}={rst}", at_sender=True) - except (IndexError, KeyError): - await nbnhhsh.finish("没有找到对应的翻译....") diff --git a/plugins/one_friend/__init__.py b/plugins/one_friend/__init__.py deleted file mode 100755 index 2c6add40..00000000 --- a/plugins/one_friend/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from io import BytesIO -from random import choice -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message -from utils.utils import get_message_at, get_user_avatar, get_message_text -from utils.message_builder import image -from utils.image_utils import BuildImage -from nonebot.params import RegexGroup -from typing import Tuple, Any - -__zx_plugin_name__ = "我有一个朋友" -__plugin_usage__ = """ -usage: - 我有一个朋友他...,不知道是不是你 - 指令: - 我有一个朋友想问问 [文本] ?[at]: 当at时你的朋友就是艾特对象 -""".strip() -__plugin_des__ = "我有一个朋友想问问..." -__plugin_cmd__ = ["我有一个朋友想问问[文本] ?[at]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["我有一个朋友想问问", "我有一个朋友"], -} - -one_friend = on_regex( - "^我.{0,4}朋友.{0,2}(?:想问问|说|让我问问|想问|让我问|想知道|让我帮他问问|让我帮他问|让我帮忙问|让我帮忙问问|问)(.{0,30})$", - priority=4, - block=True, -) - - -@one_friend.handle() -async def _(bot: Bot, event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - qq = get_message_at(event.json()) - if not qq: - qq = choice( - [ - x["user_id"] - for x in await bot.get_group_member_list( - group_id=event.group_id - ) - ] - ) - user_name = "朋友" - else: - qq = qq[0] - at_user = await bot.get_group_member_info(group_id=event.group_id, user_id=qq) - user_name = at_user["card"] or at_user["nickname"] - msg = get_message_text(Message(reg_group[0])).strip() - if not msg: - msg = "都不知道问什么" - msg = msg.replace("他", "我").replace("她", "我").replace("它", "我") - x = await get_user_avatar(qq) - if x: - ava = BuildImage(200, 100, background=BytesIO(x)) - else: - ava = BuildImage(200, 100, color=(0, 0, 0)) - ava.circle() - text = BuildImage(400, 30, font_size=30) - text.text((0, 0), user_name) - A = BuildImage(700, 150, font_size=25, color="white") - await A.apaste(ava, (30, 25), True) - await A.apaste(text, (150, 38)) - await A.atext((150, 85), msg, (125, 125, 125)) - - await one_friend.send(image(b64=A.pic2bs4())) diff --git a/plugins/open_cases/__init__.py b/plugins/open_cases/__init__.py deleted file mode 100755 index 4c1415a0..00000000 --- a/plugins/open_cases/__init__.py +++ /dev/null @@ -1,326 +0,0 @@ -import asyncio -import random -from datetime import datetime, timedelta -from typing import Any, List, Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg, RegexGroup -from nonebot.permission import SUPERUSER -from nonebot.plugin import MatcherGroup -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.depends import OneCommand -from utils.image_utils import text2image -from utils.message_builder import image -from utils.utils import CN2NUM, is_number, scheduler - -from .open_cases_c import ( - auto_update, - get_my_knifes, - group_statistics, - open_case, - open_multiple_case, - total_open_statistics, -) -from .utils import ( - CASE2ID, - KNIFE2ID, - CaseManager, - build_case_image, - download_image, - get_skin_case, - init_skin_trends, - reset_count_daily, - update_skin_data, -) - -__zx_plugin_name__ = "开箱" -__plugin_usage__ = """ -usage: - 看看你的人品罢了 - 模拟开箱,完美公布的真实概率,只想看看替你省了多少钱 - 指令: - 开箱 ?[武器箱] - [1-30]连开箱 ?[武器箱] - 我的开箱 - 我的金色 - 群开箱统计 - 查看武器箱?[武器箱] - * 不包含[武器箱]时随机开箱 * - 示例: 查看武器箱 - 示例: 查看武器箱英勇 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 更新皮肤指令 - 重置开箱: 重置今日开箱所有次数 - 指令: - 更新武器箱 ?[武器箱/ALL] - 更新皮肤 ?[名称/ALL1] - 更新皮肤 ?[名称/ALL1] -S: (必定更新罕见皮肤所属箱子) - 更新武器箱图片 - * 不指定武器箱时则全部更新 * - * 过多的爬取会导致账号API被封 * -""".strip() -__plugin_des__ = "csgo模拟开箱[戒赌]" -__plugin_cmd__ = [ - "开箱 ?[武器箱]", - "[1-30]连开箱 ?[武器箱]", - "我的开箱", - "我的金色", - "群开箱统计", - "查看武器箱?[武器箱]", - "更新武器箱 ?[武器箱] [_superuser]", - "更新皮肤 ?[刀具名称] [_superuser]", - "更新武器箱图片 [_superuser]", -] -__plugin_type__ = ("抽卡相关", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["csgo开箱", "开箱"], -} -__plugin_task__ = {"open_case_reset_remind": "每日开箱重置提醒"} -__plugin_cd_limit__ = {"rst": "着什么急啊,慢慢来!"} -__plugin_resources__ = {f"cases": IMAGE_PATH} -__plugin_configs__ = { - "INITIAL_OPEN_CASE_COUNT": { - "value": 20, - "help": "初始每日开箱次数", - "default_value": 20, - "type": int, - }, - "EACH_IMPRESSION_ADD_COUNT": { - "value": 3, - "help": "每 * 点好感度额外增加开箱次数", - "default_value": 3, - "type": int, - }, - "COOKIE": {"value": None, "help": "BUFF的cookie", "type": str}, - "BUFF_PROXY": {"value": None, "help": "使用代理访问BUFF"}, - "DAILY_UPDATE": { - "value": None, - "help": "每日自动更新的武器箱,存在'ALL'时则更新全部武器箱", - "type": List[str], - }, -} - -Config.add_plugin_config( - "_task", - "DEFAULT_OPEN_CASE_RESET_REMIND", - True, - help_="被动 每日开箱重置提醒 进群默认开关状态", - default_value=True, - type=bool, -) - - -cases_matcher_group = MatcherGroup(priority=5, permission=GROUP, block=True) - - -k_open_case = cases_matcher_group.on_command("开箱") -reload_count = cases_matcher_group.on_command("重置开箱", permission=SUPERUSER) -total_case_data = cases_matcher_group.on_command( - "我的开箱", aliases={"开箱统计", "开箱查询", "查询开箱"} -) -group_open_case_statistics = cases_matcher_group.on_command("群开箱统计") -open_multiple = cases_matcher_group.on_regex("(.*)连开箱(.*)?") -update_case = on_command( - "更新武器箱", aliases={"更新皮肤"}, priority=1, permission=SUPERUSER, block=True -) -update_case_image = on_command("更新武器箱图片", priority=1, permission=SUPERUSER, block=True) -show_case = on_command("查看武器箱", priority=5, block=True) -my_knifes = on_command("我的金色", priority=1, permission=GROUP, block=True) -show_skin = on_command("查看皮肤", priority=5, block=True) -price_trends = on_command("价格趋势", priority=5, block=True) - - -@price_trends.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().replace("武器箱", "").strip() - if not msg: - await price_trends.finish("未指定皮肤") - msg_split = msg.split() - if len(msg_split) < 3: - await price_trends.finish("参数不足, [类型名称] [皮肤名称] [磨损程度] ?[天数=7]") - abrasion = msg_split[2] - day = 7 - if len(msg_split) > 3: - if not is_number(msg_split[3]): - await price_trends.finish("天数必须为数字") - day = int(msg_split[3]) - if day <= 0 or day > 180: - await price_trends.finish("天数必须大于0且小于180") - result = await init_skin_trends(msg_split[0], msg_split[1], msg_split[2], day) - if not result: - await price_trends.finish("未查询到数据") - await price_trends.send( - image(await init_skin_trends(msg_split[0], msg_split[1], msg_split[2], day)) - ) - - -@reload_count.handle() -async def _(event: GroupMessageEvent): - await reset_count_daily() - - -@k_open_case.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - case_name = arg.extract_plain_text().strip() - case_name = case_name.replace("武器箱", "").strip() - result = await open_case(event.user_id, event.group_id, case_name) - await k_open_case.finish(result, at_sender=True) - - -@total_case_data.handle() -async def _(event: GroupMessageEvent): - await total_case_data.finish( - await total_open_statistics(event.user_id, event.group_id), - at_sender=True, - ) - - -@group_open_case_statistics.handle() -async def _(event: GroupMessageEvent): - await group_open_case_statistics.finish(await group_statistics(event.group_id)) - - -@my_knifes.handle() -async def _(event: GroupMessageEvent): - await my_knifes.finish( - await get_my_knifes(event.user_id, event.group_id), at_sender=True - ) - - -@open_multiple.handle() -async def _( - event: GroupMessageEvent, state: T_State, reg_group: Tuple[Any, ...] = RegexGroup() -): - num, case_name = reg_group - if is_number(num) or CN2NUM.get(num): - try: - num = CN2NUM[num] - except KeyError: - num = int(num) - if num > 30: - await open_multiple.finish("开箱次数不要超过30啊笨蛋!", at_sender=True) - if num < 0: - await open_multiple.finish("再负开箱就扣你明天开箱数了!", at_sender=True) - else: - await open_multiple.finish("必须要是数字切不要超过30啊笨蛋!中文也可!", at_sender=True) - case_name = case_name.replace("武器箱", "").strip() - await open_multiple.finish( - await open_multiple_case(event.user_id, event.group_id, case_name, num), - at_sender=True, - ) - - -@update_case.handle() -async def _(event: MessageEvent, arg: Message = CommandArg(), cmd: str = OneCommand()): - msg = arg.extract_plain_text().strip() - is_update_case_name = "-S" in msg - msg = msg.replace("-S", "").strip() - if not msg: - case_list = [] - skin_list = [] - for i, case_name in enumerate(CASE2ID): - if case_name in CaseManager.CURRENT_CASES: - case_list.append(f"{i+1}.{case_name} [已更新]") - else: - case_list.append(f"{i+1}.{case_name}") - for skin_name in KNIFE2ID: - skin_list.append(f"{skin_name}") - text = "武器箱:\n" + "\n".join(case_list) + "\n皮肤:\n" + ", ".join(skin_list) - await update_case.finish( - "未指定武器箱, 当前已包含武器箱/皮肤\n" - + image(await text2image(text, padding=20, color="#f9f6f2")) - ) - if msg in ["ALL", "ALL1"]: - if msg == "ALL": - case_list = list(CASE2ID.keys()) - type_ = "武器箱" - else: - case_list = list(KNIFE2ID.keys()) - type_ = "罕见皮肤" - await update_case.send(f"即将更新所有{type_}, 请稍等") - for i, case_name in enumerate(case_list): - try: - info = await update_skin_data(case_name, is_update_case_name) - if "请先登录" in info: - await update_case.send(f"未登录, 已停止更新, 请配置BUFF token...") - return - rand = random.randint(300, 500) - result = f"更新全部{type_}完成" - if i < len(case_list) - 1: - next_case = case_list[i + 1] - result = f"将在 {rand} 秒后更新下一{type_}: {next_case}" - await update_case.send(f"{info}, {result}") - logger.info(f"info, {result}", "更新武器箱") - await asyncio.sleep(rand) - except Exception as e: - logger.error(f"更新{type_}: {case_name}", e=e) - await update_case.send(f"更新{type_}: {case_name} 发生错误: {type(e)}: {e}") - await update_case.send(f"更新全部{type_}完成") - else: - await update_case.send(f"开始{cmd}: {msg}, 请稍等") - try: - await update_case.send( - await update_skin_data(msg, is_update_case_name), at_sender=True - ) - except Exception as e: - logger.error(f"{cmd}: {msg}", e=e) - await update_case.send(f"成功{cmd}: {msg} 发生错误: {type(e)}: {e}") - - -@show_case.handle() -async def _(arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - result = await build_case_image(msg) - if isinstance(result, str): - await show_case.send(result) - else: - await show_case.send(image(result)) - - -@update_case_image.handle() -async def _(arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - await update_case_image.send("开始更新图片...") - await download_image(msg) - await update_case_image.send("更新图片完成...", at_sender=True) - - -# 重置开箱 -@scheduler.scheduled_job( - "cron", - hour=0, - minute=1, -) -async def _(): - await reset_count_daily() - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=10, -) -async def _(): - now = datetime.now() - hour = random.choice([0, 1, 2, 3]) - date = now + timedelta(hours=hour) - logger.debug(f"将在 {date} 时自动更新武器箱...", "更新武器箱") - scheduler.add_job( - auto_update, - "date", - run_date=date.replace(microsecond=0), - id=f"auto_update_csgo_cases", - ) diff --git a/plugins/open_cases/build_image.py b/plugins/open_cases/build_image.py deleted file mode 100644 index 972eb45f..00000000 --- a/plugins/open_cases/build_image.py +++ /dev/null @@ -1,156 +0,0 @@ -from datetime import timedelta, timezone -from typing import Optional - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.image_utils import BuildImage -from utils.utils import cn2py - -from .config import COLOR2COLOR, COLOR2NAME -from .models.buff_skin import BuffSkin - -BASE_PATH = IMAGE_PATH / "csgo_cases" - -ICON_PATH = IMAGE_PATH / "_icon" - - -async def draw_card(skin: BuffSkin, rand: str) -> BuildImage: - """构造抽取图片 - - Args: - skin (BuffSkin): BuffSkin - rand (str): 磨损 - - Returns: - BuildImage: BuildImage - """ - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" - if not file_path.exists(): - logger.warning(f"皮肤图片: {name} 不存在", "开箱") - skin_bk = BuildImage( - 460, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" - ) - if file_path.exists(): - skin_image = BuildImage(205, 153, background=file_path) - await skin_bk.apaste(skin_image, (10, 30), alpha=True) - await skin_bk.aline((220, 10, 220, 180)) - await skin_bk.atext((10, 10), skin.name, (255, 255, 255)) - name_icon = BuildImage(20, 20, background=ICON_PATH / "name_white.png") - await skin_bk.apaste(name_icon, (240, 13), True) - await skin_bk.atext((265, 15), f"名称:", (255, 255, 255), font_size=20) - await skin_bk.atext( - (300, 9), - f"{skin.skin_name + ('(St)' if skin.is_stattrak else '')}", - (255, 255, 255), - ) - tone_icon = BuildImage(20, 20, background=ICON_PATH / "tone_white.png") - await skin_bk.apaste(tone_icon, (240, 45), True) - await skin_bk.atext((265, 45), "品质:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 40), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color]) - type_icon = BuildImage(20, 20, background=ICON_PATH / "type_white.png") - await skin_bk.apaste(type_icon, (240, 73), True) - await skin_bk.atext((265, 75), "类型:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 70), skin.weapon_type, (255, 255, 255)) - price_icon = BuildImage(20, 20, background=ICON_PATH / "price_white.png") - await skin_bk.apaste(price_icon, (240, 103), True) - await skin_bk.atext((265, 105), "价格:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 102), str(skin.sell_min_price), (0, 255, 98)) - abrasion_icon = BuildImage(20, 20, background=ICON_PATH / "abrasion_white.png") - await skin_bk.apaste(abrasion_icon, (240, 133), True) - await skin_bk.atext((265, 135), "磨损:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 130), skin.abrasion, (255, 255, 255)) - await skin_bk.atext((228, 165), f"({rand})", (255, 255, 255)) - return skin_bk - - -async def generate_skin(skin: BuffSkin, update_count: int) -> Optional[BuildImage]: - """构造皮肤图片 - - Args: - skin (BuffSkin): BuffSkin - - Returns: - Optional[BuildImage]: 图片 - """ - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" - if not file_path.exists(): - logger.warning(f"皮肤图片: {name} 不存在", "查看武器箱") - if skin.color == "CASE": - case_bk = BuildImage( - 700, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" - ) - if file_path.exists(): - skin_img = BuildImage(200, 200, background=file_path) - await case_bk.apaste(skin_img, (10, 10), True) - await case_bk.aline((250, 10, 250, 190)) - await case_bk.aline((280, 160, 660, 160)) - name_icon = BuildImage(30, 30, background=ICON_PATH / "box_white.png") - await case_bk.apaste(name_icon, (260, 25), True) - await case_bk.atext((295, 30), "名称:", (255, 255, 255)) - await case_bk.atext((345, 25), skin.case_name, (255, 0, 38), font_size=30) - - type_icon = BuildImage(30, 30, background=ICON_PATH / "type_white.png") - await case_bk.apaste(type_icon, (260, 70), True) - await case_bk.atext((295, 75), "类型:", (255, 255, 255)) - await case_bk.atext((345, 72), "武器箱", (0, 157, 255), font_size=30) - - price_icon = BuildImage(30, 30, background=ICON_PATH / "price_white.png") - await case_bk.apaste(price_icon, (260, 114), True) - await case_bk.atext((295, 120), "单价:", (255, 255, 255)) - await case_bk.atext( - (340, 116), str(skin.sell_min_price), (0, 255, 98), font_size=30 - ) - - update_count_icon = BuildImage( - 40, 40, background=ICON_PATH / "reload_white.png" - ) - await case_bk.apaste(update_count_icon, (575, 10), True) - await case_bk.atext((625, 12), str(update_count), (255, 255, 255), font_size=45) - - num_icon = BuildImage(30, 30, background=ICON_PATH / "num_white.png") - await case_bk.apaste(num_icon, (455, 70), True) - await case_bk.atext((490, 75), "在售:", (255, 255, 255)) - await case_bk.atext((535, 72), str(skin.sell_num), (144, 0, 255), font_size=30) - - want_buy_icon = BuildImage(30, 30, background=ICON_PATH / "want_buy_white.png") - await case_bk.apaste(want_buy_icon, (455, 114), True) - await case_bk.atext((490, 120), "求购:", (255, 255, 255)) - await case_bk.atext((535, 116), str(skin.buy_num), (144, 0, 255), font_size=30) - - await case_bk.atext((275, 165), "更新时间", (255, 255, 255), font_size=22) - date = str( - skin.update_time.replace(microsecond=0).astimezone( - timezone(timedelta(hours=8)) - ) - ).split("+")[0] - await case_bk.atext( - (344, 170), - date, - (255, 255, 255), - font_size=30, - ) - return case_bk - else: - skin_bk = BuildImage( - 235, 250, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" - ) - if file_path.exists(): - skin_image = BuildImage(205, 153, background=file_path) - await skin_bk.apaste(skin_image, (10, 30), alpha=True) - update_count_icon = BuildImage( - 35, 35, background=ICON_PATH / "reload_white.png" - ) - await skin_bk.aline((10, 180, 220, 180)) - await skin_bk.atext((10, 10), skin.name, (255, 255, 255)) - await skin_bk.apaste(update_count_icon, (140, 10), True) - await skin_bk.atext((175, 15), str(update_count), (255, 255, 255)) - await skin_bk.atext((10, 185), f"{skin.skin_name}", (255, 255, 255), "by_width") - await skin_bk.atext((10, 218), "品质:", (255, 255, 255)) - await skin_bk.atext( - (55, 218), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color] - ) - await skin_bk.atext((100, 218), "类型:", (255, 255, 255)) - await skin_bk.atext((145, 218), skin.weapon_type, (255, 255, 255)) - return skin_bk diff --git a/plugins/parse_bilibili_json.py b/plugins/parse_bilibili_json.py deleted file mode 100755 index 4a9a91a3..00000000 --- a/plugins/parse_bilibili_json.py +++ /dev/null @@ -1,174 +0,0 @@ -import asyncio -import time - -import aiohttp -import ujson as json -from bilireq import video -from nonebot import on_message -from nonebot.adapters.onebot.v11 import ActionFailed, GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.browser import get_browser -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.manager import group_manager -from utils.message_builder import image -from utils.user_agent import get_user_agent -from utils.utils import get_local_proxy, get_message_json, get_message_text, is_number - -__zx_plugin_name__ = "B站转发解析" -__plugin_usage__ = """ -usage: - B站转发解析,解析b站分享信息,支持bv,bilibili链接,b站手机端转发卡片,cv,b23.tv,且5分钟内不解析相同url -""".strip() -__plugin_des__ = "B站转发解析" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"bilibili_parse": "b站转发解析"} -Config.add_plugin_config( - "_task", - "DEFAULT_BILIBILI_PARSE", - True, - help_="被动 B站转发解析 进群默认开关状态", - default_value=True, - type=bool -) - - -async def plugin_on_checker(event: GroupMessageEvent) -> bool: - return group_manager.get_plugin_status("parse_bilibili_json", event.group_id) - - -parse_bilibili_json = on_message( - priority=1, permission=GROUP, block=False, rule=plugin_on_checker -) - -_tmp = {} - - -@parse_bilibili_json.handle() -async def _(event: GroupMessageEvent): - vd_info = None - url = None - if get_message_json(event.json()): - try: - data = json.loads(get_message_json(event.json())[0]["data"]) - except (IndexError, KeyError): - data = None - if data: - # 转发视频 - if data.get("desc") == "哔哩哔哩" or "哔哩哔哩" in data.get("prompt"): - async with aiohttp.ClientSession(headers=get_user_agent()) as session: - async with session.get( - data["meta"]["detail_1"]["qqdocurl"], - timeout=7, - ) as response: - url = str(response.url).split("?")[0] - if url[-1] == "/": - url = url[:-1] - bvid = url.split("/")[-1] - vd_info = await video.get_video_base_info(bvid) - # 转发专栏 - if ( - data.get("meta") - and data["meta"].get("news") - and data["meta"]["news"].get("desc") == "哔哩哔哩专栏" - ): - url = data["meta"]["news"]["jumpUrl"] - page = None - try: - browser = await get_browser() - if not browser: - return - page = await browser.new_page( - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - " (KHTML, like Gecko) Chrome/93.0.4530.0 Safari/537.36" - ) - await page.goto(url, wait_until="networkidle", timeout=10000) - await page.set_viewport_size({"width": 2560, "height": 1080}) - await page.click("#app > div") - div = await page.query_selector("#app > div") - await div.screenshot( - path=f"{IMAGE_PATH}/temp/cv_{event.user_id}.png", - timeout=100000, - ) - await asyncio.get_event_loop().run_in_executor( - None, resize, TEMP_PATH / f"cv_{event.user_id}.png" - ) - await parse_bilibili_json.send( - "[[_task|bilibili_parse]]" - + image(TEMP_PATH / f"cv_{event.user_id}.png") - ) - await page.close() - logger.info( - f"USER {event.user_id} GROUP {event.group_id} 解析bilibili转发 {url}" - ) - except Exception as e: - logger.error(f"尝试解析bilibili专栏 {url} 失败 {type(e)}:{e}") - if page: - await page.close() - return - # BV - if msg := get_message_text(event.json()): - if "BV" in msg: - index = msg.find("BV") - if len(msg[index + 2 :]) >= 10: - msg = msg[index : index + 12] - url = f"https://www.bilibili.com/video/{msg}" - vd_info = await video.get_video_base_info(msg) - elif "av" in msg: - index = msg.find("av") - if len(msg[index + 2 :]) >= 1: - msg = msg[index + 2 : index + 11] - if is_number(msg): - url = f"https://www.bilibili.com/video/av{msg}" - vd_info = await video.get_video_base_info("av" + msg) - elif "https://b23.tv" in msg: - url = "https://" + msg[msg.find("b23.tv") : msg.find("b23.tv") + 14] - async with aiohttp.ClientSession(headers=get_user_agent()) as session: - async with session.get( - url, - timeout=7, - ) as response: - url = (str(response.url).split("?")[0]).strip("/") - bvid = url.split("/")[-1] - vd_info = await video.get_video_base_info(bvid) - if vd_info: - if ( - url in _tmp.keys() and time.time() - _tmp[url] > 30 - ) or url not in _tmp.keys(): - _tmp[url] = time.time() - aid = vd_info["aid"] - title = vd_info["title"] - author = vd_info["owner"]["name"] - reply = vd_info["stat"]["reply"] # 回复 - favorite = vd_info["stat"]["favorite"] # 收藏 - coin = vd_info["stat"]["coin"] # 投币 - # like = vd_info['stat']['like'] # 点赞 - # danmu = vd_info['stat']['danmaku'] # 弹幕 - date = time.strftime("%Y-%m-%d", time.localtime(vd_info["ctime"])) - try: - await parse_bilibili_json.send( - "[[_task|bilibili_parse]]" - + image(vd_info["pic"]) - + f"\nav{aid}\n标题:{title}\n" - f"UP:{author}\n" - f"上传日期:{date}\n" - f"回复:{reply},收藏:{favorite},投币:{coin}\n" - f"{url}" - ) - except ActionFailed: - logger.warning(f"{event.group_id} 发送bilibili解析失败") - else: - logger.info( - f"USER {event.user_id} GROUP {event.group_id} 解析bilibili转发 {url}" - ) - - -def resize(path: str): - A = BuildImage(0, 0, background=path, ratio=0.5) - A.save(path) diff --git a/plugins/pid_search.py b/plugins/pid_search.py deleted file mode 100755 index ff8da011..00000000 --- a/plugins/pid_search.py +++ /dev/null @@ -1,117 +0,0 @@ -from asyncio.exceptions import TimeoutError - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, CommandArg -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.manager import withdraw_message_manager -from utils.message_builder import image -from utils.utils import change_pixiv_image_links, is_number - -__zx_plugin_name__ = "pid搜索" -__plugin_usage__ = """ -usage: - 通过 pid 搜索图片 - 指令: - p搜 [pid] -""".strip() -__plugin_des__ = "通过 pid 搜索图片" -__plugin_cmd__ = ["p搜 [pid]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["p搜"], -} - -pid_search = on_command("p搜", aliases={"pixiv搜", "P搜"}, priority=5, block=True) - - -@pid_search.handle() -async def _h(event: MessageEvent, state: T_State, arg: Message = CommandArg()): - pid = arg.extract_plain_text().strip() - if pid: - state["pid"] = pid - - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -@pid_search.got("pid", prompt="需要查询的图片PID是?") -async def _g(event: MessageEvent, state: T_State, pid: str = Arg("pid")): - url = Config.get_config("hibiapi", "HIBIAPI") + "/api/pixiv/illust" - if pid in ["取消", "算了"]: - await pid_search.finish("已取消操作...") - if not is_number(pid): - await pid_search.reject_arg("pid", "笨蛋,重新输入数!字!") - for _ in range(3): - try: - data = ( - await AsyncHttpx.get( - url, - params={"id": pid}, - timeout=5, - ) - ).json() - except TimeoutError: - pass - except Exception as e: - await pid_search.finish(f"发生了一些错误..{type(e)}:{e}") - else: - if data.get("error"): - await pid_search.finish(data["error"]["user_message"], at_sender=True) - data = data["illust"] - if not data["width"] and not data["height"]: - await pid_search.finish(f"没有搜索到 PID:{pid} 的图片", at_sender=True) - pid = data["id"] - title = data["title"] - author = data["user"]["name"] - author_id = data["user"]["id"] - image_list = [] - try: - image_list.append(data["meta_single_page"]["original_image_url"]) - except KeyError: - for image_url in data["meta_pages"]: - image_list.append(image_url["image_urls"]["original"]) - for i, img_url in enumerate(image_list): - img_url = change_pixiv_image_links(img_url) - if not await AsyncHttpx.download_file( - img_url, - TEMP_PATH / f"pid_search_{event.user_id}_{i}.png", - headers=headers, - ): - await pid_search.send("图片下载失败了....", at_sender=True) - tmp = "" - if isinstance(event, GroupMessageEvent): - tmp = "\n【注】将在30后撤回......" - msg_id = await pid_search.send( - Message( - f"title:{title}\n" - f"pid:{pid}\n" - f"author:{author}\n" - f"author_id:{author_id}\n" - f'{image(TEMP_PATH / f"pid_search_{event.user_id}_{i}.png")}' - f"{tmp}" - ) - ) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询图片 PID:{pid}" - ) - if isinstance(event, GroupMessageEvent): - withdraw_message_manager.append((msg_id, 30)) - break - else: - await pid_search.finish("图片下载失败了....", at_sender=True) diff --git a/plugins/pix_gallery/__init__.py b/plugins/pix_gallery/__init__.py deleted file mode 100755 index dc9c130a..00000000 --- a/plugins/pix_gallery/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -from pathlib import Path -from typing import Tuple - -import nonebot - -from configs.config import Config - -Config.add_plugin_config( - "hibiapi", - "HIBIAPI", - "https://api.obfs.dev", - help_="如果没有自建或其他hibiapi请不要修改", - default_value="https://api.obfs.dev", -) -Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pximg.cf", help_="Pixiv反向代理") -Config.add_plugin_config( - "pix", - "PIX_IMAGE_SIZE", - "master", - name="PIX图库", - help_="PIX图库下载的画质 可能的值:original:原图,master:缩略图(加快发送速度)", - default_value="master", -) -Config.add_plugin_config( - "pix", - "SEARCH_HIBIAPI_BOOKMARKS", - 5000, - help_="最低收藏,PIX使用HIBIAPI搜索图片时达到最低收藏才会添加至图库", - default_value=5000, - type=int, -) -Config.add_plugin_config( - "pix", - "WITHDRAW_PIX_MESSAGE", - (0, 1), - help_="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", - default_value=(0, 1), - type=Tuple[int, int], -) -Config.add_plugin_config( - "pix", - "PIX_OMEGA_PIXIV_RATIO", - (10, 0), - help_="PIX图库 与 额外图库OmegaPixivIllusts 混合搜索的比例 参1:PIX图库 参2:OmegaPixivIllusts扩展图库(没有此图库请设置为0)", - default_value=(10, 0), - type=Tuple[int, int], -) -Config.add_plugin_config( - "pix", "TIMEOUT", 10, help_="下载图片超时限制(秒)", default_value=10, type=int -) - -Config.add_plugin_config( - "pix", "SHOW_INFO", True, help_="是否显示图片的基本信息,如PID等", default_value=True, type=bool -) - -# GDict["run_sql"].append("ALTER TABLE omega_pixiv_illusts ADD classified Integer;") -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/pix_gallery/pix.py b/plugins/pix_gallery/pix.py deleted file mode 100755 index a01fd792..00000000 --- a/plugins/pix_gallery/pix.py +++ /dev/null @@ -1,217 +0,0 @@ -import random - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg - -from configs.config import Config -from services.log import logger -from utils.manager import withdraw_message_manager -from utils.message_builder import custom_forward_msg, image -from utils.utils import is_number - -from ._data_source import get_image -from ._model.omega_pixiv_illusts import OmegaPixivIllusts -from ._model.pixiv import Pixiv - -__zx_plugin_name__ = "PIX" -__plugin_usage__ = """ -usage: - 查看 pix 好康图库 - 指令: - pix ?*[tags]: 通过 tag 获取相似图片,不含tag时随机抽取 - pid [uid]: 通过uid获取图片 - pix pid[pid]: 查看图库中指定pid图片 - 示例:pix 萝莉 白丝 - 示例:pix 萝莉 白丝 10 (10为数量) - 示例:pix #02 (当tag只有1个tag且为数字时,使用#标记,否则将被判定为数量) - 示例:pix 34582394 (查询指定uid图片) - 示例:pix pid:12323423 (查询指定pid图片) -""".strip() -__plugin_superuser_usage__ = """ -usage: - 超级用户额外的 pix 指令 - 指令: - pix -s ?*[tags]: 通过tag获取色图,不含tag时随机 - pix -r ?*[tags]: 通过tag获取r18图,不含tag时随机 -""".strip() -__plugin_des__ = "这里是PIX图库!" -__plugin_cmd__ = [ - "pix ?*[tags]", - "pix pid [pid]", - "pix -s ?*[tags] [_superuser]", - "pix -r ?*[tags] [_superuser]", -] -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["pix", "Pix", "PIX", "pIx"], -} -__plugin_block_limit__ = {"rst": "您有PIX图片正在处理,请稍等..."} -__plugin_configs__ = { - "MAX_ONCE_NUM2FORWARD": { - "value": None, - "help": "单次发送的图片数量达到指定值时转发为合并消息", - "default_value": None, - "type": int, - }, - "ALLOW_GROUP_SETU": { - "value": False, - "help": "允许非超级用户使用-s参数", - "default_value": False, - "type": bool, - }, - "ALLOW_GROUP_R18": { - "value": False, - "help": "允许非超级用户使用-r参数", - "default_value": False, - "type": bool, - }, -} - - -pix = on_command("pix", aliases={"PIX", "Pix"}, priority=5, block=True) - - -PIX_RATIO = None -OMEGA_RATIO = None - - -@pix.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - global PIX_RATIO, OMEGA_RATIO - if PIX_RATIO is None: - pix_omega_pixiv_ratio = Config.get_config("pix", "PIX_OMEGA_PIXIV_RATIO") - PIX_RATIO = pix_omega_pixiv_ratio[0] / ( - pix_omega_pixiv_ratio[0] + pix_omega_pixiv_ratio[1] - ) - OMEGA_RATIO = 1 - PIX_RATIO - num = 1 - keyword = arg.extract_plain_text().strip() - x = keyword.split() - if "-s" in x: - x.remove("-s") - nsfw_tag = 1 - elif "-r" in x: - x.remove("-r") - nsfw_tag = 2 - else: - nsfw_tag = 0 - if str(event.user_id) not in bot.config.superusers: - if (nsfw_tag == 1 and not Config.get_config("pix", "ALLOW_GROUP_SETU")) or ( - nsfw_tag == 2 and not Config.get_config("pix", "ALLOW_GROUP_R18") - ): - await pix.finish("你不能看这些噢,这些都是是留给管理员看的...") - if (n := len(x)) == 1: - if is_number(x[0]) and int(x[0]) < 100: - num = int(x[0]) - keyword = "" - elif x[0].startswith("#"): - keyword = x[0][1:] - elif n > 1: - if is_number(x[-1]): - num = int(x[-1]) - if num > 10: - if str(event.user_id) not in bot.config.superusers or ( - str(event.user_id) in bot.config.superusers and num > 30 - ): - num = random.randint(1, 10) - await pix.send(f"太贪心了,就给你发 {num}张 好了") - x = x[:-1] - keyword = " ".join(x) - pix_num = int(num * PIX_RATIO) + 15 if PIX_RATIO != 0 else 0 - omega_num = num - pix_num + 15 - if is_number(keyword): - if num == 1: - pix_num = 15 - omega_num = 15 - all_image = await Pixiv.query_images( - uid=int(keyword), num=pix_num, r18=1 if nsfw_tag == 2 else 0 - ) + await OmegaPixivIllusts.query_images( - uid=int(keyword), num=omega_num, nsfw_tag=nsfw_tag - ) - elif keyword.lower().startswith("pid"): - pid = keyword.replace("pid", "").replace(":", "").replace(":", "") - if not is_number(pid): - await pix.finish("PID必须是数字...", at_sender=True) - all_image = await Pixiv.query_images( - pid=int(pid), r18=1 if nsfw_tag == 2 else 0 - ) - if not all_image: - all_image = await OmegaPixivIllusts.query_images( - pid=int(pid), nsfw_tag=nsfw_tag - ) - num = len(all_image) - else: - tmp = await Pixiv.query_images( - x, r18=1 if nsfw_tag == 2 else 0, num=pix_num - ) + await OmegaPixivIllusts.query_images(x, nsfw_tag=nsfw_tag, num=omega_num) - tmp_ = [] - all_image = [] - for x in tmp: - if x.pid not in tmp_: - all_image.append(x) - tmp_.append(x.pid) - if not all_image: - await pix.finish(f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片...", at_sender=True) - msg_list = [] - for _ in range(num): - img_url = None - author = None - if not all_image: - await pix.finish("坏了...发完了,没图了...") - img = random.choice(all_image) - all_image.remove(img) - if isinstance(img, OmegaPixivIllusts): - img_url = img.url - author = img.uname - elif isinstance(img, Pixiv): - img_url = img.img_url - author = img.author - pid = img.pid - title = img.title - uid = img.uid - _img = await get_image(img_url, event.user_id) - if _img: - if Config.get_config("pix", "SHOW_INFO"): - msg_list.append( - Message( - f"title:{title}\n" - f"author:{author}\n" - f"PID:{pid}\nUID:{uid}\n" - f"{image(_img)}" - ) - ) - else: - msg_list.append(image(_img)) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看PIX图库PID: {pid}" - ) - else: - msg_list.append("这张图似乎下载失败了") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看PIX图库PID: {pid},下载图片出错" - ) - if ( - Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") - and num >= Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") - and isinstance(event, GroupMessageEvent) - ): - msg_id = await bot.send_group_forward_msg( - group_id=event.group_id, messages=custom_forward_msg(msg_list, bot.self_id) - ) - withdraw_message_manager.withdraw_message( - event, msg_id, Config.get_config("pix", "WITHDRAW_PIX_MESSAGE") - ) - else: - for msg in msg_list: - msg_id = await pix.send(msg) - withdraw_message_manager.withdraw_message( - event, msg_id, Config.get_config("pix", "WITHDRAW_PIX_MESSAGE") - ) diff --git a/plugins/pix_gallery/pix_add_keyword.py b/plugins/pix_gallery/pix_add_keyword.py deleted file mode 100755 index 0377f78d..00000000 --- a/plugins/pix_gallery/pix_add_keyword.py +++ /dev/null @@ -1,152 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.utils import is_number - -from ._data_source import uid_pid_exists -from ._model.pixiv import Pixiv -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "PIX关键词/UID/PID添加管理 [Superuser]" -__plugin_usage__ = """ -usage: - PIX关键词/UID/PID添加管理操作 - 指令: - 添加pix关键词 [Tag]: 添加一个pix搜索收录Tag - 添加pixuid [uid]: 添加一个pix搜索收录uid - 添加pixpid [pid]: 添加一个pix收录pid -""".strip() -__plugin_des__ = "PIX关键词/UID/PID添加管理" -__plugin_cmd__ = ["添加pix关键词 [Tag]", "添加pixuid [uid]", "添加pixpid [pid]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -add_keyword = on_command("添加pix关键词", aliases={"添加pix关键字"}, priority=1, block=True) - -add_black_pid = on_command("添加pix黑名单", permission=SUPERUSER, priority=1, block=True) - -# 超级用户可以通过字符 -f 来强制收录不检查是否存在 -add_uid_pid = on_command( - "添加pixuid", - aliases={ - "添加pixpid", - }, - priority=1, - block=True, -) - - -@add_keyword.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - group_id = -1 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - if msg: - # if await PixivKeywordUser.add_keyword( - # event.user_id, group_id, msg, bot.config.superusers - # ): - if not await PixivKeywordUser.exists(keyword=msg): - await PixivKeywordUser.create( - user_id=str(event.user_id), - group_id=str(group_id), - keyword=msg, - is_pass=str(event.user_id) in bot.config.superusers, - ) - await add_keyword.send( - f"已成功添加pixiv搜图关键词:{msg},请等待管理员通过该关键词!", at_sender=True - ) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 添加了pixiv搜图关键词:" + msg - ) - else: - await add_keyword.finish(f"该关键词 {msg} 已存在...") - else: - await add_keyword.finish(f"虚空关键词?.?.?.?") - - -@add_uid_pid.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - msg = arg.extract_plain_text().strip() - exists_flag = True - if msg.find("-f") != -1 and str(event.user_id) in bot.config.superusers: - exists_flag = False - msg = msg.replace("-f", "").strip() - if msg: - for msg in msg.split(): - if not is_number(msg): - await add_uid_pid.finish("UID只能是数字的说...", at_sender=True) - if cmd[0].lower().endswith("uid"): - msg = f"uid:{msg}" - else: - msg = f"pid:{msg}" - if await Pixiv.get_or_none(pid=int(msg[4:]), img_p="p0"): - await add_uid_pid.finish(f"该PID:{msg[4:]}已存在...", at_sender=True) - if not await uid_pid_exists(msg) and exists_flag: - await add_uid_pid.finish("画师或作品不存在或搜索正在CD,请稍等...", at_sender=True) - group_id = -1 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - # if await PixivKeywordUser.add_keyword( - # event.user_id, group_id, msg, bot.config.superusers - # ): - if not await PixivKeywordUser.exists(keyword=msg): - await PixivKeywordUser.create( - user_id=str(event.user_id), - group_id=str(group_id), - keyword=msg, - is_pass=str(event.user_id) in bot.config.superusers, - ) - await add_uid_pid.send( - f"已成功添加pixiv搜图UID/PID:{msg[4:]},请等待管理员通过!", at_sender=True - ) - else: - await add_uid_pid.finish(f"该UID/PID:{msg[4:]} 已存在...") - else: - await add_uid_pid.finish("湮灭吧!虚空的UID!") - - -@add_black_pid.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - pid = arg.extract_plain_text().strip() - img_p = "" - if "p" in pid: - img_p = pid.split("p")[-1] - pid = pid.replace("_", "") - pid = pid[: pid.find("p")] - if not is_number(pid): - await add_black_pid.finish("PID必须全部是数字!", at_sender=True) - # if await PixivKeywordUser.add_keyword( - # 114514, - # 114514, - # f"black:{pid}{f'_p{img_p}' if img_p else ''}", - # bot.config.superusers, - # ): - if not await PixivKeywordUser.exists( - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}" - ): - await PixivKeywordUser.create( - user_id=114514, - group_id=114514, - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}", - is_pass=str(event.user_id) in bot.config.superusers, - ) - await add_black_pid.send(f"已添加PID:{pid} 至黑名单中...") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 添加了pixiv搜图黑名单 PID:{pid}" - ) - else: - await add_black_pid.send(f"PID:{pid} 已添加黑名单中,添加失败...") diff --git a/plugins/pix_gallery/pix_pass_del_keyword.py b/plugins/pix_gallery/pix_pass_del_keyword.py deleted file mode 100755 index 37045d80..00000000 --- a/plugins/pix_gallery/pix_pass_del_keyword.py +++ /dev/null @@ -1,205 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.message_builder import at -from utils.utils import is_number - -from ._data_source import remove_image -from ._model.pixiv import Pixiv -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "PIX关键词/UID/PID删除管理 [Superuser]" -__plugin_usage__ = """ -usage: - PIX关键词/UID/PID删除管理操作 - 指令: - 通过pix关键词 [关键词/pid/uid] - 取消pix关键词 [关键词/pid/uid] - 删除pix关键词 [关键词/pid/uid] - 删除pix图片 *[pid] - 示例:通过pix关键词萝莉 - 示例:通过pix关键词uid:123456 - 示例:通过pix关键词pid:123456 - 示例:删除pix图片4223442 -""".strip() -__plugin_des__ = "PIX关键词/UID/PID删除管理" -__plugin_cmd__ = [ - "通过pix关键词 [关键词/pid/uid]", - "取消pix关键词 [关键词/pid/uid]", - "删除pix关键词 [关键词/pid/uid]", - "删除pix图片 *[pid]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -pass_keyword = on_command( - "通过pix关键词", - aliases={"通过pix关键字", "取消pix关键词", "取消pix关键字"}, - permission=SUPERUSER, - priority=1, - block=True, -) - -del_keyword = on_command( - "删除pix关键词", aliases={"删除pix关键字"}, permission=SUPERUSER, priority=1, block=True -) - -del_pic = on_command("删除pix图片", permission=SUPERUSER, priority=1, block=True) - - -@del_keyword.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - await del_keyword.finish("好好输入要删除什么关键字啊笨蛋!") - if is_number(msg): - msg = f"uid:{msg}" - if msg.lower().startswith("pid"): - msg = "pid:" + msg.replace("pid", "").replace(":", "") - if data := await PixivKeywordUser.get_or_none(keyword=msg): - await data.delete() - await del_keyword.send(f"删除搜图关键词/UID:{msg} 成功...") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 删除了pixiv搜图关键词:" + msg - ) - else: - await del_keyword.send(f"未查询到搜索关键词/UID/PID:{msg},删除失败!") - - -@del_pic.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - pid_arr = arg.extract_plain_text().strip() - if pid_arr: - msg = "" - black_pid = "" - flag = False - pid_arr = pid_arr.split() - if pid_arr[-1] in ["-black", "-b"]: - flag = True - pid_arr = pid_arr[:-1] - for pid in pid_arr: - img_p = None - if "p" in pid or "ugoira" in pid: - if "p" in pid: - img_p = pid.split("p")[-1] - pid = pid.replace("_", "") - pid = pid[: pid.find("p")] - elif "ugoira" in pid: - img_p = pid.split("ugoira")[-1] - pid = pid.replace("_", "") - pid = pid[: pid.find("ugoira")] - if is_number(pid): - if await Pixiv.query_images(pid=int(pid), r18=2): - if await remove_image(int(pid), img_p): - msg += f'{pid}{f"_p{img_p}" if img_p else ""},' - if flag: - # if await PixivKeywordUser.add_keyword( - # 114514, - # 114514, - # f"black:{pid}{f'_p{img_p}' if img_p else ''}", - # bot.config.superusers, - # ): - if await PixivKeywordUser.exists( - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}" - ): - await PixivKeywordUser.create( - user_id="114514", - group_id="114514", - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}", - is_pass=False, - ) - black_pid += f'{pid}{f"_p{img_p}" if img_p else ""},' - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 删除了PIX图片 PID:{pid}{f'_p{img_p}' if img_p else ''}" - ) - # else: - # await del_pic.send( - # f"PIX:删除pid:{pid}{f'_p{img_p}' if img_p else ''} 失败.." - # ) - else: - await del_pic.send( - f"PIX:图片pix:{pid}{f'_p{img_p}' if img_p else ''} 不存在...无法删除.." - ) - else: - await del_pic.send(f"PID必须为数字!pid:{pid}", at_sender=True) - await del_pic.send(f"PIX:成功删除图片:{msg[:-1]}") - if flag: - await del_pic.send(f"成功图片PID加入黑名单:{black_pid[:-1]}") - else: - await del_pic.send("虚空删除?") - - -@pass_keyword.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - tmp = {"group": {}, "private": {}} - msg = arg.extract_plain_text().strip() - if not msg: - await pass_keyword.finish("通过虚空的关键词/UID?离谱...") - msg = msg.split() - flag = cmd[0][:2] == "通过" - for x in msg: - if x.lower().startswith("uid"): - x = x.replace("uid", "").replace(":", "") - x = f"uid:{x}" - elif x.lower().startswith("pid"): - x = x.replace("pid", "").replace(":", "") - x = f"pid:{x}" - if x.lower().find("pid") != -1 or x.lower().find("uid") != -1: - if not is_number(x[4:]): - await pass_keyword.send(f"UID/PID:{x} 非全数字,跳过该关键词...") - continue - data = await PixivKeywordUser.get_or_none(keyword=x) - user_id = 0 - group_id = 0 - if data: - data.is_pass = flag - await data.save(update_fields=["is_pass"]) - user_id, group_id = data.user_id, data.group_id - if not user_id: - await pass_keyword.send(f"未找到关键词/UID:{x},请检查关键词/UID是否存在...") - continue - if flag: - if group_id == -1: - if not tmp["private"].get(user_id): - tmp["private"][user_id] = {"keyword": [x]} - else: - tmp["private"][user_id]["keyword"].append(x) - else: - if not tmp["group"].get(group_id): - tmp["group"][group_id] = {} - if not tmp["group"][group_id].get(user_id): - tmp["group"][group_id][user_id] = {"keyword": [x]} - else: - tmp["group"][group_id][user_id]["keyword"].append(x) - msg = " ".join(msg) - await pass_keyword.send(f"已成功{cmd[0][:2]}搜图关键词:{msg}....") - for user in tmp["private"]: - x = ",".join(tmp["private"][user]["keyword"]) - await bot.send_private_msg( - user_id=user, message=f"你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..." - ) - for group in tmp["group"]: - for user in tmp["group"][group]: - x = ",".join(tmp["group"][group][user]["keyword"]) - await bot.send_group_msg( - group_id=group, - message=Message(f"{at(user)}你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..."), - ) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 通过了pixiv搜图关键词/UID:" + msg - ) diff --git a/plugins/pix_gallery/pix_show_info.py b/plugins/pix_gallery/pix_show_info.py deleted file mode 100755 index 2b8aaab9..00000000 --- a/plugins/pix_gallery/pix_show_info.py +++ /dev/null @@ -1,85 +0,0 @@ -import asyncio - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent -from nonebot.params import CommandArg - -from utils.message_builder import image - -from ._data_source import gen_keyword_pic, get_keyword_num -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "查看pix图库" -__plugin_usage__ = """ -usage: - 查看pix图库 - 指令: - 查看pix图库 ?[tags]: 查看指定tag图片数量,为空时查看整个图库 -""".strip() -__plugin_des__ = "让我看看管理员私藏了多少货" -__plugin_cmd__ = ["查看pix图库 ?[tags]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查看pix图库"], -} - - -my_keyword = on_command("我的pix关键词", aliases={"我的pix关键字"}, priority=1, block=True) - -show_keyword = on_command("显示pix关键词", aliases={"显示pix关键字"}, priority=1, block=True) - -show_pix = on_command("查看pix图库", priority=1, block=True) - - -@my_keyword.handle() -async def _(event: MessageEvent): - data = await PixivKeywordUser.filter(user_id=str(event.user_id)).values_list( - "keyword", flat=True - ) - if not data: - await my_keyword.finish("您目前没有提供任何Pixiv搜图关键字...", at_sender=True) - await my_keyword.send(f"您目前提供的如下关键字:\n\t" + ",".join(data)) - - -@show_keyword.handle() -async def _(bot: Bot, event: MessageEvent): - _pass_keyword, not_pass_keyword = await PixivKeywordUser.get_current_keyword() - if _pass_keyword or not_pass_keyword: - await show_keyword.send( - image( - b64=await asyncio.get_event_loop().run_in_executor( - None, - gen_keyword_pic, - _pass_keyword, - not_pass_keyword, - str(event.user_id) in bot.config.superusers, - ) - ) - ) - else: - if str(event.user_id) in bot.config.superusers: - await show_keyword.finish(f"目前没有已收录或待收录的搜索关键词...") - else: - await show_keyword.finish(f"目前没有已收录的搜索关键词...") - - -@show_pix.handle() -async def _(arg: Message = CommandArg()): - keyword = arg.extract_plain_text().strip() - count, r18_count, count_, setu_count, r18_count_ = await get_keyword_num(keyword) - await show_pix.send( - f"PIX图库:{keyword}\n" - f"总数:{count + r18_count}\n" - f"美图:{count}\n" - f"R18:{r18_count}\n" - f"---------------\n" - f"Omega图库:{keyword}\n" - f"总数:{count_ + setu_count + r18_count_}\n" - f"美图:{count_}\n" - f"色图:{setu_count}\n" - f"R18:{r18_count_}" - ) diff --git a/plugins/pixiv_rank_search/__init__.py b/plugins/pixiv_rank_search/__init__.py deleted file mode 100755 index 351f5a49..00000000 --- a/plugins/pixiv_rank_search/__init__.py +++ /dev/null @@ -1,230 +0,0 @@ -import time -from asyncio.exceptions import TimeoutError -from typing import Type - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import ( - Bot, - GroupMessageEvent, - Message, - MessageEvent, - NetworkError, -) -from nonebot.matcher import Matcher -from nonebot.params import CommandArg -from nonebot.rule import to_me - -from configs.config import Config -from services.log import logger -from utils.message_builder import custom_forward_msg -from utils.utils import is_number - -from .data_source import download_pixiv_imgs, get_pixiv_urls, search_pixiv_urls - -__zx_plugin_name__ = "P站排行/搜图" - -__plugin_usage__ = """ -usage: - P站排行: - 可选参数: - 类型: - 1. 日排行 - 2. 周排行 - 3. 月排行 - 4. 原创排行 - 5. 新人排行 - 6. R18日排行 - 7. R18周排行 - 8. R18受男性欢迎排行 - 9. R18重口排行【慎重!】 - 【使用时选择参数序号即可,R18仅可私聊】 - p站排行 ?[参数] ?[数量] ?[日期] - 示例: - p站排行榜 [无参数默认为日榜] - p站排行榜 1 - p站排行榜 1 5 - p站排行榜 1 5 2018-4-25 - 【注意空格!!】【在线搜索会较慢】 - --------------------------------- - P站搜图: - 搜图 [关键词] ?[数量] ?[页数=1] ?[r18](不屏蔽R-18) - 示例: - 搜图 樱岛麻衣 - 搜图 樱岛麻衣 5 - 搜图 樱岛麻衣 5 r18 - 搜图 樱岛麻衣#1000users 5 - 【多个关键词用#分割】 - 【默认为 热度排序】 - 【注意空格!!】【在线搜索会较慢】【数量可能不符?可能该页数量不够,也可能被R-18屏蔽】 -""".strip() -__plugin_des__ = "P站排行榜直接冲,P站搜图跟着冲" -__plugin_cmd__ = ["p站排行 ?[参数] ?[数量] ?[日期]", "搜图 [关键词] ?[数量] ?[页数=1] ?[r18](不屏蔽R-18)"] -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 9, - "default_status": True, - "limit_superuser": False, - "cmd": ["p站排行", "搜图", "p站搜图", "P站搜图"], -} -__plugin_block_limit__ = {"rst": "P站排行榜或搜图正在搜索,请不要重复触发命令..."} -__plugin_configs__ = { - "TIMEOUT": {"value": 10, "help": "图片下载超时限制", "default_value": 10, "type": int}, - "MAX_PAGE_LIMIT": { - "value": 20, - "help": "作品最大页数限制,超过的作品会被略过", - "default_value": 20, - "type": int, - }, - "ALLOW_GROUP_R18": { - "value": False, - "help": "允许群聊中使用 r18 参数", - "default_value": False, - "type": bool, - }, -} -Config.add_plugin_config( - "hibiapi", - "HIBIAPI", - "https://api.obfs.dev", - help_="如果没有自建或其他hibiapi请不要修改", - default_value="https://api.obfs.dev", -) -Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pixiv.re", help_="Pixiv反向代理") - - -rank_dict = { - "1": "day", - "2": "week", - "3": "month", - "4": "week_original", - "5": "week_rookie", - "6": "day_r18", - "7": "week_r18", - "8": "day_male_r18", - "9": "week_r18g", -} - - -pixiv_rank = on_command( - "p站排行", - aliases={"P站排行榜", "p站排行榜", "P站排行榜", "P站排行"}, - priority=5, - block=True, - rule=to_me(), -) -pixiv_keyword = on_command("搜图", priority=5, block=True, rule=to_me()) - - -@pixiv_rank.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().strip() - msg = msg.split(" ") - msg = [m for m in msg if m] - code = 0 - info_list = [] - if not msg: - msg = ["1"] - if msg[0] in ["6", "7", "8", "9"]: - if event.message_type == "group": - await pixiv_rank.finish("羞羞脸!私聊里自己看!", at_sender=True) - if (n := len(msg)) == 0 or msg[0] == "": - info_list, code = await get_pixiv_urls(rank_dict.get("1")) - elif n == 1: - if msg[0] not in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]: - await pixiv_rank.finish("要好好输入要看什么类型的排行榜呀!", at_sender=True) - info_list, code = await get_pixiv_urls(rank_dict.get(msg[0])) - elif n == 2: - info_list, code = await get_pixiv_urls(rank_dict.get(msg[0]), int(msg[1])) - elif n == 3: - if not check_date(msg[2]): - await pixiv_rank.finish("日期格式错误了", at_sender=True) - info_list, code = await get_pixiv_urls( - rank_dict.get(msg[0]), int(msg[1]), date=msg[2] - ) - else: - await pixiv_rank.finish("格式错了噢,参数不够?看看帮助?", at_sender=True) - if code != 200 and info_list: - await pixiv_rank.finish(info_list[0]) - if not info_list: - await pixiv_rank.finish("没有找到啊,等等再试试吧~V", at_sender=True) - await send_image(info_list, pixiv_rank, bot, event) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看了P站排行榜 code:{msg[0]}" - ) - - -@pixiv_keyword.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if isinstance(event, GroupMessageEvent): - if "r18" in msg.lower() and not Config.get_config( - "pixiv_rank_search", "ALLOW_GROUP_R18" - ): - await pixiv_keyword.finish("(脸红#) 你不会害羞的 八嘎!", at_sender=True) - r18 = 0 if "r18" in msg else 1 - msg = msg.replace("r18", "").strip().split() - msg = [m.strip() for m in msg if m] - keyword = None - info_list = None - num = 10 - page = 1 - if (n := len(msg)) > 0: - keyword = msg[0].replace("#", " ") - if n > 1: - if not is_number(msg[1]): - await pixiv_keyword.finish("图片数量必须是数字!", at_sender=True) - num = int(msg[1]) - if n > 2: - if not is_number(msg[2]): - await pixiv_keyword.finish("页数数量必须是数字!", at_sender=True) - page = int(msg[2]) - if keyword: - info_list, code = await search_pixiv_urls(keyword, num, page, r18) - if code != 200: - await pixiv_keyword.finish(info_list[0]) - if not info_list: - await pixiv_keyword.finish("没有找到啊,等等再试试吧~V", at_sender=True) - await send_image(info_list, pixiv_keyword, bot, event) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看了搜索 {keyword} R18:{r18}" - ) - - -def check_date(date): - try: - time.strptime(date, "%Y-%m-%d") - return True - except: - return False - - -async def send_image( - info_list: list, matcher: Type[Matcher], bot: Bot, event: MessageEvent -): - if isinstance(event, GroupMessageEvent): - await pixiv_rank.send("开始下载整理数据...") - idx = 0 - mes_list = [] - for title, author, urls in info_list: - _message = ( - f"title: {title}\nauthor: {author}\n" - + await download_pixiv_imgs(urls, event.user_id, idx) - ) - mes_list.append(_message) - idx += 1 - mes_list = custom_forward_msg(mes_list, bot.self_id) - await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) - else: - for title, author, urls in info_list: - try: - await matcher.send( - f"title: {title}\n" - f"author: {author}\n" - + await download_pixiv_imgs(urls, event.user_id) - ) - except (NetworkError, TimeoutError): - await matcher.send("这张图网络直接炸掉了!", at_sender=True) diff --git a/plugins/pixiv_rank_search/data_source.py b/plugins/pixiv_rank_search/data_source.py deleted file mode 100755 index d9b007a0..00000000 --- a/plugins/pixiv_rank_search/data_source.py +++ /dev/null @@ -1,162 +0,0 @@ -import platform -from asyncio.exceptions import TimeoutError -from pathlib import Path -from typing import Optional - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.message_builder import image -from utils.utils import change_img_md5 - -# if platform.system() == "Windows": -# import asyncio -# -# asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -async def get_pixiv_urls( - mode: str, num: int = 10, page: int = 1, date: Optional[str] = None -) -> "list, int": - """ - 拿到pixiv rank图片url - :param mode: 模式 - :param num: 数量 - :param page: 页数 - :param date: 日期 - """ - params = {"mode": mode, "page": page} - if date: - params["date"] = date - hibiapi = Config.get_config("hibiapi", "HIBIAPI") - hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi - rank_url = f"{hibiapi}/api/pixiv/rank" - return await parser_data(rank_url, num, params, "rank") - - -async def search_pixiv_urls( - keyword: str, num: int, page: int, r18: int -) -> "list, list": - """ - 搜图图片的url - :param keyword: 关键词 - :param num: 数量 - :param page: 页数 - :param r18: 是否r18 - """ - params = {"word": keyword, "page": page} - hibiapi = Config.get_config("hibiapi", "HIBIAPI") - hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi - search_url = f"{hibiapi}/api/pixiv/search" - return await parser_data(search_url, num, params, "search", r18) - - -async def parser_data( - url: str, num: int, params: dict, type_: str, r18: int = 0 -) -> "list, int": - """ - 解析数据 - :param url: hibiapi搜索url - :param num: 数量 - :param params: 参数 - :param type_: 类型,rank或search - :param r18: 是否r18 - """ - info_list = [] - for _ in range(3): - try: - response = await AsyncHttpx.get( - url, - params=params, - timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), - ) - if response.status_code == 200: - data = response.json() - if data.get("illusts"): - data = data["illusts"] - break - except TimeoutError: - pass - except Exception as e: - logger.error(f"P站排行/搜图解析数据发生错误 {type(e)}:{e}") - return ["发生了一些些错误..."], 995 - else: - return ["网络不太好?没有该页数?也许过一会就好了..."], 998 - num = num if num < 30 else 30 - _data = [] - for x in data: - if x["page_count"] < Config.get_config("pixiv_rank_search", "MAX_PAGE_LIMIT"): - _data.append(x) - if len(_data) == num: - break - for x in _data: - if type_ == "search" and r18 == 1: - if "R-18" in str(x["tags"]): - continue - title = x["title"] - author = x["user"]["name"] - urls = [] - if x["page_count"] == 1: - urls.append(x["image_urls"]["large"]) - else: - for j in x["meta_pages"]: - urls.append(j["image_urls"]["large"]) - info_list.append((title, author, urls)) - return info_list, 200 - - -async def download_pixiv_imgs( - urls: list, user_id: int, forward_msg_index: int = None -) -> str: - """ - 下载图片 - :param urls: 图片链接 - :param user_id: 用户id - :param forward_msg_index: 转发消息中的图片排序 - """ - result = "" - index = 0 - for url in urls: - ws_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") - if ws_url: - url = ( - url.replace("i.pximg.net", ws_url) - .replace("i.pixiv.cat", ws_url) - .replace("_webp", "") - ) - try: - file = ( - TEMP_PATH / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg" - if forward_msg_index is not None - else TEMP_PATH / f"{user_id}_{index}_pixiv.jpg" - ) - file = Path(file) - try: - if await AsyncHttpx.download_file( - url, - file, - timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), - ): - change_img_md5(file) - if forward_msg_index is not None: - result += image( - TEMP_PATH - / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg", - ) - else: - result += image(TEMP_PATH / f"{user_id}_{index}_pixiv.jpg") - index += 1 - except OSError: - if file.exists(): - file.unlink() - except Exception as e: - logger.error(f"P站排行/搜图下载图片错误 {type(e)}:{e}") - return result diff --git a/plugins/quotations.py b/plugins/quotations.py deleted file mode 100755 index 85fd3d9c..00000000 --- a/plugins/quotations.py +++ /dev/null @@ -1,40 +0,0 @@ -from nonebot import on_regex -from services.log import logger -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent -from nonebot.typing import T_State -from utils.http_utils import AsyncHttpx - - -__zx_plugin_name__ = "一言二次元语录" -__plugin_usage__ = """ -usage: - 一言二次元语录 - 指令: - 语录/二次元 -""".strip() -__plugin_des__ = "二次元语录给你力量" -__plugin_cmd__ = ["语录/二次元"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["语录", "二次元"], -} - - -quotations = on_regex("^(语录|二次元)$", priority=5, block=True) - -url = "https://international.v1.hitokoto.cn/?c=a" - - -@quotations.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - data = (await AsyncHttpx.get(url, timeout=5)).json() - result = f'{data["hitokoto"]}\t——{data["from"]}' - await quotations.send(result) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 发送语录:" - + result - ) diff --git a/plugins/roll.py b/plugins/roll.py deleted file mode 100755 index 6b3be616..00000000 --- a/plugins/roll.py +++ /dev/null @@ -1,65 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from services.log import logger -from configs.config import NICKNAME -import random -import asyncio - - -__zx_plugin_name__ = "roll" -__plugin_usage__ = """ -usage: - 随机数字 或 随机选择事件 - 指令: - roll: 随机 0-100 的数字 - roll *[文本]: 随机事件 - 示例:roll 吃饭 睡觉 打游戏 -""".strip() -__plugin_des__ = "犹豫不决吗?那就让我帮你决定吧" -__plugin_cmd__ = ["roll", "roll *[文本]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["roll"], -} - - -roll = on_command("roll", priority=5, block=True) - - -@roll.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - if not msg: - await roll.finish(f"roll: {random.randint(0, 100)}", at_sender=True) - user_name = event.sender.card or event.sender.nickname - await roll.send( - random.choice( - [ - "转动命运的齿轮,拨开眼前迷雾...", - f"启动吧,命运的水晶球,为{user_name}指引方向!", - "嗯哼,在此刻转动吧!命运!", - f"在此祈愿,请为{user_name}降下指引...", - ] - ) - ) - await asyncio.sleep(1) - x = random.choice(msg) - await roll.send( - random.choice( - [ - f"让{NICKNAME}看看是什么结果!答案是:‘{x}’", - f"根据命运的指引,接下来{user_name} ‘{x}’ 会比较好", - f"祈愿被回应了!是 ‘{x}’!", - f"结束了,{user_name},命运之轮停在了 ‘{x}’!", - ] - ) - ) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 发送roll:{msg}" - ) diff --git a/plugins/russian/__init__.py b/plugins/russian/__init__.py deleted file mode 100755 index 5d3aa444..00000000 --- a/plugins/russian/__init__.py +++ /dev/null @@ -1,536 +0,0 @@ -import asyncio -import random -import time -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GROUP, Bot, GroupMessageEvent, Message -from nonebot.params import ArgStr, Command, CommandArg -from nonebot.typing import T_State - -from configs.config import NICKNAME, Config -from models.bag_user import BagUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.image_utils import text2image -from utils.message_builder import at, image -from utils.utils import get_message_at, is_number - -from .data_source import rank -from .model import RussianUser - -__zx_plugin_name__ = "俄罗斯轮盘" -__plugin_usage__ = """ -usage: - 又到了决斗时刻 - 指令: - 装弹 [子弹数] ?[金额=200] ?[at]: 开启游戏,装填子弹,可选自定义金额,或邀请决斗对象 - 接受对决: 接受当前存在的对决 - 拒绝对决: 拒绝邀请的对决 - 开枪: 开出未知的一枪 - 结算: 强行结束当前比赛 (仅当一方未开枪超过30秒时可使用) - 我的战绩: 对,你的战绩 - 胜场排行/败场排行/欧洲人排行/慈善家排行/最高连胜排行/最高连败排行: 各种排行榜 - 示例:装弹 3 100 @sdd - * 注:同一时间群内只能有一场对决 * -""".strip() -__plugin_des__ = "虽然是运气游戏,但这可是战场啊少年" -__plugin_cmd__ = [ - "装弹 [子弹数] ?[金额=200] ?[at]", - "接受对决", - "拒绝对决", - "开枪", - "结算", - "我的战绩", - "胜场排行/败场排行/欧洲人排行/慈善家排行/最高连胜排行/最高连败排行", -] -__plugin_type__ = ("群内小游戏", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["俄罗斯轮盘", "装弹"], -} -__plugin_configs__ = { - "MAX_RUSSIAN_BET_GOLD": { - "value": 1000, - "help": "俄罗斯轮盘最大赌注金额", - "default_value": 1000, - "type": int, - } -} - -rs_player = {} - -russian = on_command( - "俄罗斯轮盘", aliases={"装弹", "俄罗斯转盘"}, permission=GROUP, priority=5, block=True -) - -accept = on_command( - "接受对决", aliases={"接受决斗", "接受挑战"}, permission=GROUP, priority=5, block=True -) - -refuse = on_command( - "拒绝对决", aliases={"拒绝决斗", "拒绝挑战"}, permission=GROUP, priority=5, block=True -) - -shot = on_command( - "开枪", aliases={"咔", "嘭", "嘣"}, permission=GROUP, priority=5, block=True -) - -settlement = on_command("结算", permission=GROUP, priority=5, block=True) - -record = on_command("我的战绩", permission=GROUP, priority=5, block=True) - -russian_rank = on_command( - "胜场排行", - aliases={"胜利排行", "败场排行", "失败排行", "欧洲人排行", "慈善家排行", "最高连胜排行", "最高连败排行"}, - permission=GROUP, - priority=5, - block=True, -) - - -@accept.handle() -async def _(event: GroupMessageEvent): - global rs_player - try: - if rs_player[event.group_id][1] == 0: - await accept.finish("目前没有发起对决,你接受个啥?速速装弹!", at_sender=True) - except KeyError: - await accept.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) - if rs_player[event.group_id][2] != 0: - if ( - rs_player[event.group_id][1] == event.user_id - or rs_player[event.group_id][2] == event.user_id - ): - await accept.finish(f"你已经身处决斗之中了啊,给我认真一点啊!", at_sender=True) - else: - await accept.finish("已经有人接受对决了,你还是乖乖等待下一场吧!", at_sender=True) - if rs_player[event.group_id][1] == event.user_id: - await accept.finish("请不要自己枪毙自己!换人来接受对决...", at_sender=True) - if ( - rs_player[event.group_id]["at"] != 0 - and rs_player[event.group_id]["at"] != event.user_id - ): - await accept.finish( - Message(f'这场对决是邀请 {at(rs_player[event.group_id]["at"])}的,不要捣乱!'), - at_sender=True, - ) - if time.time() - rs_player[event.group_id]["time"] > 30: - rs_player[event.group_id] = {} - await accept.finish("这场对决邀请已经过时了,请重新发起决斗...", at_sender=True) - - user_money = await BagUser.get_gold(event.user_id, event.group_id) - if user_money < rs_player[event.group_id]["money"]: - if ( - rs_player[event.group_id]["at"] != 0 - and rs_player[event.group_id]["at"] == event.user_id - ): - rs_player[event.group_id] = {} - await accept.finish("你的金币不足以接受这场对决!对决还未开始便结束了,请重新装弹!", at_sender=True) - else: - await accept.finish("你的金币不足以接受这场对决!", at_sender=True) - - player2_name = event.sender.card or event.sender.nickname - - rs_player[event.group_id][2] = event.user_id - rs_player[event.group_id]["player2"] = player2_name - rs_player[event.group_id]["time"] = time.time() - - await accept.send( - Message(f"{player2_name}接受了对决!\n" f"请{at(rs_player[event.group_id][1])}先开枪!") - ) - - -@refuse.handle() -async def _(event: GroupMessageEvent): - global rs_player - try: - if rs_player[event.group_id][1] == 0: - await accept.finish("你要拒绝啥?明明都没有人发起对决的说!", at_sender=True) - except KeyError: - await refuse.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) - if ( - rs_player[event.group_id]["at"] != 0 - and event.user_id != rs_player[event.group_id]["at"] - ): - await accept.finish("又不是找你决斗,你拒绝什么啊!气!", at_sender=True) - if rs_player[event.group_id]["at"] == event.user_id: - at_player_name = ( - await GroupInfoUser.get_or_none( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - ).user_name - await accept.send( - Message(f"{at(rs_player[event.group_id][1])}\n" f"{at_player_name}拒绝了你的对决!") - ) - rs_player[event.group_id] = {} - - -@settlement.handle() -async def _(bot: Bot, event: GroupMessageEvent): - global rs_player - if ( - not rs_player.get(event.group_id) - or rs_player[event.group_id][1] == 0 - or rs_player[event.group_id][2] == 0 - ): - await settlement.finish("比赛并没有开始...无法结算...", at_sender=True) - if ( - event.user_id != rs_player[event.group_id][1] - and event.user_id != rs_player[event.group_id][2] - ): - await settlement.finish("吃瓜群众不要捣乱!黄牌警告!", at_sender=True) - if time.time() - rs_player[event.group_id]["time"] <= 30: - await settlement.finish( - f'{rs_player[event.group_id]["player1"]} 和' - f' {rs_player[event.group_id]["player2"]} 比赛并未超时,请继续比赛...' - ) - win_name = ( - rs_player[event.group_id]["player1"] - if rs_player[event.group_id][2] == rs_player[event.group_id]["next"] - else rs_player[event.group_id]["player2"] - ) - await settlement.send(f"这场对决是 {win_name} 胜利了") - await end_game(bot, event) - - -@russian.handle() -async def _( - bot: Bot, event: GroupMessageEvent, state: T_State, arg: Message = CommandArg() -): - global rs_player - msg = arg.extract_plain_text().strip() - try: - if ( - rs_player[event.group_id][1] - and not rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] <= 30 - ): - await russian.finish( - f'现在是 {rs_player[event.group_id]["player1"]} 发起的对决\n请等待比赛结束后再开始下一轮...' - ) - if ( - rs_player[event.group_id][1] - and rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] <= 30 - ): - await russian.finish( - f'{rs_player[event.group_id]["player1"]} 和' - f' {rs_player[event.group_id]["player2"]}的对决还未结束!' - ) - if ( - rs_player[event.group_id][1] - and rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] > 30 - ): - await russian.send("决斗已过时,强行结算...") - await end_game(bot, event) - if ( - not rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] > 30 - ): - rs_player[event.group_id][1] = 0 - rs_player[event.group_id][2] = 0 - rs_player[event.group_id]["at"] = 0 - except KeyError: - pass - if msg: - msg = msg.split() - if len(msg) == 1: - msg = msg[0] - if is_number(msg) and not (int(msg) < 1 or int(msg) > 6): - state["bullet_num"] = int(msg) - else: - money = msg[1].strip() - msg = msg[0].strip() - if is_number(msg) and not (int(msg) < 1 or int(msg) > 6): - state["bullet_num"] = int(msg) - if is_number(money) and 0 < int(money) <= Config.get_config( - "russian", "MAX_RUSSIAN_BET_GOLD" - ): - state["money"] = int(money) - else: - state["money"] = 200 - await russian.send( - f"赌注金额超过限制({Config.get_config('russian', 'MAX_RUSSIAN_BET_GOLD')}),已改为200(默认)" - ) - state["at"] = get_message_at(event.json()) - - -@russian.got("bullet_num", prompt="请输入装填子弹的数量!(最多6颗)") -async def _( - event: GroupMessageEvent, state: T_State, bullet_num: str = ArgStr("bullet_num") -): - global rs_player - if bullet_num in ["取消", "算了"]: - await russian.finish("已取消操作...") - try: - if rs_player[event.group_id][1] != 0: - await russian.finish("决斗已开始...", at_sender=True) - except KeyError: - pass - if not is_number(bullet_num): - await russian.reject_arg("bullet_num", "输入子弹数量必须是数字啊喂!") - bullet_num = int(bullet_num) - if bullet_num < 1 or bullet_num > 6: - await russian.reject_arg("bullet_num", "子弹数量必须大于0小于7!") - at_ = state["at"] if state.get("at") else [] - money = state["money"] if state.get("money") else 200 - user_money = await BagUser.get_gold(event.user_id, event.group_id) - if bullet_num < 0 or bullet_num > 6: - await russian.reject("子弹数量必须大于0小于7!速速重新装弹!") - if money > Config.get_config("russian", "MAX_RUSSIAN_BET_GOLD"): - await russian.finish( - f"太多了!单次金额不能超过{Config.get_config('russian', 'MAX_RUSSIAN_BET_GOLD')}!", - at_sender=True, - ) - if money > user_money: - await russian.finish("你没有足够的钱支撑起这场挑战", at_sender=True) - - player1_name = event.sender.card or event.sender.nickname - - if at_: - at_ = at_[0] - try: - at_player_name = ( - await GroupInfoUser.get_or_none(user_id=at_, group_id=event.group_id) - ).user_name - except AttributeError: - at_player_name = at(at_) - msg = f"{player1_name} 向 {at(at_)} 发起了决斗!请 {at_player_name} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!" - else: - at_ = 0 - msg = "若30秒内无人接受挑战则此次对决作废【首次游玩请发送 ’俄罗斯轮盘帮助‘ 来查看命令】" - - rs_player[event.group_id] = { - 1: event.user_id, - "player1": player1_name, - 2: 0, - "player2": "", - "at": at_, - "next": event.user_id, - "money": money, - "bullet": random_bullet(bullet_num), - "bullet_num": bullet_num, - "null_bullet_num": 7 - bullet_num, - "index": 0, - "time": time.time(), - } - - await russian.send( - Message( - ("咔 " * bullet_num)[:-1] + f",装填完毕\n挑战金额:{money}\n" - f"第一枪的概率为:{str(float(bullet_num) / 7.0 * 100)[:5]}%\n" - f"{msg}" - ) - ) - - -@shot.handle() -async def _(bot: Bot, event: GroupMessageEvent): - global rs_player - try: - if time.time() - rs_player[event.group_id]["time"] > 30: - if rs_player[event.group_id][2] == 0: - rs_player[event.group_id][1] = 0 - await shot.finish("这场对决已经过时了,请重新装弹吧!", at_sender=True) - else: - await shot.send("决斗已过时,强行结算...") - await end_game(bot, event) - return - except KeyError: - await shot.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) - if rs_player[event.group_id][1] == 0: - await shot.finish("没有对决,也还没装弹呢,请先输入 装弹 吧!", at_sender=True) - if ( - rs_player[event.group_id][1] == event.user_id - and rs_player[event.group_id][2] == 0 - ): - await shot.finish("baka,你是要枪毙自己嘛笨蛋!", at_sender=True) - if rs_player[event.group_id][2] == 0: - await shot.finish("请这位勇士先发送 接受对决 来站上擂台...", at_sender=True) - player1_name = rs_player[event.group_id]["player1"] - player2_name = rs_player[event.group_id]["player2"] - if rs_player[event.group_id]["next"] != event.user_id: - if ( - event.user_id != rs_player[event.group_id][1] - and event.user_id != rs_player[event.group_id][2] - ): - await shot.finish( - random.choice( - [ - f"不要打扰 {player1_name} 和 {player2_name} 的决斗啊!", - f"给我好好做好一个观众!不然{NICKNAME}就要生气了", - f"不要捣乱啊baka{(await GroupInfoUser.get_or_none(user_id=event.user_id, group_id=event.group_id)).user_name}!", - ] - ), - at_sender=True, - ) - await shot.finish( - f"你的左轮不是连发的!该 " - f'{(await GroupInfoUser.get_or_none(user_id=int(rs_player[event.group_id]["next"]), group_id=event.group_id)).user_name} 开枪了' - ) - if rs_player[event.group_id]["bullet"][rs_player[event.group_id]["index"]] != 1: - await shot.send( - Message( - random.choice( - [ - "呼呼,没有爆裂的声响,你活了下来", - "虽然黑洞洞的枪口很恐怖,但好在没有子弹射出来,你活下来了", - '"咔",你没死,看来运气不错', - ] - ) - + f"\n下一枪中弹的概率" - f':{str(float((rs_player[event.group_id]["bullet_num"])) / float(rs_player[event.group_id]["null_bullet_num"] - 1 + rs_player[event.group_id]["bullet_num"]) * 100)[:5]}%\n' - f"轮到 {at(rs_player[event.group_id][1] if event.user_id == rs_player[event.group_id][2] else rs_player[event.group_id][2])}了" - ) - ) - rs_player[event.group_id]["null_bullet_num"] -= 1 - rs_player[event.group_id]["next"] = ( - rs_player[event.group_id][1] - if event.user_id == rs_player[event.group_id][2] - else rs_player[event.group_id][2] - ) - rs_player[event.group_id]["time"] = time.time() - rs_player[event.group_id]["index"] += 1 - else: - await shot.send( - random.choice( - [ - '"嘭!",你直接去世了', - "眼前一黑,你直接穿越到了异世界...(死亡)", - "终究还是你先走一步...", - ] - ) - + f'\n第 {rs_player[event.group_id]["index"] + 1} 发子弹送走了你...', - at_sender=True, - ) - win_name = ( - player1_name - if event.user_id == rs_player[event.group_id][2] - else player2_name - ) - await asyncio.sleep(0.5) - await shot.send(f"这场对决是 {win_name} 胜利了") - await end_game(bot, event) - - -async def end_game(bot: Bot, event: GroupMessageEvent): - global rs_player - player1_name = rs_player[event.group_id]["player1"] - player2_name = rs_player[event.group_id]["player2"] - if rs_player[event.group_id]["next"] == rs_player[event.group_id][1]: - win_user_id = rs_player[event.group_id][2] - lose_user_id = rs_player[event.group_id][1] - win_name = player2_name - lose_name = player1_name - else: - win_user_id = rs_player[event.group_id][1] - lose_user_id = rs_player[event.group_id][2] - win_name = player1_name - lose_name = player2_name - rand = random.randint(0, 5) - money = rs_player[event.group_id]["money"] - if money > 10: - fee = int(money * float(rand) / 100) - fee = 1 if fee < 1 and rand != 0 else fee - else: - fee = 0 - await RussianUser.add_count(win_user_id, event.group_id, "win") - await RussianUser.add_count(lose_user_id, event.group_id, "lose") - await RussianUser.money(win_user_id, event.group_id, "win", money - fee) - await RussianUser.money(lose_user_id, event.group_id, "lose", money) - await BagUser.add_gold(win_user_id, event.group_id, money - fee) - await BagUser.spend_gold(lose_user_id, event.group_id, money) - win_user, _ = await RussianUser.get_or_create( - user_id=str(win_user_id), group_id=str(event.group_id) - ) - lose_user, _ = await RussianUser.get_or_create( - user_id=str(lose_user_id), group_id=str(event.group_id) - ) - bullet_str = "" - for x in rs_player[event.group_id]["bullet"]: - bullet_str += "__ " if x == 0 else "| " - logger.info(f"俄罗斯轮盘:胜者:{win_name} - 败者:{lose_name} - 金币:{money}") - rs_player[event.group_id] = {} - await bot.send( - event, - message=image( - await text2image( - f"结算:\n" - f"\t胜者:{win_name}\n" - f"\t赢取金币:{money - fee}\n" - f"\t累计胜场:{win_user.win_count}\n" - f"\t累计赚取金币:{win_user.make_money}\n" - f"-------------------\n" - f"\t败者:{lose_name}\n" - f"\t输掉金币:{money}\n" - f"\t累计败场:{lose_user.fail_count}\n" - f"\t累计输掉金币:{lose_user.lose_money}\n" - f"-------------------\n" - f"哼哼,{NICKNAME}从中收取了 {float(rand)}%({fee}金币) 作为手续费!\n" - f"子弹排列:{bullet_str[:-1]}", - padding=10, - color="#f9f6f2", - ) - ), - ) - - -@record.handle() -async def _(event: GroupMessageEvent): - user, _ = await RussianUser.get_or_create( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - await record.send( - f"俄罗斯轮盘\n" - f"总胜利场次:{user.win_count}\n" - f"当前连胜:{user.winning_streak}\n" - f"最高连胜:{user.max_winning_streak}\n" - f"总失败场次:{user.fail_count}\n" - f"当前连败:{user.losing_streak}\n" - f"最高连败:{user.max_losing_streak}\n" - f"赚取金币:{user.make_money}\n" - f"输掉金币:{user.lose_money}", - at_sender=True, - ) - - -@russian_rank.handle() -async def _( - event: GroupMessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - num = arg.extract_plain_text().strip() - if is_number(num) and 51 > int(num) > 10: - num = int(num) - else: - num = 10 - rank_image = None - if cmd[0] in ["胜场排行", "胜利排行"]: - rank_image = await rank(event.group_id, "win_rank", num) - if cmd[0] in ["败场排行", "失败排行"]: - rank_image = await rank(event.group_id, "lose_rank", num) - if cmd[0] == "欧洲人排行": - rank_image = await rank(event.group_id, "make_money", num) - if cmd[0] == "慈善家排行": - rank_image = await rank(event.group_id, "spend_money", num) - if cmd[0] == "最高连胜排行": - rank_image = await rank(event.group_id, "max_winning_streak", num) - if cmd[0] == "最高连败排行": - rank_image = await rank(event.group_id, "max_losing_streak", num) - if rank_image: - await russian_rank.send(image(b64=rank_image.pic2bs4())) - - -# 随机子弹排列 -def random_bullet(num: int) -> list: - bullet_lst = [0, 0, 0, 0, 0, 0, 0] - for i in random.sample([0, 1, 2, 3, 4, 5, 6], num): - bullet_lst[i] = 1 - return bullet_lst diff --git a/plugins/russian/data_source.py b/plugins/russian/data_source.py deleted file mode 100755 index 2f9546e0..00000000 --- a/plugins/russian/data_source.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Optional - -from utils.data_utils import init_rank -from utils.image_utils import BuildMat - -from .model import RussianUser - - -async def rank(group_id: int, itype: str, num: int) -> Optional[BuildMat]: - all_users = await RussianUser.filter(group_id=group_id).all() - all_user_id = [user.user_id for user in all_users] - if itype == "win_rank": - rank_name = "胜场排行榜" - all_user_data = [user.win_count for user in all_users] - elif itype == "lose_rank": - rank_name = "败场排行榜" - all_user_data = [user.fail_count for user in all_users] - elif itype == "make_money": - rank_name = "赢取金币排行榜" - all_user_data = [user.make_money for user in all_users] - elif itype == "spend_money": - rank_name = "输掉金币排行榜" - all_user_data = [user.lose_money for user in all_users] - elif itype == "max_winning_streak": - rank_name = "最高连胜排行榜" - all_user_data = [user.max_winning_streak for user in all_users] - else: - rank_name = "最高连败排行榜" - all_user_data = [user.max_losing_streak for user in all_users] - rst = None - if all_users: - rst = await init_rank(rank_name, all_user_id, all_user_data, group_id, num) - return rst diff --git a/plugins/search_anime/__init__.py b/plugins/search_anime/__init__.py deleted file mode 100755 index c57cd73d..00000000 --- a/plugins/search_anime/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import ArgStr, CommandArg -from nonebot.typing import T_State - -from configs.config import Config -from services.log import logger -from utils.message_builder import custom_forward_msg - -from .data_source import from_anime_get_info - -__zx_plugin_name__ = "搜番" -__plugin_usage__ = f""" -usage: - 搜索动漫资源 - 指令: - 搜番 [番剧名称或者关键词] - 示例:搜番 刀剑神域 -""".strip() -__plugin_des__ = "找不到想看的动漫吗?" -__plugin_cmd__ = ["搜番 [番剧名称或者关键词]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["搜番"], -} -__plugin_block_limit__ = {"rst": "搜索还未完成,不要重复触发!"} -__plugin_configs__ = { - "SEARCH_ANIME_MAX_INFO": { - "value": 20, - "help": "搜索动漫返回的最大数量", - "default_value": 20, - "type": int, - } -} - -search_anime = on_command("搜番", aliases={"搜动漫"}, priority=5, block=True) - - -@search_anime.handle() -async def _(state: T_State, arg: Message = CommandArg()): - if arg.extract_plain_text().strip(): - state["anime"] = arg.extract_plain_text().strip() - - -@search_anime.got("anime", prompt="是不是少了番名?") -async def _( - bot: Bot, event: MessageEvent, state: T_State, key_word: str = ArgStr("anime") -): - await search_anime.send(f"开始搜番 {key_word}", at_sender=True) - anime_report = await from_anime_get_info( - key_word, - Config.get_config("search_anime", "SEARCH_ANIME_MAX_INFO"), - ) - if anime_report: - if isinstance(anime_report, str): - await search_anime.finish(anime_report) - if isinstance(event, GroupMessageEvent): - mes_list = custom_forward_msg(anime_report, bot.self_id) - await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) - else: - await search_anime.send("\n\n".join(anime_report)) - logger.info( - f"USER {event.user_id} GROUP" - f" {event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 搜索番剧 {key_word} 成功" - ) - else: - logger.warning(f"未找到番剧 {key_word}") - await search_anime.send(f"未找到番剧 {key_word}(也有可能是超时,再尝试一下?)") diff --git a/plugins/search_buff_skin_price/__init__.py b/plugins/search_buff_skin_price/__init__.py deleted file mode 100755 index ad903b08..00000000 --- a/plugins/search_buff_skin_price/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -from nonebot import on_command -from .data_source import get_price, update_buff_cookie -from services.log import logger -from nonebot.typing import T_State -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.rule import to_me -from nonebot.permission import SUPERUSER -from nonebot.params import CommandArg, ArgStr -from configs.config import NICKNAME - - -__zx_plugin_name__ = "BUFF查询皮肤" -__plugin_usage__ = """ -usage: - 在线实时获取BUFF指定皮肤所有磨损底价 - 指令: - 查询皮肤 [枪械名] [皮肤名称] - 示例:查询皮肤 ak47 二西莫夫 -""".strip() -__plugin_des__ = "BUFF皮肤底价查询" -__plugin_cmd__ = ["查询皮肤 [枪械名] [皮肤名称]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查询皮肤"], -} -__plugin_block_limit__ = {"rst": "您有皮肤正在搜索,请稍等..."} -__plugin_configs__ = { - "BUFF_PROXY": {"value": None, "help": "BUFF代理,有些厂ip可能被屏蔽"}, - "COOKIE": {"value": None, "help": "BUFF的账号cookie"}, -} - - -search_skin = on_command("查询皮肤", aliases={"皮肤查询"}, priority=5, block=True) - - -@search_skin.handle() -async def _(event: MessageEvent, state: T_State, arg: Message = CommandArg()): - raw_arg = arg.extract_plain_text().strip() - if raw_arg: - args = raw_arg.split() - if len(args) >= 2: - state["name"] = args[0] - state["skin"] = args[1] - - -@search_skin.got("name", prompt="要查询什么武器呢?") -@search_skin.got("skin", prompt="要查询该武器的什么皮肤呢?") -async def arg_handle( - event: MessageEvent, - state: T_State, - name: str = ArgStr("name"), - skin: str = ArgStr("skin"), -): - if name in ["算了", "取消"] or skin in ["算了", "取消"]: - await search_skin.finish("已取消操作...") - result = "" - if name in ["ak", "ak47"]: - name = "ak-47" - name = name + " | " + skin - try: - result, status_code = await get_price(name) - except FileNotFoundError: - await search_skin.finish(f'请先对{NICKNAME}说"设置cookie"来设置cookie!') - if status_code in [996, 997, 998]: - await search_skin.finish(result) - if result: - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询皮肤:" - + name - ) - await search_skin.finish(result) - else: - logger.info( - f"USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}" - f" 查询皮肤:{name} 没有查询到" - ) - await search_skin.finish("没有查询到哦,请检查格式吧") - - -update_buff_session = on_command( - "更新cookie", aliases={"设置cookie"}, rule=to_me(), permission=SUPERUSER, priority=1 -) - - -@update_buff_session.handle() -async def _(event: MessageEvent): - await update_buff_session.finish( - update_buff_cookie(str(event.get_message())), at_sender=True - ) diff --git a/plugins/search_image/__init__.py b/plugins/search_image/__init__.py deleted file mode 100644 index d32ebf5c..00000000 --- a/plugins/search_image/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg, Depends -from nonebot.plugin import on_command -from nonebot.typing import T_State - -from services.log import logger -from utils.message_builder import custom_forward_msg -from utils.utils import get_message_img - -from .saucenao import get_saucenao_image - -__zx_plugin_name__ = "识图" -__plugin_usage__ = """ -usage: - 识别图片 [二次元图片] - 指令: - 识图 [图片] -""".strip() -__plugin_des__ = "以图搜图,看破本源" -__plugin_cmd__ = ["识图"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["识图"], -} -__plugin_configs__ = { - "MAX_FIND_IMAGE_COUNT": { - "value": 3, - "help": "识图返回的最大结果数", - "default_value": 3, - "type": int, - }, - "API_KEY": { - "value": None, - "help": "Saucenao的API_KEY,通过 https://saucenao.com/user.php?page=search-api 注册获取", - }, -} - - -search_image = on_command("识图", block=True, priority=5) - - -async def get_image_info(mod: str, url: str): - if mod == "saucenao": - return await get_saucenao_image(url) - - -def parse_image(key: str): - async def _key_parser(state: T_State, img: Message = Arg(key)): - if not get_message_img(img): - await search_image.reject_arg(key, "请发送要识别的图片!") - state[key] = img - - return _key_parser - - -@search_image.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - state["mod"] = msg - else: - state["mod"] = "saucenao" - if get_message_img(event.json()): - state["img"] = event.message - - -@search_image.got("img", prompt="图来!", parameterless=[Depends(parse_image("img"))]) -async def _( - bot: Bot, - event: MessageEvent, - state: T_State, - mod: str = ArgStr("mod"), - img: Message = Arg("img"), -): - img = get_message_img(img)[0] - await search_image.send("开始处理图片...") - msg = await get_image_info(mod, img) - if isinstance(msg, str): - await search_image.finish(msg, at_sender=True) - if isinstance(event, GroupMessageEvent): - await bot.send_group_forward_msg( - group_id=event.group_id, messages=custom_forward_msg(msg, bot.self_id) - ) - else: - for m in msg[1:]: - await search_image.send(m) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 识图:" + img - ) diff --git a/plugins/self_message/__init__.py b/plugins/self_message/__init__.py deleted file mode 100644 index 6d87f811..00000000 --- a/plugins/self_message/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -from datetime import datetime - -from nonebot import on -from nonebot.adapters.onebot.v11 import ( - Bot, - Event, - GroupMessageEvent, - PrivateMessageEvent, -) -from nonebot.message import handle_event - -from configs.config import Config - -from ._rule import rule - -__zx_plugin_name__ = "自身消息触发 [Hidden]" -__plugin_version__ = 0.1 - -Config.add_plugin_config( - "self_message", - "STATUS", - False, - help_="允许真寻自身触发命令,需要在go-cqhttp配置文件中report-self-message修改为true,触发命令时需前缀cmd且受权限影响,例如:cmd签到", - default_value=False, - type=bool, -) - -message_sent = on( - type="message_sent", - priority=999, - block=False, - rule=rule(), -) - - -@message_sent.handle() -async def handle_message_sent(bot: Bot, event: Event): - msg = str(getattr(event, "message", "")) - self_id = event.self_id - user_id = getattr(event, "user_id", -1) - msg_id = getattr(event, "message_id", -1) - msg_type = getattr(event, "message_type") - if ( - str(user_id) not in bot.config.superusers and self_id != user_id - ) or not msg.lower().startswith("cmd"): - return - msg = msg[3:] - if msg_type == "group": - new_event = GroupMessageEvent.parse_obj( - { - "time": getattr(event, "time", int(datetime.now().timestamp())), - "self_id": self_id, - "user_id": user_id, - "message": msg, - "raw_message": getattr(event, "raw_message", ""), - "post_type": "message", - "sub_type": getattr(event, "sub_type", "normal"), - "group_id": getattr(event, "group_id", -1), - "message_type": getattr(event, "message_type", "group"), - "message_id": msg_id, - "font": getattr(event, "font", 0), - "sender": getattr(event, "sender", {"user_id": user_id}), - "to_me": True, - } - ) - # await _check_reply(bot, new_event) - # _check_at_me(bot, new_event) - # _check_nickname(bot, new_event) - await handle_event(bot=bot, event=new_event) - elif msg_type == "private": - target_id = getattr(event, "target_id") - if target_id == user_id: - new_event = PrivateMessageEvent.parse_obj( - { - "time": getattr(event, "time", int(datetime.now().timestamp())), - "self_id": self_id, - "user_id": user_id, - "message": getattr(event, "message", ""), - "raw_message": getattr(event, "raw_message", ""), - "post_type": "message", - "sub_type": getattr(event, "sub_type"), - "message_type": msg_type, - "message_id": msg_id, - "font": getattr(event, "font", 0), - "sender": getattr(event, "sender"), - "to_me": True, - "target_id": target_id, - } - ) - await handle_event(bot=bot, event=new_event) diff --git a/plugins/self_message/_rule.py b/plugins/self_message/_rule.py deleted file mode 100644 index eb1f1cca..00000000 --- a/plugins/self_message/_rule.py +++ /dev/null @@ -1,9 +0,0 @@ -from configs.config import Config -from nonebot.internal.rule import Rule - - -def rule() -> Rule: - async def _rule() -> bool: - return Config.get_config("self_message", "STATUS") - - return Rule(_rule) diff --git a/plugins/send_dinggong_voice/__init__.py b/plugins/send_dinggong_voice/__init__.py deleted file mode 100755 index 5fe37c44..00000000 --- a/plugins/send_dinggong_voice/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import random - -from nonebot import on_keyword -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent -from nonebot.rule import to_me -from nonebot.typing import T_State - -from configs.path_config import RECORD_PATH -from services.log import logger -from utils.message_builder import record - -__zx_plugin_name__ = "骂我" -__plugin_usage__ = """ -usage: - 多骂我一点,球球了 - 指令: - 骂老子 -""".strip() -__plugin_des__ = "请狠狠的骂我一次!" -__plugin_cmd__ = ["骂老子/骂我"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["骂老子", "骂我"], -} -__plugin_cd_limit__ = {"cd": 3, "rst": "就...就算求我骂你也得慢慢来..."} - - -dg_voice = on_keyword({"骂"}, rule=to_me(), priority=5, block=True) - - -@dg_voice.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - if len(str((event.get_message()))) > 1: - voice = random.choice(os.listdir(RECORD_PATH / "dinggong")) - result = record( - RECORD_PATH / "dinggong" / voice, - ) - await dg_voice.send(result) - await dg_voice.send(voice.split("_")[1]) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 发送钉宫骂人:" - + result - ) diff --git a/plugins/send_setu_/__init__.py b/plugins/send_setu_/__init__.py deleted file mode 100755 index 5f878a25..00000000 --- a/plugins/send_setu_/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import nonebot - - -nonebot.load_plugins("plugins/send_setu_") diff --git a/plugins/send_setu_/send_setu/__init__.py b/plugins/send_setu_/send_setu/__init__.py deleted file mode 100755 index b08cf58c..00000000 --- a/plugins/send_setu_/send_setu/__init__.py +++ /dev/null @@ -1,426 +0,0 @@ -import random -from typing import Any, Optional, Tuple, Type - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - Event, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, -) -from nonebot.matcher import Matcher -from nonebot.message import run_postprocessor -from nonebot.params import CommandArg, RegexGroup -from nonebot.typing import T_State - -from configs.config import NICKNAME, Config -from models.sign_group_user import SignGroupUser -from services.log import logger -from utils.depends import OneCommand -from utils.manager import withdraw_message_manager -from utils.message_builder import custom_forward_msg -from utils.utils import is_number - -from .._model import Setu -from .data_source import ( - add_data_to_database, - check_local_exists_or_download, - gen_message, - get_luoxiang, - get_setu_list, - get_setu_urls, - search_online_setu, -) - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "色图" -__plugin_usage__ = f""" -usage: - 搜索 lolicon 图库,每日色图time... - 指令: - 色图: 随机本地色图 - 色图r: 随机在线十张r18涩图 - 色图 [id]: 本地指定id色图 - 色图 *[tags]: 在线搜索指定tag色图 - 色图r *[tags]: 同上 - [1-9]张涩图: 本地随机色图连发 - [1-9]张[tags]的涩图: 在线搜索指定tag色图连发 - [1-9]张涩图r[tags]: 同上 - 示例:色图 萝莉|少女 白丝|黑丝 - 示例:色图 萝莉 猫娘 - 注: - tag至多取前20项,| 为或,萝莉|少女=萝莉或者少女 -""".strip() -__plugin_des__ = "不要小看涩图啊混蛋!" -__plugin_cmd__ = [ - "色图 ?[id]", - "色图 ?[tags]", - "色图r ?[tags]", - "[1-9]张?[tags]色图", - "[1-9]张色图?[tags]", -] -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 9, - "default_status": True, - "limit_superuser": False, - "cmd": ["色图", "涩图", "瑟图"], -} -__plugin_block_limit__ = {} -__plugin_cd_limit__ = { - "rst": "您冲的太快了,请稍后再冲.", -} -__plugin_configs__ = { - "WITHDRAW_SETU_MESSAGE": { - "value": (0, 1), - "help": "自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", - "default_value": (0, 1), - "type": Tuple[int, int], - }, - "ONLY_USE_LOCAL_SETU": { - "value": False, - "help": "仅仅使用本地色图,不在线搜索", - "default_value": False, - "type": bool, - }, - "INITIAL_SETU_PROBABILITY": { - "value": 0.7, - "help": "初始色图概率,总概率 = 初始色图概率 + 好感度", - "default_value": 0.7, - "type": float, - }, - "DOWNLOAD_SETU": { - "value": True, - "help": "是否存储下载的色图,使用本地色图可以加快图片发送速度", - "default_value": True, - "type": bool, - }, - "TIMEOUT": {"value": 10, "help": "色图下载超时限制(秒)", "default_value": 10, "type": int}, - "SHOW_INFO": { - "value": True, - "help": "是否显示色图的基本信息,如PID等", - "default_value": True, - "type": bool, - }, - "ALLOW_GROUP_R18": { - "value": False, - "help": "在群聊中启用R18权限", - "default_value": False, - "type": bool, - }, - "MAX_ONCE_NUM2FORWARD": { - "value": None, - "help": "单次发送的图片数量达到指定值时转发为合并消息", - "default_value": None, - "type": int, - }, - "MAX_ONCE_NUM": { - "value": 10, - "help": "单次发送图片数量限制", - "default_value": 10, - "type": int, - }, -} -Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pixiv.re", help_="Pixiv反向代理") - -setu_data_list = [] - - -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: Event, - state: T_State, -): - global setu_data_list - if isinstance(event, MessageEvent): - if matcher.plugin_name == "send_setu": - # 添加数据至数据库 - try: - await add_data_to_database(setu_data_list) - logger.info("色图数据自动存储数据库成功...") - setu_data_list = [] - except Exception: - pass - - -setu = on_command( - "色图", aliases={"涩图", "不够色", "来一发", "再来点", "色图r"}, priority=5, block=True -) - -setu_reg = on_regex("(.*)[份|发|张|个|次|点](.*)[瑟|色|涩]图(r?)(.*)$", priority=5, block=True) - - -@setu.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), -): - msg = arg.extract_plain_text().strip() - if isinstance(event, GroupMessageEvent): - user, _ = await SignGroupUser.get_or_create( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - impression = user.impression - if luox := get_luoxiang(impression): - await setu.finish(luox) - r18 = False - num = 1 - # 是否看r18 - if cmd == "色图r" and isinstance(event, PrivateMessageEvent): - r18 = True - num = 10 - elif cmd == "色图r" and isinstance(event, GroupMessageEvent): - if not Config.get_config("send_setu", "ALLOW_GROUP_R18"): - await setu.finish( - random.choice(["这种不好意思的东西怎么可能给这么多人看啦", "羞羞脸!给我滚出克私聊!", "变态变态变态变态大变态!"]) - ) - else: - r18 = False - # 有 数字 的话先尝试本地色图id - if msg and is_number(msg): - setu_list, code = await get_setu_list(int(msg), r18=r18) - if code != 200: - await setu.finish(setu_list[0], at_sender=True) - setu_img, code = await check_local_exists_or_download(setu_list[0]) - msg_id = await setu.send(gen_message(setu_list[0]) + setu_img, at_sender=True) - logger.info( - f"发送色图 {setu_list[0].local_id}.jpg", - cmd, - event.user_id, - getattr(event, "group_id", None), - ) - if msg_id: - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - return - await send_setu_handle(bot, setu, event, cmd, msg, num, r18) - - -num_key = { - "一": 1, - "二": 2, - "两": 2, - "双": 2, - "三": 3, - "四": 4, - "五": 5, - "六": 6, - "七": 7, - "八": 8, - "九": 9, -} - - -@setu_reg.handle() -async def _(bot: Bot, event: MessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - if isinstance(event, GroupMessageEvent): - user, _ = await SignGroupUser.get_or_create( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - impression = user.impression - if luox := get_luoxiang(impression): - await setu.finish(luox, at_sender=True) - num, tags, r18, tags2 = reg_group - num = num or "一" - tags = tags[:-1] if tags and tags[-1] == "的" else tags - if num_key.get(num): - num = num_key[num] - try: - num = int(num) - except ValueError: - num = 1 - if ( - r18 - and not Config.get_config("send_setu", "ALLOW_GROUP_R18") - and isinstance(event, GroupMessageEvent) - ): - await setu.finish( - random.choice(["这种不好意思的东西怎么可能给这么多人看啦", "羞羞脸!给我滚出克私聊!", "变态变态变态变态大变态!"]) - ) - else: - limit = Config.get_config("send_setu", "MAX_ONCE_NUM") - if limit and num > limit: - num = limit - await setu.send(f"一次只能给你看 {num} 张哦") - await send_setu_handle( - bot, setu_reg, event, "色图r" if r18 else "色图", tags + " " + tags2, num, r18 - ) - - -async def send_setu_handle( - bot: Bot, - matcher: Type[Matcher], - event: MessageEvent, - command: str, - msg: str, - num: int, - r18: bool, -): - global setu_data_list - # 非 id,在线搜索 - tags = msg.split() - # 真寻的色图?怎么可能 - if f"{NICKNAME}" in tags: - await matcher.finish("咳咳咳,虽然我很可爱,但是我木有自己的色图~~~有的话记得发我一份呀") - # 本地先拿图,下载失败补上去 - setu_list, code = None, 200 - setu_count = await Setu.filter(is_r18=r18).count() - max_once_num2forward = Config.get_config("send_setu", "MAX_ONCE_NUM2FORWARD") - if ( - not Config.get_config("send_setu", "ONLY_USE_LOCAL_SETU") and tags - ) or setu_count <= 0: - # 先尝试获取在线图片 - urls, text_list, add_databases_list, code = await get_setu_urls( - tags, num, r18, command - ) - for x in add_databases_list: - setu_data_list.append(x) - # 未找到符合的色图,想来本地应该也没有 - if code == 401: - await setu.finish(urls[0], at_sender=True) - if code == 200: - forward_list = [] - for i in range(len(urls)): - try: - msg_id = None - setu_img, index = await search_online_setu(urls[i]) - # 下载成功的话 - if index != -1: - logger.info( - f"发送色图 {index}.png", - "command", - event.user_id, - getattr(event, "group_id", None), - ) - if ( - max_once_num2forward - and num >= max_once_num2forward - and isinstance(event, GroupMessageEvent) - ): - forward_list.append(Message(f"{text_list[i]}\n{setu_img}")) - else: - msg_id = await matcher.send( - Message(f"{text_list[i]}\n{setu_img}") - ) - else: - if setu_list is None: - setu_list, code = await get_setu_list(tags=tags, r18=r18) - if code != 200: - await setu.finish(setu_list[0], at_sender=True) - if setu_list: - setu_image = random.choice(setu_list) - setu_list.remove(setu_image) - if ( - max_once_num2forward - and num >= max_once_num2forward - and isinstance(event, GroupMessageEvent) - ): - forward_list.append( - gen_message(setu_image) - + ( - await check_local_exists_or_download(setu_image) - )[0] - ) - else: - msg_id = await matcher.send( - gen_message(setu_image) - + ( - await check_local_exists_or_download(setu_image) - )[0] - ) - logger.info( - f"发送本地色图 {setu_image.local_id}.png", - "command", - event.user_id, - getattr(event, "group_id", None), - ) - else: - msg_id = await matcher.send(text_list[i] + "\n" + setu_img) - if msg_id: - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - except ActionFailed: - await matcher.finish("坏了,这张图色过头了,我自己看看就行了!", at_sender=True) - if forward_list and isinstance(event, GroupMessageEvent): - msg_id = await bot.send_group_forward_msg( - group_id=event.group_id, - messages=custom_forward_msg(forward_list, bot.self_id), - ) - withdraw_message_manager.withdraw_message( - event, - msg_id, - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - return - if code != 200: - await matcher.finish("网络连接失败...", at_sender=True) - # 本地无图 - if setu_list is None: - setu_list, code = await get_setu_list(tags=tags, r18=r18) - if code != 200: - await matcher.finish(setu_list[0], at_sender=True) - # 开始发图 - forward_list = [] - for _ in range(num): - if not setu_list: - await setu.finish("坏了,已经没图了,被榨干了!") - setu_image = random.choice(setu_list) - setu_list.remove(setu_image) - if ( - max_once_num2forward - and num >= max_once_num2forward - and isinstance(event, GroupMessageEvent) - ): - forward_list.append( - Message( - gen_message(setu_image) - + (await check_local_exists_or_download(setu_image))[0] - ) - ) - else: - try: - msg_id = await matcher.send( - gen_message(setu_image) - + (await check_local_exists_or_download(setu_image))[0] - ) - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - logger.info( - f"发送本地色图 {setu_image.local_id}.png", - "command", - event.user_id, - getattr(event, "group_id", None), - ) - except ActionFailed: - await matcher.finish("坏了,这张图色过头了,我自己看看就行了!", at_sender=True) - if forward_list and isinstance(event, GroupMessageEvent): - msg_id = await bot.send_group_forward_msg( - group_id=event.group_id, - messages=custom_forward_msg(forward_list, bot.self_id), - ) - withdraw_message_manager.withdraw_message( - event, msg_id, Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE") - ) diff --git a/plugins/send_setu_/send_setu/data_source.py b/plugins/send_setu_/send_setu/data_source.py deleted file mode 100755 index 70cea1cd..00000000 --- a/plugins/send_setu_/send_setu/data_source.py +++ /dev/null @@ -1,267 +0,0 @@ -import asyncio -import os -import random -import re -from asyncio.exceptions import TimeoutError -from typing import Any, List, Optional, Tuple, Union - -from asyncpg.exceptions import UniqueViolationError -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import compressed_image, get_img_hash -from utils.message_builder import image -from utils.utils import change_img_md5, change_pixiv_image_links - -from .._model import Setu - -url = "https://api.lolicon.app/setu/v2" -path = "_setu" -r18_path = "_r18" -host_pattern = re.compile(r"https?://([^/]+)") - - -# 获取url -async def get_setu_urls( - tags: List[str], num: int = 1, r18: bool = False, command: str = "" -) -> Tuple[List[str], List[str], List[tuple], int]: - tags = tags[:3] if len(tags) > 3 else tags - params = { - "r18": 1 if r18 else 0, # 添加r18参数 0为否,1为是,2为混合 - "tag": tags, # 若指定tag - "num": 20, # 一次返回的结果数量 - "size": ["original"], - } - for count in range(3): - logger.debug(f"尝试获取图片URL第 {count+1} 次", "色图") - try: - response = await AsyncHttpx.get( - url, timeout=Config.get_config("send_setu", "TIMEOUT"), params=params - ) - if response.status_code == 200: - data = response.json() - if not data["error"]: - data = data["data"] - ( - urls, - text_list, - add_databases_list, - ) = await asyncio.get_event_loop().run_in_executor( - None, _setu_data_process, data, command - ) - num = num if num < len(data) else len(data) - random_idx = random.sample(range(len(data)), num) - x_urls = [] - x_text_lst = [] - for x in random_idx: - x_urls.append(urls[x]) - x_text_lst.append(text_list[x]) - if not x_urls: - return ["没找到符合条件的色图..."], [], [], 401 - return x_urls, x_text_lst, add_databases_list, 200 - else: - return ["没找到符合条件的色图..."], [], [], 401 - except TimeoutError as e: - logger.error(f"获取图片URL超时", "色图", e=e) - except Exception as e: - logger.error(f"访问页面错误", "色图", e=e) - return ["我网线被人拔了..QAQ"], [], [], 999 - - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -async def search_online_setu( - url_: str, id_: Optional[int] = None, path_: Optional[str] = None -) -> Tuple[Union[MessageSegment, str], int]: - """ - 下载色图 - :param url_: 色图url - :param id_: 本地id - :param path_: 存储路径 - """ - url_ = change_pixiv_image_links(url_) - index = random.randint(1, 100000) if id_ is None else id_ - base_path = IMAGE_PATH / path_ if path_ else TEMP_PATH - file_name = f"{index}_temp_setu.jpg" if path_ == TEMP_PATH else f"{index}.jpg" - file = base_path / file_name - base_path.mkdir(parents=True, exist_ok=True) - for i in range(3): - logger.debug(f"尝试在线搜索第 {i+1} 次", "色图") - try: - if not await AsyncHttpx.download_file( - url_, - file, - timeout=Config.get_config("send_setu", "TIMEOUT"), - ): - continue - if id_ is not None: - if os.path.getsize(base_path / f"{index}.jpg") > 1024 * 1024 * 1.5: - compressed_image( - base_path / f"{index}.jpg", - ) - logger.info(f"下载 lolicon 图片 {url_} 成功, id:{index}") - change_img_md5(file) - return image(file), index - except TimeoutError as e: - logger.error(f"下载图片超时", "色图", e=e) - except Exception as e: - logger.error(f"下载图片错误", "色图", e=e) - return "图片被小怪兽恰掉啦..!QAQ", -1 - - -# 检测本地是否有id涩图,无的话则下载 -async def check_local_exists_or_download( - setu_image: Setu, -) -> Tuple[Union[MessageSegment, str], int]: - path_ = None - id_ = None - if Config.get_config("send_setu", "DOWNLOAD_SETU"): - id_ = setu_image.local_id - path_ = r18_path if setu_image.is_r18 else path - file = IMAGE_PATH / path_ / f"{setu_image.local_id}.jpg" - if file.exists(): - change_img_md5(file) - return image(file), 200 - return await search_online_setu(setu_image.img_url, id_, path_) - - -# 添加涩图数据到数据库 -async def add_data_to_database(lst: List[tuple]): - tmp = [] - for x in lst: - if x not in tmp: - tmp.append(x) - if tmp: - for x in tmp: - try: - idx = await Setu.filter(is_r18="R-18" in x[5]).count() - if not await Setu.exists(pid=x[2], img_url=x[4]): - await Setu.create( - local_id=idx, - title=x[0], - author=x[1], - pid=x[2], - img_hash=x[3], - img_url=x[4], - tags=x[5], - is_r18="R-18" in x[5], - ) - except UniqueViolationError: - pass - - -# 拿到本地色图列表 -async def get_setu_list( - index: Optional[int] = None, tags: Optional[List[str]] = None, r18: bool = False -) -> Tuple[list, int]: - if index: - image_count = await Setu.filter(is_r18=r18).count() - 1 - if index < 0 or index > image_count: - return [f"超过当前上下限!({image_count})"], 999 - image_list = [await Setu.query_image(index, r18=r18)] - elif tags: - image_list = await Setu.query_image(tags=tags, r18=r18) - else: - image_list = await Setu.query_image(r18=r18) - if not image_list: - return ["没找到符合条件的色图..."], 998 - return image_list, 200 # type: ignore - - -# 初始化消息 -def gen_message(setu_image: Setu) -> str: - """判断是否获取图片信息 - - Args: - setu_image (Setu): Setu - - Returns: - str: 图片信息 - """ - local_id = setu_image.local_id - title = setu_image.title - author = setu_image.author - pid = setu_image.pid - if Config.get_config("send_setu", "SHOW_INFO"): - return f"id:{local_id}\n" f"title:{title}\n" f"author:{author}\n" f"PID:{pid}\n" - return "" - - -# 罗翔老师! -def get_luoxiang(impression): - initial_setu_probability = Config.get_config( - "send_setu", "INITIAL_SETU_PROBABILITY" - ) - if initial_setu_probability: - probability = float(impression) + initial_setu_probability * 100 - if probability < random.randint(1, 101): - return ( - "我为什么要给你发这个?" - + image( - IMAGE_PATH - / "luoxiang" - / random.choice(os.listdir(IMAGE_PATH / "luoxiang")) - ) - + f"\n(快向{NICKNAME}签到提升好感度吧!)" - ) - return None - - -async def find_img_index(img_url, user_id): - if not await AsyncHttpx.download_file( - img_url, - TEMP_PATH / f"{user_id}_find_setu_index.jpg", - timeout=Config.get_config("send_setu", "TIMEOUT"), - ): - return "检索图片下载上失败..." - img_hash = str(get_img_hash(TEMP_PATH / f"{user_id}_find_setu_index.jpg")) - if setu_img := await Setu.get_or_none(img_hash=img_hash): - return ( - f"id:{setu_img.local_id}\n" - f"title:{setu_img.title}\n" - f"author:{setu_img.author}\n" - f"PID:{setu_img.pid}" - ) - return "该图不在色图库中或色图库未更新!" - - -# 处理色图数据 -def _setu_data_process( - data: dict, command: str -) -> Tuple[List[str], List[str], List[Tuple[Any, ...]]]: - urls = [] - text_list = [] - add_databases_list = [] - for i in range(len(data)): - img_url = data[i]["urls"]["original"] - img_url = change_pixiv_image_links(img_url) - title = data[i]["title"] - author = data[i]["author"] - pid = data[i]["pid"] - urls.append(img_url) - text_list.append(f"title:{title}\nauthor:{author}\nPID:{pid}") - tags = [] - for j in range(len(data[i]["tags"])): - tags.append(data[i]["tags"][j]) - if command != "色图r": - if "R-18" in tags: - tags.remove("R-18") - add_databases_list.append( - ( - title, - author, - pid, - "", - img_url, - ",".join(tags), - ) - ) - return urls, text_list, add_databases_list diff --git a/plugins/send_setu_/update_setu/__init__.py b/plugins/send_setu_/update_setu/__init__.py deleted file mode 100755 index 5c955a27..00000000 --- a/plugins/send_setu_/update_setu/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -from utils.utils import scheduler -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me -from .data_source import update_setu_img -from configs.config import Config - - -__zx_plugin_name__ = "更新色图 [Superuser]" -__plugin_usage__ = """ -usage: - 更新数据库内存在的色图 - 指令: - 更新色图 -""".strip() -__plugin_cmd__ = ["更新色图"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_block_limit__ = { - "rst": "色图正在更新..." -} - - -update_setu = on_command( - "更新色图", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -@update_setu.handle() -async def _(): - if Config.get_config("send_setu", "DOWNLOAD_SETU"): - await update_setu.send("开始更新色图...", at_sender=True) - await update_setu_img(True) - else: - await update_setu.finish("更新色图配置未开启") - - -# 更新色图 -@scheduler.scheduled_job( - "cron", - hour=4, - minute=30, -) -async def _(): - if Config.get_config("send_setu", "DOWNLOAD_SETU"): - await update_setu_img() diff --git a/plugins/sign_in/__init__.py b/plugins/sign_in/__init__.py deleted file mode 100755 index 12299138..00000000 --- a/plugins/sign_in/__init__.py +++ /dev/null @@ -1,174 +0,0 @@ -from pathlib import Path -from typing import Any, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg, RegexGroup - -from configs.config import Config -from configs.path_config import DATA_PATH -from services.log import logger -from utils.message_builder import image -from utils.utils import is_number, scheduler - -from .goods_register import driver -from .group_user_checkin import ( - check_in_all, - group_impression_rank, - group_user_check, - group_user_check_in, - impression_rank, -) -from .utils import clear_sign_data_pic - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "签到" -__plugin_usage__ = """ -usage: - 每日签到 - 会影响色图概率和开箱次数,以及签到的随机道具获取 - 指令: - 签到 ?[all]: all代表签到所有群 - 我的签到 - 好感度排行 - 好感度总排行 - * 签到时有 3% 概率 * 2 * -""".strip() -__plugin_des__ = "每日签到,证明你在这里" -__plugin_cmd__ = ["签到 ?[all]", "我的签到", "好感度排行", "好感度总排行"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["签到"], -} -__plugin_cd_limit__ = {} -__plugin_configs__ = { - "MAX_SIGN_GOLD": { - "value": 200, - "help": "签到好感度加成额外获得的最大金币数", - "default_value": 200, - "type": int, - }, - "SIGN_CARD1_PROB": { - "value": 0.2, - "help": "签到好感度双倍加持卡Ⅰ掉落概率", - "default_value": 0.2, - "type": float, - }, - "SIGN_CARD2_PROB": { - "value": 0.09, - "help": "签到好感度双倍加持卡Ⅱ掉落概率", - "default_value": 0.09, - "type": float, - }, - "SIGN_CARD3_PROB": { - "value": 0.05, - "help": "签到好感度双倍加持卡Ⅲ掉落概率", - "default_value": 0.05, - "type": float, - }, -} - -Config.add_plugin_config( - "send_setu", - "INITIAL_SETU_PROBABILITY", - 0.7, - help_="初始色图概率,总概率 = 初始色图概率 + 好感度", - default_value=0.7, - type=float, -) - - -_file = DATA_PATH / "not_show_sign_rank_user.json" -try: - data = json.load(open(_file, "r", encoding="utf8")) -except (FileNotFoundError, ValueError, TypeError): - data = {"0": []} - - -sign = on_regex("^签到(all)?$", priority=5, permission=GROUP, block=True) -my_sign = on_command( - cmd="我的签到", aliases={"好感度"}, priority=5, permission=GROUP, block=True -) -sign_rank = on_command( - cmd="积分排行", - aliases={"好感度排行", "签到排行", "积分排行", "好感排行", "好感度排名,签到排名,积分排名"}, - priority=5, - permission=GROUP, - block=True, -) -total_sign_rank = on_command( - "签到总排行", aliases={"好感度总排行", "好感度总榜", "签到总榜"}, priority=5, block=True -) - - -@sign.handle() -async def _(event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - nickname = event.sender.card or event.sender.nickname - await sign.send( - await group_user_check_in(nickname, event.user_id, event.group_id), - at_sender=True, - ) - if reg_group[0]: - await check_in_all(nickname, event.user_id) - - -@my_sign.handle() -async def _(event: GroupMessageEvent): - nickname = event.sender.card or event.sender.nickname - await my_sign.send( - await group_user_check(nickname, event.user_id, event.group_id), - at_sender=True, - ) - - -@sign_rank.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - num = arg.extract_plain_text().strip() - if is_number(num) and 51 > int(num) > 10: - num = int(num) - else: - num = 10 - _image = await group_impression_rank(event.group_id, num) - if _image: - await sign_rank.send(image(b64=_image.pic2bs4())) - - -@total_sign_rank.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - await total_sign_rank.send("请稍等..正在整理数据...") - await total_sign_rank.send(image(b64=await impression_rank(0, data))) - elif msg in ["屏蔽我"]: - if event.user_id in data["0"]: - await total_sign_rank.finish("您已经在屏蔽名单中了,请勿重复添加!", at_sender=True) - data["0"].append(event.user_id) - await total_sign_rank.send("设置成功,您不会出现在签到总榜中!", at_sender=True) - elif msg in ["显示我"]: - if event.user_id not in data["0"]: - await total_sign_rank.finish("您不在屏蔽名单中!", at_sender=True) - data["0"].remove(event.user_id) - await total_sign_rank.send("设置成功,签到总榜将会显示您的头像名称以及好感度!", at_sender=True) - with open(_file, "w", encoding="utf8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - -@scheduler.scheduled_job( - "interval", - hours=1, -) -async def _(): - try: - clear_sign_data_pic() - logger.info("清理日常签到图片数据数据完成....") - except Exception as e: - logger.error(f"清理日常签到图片数据数据失败..{type(e)}: {e}") diff --git a/plugins/sign_in/config.py b/plugins/sign_in/config.py deleted file mode 100755 index 025de8dd..00000000 --- a/plugins/sign_in/config.py +++ /dev/null @@ -1,64 +0,0 @@ -from configs.path_config import IMAGE_PATH - - -SIGN_RESOURCE_PATH = IMAGE_PATH / 'sign' / 'sign_res' -SIGN_TODAY_CARD_PATH = IMAGE_PATH / 'sign' / 'today_card' -SIGN_BORDER_PATH = SIGN_RESOURCE_PATH / 'border' -SIGN_BACKGROUND_PATH = SIGN_RESOURCE_PATH / 'background' - -SIGN_BORDER_PATH.mkdir(exist_ok=True, parents=True) -SIGN_BACKGROUND_PATH.mkdir(exist_ok=True, parents=True) - - -lik2relation = { - '0': '路人', - '1': '陌生', - '2': '初识', - '3': '普通', - '4': '熟悉', - '5': '信赖', - '6': '相知', - '7': '厚谊', - '8': '亲密' -} - -level2attitude = { - '0': '排斥', - '1': '警惕', - '2': '可以交流', - '3': '一般', - '4': '是个好人', - '5': '好朋友', - '6': '可以分享小秘密', - '7': '喜欢', - '8': '恋人' -} - -weekdays = { - 1: 'Mon', - 2: 'Tue', - 3: 'Wed', - 4: 'Thu', - 5: 'Fri', - 6: 'Sat', - 7: 'Sun' -} - -lik2level = { - 9999: '9', - 400: '8', - 270: '7', - 200: '6', - 140: '5', - 90: '4', - 50: '3', - 25: '2', - 10: '1', - 0: '0' -} - - - - - - diff --git a/plugins/sign_in/goods_register.py b/plugins/sign_in/goods_register.py deleted file mode 100644 index e8cf9e5a..00000000 --- a/plugins/sign_in/goods_register.py +++ /dev/null @@ -1,59 +0,0 @@ -import nonebot -from nonebot import Driver - -from configs.config import Config -from models.sign_group_user import SignGroupUser -from utils.decorator.shop import NotMeetUseConditionsException, shop_register - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - """ - 导入内置的三个商品 - """ - - @shop_register( - name=("好感度双倍加持卡Ⅰ", "好感度双倍加持卡Ⅱ", "好感度双倍加持卡Ⅲ"), - price=(30, 150, 250), - des=( - "下次签到双倍好感度概率 + 10%(谁才是真命天子?)(同类商品将覆盖)", - "下次签到双倍好感度概率 + 20%(平平庸庸)(同类商品将覆盖)", - "下次签到双倍好感度概率 + 30%(金币才是真命天子!)(同类商品将覆盖)", - ), - load_status=bool(Config.get_config("shop", "IMPORT_DEFAULT_SHOP_GOODS")), - icon=( - "favorability_card_1.png", - "favorability_card_2.png", - "favorability_card_3.png", - ), - **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, - ) - async def sign_card(user_id: int, group_id: int, prob: float): - user, _ = await SignGroupUser.get_or_create(user_id=str(user_id), group_id=str(group_id)) - user.add_probability = prob - await user.save(update_fields=["add_probability"]) - - @shop_register( - name="测试道具A", - price=99, - des="随便侧而出", - load_status=False, - icon="sword.png", - ) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "使用测试道具") - - @shop_register.before_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第一个使用前函数(before handle)") - - @shop_register.before_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第二个使用前函数(before handle)222") - raise NotMeetUseConditionsException("太笨了!") # 抛出异常,阻断使用,并返回信息 - - @shop_register.after_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第一个使用后函数(after handle)") diff --git a/plugins/sign_in/group_user_checkin.py b/plugins/sign_in/group_user_checkin.py deleted file mode 100755 index ce5f9721..00000000 --- a/plugins/sign_in/group_user_checkin.py +++ /dev/null @@ -1,205 +0,0 @@ -import asyncio -import math -import os -import random -import secrets -from datetime import datetime, timedelta -from io import BytesIO -from typing import Optional - -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import NICKNAME -from models.bag_user import BagUser -from models.group_member_info import GroupInfoUser -from models.sign_group_user import SignGroupUser -from services.log import logger -from utils.data_utils import init_rank -from utils.image_utils import BuildImage, BuildMat -from utils.utils import get_user_avatar - -from .random_event import random_event -from .utils import SIGN_TODAY_CARD_PATH, get_card - - -async def group_user_check_in( - nickname: str, user_id: int, group: int -) -> MessageSegment: - "Returns string describing the result of checking in" - present = datetime.now() - # 取得相应用户 - user, is_create = await SignGroupUser.get_or_create( - user_id=str(user_id), group_id=str(group) - ) - # 如果同一天签到过,特殊处理 - if not is_create and ( - user.checkin_time_last.date() >= present.date() - or f"{user}_{group}_sign_{datetime.now().date()}" - in os.listdir(SIGN_TODAY_CARD_PATH) - ): - gold = await BagUser.get_gold(user_id, group) - return await get_card(user, nickname, -1, gold, "") - return await _handle_check_in(nickname, user_id, group, present) # ok - - -async def check_in_all(nickname: str, user_id: str): - """ - 说明: - 签到所有群 - 参数: - :param nickname: 昵称 - :param user_id: 用户id - """ - present = datetime.now() - for u in await SignGroupUser.filter(user_id=user_id).all(): - group = u.group_id - if not ( - u.checkin_time_last.date() >= present.date() - or f"{u}_{group}_sign_{datetime.now().date()}" - in os.listdir(SIGN_TODAY_CARD_PATH) - ): - await _handle_check_in(nickname, user_id, group, present) - - -async def _handle_check_in( - nickname: str, user_id: str, group: str, present: datetime -) -> MessageSegment: - user, _ = await SignGroupUser.get_or_create(user_id=user_id, group_id=group) - impression_added = (secrets.randbelow(99) + 1) / 100 - critx2 = random.random() - add_probability = float(user.add_probability) - specify_probability = user.specify_probability - if critx2 + add_probability > 0.97: - impression_added *= 2 - elif critx2 < specify_probability: - impression_added *= 2 - await SignGroupUser.sign(user, impression_added) - gold = random.randint(1, 100) - gift, gift_type = random_event(float(user.impression)) - if gift_type == "gold": - await BagUser.add_gold(user_id, group, gold + gift) - gift = f"额外金币 + {gift}" - else: - await BagUser.add_gold(user_id, group, gold) - await BagUser.add_property(user_id, group, gift) - gift += " + 1" - - logger.info( - f"(USER {user.user_id}, GROUP {user.group_id})" - f" CHECKED IN successfully. score: {user.impression:.2f} " - f"(+{impression_added:.2f}).获取金币:{gold + gift if gift == 'gold' else gold}" - ) - if critx2 + add_probability > 0.97 or critx2 < specify_probability: - return await get_card(user, nickname, impression_added, gold, gift, True) - else: - return await get_card(user, nickname, impression_added, gold, gift) - - -async def group_user_check(nickname: str, user_id: str, group: str) -> MessageSegment: - # heuristic: if users find they have never checked in they are probable to check in - user, _ = await SignGroupUser.get_or_create( - user_id=str(user_id), group_id=str(group) - ) - gold = await BagUser.get_gold(user_id, group) - return await get_card(user, nickname, None, gold, "", is_card_view=True) - - -async def group_impression_rank(group: int, num: int) -> Optional[BuildMat]: - user_qq_list, impression_list, _ = await SignGroupUser.get_all_impression(group) - return await init_rank("好感度排行榜", user_qq_list, impression_list, group, num) - - -async def random_gold(user_id, group_id, impression): - if impression < 1: - impression = 1 - gold = random.randint(1, 100) + random.randint(1, int(impression)) - if await BagUser.add_gold(user_id, group_id, gold): - return gold - else: - return 0 - - -# 签到总榜 -async def impression_rank(group_id: int, data: dict): - user_qq_list, impression_list, group_list = await SignGroupUser.get_all_impression( - group_id - ) - users, impressions, groups = [], [], [] - num = 0 - for i in range(105 if len(user_qq_list) > 105 else len(user_qq_list)): - impression = max(impression_list) - index = impression_list.index(impression) - user = user_qq_list[index] - group = group_list[index] - user_qq_list.pop(index) - impression_list.pop(index) - group_list.pop(index) - if user not in users and impression < 100000: - if user not in data["0"]: - users.append(user) - impressions.append(impression) - groups.append(group) - else: - num += 1 - for i in range(num): - impression = max(impression_list) - index = impression_list.index(impression) - user = user_qq_list[index] - group = group_list[index] - user_qq_list.pop(index) - impression_list.pop(index) - group_list.pop(index) - if user not in users and impression < 100000: - users.append(user) - impressions.append(impression) - groups.append(group) - return (await asyncio.gather(*[_pst(users, impressions, groups)]))[0] - - -async def _pst(users: list, impressions: list, groups: list): - lens = len(users) - count = math.ceil(lens / 33) - width = 10 - idx = 0 - A = BuildImage(1740, 3300, color="#FFE4C4") - for _ in range(count): - col_img = BuildImage(550, 3300, 550, 100, color="#FFE4C4") - for _ in range(33 if int(lens / 33) >= 1 else lens % 33 - 1): - idx += 1 - if idx > 100: - break - impression = max(impressions) - index = impressions.index(impression) - user = users[index] - group = groups[index] - impressions.pop(index) - users.pop(index) - groups.pop(index) - if user_ := await GroupInfoUser.get_or_none( - user_id=str(user), group_id=str(group) - ): - user_name = user_.user_name - else: - user_name = f"我名字呢?" - user_name = user_name if len(user_name) < 11 else user_name[:10] + "..." - ava = await get_user_avatar(user) - if ava: - ava = BuildImage(50, 50, background=BytesIO(ava)) - else: - ava = BuildImage(50, 50, color="white") - ava.circle() - bk = BuildImage(550, 100, color="#FFE4C4", font_size=30) - font_w, font_h = bk.getsize(f"{idx}") - bk.text((5, int((100 - font_h) / 2)), f"{idx}.") - bk.paste(ava, (55, int((100 - 50) / 2)), True) - bk.text((120, int((100 - font_h) / 2)), f"{user_name}") - bk.text((460, int((100 - font_h) / 2)), f"[{impression:.2f}]") - col_img.paste(bk) - A.paste(col_img, (width, 0)) - lens -= 33 - width += 580 - W = BuildImage(1740, 3700, color="#FFE4C4", font_size=130) - W.paste(A, (0, 260)) - font_w, font_h = W.getsize(f"{NICKNAME}的好感度总榜") - W.text((int((1740 - font_w) / 2), int((260 - font_h) / 2)), f"{NICKNAME}的好感度总榜") - return W.pic2bs4() diff --git a/plugins/sign_in/utils.py b/plugins/sign_in/utils.py deleted file mode 100755 index d8a86b0f..00000000 --- a/plugins/sign_in/utils.py +++ /dev/null @@ -1,361 +0,0 @@ -import asyncio -import os -import random -from datetime import datetime -from io import BytesIO -from pathlib import Path -from typing import List, Optional - -import nonebot -from nonebot import Driver -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH -from models.group_member_info import GroupInfoUser -from models.sign_group_user import SignGroupUser -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import get_user_avatar - -from .config import ( - SIGN_BACKGROUND_PATH, - SIGN_BORDER_PATH, - SIGN_RESOURCE_PATH, - SIGN_TODAY_CARD_PATH, - level2attitude, - lik2level, - lik2relation, -) - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def init_image(): - SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) - SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) - if not await GroupInfoUser.get_or_none(user_id="114514"): - await GroupInfoUser.create( - user_id="114514", - group_id="114514", - user_name="", - uid=0, - ) - generate_progress_bar_pic() - clear_sign_data_pic() - - -async def get_card( - user: SignGroupUser, - nickname: str, - add_impression: Optional[float], - gold: Optional[int], - gift: str, - is_double: bool = False, - is_card_view: bool = False, -) -> MessageSegment: - user_id = user.user_id - date = datetime.now().date() - _type = "view" if is_card_view else "sign" - card_file = ( - Path(SIGN_TODAY_CARD_PATH) / f"{user_id}_{user.group_id}_{_type}_{date}.png" - ) - if card_file.exists(): - return image( - IMAGE_PATH - / "sign" - / "today_card" - / f"{user_id}_{user.group_id}_{_type}_{date}.png" - ) - else: - if add_impression == -1: - card_file = ( - Path(SIGN_TODAY_CARD_PATH) - / f"{user_id}_{user.group_id}_view_{date}.png" - ) - if card_file.exists(): - return image( - IMAGE_PATH - / "sign" - / "today_card" - / f"{user_id}_{user.group_id}_view_{date}.png" - ) - is_card_view = True - ava = BytesIO(await get_user_avatar(user_id)) - uid = await GroupInfoUser.get_group_member_uid(user.user_id, user.group_id) - impression_list = None - if is_card_view: - _, impression_list, _ = await SignGroupUser.get_all_impression( - user.group_id - ) - return await asyncio.get_event_loop().run_in_executor( - None, - _generate_card, - user, - nickname, - user_id, - add_impression, - gold, - gift, - uid, - ava, - impression_list, - is_double, - is_card_view, - ) - - -def _generate_card( - user: "SignGroupUser", - nickname: str, - user_id: str, - impression: Optional[float], - gold: Optional[int], - gift: str, - uid: str, - ava_bytes: BytesIO, - impression_list: List[float], - is_double: bool = False, - is_card_view: bool = False, -) -> MessageSegment: - ava_bk = BuildImage(140, 140, is_alpha=True) - ava_border = BuildImage( - 140, - 140, - background=SIGN_BORDER_PATH / "ava_border_01.png", - ) - ava = BuildImage(102, 102, background=ava_bytes) - ava.circle() - ava_bk.paste(ava, center_type="center") - ava_bk.paste(ava_border, alpha=True, center_type="center") - add_impression = impression - impression = float(user.impression) - info_img = BuildImage(250, 150, color=(255, 255, 255, 0), font_size=15) - level, next_impression, previous_impression = get_level_and_next_impression( - impression - ) - interpolation = next_impression - impression - if level == "9": - level = "8" - interpolation = 0 - info_img.text((0, 0), f"· 好感度等级:{level} [{lik2relation[level]}]") - info_img.text((0, 20), f"· {NICKNAME}对你的态度:{level2attitude[level]}") - info_img.text((0, 40), f"· 距离升级还差 {interpolation:.2f} 好感度") - - bar_bk = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar_white.png") - bar = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar.png") - ratio = 1 - (next_impression - user.impression) / ( - next_impression - previous_impression - ) - if next_impression == 0: - ratio = 0 - bar.resize(w=int(bar.w * ratio) or bar.w, h=bar.h) - bar_bk.paste( - bar, - alpha=True, - ) - font_size = 30 - if "好感度双倍加持卡" in gift: - font_size = 20 - gift_border = BuildImage( - 270, - 100, - background=SIGN_BORDER_PATH / "gift_border_02.png", - font_size=font_size, - ) - gift_border.text((0, 0), gift, center_type="center") - - bk = BuildImage( - 876, - 424, - background=SIGN_BACKGROUND_PATH - / random.choice(os.listdir(SIGN_BACKGROUND_PATH)), - font_size=25, - ) - A = BuildImage(876, 274, background=SIGN_RESOURCE_PATH / "white.png") - line = BuildImage(2, 180, color="black") - A.transparent(2) - A.paste(ava_bk, (25, 80), True) - A.paste(line, (200, 70)) - - nickname_img = BuildImage( - 0, - 0, - plain_text=nickname, - color=(255, 255, 255, 0), - font_size=50, - font_color=(255, 255, 255), - ) - if uid: - uid = f"{uid}".rjust(12, "0") - uid = uid[:4] + " " + uid[4:8] + " " + uid[8:] - else: - uid = "XXXX XXXX XXXX" - uid_img = BuildImage( - 0, - 0, - plain_text=f"UID: {uid}", - color=(255, 255, 255, 0), - font_size=30, - font_color=(255, 255, 255), - ) - sign_day_img = BuildImage( - 0, - 0, - plain_text=f"{user.checkin_count}", - color=(255, 255, 255, 0), - font_size=40, - font_color=(211, 64, 33), - ) - lik_text1_img = BuildImage( - 0, 0, plain_text="当前", color=(255, 255, 255, 0), font_size=20 - ) - lik_text2_img = BuildImage( - 0, - 0, - plain_text=f"好感度:{user.impression:.2f}", - color=(255, 255, 255, 0), - font_size=30, - ) - watermark = BuildImage( - 0, - 0, - plain_text=f"{NICKNAME}@{datetime.now().year}", - color=(255, 255, 255, 0), - font_size=15, - font_color=(155, 155, 155), - ) - today_data = BuildImage(300, 300, color=(255, 255, 255, 0), font_size=20) - if is_card_view: - today_sign_text_img = BuildImage( - 0, 0, plain_text="", color=(255, 255, 255, 0), font_size=30 - ) - if impression_list: - impression_list.sort(reverse=True) - index = impression_list.index(impression) - rank_img = BuildImage( - 0, - 0, - plain_text=f"* 此群好感排名第 {index + 1} 位", - color=(255, 255, 255, 0), - font_size=30, - ) - A.paste(rank_img, ((A.w - rank_img.w - 10), 20), True) - today_data.text( - (0, 0), - f"上次签到日期:{'从未' if user.checkin_time_last == datetime.min else user.checkin_time_last.date()}", - ) - today_data.text((0, 25), f"总金币:{gold}") - default_setu_prob = ( - Config.get_config("send_setu", "INITIAL_SETU_PROBABILITY") * 100 # type: ignore - ) - today_data.text( - (0, 50), - f"色图概率:{(default_setu_prob + float(user.impression) if user.impression < 100 else 100):.2f}%", - ) - today_data.text((0, 75), f"开箱次数:{(20 + int(user.impression / 3))}") - _type = "view" - else: - A.paste(gift_border, (570, 140), True) - today_sign_text_img = BuildImage( - 0, 0, plain_text="今日签到", color=(255, 255, 255, 0), font_size=30 - ) - if is_double: - today_data.text((0, 0), f"好感度 + {add_impression / 2:.2f} × 2") - else: - today_data.text((0, 0), f"好感度 + {add_impression:.2f}") - today_data.text((0, 25), f"金币 + {gold}") - _type = "sign" - current_date = datetime.now() - current_datetime_str = current_date.strftime("%Y-%m-%d %a %H:%M:%S") - data = current_date.date() - data_img = BuildImage( - 0, - 0, - plain_text=f"时间:{current_datetime_str}", - color=(255, 255, 255, 0), - font_size=20, - ) - bk.paste(nickname_img, (30, 15), True) - bk.paste(uid_img, (30, 85), True) - bk.paste(A, (0, 150), alpha=True) - bk.text((30, 167), "Accumulative check-in for") - _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.w + 45 - bk.paste(sign_day_img, (346, 158), True) - bk.text((_x, 167), "days") - bk.paste(data_img, (220, 370), True) - bk.paste(lik_text1_img, (220, 240), True) - bk.paste(lik_text2_img, (262, 234), True) - bk.paste(bar_bk, (225, 275), True) - bk.paste(info_img, (220, 305), True) - bk.paste(today_sign_text_img, (550, 180), True) - bk.paste(today_data, (580, 220), True) - bk.paste(watermark, (15, 400), True) - bk.save(SIGN_TODAY_CARD_PATH / f"{user_id}_{user.group_id}_{_type}_{data}.png") - return image( - IMAGE_PATH - / "sign" - / "today_card" - / f"{user_id}_{user.group_id}_{_type}_{data}.png" - ) - - -def generate_progress_bar_pic(): - bg_2 = (254, 1, 254) - bg_1 = (0, 245, 246) - - bk = BuildImage(1000, 50, is_alpha=True) - img_x = BuildImage(50, 50, color=bg_2) - img_x.circle() - img_x.crop((25, 0, 50, 50)) - img_y = BuildImage(50, 50, color=bg_1) - img_y.circle() - img_y.crop((0, 0, 25, 50)) - A = BuildImage(950, 50) - width, height = A.size - - step_r = (bg_2[0] - bg_1[0]) / width - step_g = (bg_2[1] - bg_1[1]) / width - step_b = (bg_2[2] - bg_1[2]) / width - - for y in range(0, width): - bg_r = round(bg_1[0] + step_r * y) - bg_g = round(bg_1[1] + step_g * y) - bg_b = round(bg_1[2] + step_b * y) - for x in range(0, height): - A.point((y, x), fill=(bg_r, bg_g, bg_b)) - bk.paste(img_y, (0, 0), True) - bk.paste(A, (25, 0)) - bk.paste(img_x, (975, 0), True) - bk.save(SIGN_RESOURCE_PATH / "bar.png") - - A = BuildImage(950, 50) - bk = BuildImage(1000, 50, is_alpha=True) - img_x = BuildImage(50, 50) - img_x.circle() - img_x.crop((25, 0, 50, 50)) - img_y = BuildImage(50, 50) - img_y.circle() - img_y.crop((0, 0, 25, 50)) - bk.paste(img_y, (0, 0), True) - bk.paste(A, (25, 0)) - bk.paste(img_x, (975, 0), True) - bk.save(SIGN_RESOURCE_PATH / "bar_white.png") - - -def get_level_and_next_impression(impression: float): - if impression == 0: - return lik2level[10], 10, 0 - keys = list(lik2level.keys()) - for i in range(len(keys)): - if impression > keys[i]: - return lik2level[keys[i]], keys[i - 1], keys[i] - return lik2level[10], 10, 0 - - -def clear_sign_data_pic(): - date = datetime.now().date() - for file in os.listdir(SIGN_TODAY_CARD_PATH): - if str(date) not in file: - os.remove(SIGN_TODAY_CARD_PATH / file) diff --git a/plugins/statistics/_config.py b/plugins/statistics/_config.py deleted file mode 100644 index edee1069..00000000 --- a/plugins/statistics/_config.py +++ /dev/null @@ -1,26 +0,0 @@ -from enum import Enum -from typing import NamedTuple - - -class SearchType(Enum): - - """ - 查询类型 - """ - - DAY = "day_statistics" - """天""" - WEEK = "week_statistics" - """周""" - MONTH = "month_statistics" - """月""" - TOTAL = "total_statistics" - """总数""" - - -class ParseData(NamedTuple): - - global_search: bool - """是否全局搜索""" - search_type: SearchType - """搜索类型""" diff --git a/plugins/statistics/statistics_handle.py b/plugins/statistics/statistics_handle.py deleted file mode 100755 index f0515a4b..00000000 --- a/plugins/statistics/statistics_handle.py +++ /dev/null @@ -1,281 +0,0 @@ -import asyncio -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg - -from configs.path_config import DATA_PATH, IMAGE_PATH -from models.group_info import GroupInfo -from utils.depends import OneCommand -from utils.image_utils import BuildMat -from utils.manager import plugins2settings_manager -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -__zx_plugin_name__ = "功能调用统计可视化" -__plugin_usage__ = """ -usage: - 功能调用统计可视化 - 指令: - 功能调用统计 - 日功能调用统计 - 周功能调用统计 ?[功能] - 月功能调用统计 ?[功能] - 我的功能调用统计 - 我的日功能调用统计 ?[功能] - 我的周功能调用统计 ?[功能] - 我的月功能调用统计 ?[功能] -""".strip() -__plugin_superuser_usage__ = """ -usage: - 功能调用统计可视化 - 指令: - 全局功能调用统计 - 全局日功能调用统计 - 全局周功能调用统计 ?[功能] - 全局月功能调用统计 ?[功能] -""".strip() -__plugin_des__ = "功能调用统计可视化" -__plugin_cmd__ = [ - "功能调用统计", - "全局功能调用统计 [_superuser]", - "全局日功能调用统计 [_superuser]", - "全局周功能调用统计 ?[功能] [_superuser]", - "全局月功能调用统计 ?[功能] [_superuser]", - "周功能调用统计 ?[功能]", - "月功能调用统计 ?[功能]", - "我的功能调用统计", - "我的日功能调用统计 ?[功能]", - "我的周功能调用统计 ?[功能]", - "我的月功能调用统计 ?[功能]", -] -__plugin_type__ = ("数据统计", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["功能调用统计"], -} - - -statistics = on_command( - "功能调用统计", - aliases={ - "全局功能调用统计", - "全局日功能调用统计", - "全局周功能调用统计", - "全局月功能调用统计", - "日功能调用统计", - "周功能调用统计", - "月功能调用统计", - "我的功能调用统计", - "我的日功能调用统计", - "我的周功能调用统计", - "我的月功能调用统计", - }, - priority=5, - block=True, -) - - -statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" -statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" - - -@statistics.handle() -async def _(bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if cmd[0][:2] == "全局": - if str(event.user_id) in bot.config.superusers: - data: dict = json.load(open(statistics_group_file, "r", encoding="utf8")) - if cmd[0][2] == '日': - _type = 'day_statistics' - elif cmd[0][2] == '周': - _type = 'week_statistics' - elif cmd[0][2] == '月': - _type = 'month_statistics' - else: - _type = 'total_statistics' - tmp_dict = {} - data = data[_type] - if _type in ["day_statistics", "total_statistics"]: - for key in data['total']: - tmp_dict[key] = data['total'][key] - else: - for group in data.keys(): - if group != 'total': - for day in data[group].keys(): - for plugin_name in data[group][day].keys(): - if data[group][day][plugin_name] is not None: - if tmp_dict.get(plugin_name) is None: - tmp_dict[plugin_name] = 1 - else: - tmp_dict[plugin_name] += data[group][day][plugin_name] - bar_graph = await init_bar_graph(tmp_dict, cmd[0]) - await asyncio.get_event_loop().run_in_executor(None, bar_graph.gen_graph) - await statistics.finish(image(b64=bar_graph.pic2bs4())) - return - if cmd[0][:2] == "我的": - _type = "user" - key = str(event.user_id) - cmd = list(cmd) - cmd[0] = cmd[0][2:] - if not statistics_user_file.exists(): - await statistics.finish("统计文件不存在...", at_sender=True) - else: - if not isinstance(event, GroupMessageEvent): - await statistics.finish("请在群内调用此功能...") - _type = "group" - key = str(event.group_id) - if not statistics_group_file.exists(): - await statistics.finish("统计文件不存在...", at_sender=True) - plugin = "" - if cmd[0][0] == "日": - arg = "day_statistics" - elif cmd[0][0] == "周": - arg = "week_statistics" - elif cmd[0][0] == "月": - arg = "month_statistics" - else: - arg = "total_statistics" - if msg: - plugin = plugins2settings_manager.get_plugin_module(msg) - if not plugin: - if arg not in ["day_statistics", "total_statistics"]: - await statistics.finish("未找到此功能的调用...", at_sender=True) - if _type == "group": - data: dict = json.load(open(statistics_group_file, "r", encoding="utf8")) - if not data[arg].get(str(event.group_id)): - await statistics.finish("该群统计数据不存在...", at_sender=True) - else: - data: dict = json.load(open(statistics_user_file, "r", encoding="utf8")) - if not data[arg].get(str(event.user_id)): - await statistics.finish("该用户统计数据不存在...", at_sender=True) - day_index = data["day_index"] - data = data[arg][key] - if _type == "group": - group = await GroupInfo.filter(group_id=str(event.group_id)).first() - name = group if group else str(event.group_id) - else: - name = event.sender.card or event.sender.nickname - img = await generate_statistics_img(data, arg, name, plugin, day_index) - await statistics.send(image(b64=img)) - - -async def generate_statistics_img( - data: dict, arg: str, name: str, plugin: str, day_index: int -): - try: - plugin = plugins2settings_manager.get_plugin_data(plugin).cmd[0] - except (KeyError, IndexError, AttributeError): - pass - bar_graph = None - if arg == "day_statistics": - bar_graph = await init_bar_graph(data, f"{name} 日功能调用统计") - elif arg == "week_statistics": - if plugin: - current_week = day_index % 7 - week_lst = [] - if current_week == 0: - week_lst = [1, 2, 3, 4, 5, 6, 7] - else: - for i in range(current_week + 1, 7): - week_lst.append(str(i)) - for i in range(current_week + 1): - week_lst.append(str(i)) - count = [] - for i in range(7): - if int(week_lst[i]) == 7: - try: - count.append(data[str(0)][plugin]) - except KeyError: - count.append(0) - else: - try: - count.append(data[str(week_lst[i])][plugin]) - except KeyError: - count.append(0) - week_lst = ["7" if i == "0" else i for i in week_lst] - bar_graph = BuildMat( - y=count, - mat_type="line", - title=f"{name} 周 {plugin} 功能调用统计【为7天统计】", - x_index=week_lst, - display_num=True, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - else: - bar_graph = await init_bar_graph(update_data(data), f"{name} 周功能调用统计【为7天统计】") - elif arg == "month_statistics": - if plugin: - day_index = day_index % 30 - day_lst = [] - for i in range(day_index + 1, 30): - day_lst.append(i) - for i in range(day_index + 1): - day_lst.append(i) - count = [data[str(day_lst[i])][plugin] for i in range(30)] - day_lst = [str(x + 1) for x in day_lst] - bar_graph = BuildMat( - y=count, - mat_type="line", - title=f"{name} 月 {plugin} 功能调用统计【为30天统计】", - x_index=day_lst, - display_num=True, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - else: - bar_graph = await init_bar_graph(update_data(data), f"{name} 月功能调用统计【为30天统计】") - elif arg == "total_statistics": - bar_graph = await init_bar_graph(data, f"{name} 功能调用统计") - await asyncio.get_event_loop().run_in_executor(None, bar_graph.gen_graph) - return bar_graph.pic2bs4() - - -async def init_bar_graph(data: dict, title: str) -> BuildMat: - return await asyncio.get_event_loop().run_in_executor(None, _init_bar_graph, data, title) - - -def _init_bar_graph(data: dict, title: str) -> BuildMat: - bar_graph = BuildMat( - y=[data[x] for x in data.keys() if data[x] != 0], - mat_type="barh", - title=title, - x_index=[x for x in data.keys() if data[x] != 0], - display_num=True, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - return bar_graph - - -def update_data(data: dict): - tmp_dict = {} - for day in data.keys(): - for plugin_name in data[day].keys(): - # print(f'{day}:{plugin_name} = {data[day][plugin_name]}') - if data[day][plugin_name] is not None: - if tmp_dict.get(plugin_name) is None: - tmp_dict[plugin_name] = 1 - else: - tmp_dict[plugin_name] += data[day][plugin_name] - return tmp_dict \ No newline at end of file diff --git a/plugins/statistics/statistics_hook.py b/plugins/statistics/statistics_hook.py deleted file mode 100755 index 9fea8b2c..00000000 --- a/plugins/statistics/statistics_hook.py +++ /dev/null @@ -1,217 +0,0 @@ -from datetime import datetime - -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent -from nonebot.matcher import Matcher -from nonebot.message import run_postprocessor -from nonebot.typing import Optional, T_State - -from configs.path_config import DATA_PATH -from models.statistics import Statistics -from utils.manager import plugins2settings_manager -from utils.utils import scheduler - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -__zx_plugin_name__ = "功能调用统计 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -# statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" -# statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" - -# try: -# with open(statistics_group_file, "r", encoding="utf8") as f: -# _prefix_count_dict = json.load(f) -# except (FileNotFoundError, ValueError): -# _prefix_count_dict = { -# "total_statistics": { -# "total": {}, -# }, -# "day_statistics": { -# "total": {}, -# }, -# "week_statistics": { -# "total": {}, -# }, -# "month_statistics": { -# "total": {}, -# }, -# "start_time": str(datetime.now().date()), -# "day_index": 0, -# } - -# try: -# with open(statistics_user_file, "r", encoding="utf8") as f: -# _prefix_user_count_dict = json.load(f) -# except (FileNotFoundError, ValueError): -# _prefix_user_count_dict = { -# "total_statistics": { -# "total": {}, -# }, -# "day_statistics": { -# "total": {}, -# }, -# "week_statistics": { -# "total": {}, -# }, -# "month_statistics": { -# "total": {}, -# }, -# "start_time": str(datetime.now().date()), -# "day_index": 0, -# } - - -# # 以前版本转换 -# if _prefix_count_dict.get("day_index") is None: -# tmp = _prefix_count_dict.copy() -# _prefix_count_dict = { -# "total_statistics": tmp["total_statistics"], -# "day_statistics": { -# "total": {}, -# }, -# "week_statistics": { -# "total": {}, -# }, -# "month_statistics": { -# "total": {}, -# }, -# "start_time": tmp["start_time"], -# "day_index": 0, -# } - - -# 添加命令次数 -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: MessageEvent, - state: T_State, -): - # global _prefix_count_dict - if ( - matcher.type == "message" - and matcher.priority not in [1, 999] - and matcher.plugin_name not in ["update_info", "statistics_handle"] - ): - await Statistics.create( - user_id=str(event.user_id), - group_id=getattr(event, "group_id", None), - plugin_name=matcher.plugin_name, - create_time=datetime.now(), - ) - # module = matcher.plugin_name - # day_index = _prefix_count_dict["day_index"] - # try: - # group_id = str(event.group_id) - # except AttributeError: - # group_id = "total" - # user_id = str(event.user_id) - # plugin_name = plugins2settings_manager.get_plugin_data(module) - # if plugin_name and plugin_name.cmd: - # plugin_name = plugin_name.cmd[0] - # check_exists_key(group_id, user_id, plugin_name) - # for data in [_prefix_count_dict, _prefix_user_count_dict]: - # data["total_statistics"]["total"][plugin_name] += 1 - # data["day_statistics"]["total"][plugin_name] += 1 - # data["week_statistics"]["total"][plugin_name] += 1 - # data["month_statistics"]["total"][plugin_name] += 1 - # # print(_prefix_count_dict) - # if group_id != "total": - # for data in [_prefix_count_dict, _prefix_user_count_dict]: - # if data == _prefix_count_dict: - # key = group_id - # else: - # key = user_id - # data["total_statistics"][key][plugin_name] += 1 - # data["day_statistics"][key][plugin_name] += 1 - # data["week_statistics"][key][str(day_index % 7)][plugin_name] += 1 - # data["month_statistics"][key][str(day_index % 30)][plugin_name] += 1 - # with open(statistics_group_file, "w", encoding="utf8") as f: - # json.dump(_prefix_count_dict, f, indent=4, ensure_ascii=False) - # with open(statistics_user_file, "w", encoding="utf8") as f: - # json.dump(_prefix_user_count_dict, f, ensure_ascii=False, indent=4) - - -# def check_exists_key(group_id: str, user_id: str, plugin_name: str): -# global _prefix_count_dict, _prefix_user_count_dict -# for data in [_prefix_count_dict, _prefix_user_count_dict]: -# if data == _prefix_count_dict: -# key = group_id -# else: -# key = user_id -# if not data["total_statistics"]["total"].get(plugin_name): -# data["total_statistics"]["total"][plugin_name] = 0 -# if not data["day_statistics"]["total"].get(plugin_name): -# data["day_statistics"]["total"][plugin_name] = 0 -# if not data["week_statistics"]["total"].get(plugin_name): -# data["week_statistics"]["total"][plugin_name] = 0 -# if not data["month_statistics"]["total"].get(plugin_name): -# data["month_statistics"]["total"][plugin_name] = 0 - -# if not data["total_statistics"].get(key): -# data["total_statistics"][key] = {} -# if not data["total_statistics"][key].get(plugin_name): -# data["total_statistics"][key][plugin_name] = 0 -# if not data["day_statistics"].get(key): -# data["day_statistics"][key] = {} -# if not data["day_statistics"][key].get(plugin_name): -# data["day_statistics"][key][plugin_name] = 0 - -# if key != "total": -# if not data["week_statistics"].get(key): -# data["week_statistics"][key] = {} -# if data["week_statistics"][key].get("0") is None: -# for i in range(7): -# data["week_statistics"][key][str(i)] = {} -# if data["week_statistics"][key]["0"].get(plugin_name) is None: -# for i in range(7): -# data["week_statistics"][key][str(i)][plugin_name] = 0 - -# if not data["month_statistics"].get(key): -# data["month_statistics"][key] = {} -# if data["month_statistics"][key].get("0") is None: -# for i in range(30): -# data["month_statistics"][key][str(i)] = {} -# if data["month_statistics"][key]["0"].get(plugin_name) is None: -# for i in range(30): -# data["month_statistics"][key][str(i)][plugin_name] = 0 - - -# 天 -# @scheduler.scheduled_job( -# "cron", -# hour=0, -# minute=1, -# ) -# async def _(): -# for data in [_prefix_count_dict, _prefix_user_count_dict]: -# data["day_index"] += 1 -# for x in data["day_statistics"].keys(): -# for key in data["day_statistics"][x].keys(): -# try: -# data["day_statistics"][x][key] = 0 -# except KeyError: -# pass -# for type_ in ["week_statistics", "month_statistics"]: -# index = str( -# data["day_index"] % 7 -# if type_ == "week_statistics" -# else data["day_index"] % 30 -# ) -# for x in data[type_].keys(): -# try: -# for key in data[type_][x][index].keys(): -# data[type_][x][index][key] = 0 -# except KeyError: -# pass -# with open(statistics_group_file, "w", encoding="utf8") as f: -# json.dump(_prefix_count_dict, f, indent=4, ensure_ascii=False) -# with open(statistics_user_file, "w", encoding="utf8") as f: -# json.dump(_prefix_user_count_dict, f, indent=4, ensure_ascii=False) diff --git a/plugins/statistics/utils.py b/plugins/statistics/utils.py deleted file mode 100644 index c35b5672..00000000 --- a/plugins/statistics/utils.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import List -from nonebot.adapters.onebot.v11 import MessageEvent -from ._config import SearchType - - -def parse_data(cmd: str, event: MessageEvent, superusers: List[str]): - search_type = SearchType.TOTAL - if cmd[:2] == "全局": - if str(event.user_id) in superusers: - if cmd[2] == '日': - search_type = SearchType.DAY - elif cmd[2] == '周': - _type = SearchType.WEEK - elif cmd[2] == '月': - _type = SearchType.MONTH - diff --git a/plugins/translate/__init__.py b/plugins/translate/__init__.py deleted file mode 100755 index e6ac9464..00000000 --- a/plugins/translate/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Any, Tuple - -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.params import RegexGroup -from nonebot.typing import T_State - -from services.log import logger -from utils.depends import CheckConfig - -from .data_source import CheckParam, language, translate_msg - -__zx_plugin_name__ = "翻译" -__plugin_usage__ = """ -usage: - 出国旅游小助手 - Regex: 翻译(form:.*?)?(to:.*?)? (.+) - 一般只需要设置to:,form:按照百度自动检测 - 指令: - 翻译语种: (查看form与to可用值) - 示例: - 翻译 你好: 将中文翻译为英文 - 翻译 Hello: 将英文翻译为中文 - 翻译to:el 你好: 将"你好"翻译为希腊语 - 翻译to:希腊语 你好: 允许form和to使用中文 - 翻译form:zhto:jp 你好: 指定原语种并将"你好"翻译为日文 -""".strip() -__plugin_des__ = "出国旅游好助手" -__plugin_cmd__ = ["翻译"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["翻译"], -} -__plugin_configs__ = { - "APPID": { - "value": None, - "help": "百度翻译APPID", - "type": str, - }, - "SECRET_KEY": { - "value": None, - "help": "百度翻译秘钥", - "type": str, - }, -} - -translate = on_regex("^翻译(form:.*?)?(to:.*?)? (.+)", priority=5, block=True) - -translate_language = on_regex("^翻译语种$", priority=5, block=True) - - -@translate_language.handle() -async def _(event: MessageEvent): - s = "" - for key, value in language.items(): - s += f"{key}: {value}," - await translate_language.send(s[:-1]) - logger.info(f"查看翻译语种", "翻译语种", event.user_id, getattr(event, "group_id", None)) - - -@translate.handle( - parameterless=[ - CheckConfig(config="APPID"), - CheckConfig(config="SECRET_KEY"), - CheckParam(), - ] -) -async def _( - event: MessageEvent, state: T_State, reg_group: Tuple[Any, ...] = RegexGroup() -): - _, _, msg = reg_group - await translate.send(await translate_msg(msg, state["form"], state["to"])) - logger.info(f"翻译: {msg}", "翻译", event.user_id, getattr(event, "group_id", None)) diff --git a/plugins/update_gocqhttp/__init__.py b/plugins/update_gocqhttp/__init__.py deleted file mode 100755 index a5edde60..00000000 --- a/plugins/update_gocqhttp/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from pathlib import Path -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent -from nonebot.permission import SUPERUSER -from nonebot.typing import T_State - -from configs.config import Config -from services.log import logger -from utils.utils import get_bot, scheduler - -from .data_source import download_gocq_lasted, upload_gocq_lasted - -__zx_plugin_name__ = "更新gocq [Superuser]" -__plugin_usage__ = """ -usage: - 下载最新版gocq并上传至群文件 - 指令: - 更新gocq -""".strip() -__plugin_cmd__ = ["更新gocq"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "UPDATE_GOCQ_GROUP": { - "value": [], - "help": "需要为哪些群更新最新版gocq吗?(上传最新版gocq)示例:[434995955, 239483248]", - "default_value": [], - "type": List[int], - } -} - -path = str((Path() / "resources" / "gocqhttp_file").absolute()) + "/" - -lasted_gocqhttp = on_command("更新gocq", permission=SUPERUSER, priority=5, block=True) - - -@lasted_gocqhttp.handle() -async def _(bot: Bot, event: GroupMessageEvent, state: T_State): - # try: - if event.group_id in Config.get_config("update_gocqhttp", "UPDATE_GOCQ_GROUP"): - await lasted_gocqhttp.send("检测中...") - info = await download_gocq_lasted(path) - if info == "gocqhttp没有更新!": - await lasted_gocqhttp.finish("gocqhttp没有更新!") - try: - for file in os.listdir(path): - await upload_gocq_lasted(path, file, event.group_id) - logger.info(f"更新了cqhttp...{file}") - await lasted_gocqhttp.send(f"gocqhttp更新了,已上传成功!\n更新内容:\n{info}") - except Exception as e: - logger.error(f"更新gocq错误 e:{e}") - - -# 更新gocq -@scheduler.scheduled_job( - "cron", - hour=3, - minute=1, -) -async def _(): - if Config.get_config("update_gocqhttp", "UPDATE_GOCQ_GROUP"): - bot = get_bot() - try: - info = await download_gocq_lasted(path) - if info == "gocqhttp没有更新!": - logger.info("gocqhttp没有更新!") - return - for group in Config.get_config("update_gocqhttp", "UPDATE_GOCQ_GROUP"): - for file in os.listdir(path): - await upload_gocq_lasted(path, file, group) - await bot.send_group_msg( - group_id=group, message=f"gocqhttp更新了,已上传成功!\n更新内容:\n{info}" - ) - except Exception as e: - logger.error(f"自动更新gocq出错 e:{e}") diff --git a/plugins/update_gocqhttp/data_source.py b/plugins/update_gocqhttp/data_source.py deleted file mode 100755 index 1572e90e..00000000 --- a/plugins/update_gocqhttp/data_source.py +++ /dev/null @@ -1,73 +0,0 @@ -from utils.utils import get_bot -from bs4 import BeautifulSoup -from utils.http_utils import AsyncHttpx -import asyncio -import platform -import os - -# if platform.system() == "Windows": -# asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - -url = "https://github.com/Mrs4s/go-cqhttp/releases" - - -async def download_gocq_lasted(path: str): - text = (await AsyncHttpx.get(url)).text - soup = BeautifulSoup(text, "lxml") - a = soup.find("div", {"class": "release-header"}).find("a") - title = a.text - _url = a.get("href") - for file in os.listdir(path): - if file.endswith(".zip"): - if ( - file == title + "-windows-amd64.zip" - or file == title + "_windows_amd64.zip" - ): - return "gocqhttp没有更新!" - for file in os.listdir(path): - os.remove(path + file) - text = (await AsyncHttpx.get("https://github.com" + _url)).text - update_info = "" - soup = BeautifulSoup(text, "lxml") - info_div = soup.find("div", {"class": "markdown-body"}) - for p in info_div.find_all("p"): - update_info += p.text.replace("
", "\n") + "\n" - div_all = soup.select( - "div.d-flex.flex-justify-between.flex-items-center.py-1.py-md-2.Box-body.px-2" - ) - for div in div_all: - if ( - div.find("a").find("span").text == title + "-windows-amd64.zip" - or div.find("a").find("span").text == title + "-linux-arm64.tar.gz" - or div.find("a").find("span").text == "go-cqhttp_windows_amd64.zip" - or div.find("a").find("span").text == "go-cqhttp_linux_arm64.tar.gz" - ): - file_url = div.find("a").get("href") - if div.find("a").find("span").text.find("windows") == -1: - tag = "-linux-arm64.tar.gz" - else: - tag = "-windows-amd64.zip" - await AsyncHttpx.download_file( - "https://github.com" + file_url, path + title + tag - ) - return update_info - - -async def upload_gocq_lasted(path, name, group_id): - bot = get_bot() - folder_id = 0 - for folder in (await bot.get_group_root_files(group_id=group_id))["folders"]: - if folder["folder_name"] == "gocq": - folder_id = folder["folder_id"] - if not folder_id: - await bot.send_group_msg(group_id=group_id, message=f"请创建gocq文件夹后重试!") - for file in os.listdir(path): - os.remove(path + file) - else: - await bot.upload_group_file( - group_id=group_id, folder=folder_id, file=path + name, name=name - ) - - -# asyncio.get_event_loop().run_until_complete(download_gocq_lasted()) diff --git a/plugins/update_picture.py b/plugins/update_picture.py deleted file mode 100755 index bdf7f552..00000000 --- a/plugins/update_picture.py +++ /dev/null @@ -1,303 +0,0 @@ -from typing import Union - -import cv2 -import numpy as np -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg, Depends -from nonebot.rule import to_me -from nonebot.typing import T_State -from PIL import Image, ImageFilter - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage, pic2b64 -from utils.message_builder import image -from utils.utils import get_message_img, is_number - -__zx_plugin_name__ = "各种图片简易操作" -__plugin_usage__ = """ -usage: - 简易的基础图片操作,输入 指定操作 或 序号 来进行选择 - 指令: - 1.修改尺寸 [宽] [高] [图片] - 2.等比压缩 [比例] [图片] - 3.旋转图片 [角度] [图片] - 4.水平翻转 [图片] - 5.铅笔滤镜 [图片] - 6.模糊效果 [图片] - 7.锐化效果 [图片] - 8.高斯模糊 [图片] - 9.边缘检测 [图片] - 10.底色替换 [红/蓝] [红/蓝/白/绿/黄] [图片] - 示例:图片修改尺寸 100 200 [图片] - 示例:图片 2 0.3 [图片] -""".strip() -__plugin_des__ = "10种快捷的图片简易操作" -__plugin_cmd__ = [ - "改图 修改尺寸 [宽] [高] [图片]", - "改图 等比压缩 [比例] [图片]", - "改图 旋转图片 [角度] [图片]", - "改图 水平翻转 [图片]", - "改图 铅笔滤镜 [图片]", - "改图 模糊效果 [图片]", - "改图 锐化效果 [图片]", - "改图 高斯模糊 [图片]", - "改图 边缘检测 [图片]", - "改图 底色替换 [红/蓝] [红/蓝/白/绿/黄] [图片]", -] -__plugin_type__ = ("一些工具", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["修改图片", "改图", "操作图片"], -} - -method_flag = "" - -update_img = on_command( - "修改图片", aliases={"操作图片", "改图"}, priority=5, rule=to_me(), block=True -) - -method_list = [ - "修改尺寸", - "等比压缩", - "旋转图片", - "水平翻转", - "铅笔滤镜", - "模糊效果", - "锐化效果", - "高斯模糊", - "边缘检测", - "底色替换", -] -method_str = "" -method_oper = [] -for i in range(len(method_list)): - method_str += f"\n{i + 1}.{method_list[i]}" - method_oper.append(method_list[i]) - method_oper.append(str(i + 1)) - -update_img_help = BuildImage(960, 700, font_size=24) -update_img_help.text((10, 10), __plugin_usage__) -update_img_help.save(IMAGE_PATH / "update_img_help.png") - - -def parse_key(key: str): - async def _key_parser(state: T_State, inp: Union[Message, str] = Arg(key)): - if key != "img_list" and isinstance(inp, Message): - inp = inp.extract_plain_text().strip() - if inp in ["取消", "算了"]: - await update_img.finish("已取消操作..") - if key == "method": - if inp not in method_oper: - await update_img.reject_arg("method", f"操作不正确,请重新输入!{method_str}") - elif key == "x": - method = state["method"] - if method in ["1", "修改尺寸"]: - if not is_number(inp) or int(inp) < 1: - await update_img.reject_arg("x", "宽度不正确!请重新输入数字...") - elif method in ["2", "等比压缩", "3", "旋转图片"]: - if not is_number(inp): - await update_img.reject_arg("x", "比率不正确!请重新输入数字...") - elif method in ["10", "底色替换"]: - if inp not in ["红色", "蓝色", "红", "蓝"]: - await update_img.reject_arg("x", "请输入支持的被替换的底色:\n红色 蓝色") - elif key == "y": - method = state["method"] - if method in ["1", "修改尺寸"]: - if not is_number(inp) or int(inp) < 1: - await update_img.reject_arg("y", "长度不正确!请重新输入数字...") - elif method in ["10", "底色替换"]: - if inp not in [ - "红色", - "白色", - "蓝色", - "绿色", - "黄色", - "红", - "白", - "蓝", - "绿", - "黄", - ]: - await update_img.reject_arg("y", "请输入支持的替换的底色:\n红色 蓝色 白色 绿色") - elif key == "img_list": - if not get_message_img(inp): - await update_img.reject_arg("img_list", "没图?没图?没图?来图速来!") - state[key] = inp - - return _key_parser - - -@update_img.handle() -async def _(event: MessageEvent, state: T_State, arg: Message = CommandArg()): - if str(event.get_message()) in ["帮助"]: - await update_img.finish(image("update_img_help.png")) - raw_arg = arg.extract_plain_text().strip() - img_list = get_message_img(event.json()) - if raw_arg: - args = raw_arg.split("[")[0].split() - state["method"] = args[0] - if len(args) == 2: - if args[0] in ["等比压缩", "旋转图片"]: - if is_number(args[1]): - state["x"] = args[1] - state["y"] = "" - elif len(args) > 2: - if args[0] in ["修改尺寸"]: - if is_number(args[1]): - state["x"] = args[1] - if is_number(args[2]): - state["y"] = args[2] - if args[0] in ["底色替换"]: - if args[1] in ["红色", "蓝色", "蓝", "红"]: - state["x"] = args[1] - if args[2] in ["红色", "白色", "蓝色", "绿色", "黄色", "红", "白", "蓝", "绿", "黄"]: - state["y"] = args[2] - if args[0] in ["水平翻转", "铅笔滤镜", "模糊效果", "锐化效果", "高斯模糊", "边缘检测"]: - state["x"] = "" - state["y"] = "" - if img_list: - state["img_list"] = event.message - - -@update_img.got( - "method", - prompt=f"要使用图片的什么操作呢?{method_str}", - parameterless=[Depends(parse_key("method"))], -) -@update_img.got( - "x", prompt="[宽度? 比率? 旋转角度? 底色?]", parameterless=[Depends(parse_key("x"))] -) -@update_img.got("y", prompt="[长度? 0 0 底色?]", parameterless=[Depends(parse_key("y"))]) -@update_img.got( - "img_list", prompt="图呢图呢图呢图呢?GKD!", parameterless=[Depends(parse_key("img_list"))] -) -async def _( - event: MessageEvent, - state: T_State, - method: str = ArgStr("method"), - x: str = ArgStr("x"), - y: str = ArgStr("y"), - img_list: Message = Arg("img_list"), -): - x = x or "" - y = y or "" - img_list = get_message_img(img_list) - if is_number(x): - x = float(x) - if is_number(y): - y = int(y) - index = 0 - result = "" - for img_url in img_list: - if await AsyncHttpx.download_file( - img_url, TEMP_PATH / f"{event.user_id}_{index}_update.png" - ): - index += 1 - else: - await update_img.finish("获取图片超时了...", at_sender=True) - if index == 0: - return - if method in ["修改尺寸", "1"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - img = img.convert("RGB") - img = img.resize((int(x), int(y)), Image.ANTIALIAS) - result += image(b64=pic2b64(img)) - await update_img.finish(result, at_sender=True) - if method in ["等比压缩", "2"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - width, height = img.size - img = img.convert("RGB") - if width * x < 8000 and height * x < 8000: - img = img.resize((int(x * width), int(x * height))) - result += image(b64=pic2b64(img)) - else: - await update_img.finish(f"{NICKNAME}不支持图片压缩后宽或高大于8000的存在!!") - if method in ["旋转图片", "3"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - img = img.rotate(x) - result += image(b64=pic2b64(img)) - if method in ["水平翻转", "4"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - img = img.transpose(Image.FLIP_LEFT_RIGHT) - result += image(b64=pic2b64(img)) - if method in ["铅笔滤镜", "5"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.CONTOUR - ) - result += image(b64=pic2b64(img)) - if method in ["模糊效果", "6"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.BLUR - ) - result += image(b64=pic2b64(img)) - if method in ["锐化效果", "7"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.EDGE_ENHANCE - ) - result += image(b64=pic2b64(img)) - if method in ["高斯模糊", "8"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.GaussianBlur - ) - result += image(b64=pic2b64(img)) - if method in ["边缘检测", "9"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.FIND_EDGES - ) - result += image(b64=pic2b64(img)) - if method in ["底色替换", "10"]: - if x in ["蓝色", "蓝"]: - lower = np.array([90, 70, 70]) - upper = np.array([110, 255, 255]) - if x in ["红色", "红"]: - lower = np.array([0, 135, 135]) - upper = np.array([180, 245, 230]) - if y in ["蓝色", "蓝"]: - color = (255, 0, 0) - if y in ["红色", "红"]: - color = (0, 0, 255) - if y in ["白色", "白"]: - color = (255, 255, 255) - if y in ["绿色", "绿"]: - color = (0, 255, 0) - if y in ["黄色", "黄"]: - color = (0, 255, 255) - for k in range(index): - img = cv2.imread(TEMP_PATH / f"{event.user_id}_{k}_update.png") - img = cv2.resize(img, None, fx=0.3, fy=0.3) - rows, cols, channels = img.shape - hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) - mask = cv2.inRange(hsv, lower, upper) - # erode = cv2.erode(mask, None, iterations=1) - dilate = cv2.dilate(mask, None, iterations=1) - for i in range(rows): - for j in range(cols): - if dilate[i, j] == 255: - img[i, j] = color - cv2.imwrite(TEMP_PATH / f"{event.user_id}_{k}_ok_update.png", img) - for i in range(index): - result += image(TEMP_PATH / f"{event.user_id}_{i}_ok_update.png") - if is_number(method): - method = method_list[int(method) - 1] - logger.info( - f"(USER {event.user_id}, GROUP" - f" {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 使用{method}" - ) - await update_img.finish(result, at_sender=True) diff --git a/plugins/wbtop/__init__.py b/plugins/wbtop/__init__.py deleted file mode 100644 index f6a13cf6..00000000 --- a/plugins/wbtop/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from services.log import logger -from .data_source import gen_wbtop_pic,get_wbtop -from utils.utils import is_number -from configs.path_config import IMAGE_PATH -from utils.http_utils import AsyncPlaywright -import asyncio -import datetime -__zx_plugin_name__ = "微博热搜" -__plugin_usage__ = """ -usage: - 在QQ上吃个瓜 - 指令: - 微博热搜:发送实时热搜 - 微博热搜 [id]:截图该热搜页面 - 示例:微博热搜 5 -""".strip() -__plugin_des__ = "刚买完瓜,在吃瓜现场" -__plugin_cmd__ = ["微博热搜", "微博热搜 [id]"] -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier & yajiwa" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["微博热搜"], -} - -wbtop = on_command("wbtop", aliases={"微博热搜"}, priority=5, block=True) - - -wbtop_url = "https://weibo.com/ajax/side/hotSearch" - -wbtop_data = [] - - -@wbtop.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - global wbtop_data - msg = arg.extract_plain_text().strip() - if wbtop_data: - now_time = datetime.datetime.now() - if now_time > wbtop_data["time"] + datetime.timedelta(minutes=5): - data, code = await get_wbtop(wbtop_url) - if code != 200: - await wbtop.finish(data, at_sender=True) - else: - wbtop_data = data - else: - data, code = await get_wbtop(wbtop_url) - if code != 200: - await wbtop.finish(data, at_sender=True) - else: - wbtop_data = data - - if not msg: - img = await asyncio.get_event_loop().run_in_executor( - None, gen_wbtop_pic, wbtop_data["data"] - ) - await wbtop.send(img) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询微博热搜" - ) - if is_number(msg) and 0 < int(msg) <= 50: - url = wbtop_data["data"][int(msg) - 1]["url"] - await wbtop.send("开始截取数据...") - img = await AsyncPlaywright.screenshot( - url, - f"{IMAGE_PATH}/temp/wbtop_{event.user_id}.png", - "#pl_feed_main", - wait_time=12 - ) - if img: - await wbtop.finish(img) - else: - await wbtop.finish("发生了一些错误.....") - diff --git a/plugins/wbtop/data_source.py b/plugins/wbtop/data_source.py deleted file mode 100644 index c3bbc915..00000000 --- a/plugins/wbtop/data_source.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageSegment -from utils.image_utils import BuildImage -from utils.message_builder import image -from configs.path_config import IMAGE_PATH -from typing import Tuple, Union -from utils.http_utils import AsyncHttpx -import datetime - - -async def get_wbtop(url: str) -> Tuple[Union[dict, str], int]: - """ - :param url: 请求链接 - """ - n = 0 - while True: - try: - data = [] - get_response = (await AsyncHttpx.get(url, timeout=20)) - if get_response.status_code == 200: - data_json = get_response.json()['data']['realtime'] - for data_item in data_json: - # 如果是广告,则不添加 - if 'is_ad' in data_item: - continue - dic = { - 'hot_word': data_item['note'], - 'hot_word_num': str(data_item['num']), - 'url': 'https://s.weibo.com/weibo?q=%23' + data_item['word'] + '%23', - } - data.append(dic) - if not data: - return "没有搜索到...", 997 - return {'data': data, 'time': datetime.datetime.now()}, 200 - else: - if n > 2: - return f'获取失败,请十分钟后再试', 999 - else: - n += 1 - continue - except TimeoutError: - return "超时了....", 998 - - -def gen_wbtop_pic(data: dict) -> MessageSegment: - """ - 生成微博热搜图片 - :param data: 微博热搜数据 - """ - bk = BuildImage(700, 32 * 50 + 280, 700, 32, color="#797979") - wbtop_bk = BuildImage(700, 280, background=f"{IMAGE_PATH}/other/webtop.png") - bk.paste(wbtop_bk) - text_bk = BuildImage(700, 32 * 50, 700, 32, color="#797979") - for i, data in enumerate(data): - title = f"{i + 1}. {data['hot_word']}" - hot = str(data["hot_word_num"]) - img = BuildImage(700, 30, font_size=20) - w, h = img.getsize(title) - img.text((10, int((30 - h) / 2)), title) - img.text((580, int((30 - h) / 2)), hot) - text_bk.paste(img) - bk.paste(text_bk, (0, 280)) - return image(b64=bk.pic2bs4()) diff --git a/plugins/weather/__init__.py b/plugins/weather/__init__.py deleted file mode 100755 index 64eb2de0..00000000 --- a/plugins/weather/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -from nonebot import on_regex -from .data_source import get_weather_of_city, get_city_list -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from jieba import posseg -from services.log import logger -from nonebot.params import RegexGroup -from typing import Tuple, Any - - -__zx_plugin_name__ = "天气查询" -__plugin_usage__ = """ -usage: - 普普通通的查天气吧 - 指令: - [城市]天气 -""".strip() -__plugin_des__ = "出门要看看天气,不要忘了带伞" -__plugin_cmd__ = ["[城市]天气/天气[城市]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查询天气", "天气", "天气查询", "查天气"], -} - - -weather = on_regex(r".{0,10}?(.*)的?天气.{0,10}", priority=5, block=True) - - -@weather.handle() -async def _(event: MessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - msg = reg_group[0] - if msg and msg[-1] != "市": - msg += "市" - city = "" - if msg: - city_list = get_city_list() - for word in posseg.lcut(msg): - if word.flag == "ns" or word.word[:-1] in city_list: - city = str(word.word).strip() - break - if word.word == "火星": - await weather.finish( - "没想到你个小呆子还真的想看火星天气!\n火星大气中含有95%的二氧化碳,气压低,加之极度的干燥," - "就阻止了水的形成积聚。这意味着火星几乎没有云,冰层覆盖了火星的两极,它们的融化和冻结受到火星与太" - "阳远近距离的影响,它产生了强大的尘埃云,阻挡了太阳光,使冰层的融化慢下来。\n所以说火星天气太恶劣了," - "去过一次就不想再去第二次了" - ) - if city: - city_weather = await get_weather_of_city(city) - logger.info( - f'(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else "private"} ) ' - f"查询天气:" + city - ) - await weather.finish(city_weather) diff --git a/plugins/weather/data_source.py b/plugins/weather/data_source.py deleted file mode 100755 index 8fd54f5c..00000000 --- a/plugins/weather/data_source.py +++ /dev/null @@ -1,79 +0,0 @@ -from utils.message_builder import image -from configs.path_config import TEXT_PATH -from configs.config import NICKNAME -from typing import List -from nonebot import Driver -from utils.http_utils import AsyncHttpx -import ujson as json -import nonebot - -driver: Driver = nonebot.get_driver() - -china_city = TEXT_PATH / "china_city.json" - -data = {} - - -async def get_weather_of_city(city: str) -> str: - """ - 获取城市天气数据 - :param city: 城市 - """ - code = _check_exists_city(city) - if code == 999: - return "不要查一个省份的天气啊,很累人的!" - elif code == 998: - return f"{NICKNAME}没查到!!试试查火星的天气?" - else: - data_json = ( - await AsyncHttpx.get( - f"https://v0.yiketianqi.com/api?unescape=1&version=v91&appid=43656176&appsecret=I42og6Lm&ext=&cityid=&city={city[:-1]}" - ) - ).json() - if wh := data_json.get('data'): - w_type = wh[0]["wea_day"] - w_max = wh[0]["tem1"] - w_min = wh[0]["tem2"] - fengli = wh[0]["win_speed"] - ganmao = wh[0]["narrative"] - fengxiang = ','.join(wh[0].get('win', [])) - repass = f"{city}的天气是 {w_type} 天\n最高温度: {w_max}\n最低温度: {w_min}\n风力: {fengli} {fengxiang}\n{ganmao}" - return repass - else: - return data_json.get("errmsg") or "好像出错了?再试试?" - - -def _check_exists_city(city: str) -> int: - """ - 检测城市是否存在合法 - :param city: 城市名称 - """ - global data - city = city if city[-1] != "市" else city[:-1] - for province in data.keys(): - for city_ in data[province]: - if city_ == city: - return 200 - for province in data.keys(): - if city == province: - return 999 - return 998 - - -def get_city_list() -> List[str]: - """ - 获取城市列表 - """ - global data - if not data: - try: - with open(china_city, "r", encoding="utf8") as f: - data = json.load(f) - except FileNotFoundError: - data = {} - city_list = [] - for p in data.keys(): - for c in data[p]: - city_list.append(c) - city_list.append(p) - return city_list diff --git a/plugins/web_ui/api/tabs/manage/__init__.py b/plugins/web_ui/api/tabs/manage/__init__.py deleted file mode 100644 index 3bfdf9d5..00000000 --- a/plugins/web_ui/api/tabs/manage/__init__.py +++ /dev/null @@ -1,462 +0,0 @@ -import re -from typing import Literal, Optional - -import nonebot -from fastapi import APIRouter -from nonebot.adapters.onebot.v11.exception import ActionFailed -from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState -from tortoise.functions import Count -from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK - -from configs.config import NICKNAME -from models.ban_user import BanUser -from models.chat_history import ChatHistory -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from models.group_member_info import GroupInfoUser -from models.statistics import Statistics -from services.log import logger -from utils.manager import group_manager, plugin_data_manager, requests_manager -from utils.utils import get_bot - -from ....base_model import Result -from ....config import AVA_URL, GROUP_AVA_URL -from ....utils import authentication -from ...logs.log_manager import LOG_STORAGE -from .model import ( - DeleteFriend, - Friend, - FriendRequestResult, - GroupDetail, - GroupRequestResult, - GroupResult, - HandleRequest, - LeaveGroup, - Message, - MessageItem, - Plugin, - ReqResult, - SendMessage, - Task, - UpdateGroup, - UserDetail, -) - -ws_router = APIRouter() -router = APIRouter(prefix="/manage") - -SUB_PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" - -GROUP_PATTERN = r'.*?Message (-?\d*) from (\d*)@\[群:(\d*)] "(.*)"' - -PRIVATE_PATTERN = r'.*?Message (-?\d*) from (\d*) "(.*)"' - -AT_PATTERN = r'\[CQ:at,qq=(.*)\]' - -IMAGE_PATTERN = r'\[CQ:image,.*,url=(.*);.*?\]' - -@router.get("/get_group_list", dependencies=[authentication()], description="获取群组列表") -async def _(bot_id: str) -> Result: - """ - 获取群信息 - """ - if bots := nonebot.get_bots(): - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") - group_list_result = [] - try: - group_info = {} - group_list = await bots[bot_id].get_group_list() - for g in group_list: - gid = g['group_id'] - g['ava_url'] = GROUP_AVA_URL.format(gid, gid) - group_list_result.append(GroupResult(**g)) - except Exception as e: - logger.error("调用API错误", "/get_group_list", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(group_list_result, "拿到了新鲜出炉的数据!") - return Result.warning_("无Bot连接...") - - -@router.post("/update_group", dependencies=[authentication()], description="修改群组信息") -async def _(group: UpdateGroup) -> Result: - try: - group_id = group.group_id - group_manager.set_group_level(group_id, group.level) - if group.status: - group_manager.turn_on_group_bot_status(group_id) - else: - group_manager.shutdown_group_bot_status(group_id) - all_task = group_manager.get_task_data().keys() - if group.task: - for task in all_task: - if task in group.task: - group_manager.open_group_task(group_id, task) - else: - group_manager.close_group_task(group_id, task) - group_manager[group_id].close_plugins = group.close_plugins - group_manager.save() - except Exception as e: - logger.error("调用API错误", "/get_group", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(info="已完成记录!") - - -@router.get("/get_friend_list", dependencies=[authentication()], description="获取好友列表") -async def _(bot_id: str) -> Result: - """ - 获取群信息 - """ - if bots := nonebot.get_bots(): - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") - try: - friend_list = await bots[bot_id].get_friend_list() - for f in friend_list: - f['ava_url'] = AVA_URL.format(f['user_id']) - return Result.ok([Friend(**f) for f in friend_list if str(f['user_id']) != bot_id], "拿到了新鲜出炉的数据!") - except Exception as e: - logger.error("调用API错误", "/get_group_list", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.warning_("无Bot连接...") - - -@router.get("/get_request_count", dependencies=[authentication()], description="获取请求数量") -def _() -> Result: - data = { - "friend_count": len(requests_manager.get_data().get("private") or []), - "group_count": len(requests_manager.get_data().get("group") or []), - } - return Result.ok(data, f"{NICKNAME}带来了最新的数据!") - - -@router.get("/get_request_list", dependencies=[authentication()], description="获取请求列表") -def _() -> Result: - try: - req_result = ReqResult() - data = requests_manager.get_data() - for type_ in requests_manager.get_data(): - for x in data[type_]: - data[type_][x]["oid"] = x - data[type_][x]['type'] = type_ - if type_ == "private": - data[type_][x]['ava_url'] = AVA_URL.format(data[type_][x]['id']) - req_result.friend.append(FriendRequestResult(**data[type_][x])) - else: - gid = data[type_][x]['id'] - data[type_][x]['ava_url'] = GROUP_AVA_URL.format(gid, gid) - req_result.group.append(GroupRequestResult(**data[type_][x])) - req_result.friend.reverse() - req_result.group.reverse() - except Exception as e: - logger.error("调用API错误", "/get_request", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(req_result, f"{NICKNAME}带来了最新的数据!") - - -@router.delete("/clear_request", dependencies=[authentication()], description="清空请求列表") -def _(request_type: Literal["private", "group"]) -> Result: - """ - 清空请求 - :param type_: 类型 - """ - requests_manager.clear(request_type) - return Result.ok(info="成功清除了数据!") - - -@router.post("/refuse_request", dependencies=[authentication()], description="拒绝请求") -async def _(parma: HandleRequest) -> Result: - """ - 操作请求 - :param parma: 参数 - """ - try: - if bots := nonebot.get_bots(): - bot_id = parma.bot_id - if bot_id not in nonebot.get_bots(): - return Result.warning_("指定Bot未连接...") - try: - flag = await requests_manager.refused(bots[bot_id], parma.flag, parma.request_type) # type: ignore - except ActionFailed as e: - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") - if flag == 1: - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.warning_("该请求已失效...") - elif flag == 2: - return Result.warning_("未找到此Id请求...") - return Result.ok(info="成功处理了请求!") - return Result.warning_("无Bot连接...") - except Exception as e: - logger.error("调用API错误", "/refuse_request", e=e) - return Result.fail(f"{type(e)}: {e}") - - -@router.post("/delete_request", dependencies=[authentication()], description="忽略请求") -async def _(parma: HandleRequest) -> Result: - """ - 操作请求 - :param parma: 参数 - """ - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.ok(info="成功处理了请求!") - - -@router.post("/approve_request", dependencies=[authentication()], description="同意请求") -async def _(parma: HandleRequest) -> Result: - """ - 操作请求 - :param parma: 参数 - """ - try: - if bots := nonebot.get_bots(): - bot_id = parma.bot_id - if bot_id not in nonebot.get_bots(): - return Result.warning_("指定Bot未连接...") - if parma.request_type == "group": - if rid := requests_manager.get_group_id(parma.flag): - if group := await GroupInfo.get_or_none(group_id=str(rid)): - await group.update_or_create(group_flag=1) - else: - group_info = await bots[bot_id].get_group_info(group_id=rid) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) - try: - await requests_manager.approve(bots[bot_id], parma.flag, parma.request_type) # type: ignore - return Result.ok(info="成功处理了请求!") - except ActionFailed as e: - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") - return Result.warning_("无Bot连接...") - except Exception as e: - logger.error("调用API错误", "/approve_request", e=e) - return Result.fail(f"{type(e)}: {e}") - - -@router.post("/leave_group", dependencies=[authentication()], description="退群") -async def _(param: LeaveGroup) -> Result: - try: - if bots := nonebot.get_bots(): - bot_id = param.bot_id - group_list = await bots[bot_id].get_group_list() - if param.group_id not in [str(g["group_id"]) for g in group_list]: - return Result.warning_("Bot未在该群聊中...") - await bots[bot_id].set_group_leave(group_id=param.group_id) - return Result.ok(info="成功处理了请求!") - return Result.warning_("无Bot连接...") - except Exception as e: - logger.error("调用API错误", "/leave_group", e=e) - return Result.fail(f"{type(e)}: {e}") - - -@router.post("/delete_friend", dependencies=[authentication()], description="删除好友") -async def _(param: DeleteFriend) -> Result: - try: - if bots := nonebot.get_bots(): - bot_id = param.bot_id - friend_list = await bots[bot_id].get_friend_list() - if param.user_id not in [str(g["user_id"]) for g in friend_list]: - return Result.warning_("Bot未有其好友...") - await bots[bot_id].delete_friend(user_id=param.user_id) - return Result.ok(info="成功处理了请求!") - return Result.warning_("Bot未连接...") - except Exception as e: - logger.error("调用API错误", "/delete_friend", e=e) - return Result.fail(f"{type(e)}: {e}") - - - -@router.get("/get_friend_detail", dependencies=[authentication()], description="获取好友详情") -async def _(bot_id: str, user_id: str) -> Result: - if bots := nonebot.get_bots(): - if bot_id in bots: - if fd := [x for x in await bots[bot_id].get_friend_list() if str(x['user_id']) == user_id]: - like_plugin_list = ( - await Statistics.filter(user_id=user_id).annotate(count=Count("id")) - .group_by("plugin_name").order_by("-count").limit(5) - .values_list("plugin_name", "count") - ) - like_plugin = {} - for data in like_plugin_list: - name = data[0] - if plugin_data := plugin_data_manager.get(data[0]): - name = plugin_data.name - like_plugin[name] = data[1] - user = fd[0] - user_detail = UserDetail( - user_id=user_id, - ava_url=AVA_URL.format(user_id), - nickname=user['nickname'], - remark=user['remark'], - is_ban=await BanUser.is_ban(user_id), - chat_count=await ChatHistory.filter(user_id=user_id).count(), - call_count=await Statistics.filter(user_id=user_id).count(), - like_plugin=like_plugin, - ) - return Result.ok(user_detail) - else: - return Result.warning_("未添加指定好友...") - return Result.warning_("无Bot连接...") - - -@router.get("/get_group_detail", dependencies=[authentication()], description="获取群组详情") -async def _(bot_id: str, group_id: str) -> Result: - if bots := nonebot.get_bots(): - if bot_id in bots: - group_info = await bots[bot_id].get_group_info(group_id=int(group_id)) - g = group_manager[group_id] - if not g: - return Result.warning_("指定群组未被收录...") - if group_info: - like_plugin_list = ( - await Statistics.filter(group_id=group_id).annotate(count=Count("id")) - .group_by("plugin_name").order_by("-count").limit(5) - .values_list("plugin_name", "count") - ) - like_plugin = {} - for data in like_plugin_list: - name = data[0] - if plugin_data := plugin_data_manager.get(data[0]): - name = plugin_data.name - like_plugin[name] = data[1] - close_plugins = [] - for module in g.close_plugins: - module_ = module.replace(":super", "") - is_super_block = module.endswith(":super") - plugin = Plugin(module=module_, plugin_name=module, is_super_block=is_super_block) - if plugin_data := plugin_data_manager.get(module_): - plugin.plugin_name = plugin_data.name - close_plugins.append(plugin) - task_list = [] - task_data = group_manager.get_task_data() - for tn, status in g.group_task_status.items(): - task_list.append( - Task( - name=tn, - zh_name=task_data.get(tn) or tn, - status=status - ) - ) - group_detail = GroupDetail( - group_id=group_id, - ava_url=GROUP_AVA_URL.format(group_id, group_id), - name=group_info['group_name'], - member_count=group_info['member_count'], - max_member_count=group_info['max_member_count'], - chat_count=await ChatHistory.filter(group_id=group_id).count(), - call_count=await Statistics.filter(group_id=group_id).count(), - like_plugin=like_plugin, - level=g.level, - status=g.status, - close_plugins=close_plugins, - task=task_list - ) - return Result.ok(group_detail) - else: - return Result.warning_("未添加指定群组...") - return Result.warning_("无Bot连接...") - - -@router.post("/send_message", dependencies=[authentication()], description="获取群组详情") -async def _(param: SendMessage) -> Result: - if bots := nonebot.get_bots(): - if param.bot_id in bots: - try: - if param.user_id: - await bots[param.bot_id].send_private_msg(user_id=str(param.user_id), message=param.message) - else: - await bots[param.bot_id].send_group_msg(group_id=str(param.group_id), message=param.message) - except Exception as e: - return Result.fail(str(e)) - return Result.ok("发送成功!") - return Result.warning_("指定Bot未连接...") - return Result.warning_("无Bot连接...") - -MSG_LIST = [] - -ID2NAME = {} - - -async def message_handle(sub_log: str, type: Literal["private", "group"]): - global MSG_LIST, ID2NAME - pattern = PRIVATE_PATTERN if type == 'private' else GROUP_PATTERN - msg_id = None - uid = None - gid = None - msg = None - img_list = re.findall(IMAGE_PATTERN, sub_log) - if r := re.search(pattern, sub_log): - if type == 'private': - msg_id = r.group(1) - uid = r.group(2) - msg = r.group(3) - if uid not in ID2NAME: - user = await FriendUser.filter(user_id=uid).first() - ID2NAME[uid] = user.user_name or user.nickname - else: - msg_id = r.group(1) - uid = r.group(2) - gid = r.group(3) - msg = r.group(4) - if gid not in ID2NAME: - user = await GroupInfoUser.filter(user_id=uid, group_id=gid).first() - ID2NAME[uid] = user.user_name or user.nickname - if at_list := re.findall(AT_PATTERN, msg): - user_list = await GroupInfoUser.filter(user_id__in=at_list, group_id=gid).all() - id2name = {u.user_id: (u.user_name or u.nickname) for u in user_list} - for qq in at_list: - msg = re.sub(rf'\[CQ:at,qq={qq}\]', f"@{id2name[qq] or ''}", msg) - if msg_id in MSG_LIST: - return - MSG_LIST.append(msg_id) - messages = [] - rep = re.split(r'\[CQ:image.*\]', msg) - if img_list: - for i in range(len(rep)): - messages.append(MessageItem(type="text", msg=rep[i])) - if i < len(img_list): - messages.append(MessageItem(type="img", msg=img_list[i])) - else: - messages = [MessageItem(type="text", msg=x) for x in rep] - return Message( - object_id=uid if type == 'private' else gid, - user_id=uid, - group_id=gid, - message=messages, - name=ID2NAME.get(uid) or "", - ava_url=AVA_URL.format(uid), - ) - -@ws_router.websocket("/chat") -async def _(websocket: WebSocket): - await websocket.accept() - - async def log_listener(log: str): - global MSG_LIST, ID2NAME - sub_log = re.sub(SUB_PATTERN, "", log) - img_list = re.findall(IMAGE_PATTERN, sub_log) - if "message.private.friend" in log: - if message := await message_handle(sub_log, 'private'): - await websocket.send_json(message.dict()) - else: - if r := re.search(GROUP_PATTERN, sub_log): - if message := await message_handle(sub_log, 'group'): - await websocket.send_json(message.dict()) - if len(MSG_LIST) > 30: - MSG_LIST = MSG_LIST[-1:] - LOG_STORAGE.listeners.add(log_listener) - try: - while websocket.client_state == WebSocketState.CONNECTED: - recv = await websocket.receive() - except WebSocketDisconnect: - pass - finally: - LOG_STORAGE.listeners.remove(log_listener) - return \ No newline at end of file diff --git a/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/plugins/web_ui/api/tabs/plugin_manage/__init__.py deleted file mode 100644 index 20e5fd97..00000000 --- a/plugins/web_ui/api/tabs/plugin_manage/__init__.py +++ /dev/null @@ -1,198 +0,0 @@ -import re -from typing import List, Optional - -import cattrs -from fastapi import APIRouter, Query - -from configs.config import Config -from services.log import logger -from utils.manager import plugin_data_manager, plugins2settings_manager, plugins_manager -from utils.manager.models import PluginData, PluginSetting, PluginType - -from ....base_model import Result -from ....utils import authentication -from .model import ( - PluginConfig, - PluginCount, - PluginDetail, - PluginInfo, - PluginSwitch, - UpdatePlugin, -) - -router = APIRouter(prefix="/plugin") - - -@router.get("/get_plugin_list", dependencies=[authentication()], deprecated="获取插件列表") -def _( - plugin_type: List[PluginType] = Query(None), menu_type: Optional[str] = None -) -> Result: - """ - 获取插件列表 - :param plugin_type: 类型 normal, superuser, hidden, admin - :param menu_type: 菜单类型 - """ - try: - plugin_list: List[PluginInfo] = [] - for module in plugin_data_manager.keys(): - plugin_data: Optional[PluginData] = plugin_data_manager[module] - if plugin_data and plugin_data.plugin_type in plugin_type: - setting = plugin_data.plugin_setting or PluginSetting() - plugin = plugin_data.plugin_status - menu_type_ = getattr(setting, "plugin_type", ["无"])[0] - if menu_type and menu_type != menu_type_: - continue - plugin_info = PluginInfo( - module=module, - plugin_name=plugin_data.name, - default_status=getattr(setting, "default_status", False), - limit_superuser=getattr(setting, "limit_superuser", False), - cost_gold=getattr(setting, "cost_gold", 0), - menu_type=menu_type_, - version=(plugin.version or 0) if plugin else 0, - level=getattr(setting, "level", 5), - status=plugin.status if plugin else False, - author=plugin.author if plugin else None - ) - plugin_info.version = (plugin.version or 0) if plugin else 0 - plugin_list.append(plugin_info) - except Exception as e: - logger.error("调用API错误", "/get_plugins", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(plugin_list, "拿到了新鲜出炉的数据!") - -@router.get("/get_plugin_count", dependencies=[authentication()], deprecated="获取插件数量") -def _() -> Result: - plugin_count = PluginCount() - for module in plugin_data_manager.keys(): - plugin_data: Optional[PluginData] = plugin_data_manager[module] - if plugin_data and plugin_data.plugin_type == PluginType.NORMAL: - plugin_count.normal += 1 - elif plugin_data and plugin_data.plugin_type == PluginType.ADMIN: - plugin_count.admin += 1 - elif plugin_data and plugin_data.plugin_type == PluginType.SUPERUSER: - plugin_count.superuser += 1 - else: - plugin_count.other += 1 - return Result.ok(plugin_count) - -@router.post("/update_plugin", dependencies=[authentication()], description="更新插件参数") -def _(plugin: UpdatePlugin) -> Result: - """ - 修改插件信息 - :param plugin: 插件内容 - """ - try: - module = plugin.module - if p2s := plugins2settings_manager.get(module): - p2s.default_status = plugin.default_status - p2s.limit_superuser = plugin.limit_superuser - p2s.cost_gold = plugin.cost_gold - # p2s.cmd = plugin.cmd.split(",") if plugin.cmd else [] - p2s.level = plugin.level - menu_lin = None - if len(p2s.plugin_type) > 1: - menu_lin = p2s.menu_type[1] - if menu_lin is not None: - p2s.plugin_type = (plugin.menu_type, menu_lin) - else: - p2s.plugin_type = (plugin.menu_type,) - if pm := plugins_manager.get(module): - if plugin.block_type: - pm.block_type = plugin.block_type - pm.status = False - else: - pm.block_type = None - pm.status = True - plugins2settings_manager.save() - plugins_manager.save() - # 配置项 - if plugin.configs and (configs := Config.get(module)): - for key in plugin.configs: - if c := configs.configs.get(key): - value = plugin.configs[key] - # if isinstance(c.value, (list, tuple)) or isinstance( - # c.default_value, (list, tuple) - # ): - # value = value.split(",") - if c.type and value is not None: - value = cattrs.structure(value, c.type) - Config.set_config(module, key, value) - plugin_data_manager.reload() - except Exception as e: - logger.error("调用API错误", "/update_plugins", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(info="已经帮你写好啦!") - - -@router.post("/change_switch", dependencies=[authentication()], description="开关插件") -def _(param: PluginSwitch) -> Result: - if pm := plugins_manager.get(param.module): - pm.block_type = None if param.status else 'all' - pm.status = param.status - plugins_manager.save() - return Result.ok(info="成功改变了开关状态!") - return Result.warning_("未获取该插件的配置!") - - -@router.get("/get_plugin_menu_type", dependencies=[authentication()], description="获取插件类型") -def _() -> Result: - menu_type_list = [] - for module in plugin_data_manager.keys(): - plugin_data: Optional[PluginData] = plugin_data_manager[module] - if plugin_data: - setting = plugin_data.plugin_setting or PluginSetting() - menu_type = getattr(setting, "plugin_type", ["无"])[0] - if menu_type not in menu_type_list: - menu_type_list.append(menu_type) - return Result.ok(menu_type_list) - - -@router.get("/get_plugin", dependencies=[authentication()], description="获取插件详情") -def _(module: str) -> Result: - if plugin_data := plugin_data_manager.get(module): - setting = plugin_data.plugin_setting or PluginSetting() - plugin = plugin_data.plugin_status - config_list = [] - if config := Config.get(module): - for cfg in config.configs: - type_str = "" - type_inner = None - x = str(config.configs[cfg].type) - r = re.search(r"",str(config.configs[cfg].type)) - if r: - type_str = r.group(1) - else: - r = re.search(r"typing\.(.*)\[(.*)\]",str(config.configs[cfg].type)) - if r: - type_str = r.group(1) - if type_str: - type_str = type_str.lower() - type_inner = r.group(2) - if type_inner: - type_inner = [x.strip() for x in type_inner.split(",")] - config_list.append(PluginConfig( - module=module, - key=cfg, - value=config.configs[cfg].value, - help=config.configs[cfg].help, - default_value=config.configs[cfg].default_value, - type=type_str, - type_inner=type_inner - )) - plugin_info = PluginDetail( - module=module, - plugin_name=plugin_data.name, - default_status=getattr(setting, "default_status", False), - limit_superuser=getattr(setting, "limit_superuser", False), - cost_gold=getattr(setting, "cost_gold", 0), - menu_type=getattr(setting, "plugin_type", ["无"])[0], - version=(plugin.version or 0) if plugin else 0, - level=getattr(setting, "level", 5), - status=plugin.status if plugin else False, - author=plugin.author if plugin else None, - config_list=config_list, - block_type=getattr(plugin, "block_type", None) - ) - return Result.ok(plugin_info) - return Result.warning_("未获取到插件详情...") \ No newline at end of file diff --git a/plugins/web_ui/config.py b/plugins/web_ui/config.py deleted file mode 100644 index b6e6fde0..00000000 --- a/plugins/web_ui/config.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime -from typing import Any, Dict, List, Optional, Union - -import nonebot -from fastapi import APIRouter -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel -from strenum import StrEnum - -app = nonebot.get_app() - -origins = ["*"] - -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160" - -GROUP_AVA_URL = "http://p.qlogo.cn/gh/{}/{}/640/" - - -class QueryDateType(StrEnum): - - """ - 查询日期类型 - """ - - DAY = "day" - """日""" - WEEK = "week" - """周""" - MONTH = "month" - """月""" - YEAR = "year" - """年""" - - -# class SystemNetwork(BaseModel): -# """ -# 系统网络状态 -# """ - -# baidu: int -# google: int - - -# class SystemFolderSize(BaseModel): -# """ -# 资源文件占比 -# """ - -# font_dir_size: float -# image_dir_size: float -# text_dir_size: float -# record_dir_size: float -# temp_dir_size: float -# data_dir_size: float -# log_dir_size: float -# check_time: datetime - - -# class SystemStatusList(BaseModel): -# """ -# 状态记录 -# """ - -# cpu_data: List[Dict[str, Union[float, str]]] -# memory_data: List[Dict[str, Union[float, str]]] -# disk_data: List[Dict[str, Union[float, str]]] - - -# class SystemResult(BaseModel): -# """ -# 系统api返回 -# """ - -# status: SystemStatus -# network: SystemNetwork -# disk: SystemFolderSize -# check_time: datetime diff --git a/plugins/what_anime/__init__.py b/plugins/what_anime/__init__.py deleted file mode 100755 index bae70bd2..00000000 --- a/plugins/what_anime/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message, Bot -from nonebot.internal.params import ArgStr, Arg -from nonebot.params import CommandArg - -from .data_source import get_anime -from nonebot import on_command -from nonebot.typing import T_State -from utils.utils import get_message_img -from services.log import logger - - -__zx_plugin_name__ = "识番" -__plugin_usage__ = """ -usage: - api.trace.moe 以图识番 - 指令: - 识番 [图片] -""".strip() -__plugin_des__ = "以图识番" -__plugin_cmd__ = ["识番 [图片]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["识番"], -} - - -what_anime = on_command("识番", priority=5, block=True) - - -@what_anime.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State, args: Message = CommandArg()): - img_url = get_message_img(event.json()) - if img_url: - state["img_url"] = args - - -@what_anime.got("img_url", prompt="虚空识番?来图来图GKD") -async def _(bot: Bot, event: MessageEvent, state: T_State, img_url: Message = Arg("img_url")): - img_url = get_message_img(img_url) - if not img_url: - await what_anime.reject_arg("img_url", "发送的必须是图片!") - img_url = img_url[0] - await what_anime.send("开始识别.....") - anime_data_report = await get_anime(img_url) - if anime_data_report: - await what_anime.send(anime_data_report, at_sender=True) - logger.info( - f"USER {event.user_id} GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}" - f" 识番 {img_url} --> {anime_data_report}" - ) - else: - logger.info( - f"USER {event.user_id} GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 识番 {img_url} 未找到" - ) - await what_anime.send(f"没有寻找到该番剧,果咩..", at_sender=True) diff --git a/plugins/white2black_image.py b/plugins/white2black_image.py deleted file mode 100755 index 730740c7..00000000 --- a/plugins/white2black_image.py +++ /dev/null @@ -1,143 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot import on_command -from nonebot.params import CommandArg -from utils.utils import get_message_img, is_chinese -from utils.message_builder import image -from configs.path_config import TEMP_PATH -from utils.image_utils import BuildImage -from services.log import logger -from utils.http_utils import AsyncHttpx - -# ZH_CN2EN 中文 » 英语 -# ZH_CN2JA 中文 » 日语 -# ZH_CN2KR 中文 » 韩语 -# ZH_CN2FR 中文 » 法语 -# ZH_CN2RU 中文 » 俄语 -# ZH_CN2SP 中文 » 西语 -# EN2ZH_CN 英语 » 中文 -# JA2ZH_CN 日语 » 中文 -# KR2ZH_CN 韩语 » 中文 -# FR2ZH_CN 法语 » 中文 -# RU2ZH_CN 俄语 » 中文 -# SP2ZH_CN 西语 » 中文 - - -__zx_plugin_name__ = "黑白草图" -__plugin_usage__ = """ -usage: - 将图片黑白化并配上中文与日语 - 指令: - 黑白图 [文本] [图片] -""".strip() -__plugin_des__ = "为设想过得黑白草图" -__plugin_cmd__ = ["黑白图"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["黑白图", "黑白草图"], -} - -w2b_img = on_command("黑白草图", aliases={"黑白图"}, priority=5, block=True) - - -@w2b_img.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - # try: - img = get_message_img(event.json()) - msg = arg.extract_plain_text().strip() - if not img or not msg: - await w2b_img.finish(f"格式错误:\n" + __plugin_usage__) - img = img[0] - if not await AsyncHttpx.download_file( - img, TEMP_PATH / f"{event.user_id}_w2b.png" - ): - await w2b_img.finish("下载图片失败...请稍后再试...") - msg = await get_translate(msg) - w2b = BuildImage(0, 0, background=TEMP_PATH / f"{event.user_id}_w2b.png") - w2b.convert("L") - msg_sp = msg.split("<|>") - w, h = w2b.size - add_h, font_size = init_h_font_size(h) - bg = BuildImage(w, h + add_h, color="black", font_size=int(font_size)) - bg.paste(w2b) - chinese_msg = formalization_msg(msg) - if not bg.check_font_size(chinese_msg): - if len(msg_sp) == 1: - centered_text(bg, chinese_msg, add_h) - else: - centered_text(bg, chinese_msg + "<|>" + msg_sp[1], add_h) - elif not bg.check_font_size(msg_sp[0]): - centered_text(bg, msg, add_h) - else: - ratio = (bg.getsize(msg_sp[0])[0] + 20) / bg.w - add_h = add_h * ratio - bg.resize(ratio) - centered_text(bg, msg, add_h) - await w2b_img.send(image(b64=bg.pic2bs4())) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 制作黑白草图 {msg}" - ) - - -def centered_text(img: BuildImage, text: str, add_h: int): - top_h = img.h - add_h + (img.h / 100) - bottom_h = img.h - (img.h / 100) - text_sp = text.split("<|>") - w, h = img.getsize(text_sp[0]) - if len(text_sp) == 1: - w = int((img.w - w) / 2) - h = int(top_h + (bottom_h - top_h - h) / 2) - img.text((w, h), text_sp[0], (255, 255, 255)) - else: - br_h = int(top_h + (bottom_h - top_h) / 2) - w = int((img.w - w) / 2) - h = int(top_h + (br_h - top_h - h) / 2) - img.text((w, h), text_sp[0], (255, 255, 255)) - w, h = img.getsize(text_sp[1]) - w = int((img.w - w) / 2) - h = int(br_h + (bottom_h - br_h - h) / 2) - img.text((w, h), text_sp[1], (255, 255, 255)) - - -async def get_translate(msg: str) -> str: - url = f"http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule&smartresult=ugc&sessionFrom=null" - data = { - "type": "ZH_CN2JA", - "i": msg, - "doctype": "json", - "version": "2.1", - "keyfrom": "fanyi.web", - "ue": "UTF-8", - "action": "FY_BY_CLICKBUTTON", - "typoResult": "true", - } - data = (await AsyncHttpx.post(url, data=data)).json() - if data["errorCode"] == 0: - translate = data["translateResult"][0][0]["tgt"] - msg += "<|>" + translate - return msg - - -def formalization_msg(msg: str) -> str: - rst = "" - for i in range(len(msg)): - if is_chinese(msg[i]): - rst += msg[i] + " " - else: - rst += msg[i] - if i + 1 < len(msg) and is_chinese(msg[i + 1]) and msg[i].isalpha(): - rst += " " - return rst - - -def init_h_font_size(h): - # 高度 字体 - if h < 400: - return init_h_font_size(400) - elif 400 < h < 800: - return init_h_font_size(800) - return h * 0.2, h * 0.05 diff --git a/plugins/withdraw.py b/plugins/withdraw.py deleted file mode 100755 index 795c8cc7..00000000 --- a/plugins/withdraw.py +++ /dev/null @@ -1,29 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent -from nonebot.typing import T_State -import re - - -__zx_plugin_name__ = "消息撤回 [Admin]" -__plugin_usage__ = """ -usage: - 简易的消息撤回机制 - 指令: - [回复]撤回 -""".strip() -__plugin_des__ = "消息撤回机制" -__plugin_cmd__ = ["[回复]撤回"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": 0, -} - - -withdraw_msg = on_command("撤回", priority=5, block=True) - - -@withdraw_msg.handle() -async def _(bot: Bot, event: GroupMessageEvent, state: T_State): - if event.reply: - await bot.delete_msg(message_id=event.reply.message_id) diff --git a/plugins/word_bank/__init__.py b/plugins/word_bank/__init__.py deleted file mode 100644 index 53560769..00000000 --- a/plugins/word_bank/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from pathlib import Path - -import nonebot - -from configs.config import Config -from utils.utils import GDict - -Config.add_plugin_config( - "word_bank", - "WORD_BANK_LEVEL [LEVEL]", - 5, - name="词库问答", - help_="设置增删词库的权限等级", - default_value=5, - type=int, -) - -GDict["run_sql"].append("ALTER TABLE word_bank2 ADD to_me VARCHAR(255);") - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/word_bank/_data_source.py b/plugins/word_bank/_data_source.py deleted file mode 100644 index ea383222..00000000 --- a/plugins/word_bank/_data_source.py +++ /dev/null @@ -1,270 +0,0 @@ -import random -import time -from pathlib import Path -from typing import Any, List, Optional, Tuple, Union - -import nonebot -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from services import logger -from utils.image_utils import text2image -from utils.message_builder import image -from utils.utils import is_number - -from ._model import WordBank - -driver = nonebot.get_driver() - - -async def get_problem_str( - id_: Union[str, int], group_id: Optional[str] = None, word_scope: int = 1 -) -> Tuple[str, int]: - """ - 说明: - 通过id获取问题字符串 - 参数: - :param id_: 下标 - :param group_id: 群号 - :param word_scope: 获取类型 - """ - if word_scope in [0, 2]: - all_problem = await WordBank.get_problem_by_scope(word_scope) - elif group_id: - all_problem = await WordBank.get_group_all_problem(group_id) - else: - raise Exception("词条类型与群组id不能为空") - if isinstance(id_, str) and id_.startswith("id:"): - id_ = id_.split(":")[-1] - if not is_number(id_) or int(id_) < 0 or int(id_) > len(all_problem): - return "id必须为数字且在范围内", 999 - return all_problem[int(id_)][0], 200 - - -async def update_word( - params: str, group_id: Optional[str] = None, word_scope: int = 1 -) -> str: - """ - 说明: - 修改群词条 - 参数: - :param params: 参数 - :param group_id: 群号 - :param word_scope: 词条范围 - """ - return await word_handle(params, group_id, "update", word_scope) - - -async def delete_word( - params: str, group_id: Optional[str] = None, word_scope: int = 1 -) -> str: - """ - 说明: - 删除群词条 - 参数: - :param params: 参数 - :param group_id: 群号 - :param word_scope: 词条范围 - """ - return await word_handle(params, group_id, "delete", word_scope) - - -async def word_handle( - params_: str, group_id: Optional[str], type_: str, word_scope: int = 0 -) -> str: - """ - 说明: - 词条操作 - 参数: - :param params: 参数 - :param group_id: 群号 - :param type_: 类型 - :param word_scope: 词条范围 - """ - params = params_.split() - problem = params[0] - if problem.startswith("id:"): - problem, code = await get_problem_str(problem, group_id, word_scope) - if code != 200: - return problem - if type_ == "delete": - index = params[1] if len(params) > 1 else None - if index: - answer_num = len( - await WordBank.get_problem_all_answer(problem, group_id=group_id) - ) - if not is_number(index) or int(index) < 0 or int(index) > answer_num: - return "指定回答下标id必须为数字且在范围内" - index = int(index) - if await WordBank.delete_group_problem(problem, group_id, index, word_scope): # type: ignore - return "删除词条成功" - return "词条不存在" - if type_ == "update": - replace_str = params[1] - await WordBank.update_group_problem( - problem, replace_str, group_id, word_scope=word_scope - ) - return "修改词条成功" - return "类型错误" - - -async def show_word( - problem: str, - id_: Optional[int], - gid: Optional[int], - group_id: Optional[str] = None, - word_scope: Optional[int] = None, -) -> Union[str, List[Union[str, Message]]]: - if problem: - msg_list = [] - if word_scope is not None: - problem = (await WordBank.get_problem_by_scope(word_scope))[id_][0] # type: ignore - id_ = None - _problem_list = await WordBank.get_problem_all_answer( - problem, - id_ if id_ is not None else gid, - group_id if gid is None else None, - word_scope, - ) - for index, msg in enumerate(_problem_list): - if isinstance(msg, Message): - tmp = "" - for seg in msg: - tmp += seg - msg = tmp - msg_list.append(f"{index}." + msg) - msg_list = [ - f'词条:{problem or (f"id: {id_}" if id_ is not None else f"gid: {gid}")} 的回答' - ] + msg_list - return msg_list # type: ignore - else: - if group_id: - _problem_list = await WordBank.get_group_all_problem(group_id) - elif word_scope is not None: - _problem_list = await WordBank.get_problem_by_scope(word_scope) - else: - raise Exception("群组id和词条范围不能都为空") - global_problem_list = await WordBank.get_problem_by_scope(0) - if not _problem_list and not global_problem_list: - return "未收录任何词条.." - msg_list = await build_message(_problem_list) - global_msg_list = await build_message(global_problem_list) - if global_msg_list: - msg_list.append("###以下为全局词条###") - msg_list = msg_list + global_msg_list - return msg_list - - -async def build_message(_problem_list: List[Tuple[Any, Union[MessageSegment, str]]]): - index = 0 - str_temp_list = [] - msg_list = [] - temp_str = "" - for _, problem in _problem_list: - if len(temp_str.split("\n")) > 50: - img = await text2image( - temp_str, - padding=10, - color="#f9f6f2", - ) - msg_list.append(image(b64=img.pic2bs4())) - temp_str = "" - if isinstance(problem, str): - if problem not in str_temp_list: - str_temp_list.append(problem) - temp_str += f"{index}. {problem}\n" - else: - if temp_str: - img = await text2image( - temp_str, - padding=10, - color="#f9f6f2", - ) - msg_list.append(image(b64=img.pic2bs4())) - temp_str = "" - msg_list.append(f"{index}." + problem) - index += 1 - if temp_str: - img = await text2image( - temp_str, - padding=10, - color="#f9f6f2", - ) - msg_list.append(image(b64=img.pic2bs4())) - return msg_list - - -@driver.on_startup -async def _(): - try: - from ._old_model import WordBank as OldWordBank - except ModuleNotFoundError: - return - if await WordBank.get_group_all_problem(0): - return - logger.info("开始迁移词条 纯文本 数据") - try: - word_list = await OldWordBank.get_all() - new_answer_path = Path() / "data" / "word_bank" / "answer" - new_problem_path = Path() / "data" / "word_bank" / "problem" - new_answer_path.mkdir(exist_ok=True, parents=True) - for word in word_list: - problem: str = word.problem - user_id = word.user_id - group_id = word.group_id - format_ = word.format - answer = word.answer - # 仅对纯文本做处理 - if ( - "[CQ" not in problem - and "[CQ" not in answer - and "[_to_me" not in problem - ): - if not format_: - await WordBank.add_problem_answer( - user_id, group_id, 1, 0, problem, answer - ) - else: - placeholder = [] - for m in format_.split(""): - x = m.split("<_s>") - if x[0]: - idx, file_name = x[0], x[1] - if "jpg" in file_name: - answer = answer.replace( - f"[__placeholder_{idx}]", - f"[image:placeholder_{idx}]", - ) - file = ( - Path() - / "data" - / "word_bank" - / f"{group_id}" - / file_name - ) - rand = int(time.time()) + random.randint(1, 100000) - if file.exists(): - new_file = ( - new_answer_path - / f"{group_id}" - / f"{user_id}_{rand}.jpg" - ) - new_file.parent.mkdir(exist_ok=True, parents=True) - with open(file, "rb") as rb: - with open(new_file, "wb") as wb: - wb.write(rb.read()) - # file.rename(new_file) - placeholder.append( - f"answer/{group_id}/{user_id}_{rand}.jpg" - ) - await WordBank._move( - user_id, - group_id, - problem, - answer, - ",".join(placeholder), - ) - await WordBank.add_problem_answer(0, 0, 999, 0, "_[OK", "_[OK") - logger.info("词条 纯文本 数据迁移完成") - (Path() / "plugins" / "word_bank" / "_old_model.py").unlink() - except Exception as e: - logger.warning(f"迁移词条发生错误,如果为首次安装请无视 {type(e)}:{e}") diff --git a/plugins/word_bank/_rule.py b/plugins/word_bank/_rule.py deleted file mode 100644 index 6b787d5c..00000000 --- a/plugins/word_bank/_rule.py +++ /dev/null @@ -1,53 +0,0 @@ -from io import BytesIO -from typing import List - -import imagehash -from nonebot.adapters.onebot.v11 import Bot, MessageEvent -from nonebot.typing import T_State -from PIL import Image - -from services.log import logger -from utils.depends import ImageList -from utils.http_utils import AsyncHttpx -from utils.utils import get_message_at, get_message_img, get_message_text - -from ._model import WordBank - - -async def check( - bot: Bot, - event: MessageEvent, - state: T_State, -) -> bool: - text = get_message_text(event.message) - img_list = get_message_img(event.message) - problem = text - if not text and len(img_list) == 1: - try: - r = await AsyncHttpx.get(img_list[0]) - problem = str(imagehash.average_hash(Image.open(BytesIO(r.content)))) - except Exception as e: - logger.warning(f"获取图片失败", "词条检测", e=e) - if get_message_at(event.message): - temp = "" - for seg in event.message: - if seg.type == "at": - temp += f"[at:{seg.data['qq']}]" - elif seg.type == "text": - temp += seg.data["text"] - problem = temp - if event.to_me and bot.config.nickname: - if str(event.original_message).startswith("[CQ:at"): - problem = f"[at:{bot.self_id}]" + problem - else: - if problem and bot.config.nickname: - nickname = [ - nk - for nk in bot.config.nickname - if str(event.original_message).startswith(nk) - ] - problem = nickname[0] + problem if nickname else problem - if problem and (await WordBank.check_problem(event, problem) is not None): - state["problem"] = problem - return True - return False diff --git a/plugins/word_bank/message_handle.py b/plugins/word_bank/message_handle.py deleted file mode 100644 index 9f9fe4d7..00000000 --- a/plugins/word_bank/message_handle.py +++ /dev/null @@ -1,29 +0,0 @@ -from nonebot import on_message -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.typing import T_State - -from configs.path_config import DATA_PATH -from services import logger - -from ._model import WordBank -from ._rule import check - -__zx_plugin_name__ = "词库问答回复操作 [Hidden]" - -data_dir = DATA_PATH / "word_bank" -data_dir.mkdir(parents=True, exist_ok=True) - -message_handle = on_message(priority=6, block=True, rule=check) - - -@message_handle.handle() -async def _(event: MessageEvent, state: T_State): - if problem := state.get("problem"): - if msg := await WordBank.get_answer(event, problem): - await message_handle.send(msg) - logger.info( - f" 触发词条 {problem}", - "词条检测", - event.user_id, - getattr(event, "group_id", None), - ) diff --git a/plugins/word_bank/word_handle.py b/plugins/word_bank/word_handle.py deleted file mode 100644 index 92677dcf..00000000 --- a/plugins/word_bank/word_handle.py +++ /dev/null @@ -1,359 +0,0 @@ -import re -from typing import Any, List, Optional, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import ( - Bot, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, - unescape, -) -from nonebot.exception import FinishedException -from nonebot.internal.params import Arg, ArgStr -from nonebot.params import Command, CommandArg, RegexGroup -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import DATA_PATH -from services.log import logger -from utils.depends import AtList, ImageList -from utils.message_builder import custom_forward_msg -from utils.utils import get_message_at, get_message_img, is_number - -from ._config import scope2int, type2int -from ._data_source import delete_word, show_word, update_word -from ._model import WordBank - -__zx_plugin_name__ = "词库问答 [Admin]" -__plugin_usage__ = r""" -usage: - 对指定问题的随机回答,对相同问题可以设置多个不同回答 - 删除词条后每个词条的id可能会变化,请查看后再删除 - 更推荐使用id方式删除 - 问题回答支持的CQ:at, face, image - 查看词条命令:群聊时为 群词条+全局词条,私聊时为 私聊词条+全局词条 - 添加词条正则:添加词条(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*) - 正则问可以通过$1类推()捕获的组 - 指令: - 添加词条 ?[模糊|正则|图片]问...答...:添加问答词条,可重复添加相同问题的不同回答 - 删除词条 [问题/下标] ?[下标]:删除指定词条指定或全部回答 - 修改词条 [问题/下标] [新问题]:修改词条问题 - 查看词条 ?[问题/下标]:查看全部词条或对应词条回答 - 示例:添加词条问图片答嗨嗨嗨 - [图片]... - 示例:添加词条@萝莉 我来啦 - 示例:添加词条问谁是萝莉答是我 - 示例:添加词条正则问那个(.+)是萝莉答没错$1是萝莉 - 示例:删除词条 谁是萝莉 - 示例:删除词条 谁是萝莉 0 - 示例:删除词条 id:0 1 - 示例:修改词条 谁是萝莉 是你 - 示例:修改词条 id:0 是你 - 示例:查看词条 - 示例:查看词条 谁是萝莉 - 示例:查看词条 id:0 (群/私聊词条) - 示例:查看词条 gid:0 (全局词条) -""".strip() -__plugin_superuser_usage__ = r""" -usage: - 在私聊中超级用户额外设置 - 指令: - (全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*):添加问答词条,可重复添加相同问题的不同回答 - 全局添加词条 - 私聊添加词条 - (私聊情况下)删除词条: 删除私聊词条 - (私聊情况下)删除全局词条 - (私聊情况下)修改词条: 修改私聊词条 - (私聊情况下)修改全局词条 - 用法与普通用法相同 -""".strip() -__plugin_des__ = "自定义词条内容随机回复" -__plugin_cmd__ = [ - "添加词条 ?[模糊/关键字]问...答..", - "删除词条 [问题/下标] ?[下标]", - "修改词条 [问题/下标] ?[下标/新回答] [新回答]", - "查看词条 ?[问题/下标]", -] -__plugin_version__ = 0.3 -__plugin_author__ = "HibiKier & yajiwa" -__plugin_settings__ = { - "admin_level": Config.get_config("word_bank", "WORD_BANK_LEVEL [LEVEL]"), - "cmd": ["词库问答", "添加词条", "删除词条", "修改词条", "查看词条"], -} - -data_dir = DATA_PATH / "word_bank" -data_dir.mkdir(parents=True, exist_ok=True) - -add_word = on_regex( - r"^(全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*)", priority=5, block=True -) - -delete_word_matcher = on_command("删除词条", aliases={"删除全局词条"}, priority=5, block=True) - -update_word_matcher = on_command("修改词条", aliases={"修改全局词条"}, priority=5, block=True) - -show_word_matcher = on_command("显示词条", aliases={"查看词条"}, priority=5, block=True) - - -@add_word.handle() -async def _( - bot: Bot, - event: MessageEvent, - state: T_State, - reg_group: Tuple[Any, ...] = RegexGroup(), - img_list: List[str] = ImageList(), - at_list: List[int] = AtList(), -): - if ( - isinstance(event, PrivateMessageEvent) - and str(event.user_id) not in bot.config.superusers - ): - await add_word.finish("权限不足捏") - word_scope, word_type, problem, answer = reg_group - if not word_scope and isinstance(event, PrivateMessageEvent): - word_scope = "私聊" - if ( - word_scope - and word_scope in ["全局", "私聊"] - and str(event.user_id) not in bot.config.superusers - ): - await add_word.finish("权限不足,无法添加该范围词条") - if (not problem or not problem.strip()) and word_type != "图片": - await add_word.finish("词条问题不能为空!") - if (not answer or not answer.strip()) and not len(img_list) and not len(at_list): - await add_word.finish("词条回答不能为空!") - if word_type != "图片": - state["problem_image"] = "YES" - answer = event.message - # 对at问题对额外处理 - if at_list: - is_first = True - cur_p = None - answer = "" - problem = "" - for index, seg in enumerate(event.message): - r = re.search("添加词条(模糊|正则|图片)?问", str(seg)) - if seg.type == "text" and r and is_first: - is_first = False - seg_ = str(seg).split(f"添加词条{r.group(1) or ''}问")[-1] - cur_p = "problem" - # 纯文本 - if "答" in seg_: - answer_index = seg_.index("答") - problem = unescape(seg_[:answer_index]).strip() - answer = unescape(seg_[answer_index + 1 :]).strip() - cur_p = "answer" - else: - problem = unescape(seg_) - continue - if cur_p == "problem": - if seg.type == "text" and "答" in str(seg): - seg_ = str(seg) - answer_index = seg_.index("答") - problem += seg_[:answer_index] - answer += seg_[answer_index + 1 :] - cur_p = "answer" - else: - if seg.type == "at": - problem += f"[at:{seg.data['qq']}]" - else: - problem += ( - unescape(str(seg)).strip() if seg.type == "text" else seg - ) - else: - if seg.type == "text": - answer += unescape(str(seg)) - else: - answer += seg - event.message[0] = event.message[0].data["text"].split("答", maxsplit=1)[-1].strip() - state["word_scope"] = word_scope - state["word_type"] = word_type - state["problem"] = problem - state["answer"] = answer - - -@add_word.got("problem_image", prompt="请发送该回答设置的问题图片") -async def _( - bot: Bot, - event: MessageEvent, - word_scope: Optional[str] = ArgStr("word_scope"), - word_type: Optional[str] = ArgStr("word_type"), - problem: Optional[str] = ArgStr("problem"), - answer: Message = Arg("answer"), - problem_image: Message = Arg("problem_image"), -): - try: - if word_type == "正则" and problem: - problem = unescape(problem) - try: - re.compile(problem) - except re.error: - await add_word.finish(f"添加词条失败,正则表达式 {problem} 非法!") - # if str(event.user_id) in bot.config.superusers and isinstance(event, PrivateMessageEvent): - # word_scope = "私聊" - nickname = None - if problem and bot.config.nickname: - nickname = [nk for nk in bot.config.nickname if problem.startswith(nk)] - await WordBank.add_problem_answer( - str(event.user_id), - str(event.group_id) - if isinstance(event, GroupMessageEvent) - and (not word_scope or word_scope == "私聊") - else "0", - scope2int[word_scope] if word_scope else 1, - type2int[word_type] if word_type else 0, - problem or problem_image, - answer, - nickname[0] if nickname else None, - ) - except Exception as e: - if isinstance(e, FinishedException): - await add_word.finish() - logger.error( - f"添加词条 {problem} 错误...", - "添加词条", - event.user_id, - getattr(event, "group_id", None), - e=e, - ) - await add_word.finish(f"添加词条 {problem} 发生错误!") - await add_word.send("添加词条 " + (problem or problem_image) + " 成功!") - logger.info( - f"添加词条 {problem} 成功!", "添加词条", event.user_id, getattr(event, "group_id", None) - ) - - -@delete_word_matcher.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - if not (msg := arg.extract_plain_text().strip()): - await delete_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - result = await delete_word(msg, str(event.group_id)) - await delete_word_matcher.send(result) - logger.info(f"删除词条:" + msg, "删除词条", event.user_id, event.group_id) - - -@delete_word_matcher.handle() -async def _( - bot: Bot, - event: PrivateMessageEvent, - arg: Message = CommandArg(), - cmd: Tuple[str, ...] = Command(), -): - if str(event.user_id) not in bot.config.superusers: - await delete_word_matcher.finish("权限不足捏!") - if not (msg := arg.extract_plain_text().strip()): - await delete_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - result = await delete_word(msg, word_scope=2 if cmd[0] == "删除词条" else 0) - await delete_word_matcher.send(result) - logger.info(f"删除词条:" + msg, "删除词条", event.user_id) - - -@update_word_matcher.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - if not (msg := arg.extract_plain_text().strip()): - await update_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - if len(msg.split()) < 2: - await update_word_matcher.finish("此命令需要两个参数,请查看帮助") - result = await update_word(msg, str(event.group_id)) - await update_word_matcher.send(result) - logger.info(f"更新词条词条:" + msg, "更新词条", event.user_id, event.group_id) - - -@update_word_matcher.handle() -async def _( - bot: Bot, - event: PrivateMessageEvent, - arg: Message = CommandArg(), - cmd: Tuple[str, ...] = Command(), -): - if str(event.user_id) not in bot.config.superusers: - await delete_word_matcher.finish("权限不足捏!") - if not (msg := arg.extract_plain_text().strip()): - await update_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - if len(msg.split()) < 2: - await update_word_matcher.finish("此命令需要两个参数,请查看帮助") - result = await update_word(msg, word_scope=2 if cmd[0] == "修改词条" else 0) - await update_word_matcher.send(result) - logger.info(f"更新词条词条:" + msg, "更新词条", event.user_id) - - -@show_word_matcher.handle() -async def _(bot: Bot, event: GroupMessageEvent, arg: Message = CommandArg()): - if problem := arg.extract_plain_text().strip(): - id_ = None - gid = None - if problem.startswith("id:"): - id_ = problem.split(":")[-1] - if ( - not is_number(id_) - or int(id_) < 0 - or int(id_) - >= len(await WordBank.get_group_all_problem(str(event.group_id))) - ): - await show_word_matcher.finish("id必须为数字且在范围内") - id_ = int(id_) - if problem.startswith("gid:"): - gid = problem.split(":")[-1] - if ( - not is_number(gid) - or int(gid) < 0 - or int(gid) >= len(await WordBank.get_problem_by_scope(0)) - ): - await show_word_matcher.finish("gid必须为数字且在范围内") - gid = int(gid) - msg_list = await show_word( - problem, id_, gid, None if gid else str(event.group_id) - ) - else: - msg_list = await show_word(problem, None, None, str(event.group_id)) - if isinstance(msg_list, str): - await show_word_matcher.send(msg_list) - else: - await bot.send_group_forward_msg( - group_id=event.group_id, messages=custom_forward_msg(msg_list, bot.self_id) - ) - logger.info( - f"查看词条回答:" + problem, "显示词条", event.user_id, getattr(event, "group_id", None) - ) - - -@show_word_matcher.handle() -async def _(event: PrivateMessageEvent, arg: Message = CommandArg()): - if problem := arg.extract_plain_text().strip(): - id_ = None - gid = None - if problem.startswith("id:"): - id_ = problem.split(":")[-1] - if ( - not is_number(id_) - or int(id_) < 0 - or int(id_) > len(await WordBank.get_problem_by_scope(2)) - ): - await show_word_matcher.finish("id必须为数字且在范围内") - id_ = int(id_) - if problem.startswith("gid:"): - gid = problem.split(":")[-1] - if ( - not is_number(gid) - or int(gid) < 0 - or int(gid) > len(await WordBank.get_problem_by_scope(0)) - ): - await show_word_matcher.finish("gid必须为数字且在范围内") - gid = int(gid) - msg_list = await show_word( - problem, id_, gid, word_scope=2 if id_ is not None else None - ) - else: - msg_list = await show_word(problem, None, None, word_scope=2) - if isinstance(msg_list, str): - await show_word_matcher.send(msg_list) - else: - t = "" - for msg in msg_list: - t += msg + "\n" - await show_word_matcher.send(t[:-1]) - logger.info( - f"查看词条回答:" + problem, "显示词条", event.user_id, getattr(event, "group_id", None) - ) diff --git a/plugins/word_clouds/__init__.py b/plugins/word_clouds/__init__.py deleted file mode 100644 index ab703941..00000000 --- a/plugins/word_clouds/__init__.py +++ /dev/null @@ -1,207 +0,0 @@ -import re -from datetime import datetime, timedelta -from typing import Tuple, Union - -import pytz -from nonebot import get_driver, on_command -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.adapters.onebot.v11.event import GroupMessageEvent -from nonebot.matcher import Matcher -from nonebot.params import Arg, Command, CommandArg, Depends -from nonebot.typing import T_State - -from configs.config import Config - -from .data_source import draw_word_cloud, get_list_msg - -__zx_plugin_name__ = "词云" - -__plugin_usage__ = """ -usage: - 词云 - 指令: - 今日词云:获取今天的词云 - 昨日词云:获取昨天的词云 - 本周词云:获取本周词云 - 本月词云:获取本月词云 - 年度词云:获取年度词云 - - 历史词云(支持 ISO8601 格式的日期与时间,如 2022-02-22T22:22:22) - 获取某日的词云 - 历史词云 2022-01-01 - 获取指定时间段的词云 - 历史词云 - 示例:历史词云 2022-01-01~2022-02-22 - 示例:历史词云 2022-02-22T11:11:11~2022-02-22T22:22:22 - - 如果想要获取自己的发言,可在命令前添加 我的 - 示例:我的今日词云 -""".strip() -__plugin_des__ = "词云" -__plugin_cmd__ = ["今日词云", "昨日词云", "本周词云"] -__plugin_version__ = 0.1 -__plugin_author__ = "yajiwa" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": __plugin_cmd__, -} -wordcloud_cmd = on_command( - "wordcloud", - aliases={ - "词云", - "今日词云", - "昨日词云", - "本周词云", - "本月词云", - "年度词云", - "历史词云", - "我的今日词云", - "我的昨日词云", - "我的本周词云", - "我的本月词云", - "我的年度词云", - "我的历史词云", - }, - block=True, - priority=5, -) -Config.add_plugin_config( - "word_clouds", - "WORD_CLOUDS_TEMPLATE", - 1, - help_="词云模板 参1:图片生成,默认使用真寻图片,可在项目路径resources/image/wordcloud下配置图片,多张则随机 | 参2/其他:黑底图片", - type=int, -) - - -def parse_datetime(key: str): - """解析数字,并将结果存入 state 中""" - - async def _key_parser( - matcher: Matcher, - state: T_State, - input_: Union[datetime, Message] = Arg(key), - ): - if isinstance(input_, datetime): - return - - plaintext = input_.extract_plain_text() - try: - state[key] = get_datetime_fromisoformat_with_timezone(plaintext) - except ValueError: - await matcher.reject_arg(key, "请输入正确的日期,不然我没法理解呢!") - - return _key_parser - - -def get_datetime_now_with_timezone() -> datetime: - """获取当前时间,并包含时区信息""" - return datetime.now().astimezone() - - -def get_datetime_fromisoformat_with_timezone(date_string: str) -> datetime: - """从 iso8601 格式字符串中获取时间,并包含时区信息""" - return datetime.fromisoformat(date_string).astimezone() - - -@wordcloud_cmd.handle() -async def handle_first_receive( - event: GroupMessageEvent, - state: T_State, - commands: Tuple[str, ...] = Command(), - args: Message = CommandArg(), -): - command = commands[0] - - if command.startswith("我的"): - state["my"] = True - command = command[2:] - else: - state["my"] = False - - if command == "今日词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace(hour=0, minute=0, second=0, microsecond=0) - state["stop"] = dt - elif command == "昨日词云": - dt = get_datetime_now_with_timezone() - state["stop"] = dt.replace(hour=0, minute=0, second=0, microsecond=0) - state["start"] = state["stop"] - timedelta(days=1) - elif command == "本周词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace( - hour=0, minute=0, second=0, microsecond=0 - ) - timedelta(days=dt.weekday()) - state["stop"] = dt - elif command == "本月词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - state["stop"] = dt - elif command == "年度词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - state["stop"] = dt - elif command == "历史词云": - plaintext = args.extract_plain_text().strip() - match = re.match(r"^(.+?)(?:~(.+))?$", plaintext) - if match: - start = match.group(1) - stop = match.group(2) - try: - state["start"] = get_datetime_fromisoformat_with_timezone(start) - if stop: - state["stop"] = get_datetime_fromisoformat_with_timezone(stop) - else: - # 如果没有指定结束日期,则认为是指查询这一天的词云 - state["start"] = state["start"].replace( - hour=0, minute=0, second=0, microsecond=0 - ) - state["stop"] = state["start"] + timedelta(days=1) - except ValueError: - await wordcloud_cmd.finish("请输入正确的日期,不然我没法理解呢!") - else: - await wordcloud_cmd.finish() - - -@wordcloud_cmd.got( - "start", - prompt="请输入你要查询的起始日期(如 2022-01-01)", - parameterless=[Depends(parse_datetime("start"))], -) -@wordcloud_cmd.got( - "stop", - prompt="请输入你要查询的结束日期(如 2022-02-22)", - parameterless=[Depends(parse_datetime("stop"))], -) -async def handle_message( - event: GroupMessageEvent, - start: datetime = Arg(), - stop: datetime = Arg(), - my: bool = Arg(), -): - # 是否只查询自己的记录 - if my: - user_id = int(event.user_id) - else: - user_id = None - # 将时间转换到 东八 时区 - messages = await get_list_msg( - user_id, - int(event.group_id), - days=( - start.astimezone(pytz.timezone("Asia/Shanghai")), - stop.astimezone(pytz.timezone("Asia/Shanghai")), - ), - ) - if messages: - image_bytes = await draw_word_cloud(messages, get_driver().config) - if image_bytes: - await wordcloud_cmd.finish(MessageSegment.image(image_bytes), at_sender=my) - else: - await wordcloud_cmd.finish("生成词云失败", at_sender=my) - else: - await wordcloud_cmd.finish("没有获取到词云数据", at_sender=my) diff --git a/plugins/word_clouds/data_source.py b/plugins/word_clouds/data_source.py deleted file mode 100644 index db99f40d..00000000 --- a/plugins/word_clouds/data_source.py +++ /dev/null @@ -1,129 +0,0 @@ -import asyncio -import os -import random -import re -from io import BytesIO -from typing import List - -import jieba -import jieba.analyse -import matplotlib.pyplot as plt -import numpy as np -from emoji import replace_emoji # type: ignore -from PIL import Image as IMG -from wordcloud import ImageColorGenerator, WordCloud - -from configs.config import Config -from configs.path_config import FONT_PATH, IMAGE_PATH -from models.chat_history import ChatHistory -from services import logger -from utils.http_utils import AsyncHttpx - - -async def pre_precess(msg: List[str], config) -> str: - return await asyncio.get_event_loop().run_in_executor( - None, _pre_precess, msg, config - ) - - -def _pre_precess(msg: List[str], config) -> str: - """对消息进行预处理""" - # 过滤掉命令 - command_start = tuple([i for i in config.command_start if i]) - msg = " ".join([m for m in msg if not m.startswith(command_start)]) - - # 去除网址 - msg = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", msg) - - # 去除 \u200b - msg = re.sub(r"[\u200b]", "", msg) - - # 去除cq码 - msg = re.sub(r"\[CQ:.*?]", "", msg) - - # 去除[] - msg = re.sub("[ (1|3);]", "", msg) - - # 去除 emoji - # https://github.com/carpedm20/emoji - msg = replace_emoji(msg) - return msg - - -async def draw_word_cloud(messages, config): - wordcloud_dir = IMAGE_PATH / "wordcloud" - wordcloud_dir.mkdir(exist_ok=True, parents=True) - # 默认用真寻图片 - zx_logo_path = wordcloud_dir / "default.png" - wordcloud_ttf = FONT_PATH / "STKAITI.TTF" - if not os.listdir(wordcloud_dir): - url = "https://ghproxy.com/https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/resources/image/wordcloud/default.png" - try: - await AsyncHttpx.download_file(url, zx_logo_path) - except Exception as e: - logger.error(f"词云图片资源下载发生错误 {type(e)}:{e}") - return False - if not wordcloud_ttf.exists(): - ttf_url = "https://ghproxy.com/https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/resources/font/STKAITI.TTF" - try: - await AsyncHttpx.download_file(ttf_url, wordcloud_ttf) - except Exception as e: - logger.error(f"词云字体资源下载发生错误 {type(e)}:{e}") - return False - - topK = min(int(len(messages)), 100000) - read_name = jieba.analyse.extract_tags( - await pre_precess(messages, config), topK=topK, withWeight=True, allowPOS=() - ) - name = [] - value = [] - for t in read_name: - name.append(t[0]) - value.append(t[1]) - for i in range(len(name)): - name[i] = str(name[i]) - dic = dict(zip(name, value)) - if Config.get_config("word_clouds", "WORD_CLOUDS_TEMPLATE") == 1: - - def random_pic(base_path: str) -> str: - path_dir = os.listdir(base_path) - path = random.sample(path_dir, 1)[0] - return str(base_path) + "/" + str(path) - - mask = np.array(IMG.open(random_pic(wordcloud_dir))) - wc = WordCloud( - font_path=f"{wordcloud_ttf}", - background_color="white", - max_font_size=100, - width=1920, - height=1080, - mask=mask, - ) - wc.generate_from_frequencies(dic) - image_colors = ImageColorGenerator(mask, default_color=(255, 255, 255)) - wc.recolor(color_func=image_colors) - plt.imshow(wc.recolor(color_func=image_colors), interpolation="bilinear") - plt.axis("off") - else: - wc = WordCloud( - font_path=str(wordcloud_ttf), - width=1920, - height=1200, - background_color="black", - ) - wc.generate_from_frequencies(dic) - bytes_io = BytesIO() - img = wc.to_image() - img.save(bytes_io, format="PNG") - return bytes_io.getvalue() - - -async def get_list_msg(user_id, group_id, days): - messages_list = await ChatHistory().get_message( - uid=user_id, gid=group_id, type_="group", days=days - ) - if messages_list: - messages = [i.plain_text for i in messages_list] - return messages - else: - return False diff --git a/plugins/yiqing/__init__.py b/plugins/yiqing/__init__.py deleted file mode 100755 index 0b8ffc5e..00000000 --- a/plugins/yiqing/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -from nonebot import on_command -from .data_source import get_yiqing_data, get_city_and_province_list -from services.log import logger -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from configs.config import NICKNAME -from .other_than import get_other_data - -__zx_plugin_name__ = "疫情查询" -__plugin_usage__ = """ -usage: - 全国疫情查询 - 指令: - 疫情 中国/美国/英国... - 疫情 [省份/城市] - * 当省份与城市重名时,可在后添加 "市" 或 "省" * - 示例:疫情 吉林 <- [省] - 示例:疫情 吉林市 <- [市] -""".strip() -__plugin_des__ = "实时疫情数据查询" -__plugin_cmd__ = ["疫情 [省份/城市]", "疫情 中国"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier & yzyyz1387" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查询疫情", "疫情", "疫情查询"], -} - - -yiqing = on_command("疫情", aliases={"查询疫情", "疫情查询"}, priority=5, block=True) - - -@yiqing.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - city_and_province_list = get_city_and_province_list() - if msg: - if msg in city_and_province_list or msg[:-1] in city_and_province_list: - result = await get_yiqing_data(msg) - if result: - await yiqing.send(result) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询疫情: {msg}" - ) - else: - await yiqing.send("查询失败!!!!", at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询疫情失败" - ) - else: - rely = await get_other_data(msg) - if rely: - await yiqing.send(rely) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询疫情成功" - ) - else: - await yiqing.send(f"{NICKNAME}没有查到{msg}的疫情查询...") diff --git a/plugins/yiqing/data_source.py b/plugins/yiqing/data_source.py deleted file mode 100755 index 9755ae53..00000000 --- a/plugins/yiqing/data_source.py +++ /dev/null @@ -1,110 +0,0 @@ -from configs.path_config import TEXT_PATH -from typing import List, Union -from utils.http_utils import AsyncHttpx -from utils.image_utils import text2image -from utils.message_builder import image -from nonebot.adapters.onebot.v11 import MessageSegment -import ujson as json - -china_city = TEXT_PATH / "china_city.json" - -data = {} - - -url = "https://api.inews.qq.com/newsqa/v1/query/inner/publish/modules/list?modules=diseaseh5Shelf" - - -async def get_yiqing_data(area: str) -> Union[str, MessageSegment]: - """ - 查看疫情数据 - :param area: 省份/城市 - """ - global data - province = None - city = None - province_type = "省" - if area == "中国": - province = area - province_type = "" - elif area[-1] == '省' or (area in data.keys() and area[-1] != "市"): - province = area if area[-1] != "省" else area[:-1] - if len(data[province]) == 1: - province_type = "市" - city = "" - else: - area = area[:-1] if area[-1] == "市" else area - for p in data.keys(): - if area in data[p]: - province = p - city = area - epidemic_data = (await AsyncHttpx.get(url)).json()["data"]["diseaseh5Shelf"] - last_update_time = epidemic_data["lastUpdateTime"] - if area == "中国": - data_ = epidemic_data["areaTree"][0] - else: - try: - data_ = [ - x - for x in epidemic_data["areaTree"][0]["children"] - if x["name"] == province - ][0] - if city: - data_ = [x for x in data_["children"] if x["name"] == city][0] - except IndexError: - return "未查询到..." - confirm = data_["total"]["confirm"] # 累计确诊 - heal = data_["total"]["heal"] # 累计治愈 - dead = data_["total"]["dead"] # 累计死亡 - now_confirm = data_["total"]["nowConfirm"] # 目前确诊 - add_confirm = data_["today"]["confirm"] # 新增确诊 - add_wzz = data_["today"]["wzz_add"] #新增无症状 - wzz=data_["total"]["wzz"] #目前无症状 - grade = "" - _grade_color = "" - # if data_["total"].get("grade"): - # grade = data_["total"]["grade"] - # if "中风险" in grade: - # _grade_color = "#fa9424" - # else: - # _grade_color = "red" - - dead_rate = f"{dead / confirm * 100:.2f}" # 死亡率 - heal_rate = f"{heal / confirm * 100:.2f}" # 治愈率 - - x = f"{city}市" if city else f"{province}{province_type}" - return image(b64=(await text2image( - f""" - {x} 疫情数据 {f"({grade})" if grade else ""}: - 目前确诊: - 确诊人数:{now_confirm}(+{add_confirm}) - 新增无症状:{add_wzz} - ----------------- - 累计数据: - 无症状人数:{wzz} - 确诊人数:{confirm} - 治愈人数:{heal} - 死亡人数:{dead} - 治愈率:{heal_rate}% - 死亡率:{dead_rate}% - 更新日期:{last_update_time} - """, font_size=30, color="#f9f6f2" - )).pic2bs4()) - - -def get_city_and_province_list() -> List[str]: - """ - 获取城市省份列表 - """ - global data - if not data: - try: - with open(china_city, "r", encoding="utf8") as f: - data = json.load(f) - except FileNotFoundError: - data = {} - city_list = ["中国"] - for p in data.keys(): - for c in data[p]: - city_list.append(c) - city_list.append(p) - return city_list diff --git a/plugins/yiqing/other_than.py b/plugins/yiqing/other_than.py deleted file mode 100644 index 28b5fca1..00000000 --- a/plugins/yiqing/other_than.py +++ /dev/null @@ -1,93 +0,0 @@ -# python3 -# -*- coding: utf-8 -*- -# @Time : 2021/12/23 23:04 -# @Author : yzyyz -# @Email : youzyyz1384@qq.com -# @File : other_than.py -# @Software: PyCharm -from utils.http_utils import AsyncHttpx -from nonebot.adapters.onebot.v11 import MessageSegment -from typing import Optional -from services.log import logger -from utils.image_utils import text2image -from utils.message_builder import image -from json.decoder import JSONDecodeError -import re -import json - -__doc__ = """爬虫实现国外疫情数据(找不到好接口)""" - - -def intcomma(value) -> str: - """ - 数字格式化 - """ - orig = str(value) - new = re.sub(r"^(-?\d+)(\d{3})", r"\g<1>,\g<2>", orig) - return new if orig == new else intcomma(new) - - -async def get_other_data(place: str, count: int = 0) -> Optional[MessageSegment]: - """ - :param place: 地名 - :param count: 递归次数 - :return: 格式化字符串 - """ - if count == 5: - return None - try: - html = ( - (await AsyncHttpx.get("https://news.ifeng.com/c/special/7uLj4F83Cqm")) - .text.replace("\n", "") - .replace(" ", "") - ) - find_data = re.compile(r"varallData=(.*?);") - sum_ = re.findall(find_data, html)[0] - sum_ = json.loads(sum_) - except JSONDecodeError: - return await get_other_data(place, count + 1) - except Exception as e: - logger.error(f"疫情查询发生错误 {type(e)}:{e}") - return None - try: - other_country = sum_["yiqing_v2"]["dataList"][29]["child"] - for country in other_country: - if place == country["name2"]: - return image( - b64=(await text2image( - f" {place} 疫情数据:\n" - "——————————————\n" - f" 新增病例:{intcomma(country['quezhen_add'])}\n" - f" 现有确诊:{intcomma(country['quezhen_xianyou'])}\n" - f" 累计确诊:{intcomma(country['quezhen'])}\n" - f" 累计治愈:{intcomma(country['zhiyu'])}\n" - f" 死亡:{intcomma(country['siwang'])}\n" - "——————————————" - # f"更新时间:{country['sys_publishDateTime']}" - # 时间无法精确到分钟,网页用了js我暂时找不到 - , - font_size=30, - color="#f9f6f2", - padding=15 - )).pic2bs4() - ) - else: - for city in country["child"]: - if place == city["name3"]: - return image( - b64=(await text2image( - f"\n{place} 疫情数据:\n" - "——————————————\n" - f"\t新增病例:{intcomma(city['quezhen_add'])}\n" - f"\t累计确诊:{intcomma(city['quezhen'])}\n" - f"\t累计治愈:{intcomma(city['zhiyu'])}\n" - f"\t死亡:{intcomma(city['siwang'])}\n" - "——————————————\n", - font_size=30, - color="#f9f6f2", - padding=15 - )).pic2bs4() - ) - except Exception as e: - logger.error(f"疫情查询发生错误 {type(e)}:{e}") - return None diff --git a/poetry.lock b/poetry.lock index 7d4904eb..9d4e8ff1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,30 @@ -# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiofiles" -version = "0.8.0" +version = "23.2.1" description = "File support for asyncio." -category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.7" files = [ - {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, - {file = "aiofiles-0.8.0.tar.gz", hash = "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"}, + {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, + {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "aiohappyeyeballs" +version = "2.3.5" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, + {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, ] [package.source] @@ -19,61 +34,119 @@ reference = "ali" [[package]] name = "aiohttp" -version = "3.7.4.post0" +version = "3.10.3" description = "Async http client/server framework (asyncio)" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, - {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc36cbdedf6f259371dbbbcaae5bb0e95b879bc501668ab6306af867577eb5db"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85466b5a695c2a7db13eb2c200af552d13e6a9313d7fa92e4ffe04a2c0ea74c1"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71bb1d97bfe7e6726267cea169fdf5df7658831bb68ec02c9c6b9f3511e108bb"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baec1eb274f78b2de54471fc4c69ecbea4275965eab4b556ef7a7698dee18bf2"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13031e7ec1188274bad243255c328cc3019e36a5a907978501256000d57a7201"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bbc55a964b8eecb341e492ae91c3bd0848324d313e1e71a27e3d96e6ee7e8e8"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8cc0564b286b625e673a2615ede60a1704d0cbbf1b24604e28c31ed37dc62aa"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f817a54059a4cfbc385a7f51696359c642088710e731e8df80d0607193ed2b73"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8542c9e5bcb2bd3115acdf5adc41cda394e7360916197805e7e32b93d821ef93"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:671efce3a4a0281060edf9a07a2f7e6230dca3a1cbc61d110eee7753d28405f7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0974f3b5b0132edcec92c3306f858ad4356a63d26b18021d859c9927616ebf27"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:44bb159b55926b57812dca1b21c34528e800963ffe130d08b049b2d6b994ada7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6ae9ae382d1c9617a91647575255ad55a48bfdde34cc2185dd558ce476bf16e9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win32.whl", hash = "sha256:aed12a54d4e1ee647376fa541e1b7621505001f9f939debf51397b9329fd88b9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b51aef59370baf7444de1572f7830f59ddbabd04e5292fa4218d02f085f8d299"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e021c4c778644e8cdc09487d65564265e6b149896a17d7c0f52e9a088cc44e1b"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24fade6dae446b183e2410a8628b80df9b7a42205c6bfc2eff783cbeedc224a2"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bc8e9f15939dacb0e1f2d15f9c41b786051c10472c7a926f5771e99b49a5957f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a9ec959b5381271c8ec9310aae1713b2aec29efa32e232e5ef7dcca0df0279"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a5d0ea8a6467b15d53b00c4e8ea8811e47c3cc1bdbc62b1aceb3076403d551f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9ed607dbbdd0d4d39b597e5bf6b0d40d844dfb0ac6a123ed79042ef08c1f87e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e66d5b506832e56add66af88c288c1d5ba0c38b535a1a59e436b300b57b23e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fda91ad797e4914cca0afa8b6cccd5d2b3569ccc88731be202f6adce39503189"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:61ccb867b2f2f53df6598eb2a93329b5eee0b00646ee79ea67d68844747a418e"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d881353264e6156f215b3cb778c9ac3184f5465c2ece5e6fce82e68946868ef"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b031ce229114825f49cec4434fa844ccb5225e266c3e146cb4bdd025a6da52f1"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5337cc742a03f9e3213b097abff8781f79de7190bbfaa987bd2b7ceb5bb0bdec"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab3361159fd3dcd0e48bbe804006d5cfb074b382666e6c064112056eb234f1a9"}, + {file = "aiohttp-3.10.3-cp311-cp311-win32.whl", hash = "sha256:05d66203a530209cbe40f102ebaac0b2214aba2a33c075d0bf825987c36f1f0b"}, + {file = "aiohttp-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:70b4a4984a70a2322b70e088d654528129783ac1ebbf7dd76627b3bd22db2f17"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:166de65e2e4e63357cfa8417cf952a519ac42f1654cb2d43ed76899e2319b1ee"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7084876352ba3833d5d214e02b32d794e3fd9cf21fdba99cff5acabeb90d9806"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d98c604c93403288591d7d6d7d6cc8a63459168f8846aeffd5b3a7f3b3e5e09"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d73b073a25a0bb8bf014345374fe2d0f63681ab5da4c22f9d2025ca3e3ea54fc"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8da6b48c20ce78f5721068f383e0e113dde034e868f1b2f5ee7cb1e95f91db57"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a9dcdccf50284b1b0dc72bc57e5bbd3cc9bf019060dfa0668f63241ccc16aa7"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56fb94bae2be58f68d000d046172d8b8e6b1b571eb02ceee5535e9633dcd559c"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf75716377aad2c718cdf66451c5cf02042085d84522aec1f9246d3e4b8641a6"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c51ed03e19c885c8e91f574e4bbe7381793f56f93229731597e4a499ffef2a5"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b84857b66fa6510a163bb083c1199d1ee091a40163cfcbbd0642495fed096204"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c124b9206b1befe0491f48185fd30a0dd51b0f4e0e7e43ac1236066215aff272"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3461d9294941937f07bbbaa6227ba799bc71cc3b22c40222568dc1cca5118f68"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08bd0754d257b2db27d6bab208c74601df6f21bfe4cb2ec7b258ba691aac64b3"}, + {file = "aiohttp-3.10.3-cp312-cp312-win32.whl", hash = "sha256:7f9159ae530297f61a00116771e57516f89a3de6ba33f314402e41560872b50a"}, + {file = "aiohttp-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:e1128c5d3a466279cb23c4aa32a0f6cb0e7d2961e74e9e421f90e74f75ec1edf"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d1100e68e70eb72eadba2b932b185ebf0f28fd2f0dbfe576cfa9d9894ef49752"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a541414578ff47c0a9b0b8b77381ea86b0c8531ab37fc587572cb662ccd80b88"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5548444ef60bf4c7b19ace21f032fa42d822e516a6940d36579f7bfa8513f9c"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba2e838b5e6a8755ac8297275c9460e729dc1522b6454aee1766c6de6d56e5e"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48665433bb59144aaf502c324694bec25867eb6630fcd831f7a893ca473fcde4"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bac352fceed158620ce2d701ad39d4c1c76d114255a7c530e057e2b9f55bdf9f"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0f670502100cdc567188c49415bebba947eb3edaa2028e1a50dd81bd13363f"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b09f38a67679e32d380fe512189ccb0b25e15afc79b23fbd5b5e48e4fc8fd9"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:cd788602e239ace64f257d1c9d39898ca65525583f0fbf0988bcba19418fe93f"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:214277dcb07ab3875f17ee1c777d446dcce75bea85846849cc9d139ab8f5081f"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:32007fdcaab789689c2ecaaf4b71f8e37bf012a15cd02c0a9db8c4d0e7989fa8"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:123e5819bfe1b87204575515cf448ab3bf1489cdeb3b61012bde716cda5853e7"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:812121a201f0c02491a5db335a737b4113151926a79ae9ed1a9f41ea225c0e3f"}, + {file = "aiohttp-3.10.3-cp38-cp38-win32.whl", hash = "sha256:b97dc9a17a59f350c0caa453a3cb35671a2ffa3a29a6ef3568b523b9113d84e5"}, + {file = "aiohttp-3.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:3731a73ddc26969d65f90471c635abd4e1546a25299b687e654ea6d2fc052394"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38d91b98b4320ffe66efa56cb0f614a05af53b675ce1b8607cdb2ac826a8d58e"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9743fa34a10a36ddd448bba8a3adc2a66a1c575c3c2940301bacd6cc896c6bf1"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7c126f532caf238031c19d169cfae3c6a59129452c990a6e84d6e7b198a001dc"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:926e68438f05703e500b06fe7148ef3013dd6f276de65c68558fa9974eeb59ad"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434b3ab75833accd0b931d11874e206e816f6e6626fd69f643d6a8269cd9166a"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d35235a44ec38109b811c3600d15d8383297a8fab8e3dec6147477ec8636712a"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59c489661edbd863edb30a8bd69ecb044bd381d1818022bc698ba1b6f80e5dd1"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50544fe498c81cb98912afabfc4e4d9d85e89f86238348e3712f7ca6a2f01dab"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09bc79275737d4dc066e0ae2951866bb36d9c6b460cb7564f111cc0427f14844"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:af4dbec58e37f5afff4f91cdf235e8e4b0bd0127a2a4fd1040e2cad3369d2f06"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b22cae3c9dd55a6b4c48c63081d31c00fc11fa9db1a20c8a50ee38c1a29539d2"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ba562736d3fbfe9241dad46c1a8994478d4a0e50796d80e29d50cabe8fbfcc3f"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f25d6c4e82d7489be84f2b1c8212fafc021b3731abdb61a563c90e37cced3a21"}, + {file = "aiohttp-3.10.3-cp39-cp39-win32.whl", hash = "sha256:b69d832e5f5fa15b1b6b2c8eb6a9fd2c0ec1fd7729cb4322ed27771afc9fc2ac"}, + {file = "aiohttp-3.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:673bb6e3249dc8825df1105f6ef74e2eab779b7ff78e96c15cadb78b04a83752"}, + {file = "aiohttp-3.10.3.tar.gz", hash = "sha256:21650e7032cc2d31fc23d353d7123e771354f2a3d5b05a5647fc30fea214e696"}, ] [package.dependencies] -async-timeout = ">=3.0,<4.0" +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" -chardet = ">=2.0,<5.0" +frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" -typing-extensions = ">=3.6.5" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["aiodns", "brotlipy", "cchardet"] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" [package.source] type = "legacy" @@ -84,7 +157,6 @@ reference = "ali" name = "aiosqlite" version = "0.17.0" description = "asyncio bridge to the standard sqlite3 module" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -102,25 +174,24 @@ reference = "ali" [[package]] name = "anyio" -version = "4.0.0" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, - {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.22)"] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] [package.source] type = "legacy" @@ -131,7 +202,6 @@ reference = "ali" name = "apscheduler" version = "3.10.4" description = "In-process task scheduler with Cron-like capabilities" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -142,7 +212,7 @@ files = [ [package.dependencies] pytz = "*" six = ">=1.4.0" -tzlocal = ">=2.0,<3.0.0 || >=4.0.0" +tzlocal = ">=2.0,<3.dev0 || >=4.dev0" [package.extras] doc = ["sphinx", "sphinx-rtd-theme"] @@ -162,15 +232,82 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "async-timeout" -version = "3.0.1" -description = "Timeout context manager for asyncio programs" -category = "main" +name = "arclet-alconna" +version = "1.8.23" +description = "A High-performance, Generality, Humane Command Line Arguments Parser Library." optional = false -python-versions = ">=3.5.3" +python-versions = ">=3.8" files = [ - {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, - {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, + {file = "arclet_alconna-1.8.23-py3-none-any.whl", hash = "sha256:d4d8a427715408399e46530ec6bdefff4de72ff5d51183fa50ce5ea56a4e2a2a"}, + {file = "arclet_alconna-1.8.23.tar.gz", hash = "sha256:f811caf60dc4231b70a6885fe1af35aa95ae93bad46566e9086b623f449c9a09"}, +] + +[package.dependencies] +nepattern = ">=0.7.6,<1.0.0" +tarina = ">=0.5.5" +typing-extensions = ">=4.5.0" + +[package.extras] +full = ["arclet-alconna-tools (>=0.2.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "arclet-alconna-tools" +version = "0.7.9" +description = "Builtin Tools for Alconna" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arclet_alconna_tools-0.7.9-py3-none-any.whl", hash = "sha256:01a3462bb9f8dbe55010b394f7a0ac11e331799d463e326738870dce191aa608"}, + {file = "arclet_alconna_tools-0.7.9.tar.gz", hash = "sha256:bded24c4157e13e2d803fe7b77ee246fda456206451337015513f150d1e4449c"}, +] + +[package.dependencies] +arclet-alconna = ">=1.8.21" +nepattern = ">=0.7.3,<1.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [package.source] @@ -180,57 +317,60 @@ reference = "ali" [[package]] name = "asyncpg" -version = "0.28.0" +version = "0.29.0" description = "An asyncio PostgreSQL driver" -category = "main" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "asyncpg-0.28.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a6d1b954d2b296292ddff4e0060f494bb4270d87fb3655dd23c5c6096d16d83"}, - {file = "asyncpg-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0740f836985fd2bd73dca42c50c6074d1d61376e134d7ad3ad7566c4f79f8184"}, - {file = "asyncpg-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e907cf620a819fab1737f2dd90c0f185e2a796f139ac7de6aa3212a8af96c050"}, - {file = "asyncpg-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b339984d55e8202e0c4b252e9573e26e5afa05617ed02252544f7b3e6de3e9"}, - {file = "asyncpg-0.28.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c402745185414e4c204a02daca3d22d732b37359db4d2e705172324e2d94e85"}, - {file = "asyncpg-0.28.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c88eef5e096296626e9688f00ab627231f709d0e7e3fb84bb4413dff81d996d7"}, - {file = "asyncpg-0.28.0-cp310-cp310-win32.whl", hash = "sha256:90a7bae882a9e65a9e448fdad3e090c2609bb4637d2a9c90bfdcebbfc334bf89"}, - {file = "asyncpg-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:76aacdcd5e2e9999e83c8fbcb748208b60925cc714a578925adcb446d709016c"}, - {file = "asyncpg-0.28.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0e08fe2c9b3618459caaef35979d45f4e4f8d4f79490c9fa3367251366af207"}, - {file = "asyncpg-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b24e521f6060ff5d35f761a623b0042c84b9c9b9fb82786aadca95a9cb4a893b"}, - {file = "asyncpg-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99417210461a41891c4ff301490a8713d1ca99b694fef05dabd7139f9d64bd6c"}, - {file = "asyncpg-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f029c5adf08c47b10bcdc857001bbef551ae51c57b3110964844a9d79ca0f267"}, - {file = "asyncpg-0.28.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d6abf6c2f5152f46fff06b0e74f25800ce8ec6c80967f0bc789974de3c652"}, - {file = "asyncpg-0.28.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d7fa81ada2807bc50fea1dc741b26a4e99258825ba55913b0ddbf199a10d69d8"}, - {file = "asyncpg-0.28.0-cp311-cp311-win32.whl", hash = "sha256:f33c5685e97821533df3ada9384e7784bd1e7865d2b22f153f2e4bd4a083e102"}, - {file = "asyncpg-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:5e7337c98fb493079d686a4a6965e8bcb059b8e1b8ec42106322fc6c1c889bb0"}, - {file = "asyncpg-0.28.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1c56092465e718a9fdcc726cc3d9dcf3a692e4834031c9a9f871d92a75d20d48"}, - {file = "asyncpg-0.28.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4acd6830a7da0eb4426249d71353e8895b350daae2380cb26d11e0d4a01c5472"}, - {file = "asyncpg-0.28.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63861bb4a540fa033a56db3bb58b0c128c56fad5d24e6d0a8c37cb29b17c1c7d"}, - {file = "asyncpg-0.28.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a93a94ae777c70772073d0512f21c74ac82a8a49be3a1d982e3f259ab5f27307"}, - {file = "asyncpg-0.28.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d14681110e51a9bc9c065c4e7944e8139076a778e56d6f6a306a26e740ed86d2"}, - {file = "asyncpg-0.28.0-cp37-cp37m-win32.whl", hash = "sha256:8aec08e7310f9ab322925ae5c768532e1d78cfb6440f63c078b8392a38aa636a"}, - {file = "asyncpg-0.28.0-cp37-cp37m-win_amd64.whl", hash = "sha256:319f5fa1ab0432bc91fb39b3960b0d591e6b5c7844dafc92c79e3f1bff96abef"}, - {file = "asyncpg-0.28.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b337ededaabc91c26bf577bfcd19b5508d879c0ad009722be5bb0a9dd30b85a0"}, - {file = "asyncpg-0.28.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d32b680a9b16d2957a0a3cc6b7fa39068baba8e6b728f2e0a148a67644578f4"}, - {file = "asyncpg-0.28.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f62f04cdf38441a70f279505ef3b4eadf64479b17e707c950515846a2df197"}, - {file = "asyncpg-0.28.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f20cac332c2576c79c2e8e6464791c1f1628416d1115935a34ddd7121bfc6a4"}, - {file = "asyncpg-0.28.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:59f9712ce01e146ff71d95d561fb68bd2d588a35a187116ef05028675462d5ed"}, - {file = "asyncpg-0.28.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc9e9f9ff1aa0eddcc3247a180ac9e9b51a62311e988809ac6152e8fb8097756"}, - {file = "asyncpg-0.28.0-cp38-cp38-win32.whl", hash = "sha256:9e721dccd3838fcff66da98709ed884df1e30a95f6ba19f595a3706b4bc757e3"}, - {file = "asyncpg-0.28.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ba7d06a0bea539e0487234511d4adf81dc8762249858ed2a580534e1720db00"}, - {file = "asyncpg-0.28.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d009b08602b8b18edef3a731f2ce6d3f57d8dac2a0a4140367e194eabd3de457"}, - {file = "asyncpg-0.28.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ec46a58d81446d580fb21b376ec6baecab7288ce5a578943e2fc7ab73bf7eb39"}, - {file = "asyncpg-0.28.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b48ceed606cce9e64fd5480a9b0b9a95cea2b798bb95129687abd8599c8b019"}, - {file = "asyncpg-0.28.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8858f713810f4fe67876728680f42e93b7e7d5c7b61cf2118ef9153ec16b9423"}, - {file = "asyncpg-0.28.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5e18438a0730d1c0c1715016eacda6e9a505fc5aa931b37c97d928d44941b4bf"}, - {file = "asyncpg-0.28.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e9c433f6fcdd61c21a715ee9128a3ca48be8ac16fa07be69262f016bb0f4dbd2"}, - {file = "asyncpg-0.28.0-cp39-cp39-win32.whl", hash = "sha256:41e97248d9076bc8e4849da9e33e051be7ba37cd507cbd51dfe4b2d99c70e3dc"}, - {file = "asyncpg-0.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ed77f00c6aacfe9d79e9eff9e21729ce92a4b38e80ea99a58ed382f42ebd55b"}, - {file = "asyncpg-0.28.0.tar.gz", hash = "sha256:7252cdc3acb2f52feaa3664280d3bcd78a46bd6c10bfd681acfffefa1120e278"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, ] +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} + [package.extras] docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] [package.source] type = "legacy" @@ -239,56 +379,22 @@ reference = "ali" [[package]] name = "attrs" -version = "23.1.0" +version = "24.2.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [package.source] type = "legacy" @@ -297,21 +403,22 @@ reference = "ali" [[package]] name = "beautifulsoup4" -version = "4.9.3" +version = "4.12.3" description = "Screen-scraping library" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, - {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, - {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [package.dependencies] -soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} +soupsieve = ">1.2" [package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] @@ -322,24 +429,42 @@ reference = "ali" [[package]] name = "bilireq" -version = "0.2.10" -description = "又一个哔哩哔哩请求库" -category = "main" +version = "0.2.3.post0" +description = "" optional = false -python-versions = "<4.0,>=3.8" +python-versions = ">=3.7,<4.0" files = [ - {file = "bilireq-0.2.10-py3-none-any.whl", hash = "sha256:685da236c2c3b0416a9172f81193180b43a4bcc259e88b27a68c0cde9ccee2c0"}, - {file = "bilireq-0.2.10.tar.gz", hash = "sha256:b5a4e825bb9b415792453d29861f0f0908b583f55a7c31ddca3c39e792ccc154"}, + {file = "bilireq-0.2.3.post0-py3-none-any.whl", hash = "sha256:8d1f98bb8fb59c0ce1dec226329353ce51e2efaad0a6b4d240437b6132648322"}, + {file = "bilireq-0.2.3.post0.tar.gz", hash = "sha256:3185c3952a2becc7d31b0c01a12fda463fa477253504a68f81ea871594887ab4"}, ] [package.dependencies] -grpcio = ">=1.56.2" -httpx = "*" -protobuf = ">=4.23.4" -rsa = ">=4.9" +grpcio = ">=1.49.1,<2.0.0" +httpx = ">=0.23.0,<0.24.0" +protobuf = ">=4.21.7,<5.0.0" +rsa = ">=4.9,<5.0" [package.extras] -qrcode = ["qrcode[pil]"] +qrcode = ["qrcode[pil] (>=7.3.1,<8.0.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "binaryornot" +version = "0.4.4" +description = "Ultra-lightweight pure Python package to check if a file is binary or text." +optional = false +python-versions = "*" +files = [ + {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, + {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, +] + +[package.dependencies] +chardet = ">=3.0.2" [package.source] type = "legacy" @@ -348,37 +473,47 @@ reference = "ali" [[package]] name = "black" -version = "22.12.0" +version = "24.8.0" description = "The uncompromising code formatter." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -389,14 +524,13 @@ reference = "ali" [[package]] name = "cachetools" -version = "5.3.1" +version = "5.4.0" description = "Extensible memoizing collections and decorators" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, ] [package.source] @@ -404,21 +538,54 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "cashews" +version = "7.1.0" +description = "cache tools with async power" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cashews-7.1.0-py3-none-any.whl", hash = "sha256:b7c1ae4d49df6fdbff88e5025d3c1156515f58724c5b96fc9a9d081afada82a8"}, + {file = "cashews-7.1.0.tar.gz", hash = "sha256:058df55a39cb15697d331e7e41c2882b58d0d323f5671316105cc78668af7705"}, +] + +[package.extras] +dill = ["dill"] +diskcache = ["diskcache (>=5.0.0)"] +lint = ["mypy (>=1.5.0)", "types-redis"] +redis = ["redis (>=4.3.1,!=5.0.1)"] +speedup = ["bitarray (<3.0.0)", "hiredis", "xxhash (<4.0.0)"] +tests = ["hypothesis (==6.100.2)", "pytest (==8.2.0)", "pytest-asyncio (==0.23.6)", "pytest-cov (==5.0.0)", "pytest-rerunfailures (==14.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "cattrs" -version = "22.2.0" +version = "23.2.3" description = "Composable complex class support for attrs and dataclasses." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cattrs-22.2.0-py3-none-any.whl", hash = "sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21"}, - {file = "cattrs-22.2.0.tar.gz", hash = "sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d"}, + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, ] [package.dependencies] -attrs = ">=20" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +attrs = ">=23.1.0" +exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} + +[package.extras] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] [package.source] type = "legacy" @@ -427,14 +594,13 @@ reference = "ali" [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [package.source] @@ -443,15 +609,202 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "main" +name = "cffi" +version = "1.17.0" +description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.8" files = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, + {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, + {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, + {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, + {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, +] + +[package.dependencies] +pycparser = "*" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [package.source] @@ -463,7 +816,6 @@ reference = "ali" name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -483,7 +835,6 @@ reference = "ali" name = "cn2an" version = "0.5.22" description = "Convert Chinese numerals and Arabic numerals." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -504,7 +855,6 @@ reference = "ali" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -518,83 +868,25 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -category = "main" +name = "cookiecutter" +version = "2.6.0" +description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "contourpy" -version = "1.1.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, + {file = "cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d"}, + {file = "cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c"}, ] [package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] +arrow = "*" +binaryornot = ">=0.4.4" +click = ">=7.0,<9.0.0" +Jinja2 = ">=2.7,<4.0.0" +python-slugify = ">=4.0.0" +pyyaml = ">=5.3.1" +requests = ">=2.23.0" +rich = "*" [package.source] type = "legacy" @@ -602,17 +894,54 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" -category = "main" +name = "cryptography" +version = "43.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, ] +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -620,14 +949,13 @@ reference = "ali" [[package]] name = "dateparser" -version = "1.1.8" +version = "1.2.0" description = "Date parsing library designed to parse dates from HTML pages" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "dateparser-1.1.8-py2.py3-none-any.whl", hash = "sha256:070b29b5bbf4b1ec2cd51c96ea040dc68a614de703910a91ad1abba18f9f379f"}, - {file = "dateparser-1.1.8.tar.gz", hash = "sha256:86b8b7517efcc558f085a142cdb7620f0921543fcabdb538c8a4c4001d8178e3"}, + {file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"}, + {file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"}, ] [package.dependencies] @@ -647,15 +975,30 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "ecdsa" -version = "0.18.0" -description = "ECDSA cryptographic signature library (pure python)" -category = "main" +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "*" files = [ - {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, - {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "ecdsa" +version = "0.19.0" +description = "ECDSA cryptographic signature library (pure python)" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, + {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, ] [package.dependencies] @@ -672,17 +1015,20 @@ reference = "ali" [[package]] name = "emoji" -version = "1.7.0" +version = "2.12.1" description = "Emoji for Python" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "emoji-1.7.0.tar.gz", hash = "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1"}, + {file = "emoji-2.12.1-py3-none-any.whl", hash = "sha256:a00d62173bdadc2510967a381810101624a2f0986145b8da0cffa42e29430235"}, + {file = "emoji-2.12.1.tar.gz", hash = "sha256:4aa0488817691aa58d83764b6c209f8a27c0b3ab3f89d1b8dceca1a62e4973eb"}, ] +[package.dependencies] +typing-extensions = ">=4.7.0" + [package.extras] -dev = ["coverage", "coveralls", "pytest"] +dev = ["coverage", "pytest (>=7.4.4)"] [package.source] type = "legacy" @@ -691,14 +1037,13 @@ reference = "ali" [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -711,25 +1056,23 @@ reference = "ali" [[package]] name = "fastapi" -version = "0.95.1" +version = "0.112.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"}, - {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"}, + {file = "fastapi-0.112.0-py3-none-any.whl", hash = "sha256:3487ded9778006a45834b8c816ec4a48d522e2631ca9e75ec5a774f1b052f821"}, + {file = "fastapi-0.112.0.tar.gz", hash = "sha256:d262bc56b7d101d1f4e8fc0ad2ac75bb9935fec504d2b7117686cec50710cf05"}, ] [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = ">=0.26.1,<0.27.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.38.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] [package.source] type = "legacy" @@ -738,14 +1081,13 @@ reference = "ali" [[package]] name = "feedparser" -version = "6.0.10" +version = "6.0.11" description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "feedparser-6.0.10-py3-none-any.whl", hash = "sha256:79c257d526d13b944e965f6095700587f27388e50ea16fd245babe4dfae7024f"}, - {file = "feedparser-6.0.10.tar.gz", hash = "sha256:27da485f4637ce7163cdeab13a80312b93b7d0c1b775bef4a47629a3110bca51"}, + {file = "feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45"}, + {file = "feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5"}, ] [package.dependencies] @@ -757,62 +1099,111 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "fonttools" -version = "4.42.1" -description = "Tools to manipulate font files" -category = "main" +name = "filelock" +version = "3.15.4" +description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed1a13a27f59d1fc1920394a7f596792e9d546c9ca5a044419dca70c37815d7c"}, - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b1ce7a45978b821a06d375b83763b27a3a5e8a2e4570b3065abad240a18760"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f720fa82a11c0f9042376fd509b5ed88dab7e3cd602eee63a1af08883b37342b"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db55cbaea02a20b49fefbd8e9d62bd481aaabe1f2301dabc575acc6b358874fa"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a35981d90feebeaef05e46e33e6b9e5b5e618504672ca9cd0ff96b171e4bfff"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:68a02bbe020dc22ee0540e040117535f06df9358106d3775e8817d826047f3fd"}, - {file = "fonttools-4.42.1-cp310-cp310-win32.whl", hash = "sha256:12a7c247d1b946829bfa2f331107a629ea77dc5391dfd34fdcd78efa61f354ca"}, - {file = "fonttools-4.42.1-cp310-cp310-win_amd64.whl", hash = "sha256:a398bdadb055f8de69f62b0fc70625f7cbdab436bbb31eef5816e28cab083ee8"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:689508b918332fb40ce117131633647731d098b1b10d092234aa959b4251add5"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e36344e48af3e3bde867a1ca54f97c308735dd8697005c2d24a86054a114a71"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7db825c8adee96fac0692e6e1ecd858cae9affb3b4812cdb9d934a898b29e"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113337c2d29665839b7d90b39f99b3cac731f72a0eda9306165a305c7c31d341"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37983b6bdab42c501202500a2be3a572f50d4efe3237e0686ee9d5f794d76b35"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6ed2662a3d9c832afa36405f8748c250be94ae5dfc5283d668308391f2102861"}, - {file = "fonttools-4.42.1-cp311-cp311-win32.whl", hash = "sha256:179737095eb98332a2744e8f12037b2977f22948cf23ff96656928923ddf560a"}, - {file = "fonttools-4.42.1-cp311-cp311-win_amd64.whl", hash = "sha256:f2b82f46917d8722e6b5eafeefb4fb585d23babd15d8246c664cd88a5bddd19c"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:62f481ac772fd68901573956231aea3e4b1ad87b9b1089a61613a91e2b50bb9b"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2f806990160d1ce42d287aa419df3ffc42dfefe60d473695fb048355fe0c6a0"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db372213d39fa33af667c2aa586a0c1235e88e9c850f5dd5c8e1f17515861868"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d18fc642fd0ac29236ff88ecfccff229ec0386090a839dd3f1162e9a7944a40"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8708b98c278012ad267ee8a7433baeb809948855e81922878118464b274c909d"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c95b0724a6deea2c8c5d3222191783ced0a2f09bd6d33f93e563f6f1a4b3b3a4"}, - {file = "fonttools-4.42.1-cp38-cp38-win32.whl", hash = "sha256:4aa79366e442dbca6e2c8595645a3a605d9eeabdb7a094d745ed6106816bef5d"}, - {file = "fonttools-4.42.1-cp38-cp38-win_amd64.whl", hash = "sha256:acb47f6f8680de24c1ab65ebde39dd035768e2a9b571a07c7b8da95f6c8815fd"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb289b7a815638a7613d46bcf324c9106804725b2bb8ad913c12b6958ffc4ec"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:53eb5091ddc8b1199330bb7b4a8a2e7995ad5d43376cadce84523d8223ef3136"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46a0ec8adbc6ff13494eb0c9c2e643b6f009ce7320cf640de106fb614e4d4360"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cc7d685b8eeca7ae69dc6416833fbfea61660684b7089bca666067cb2937dcf"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:be24fcb80493b2c94eae21df70017351851652a37de514de553435b256b2f249"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:515607ec756d7865f23070682622c49d922901943697871fc292277cf1e71967"}, - {file = "fonttools-4.42.1-cp39-cp39-win32.whl", hash = "sha256:0eb79a2da5eb6457a6f8ab904838454accc7d4cccdaff1fd2bd3a0679ea33d64"}, - {file = "fonttools-4.42.1-cp39-cp39-win_amd64.whl", hash = "sha256:7286aed4ea271df9eab8d7a9b29e507094b51397812f7ce051ecd77915a6e26b"}, - {file = "fonttools-4.42.1-py3-none-any.whl", hash = "sha256:9398f244e28e0596e2ee6024f808b06060109e33ed38dcc9bded452fd9bbb853"}, - {file = "fonttools-4.42.1.tar.gz", hash = "sha256:c391cd5af88aacaf41dd7cfb96eeedfad297b5899a39e12f4c2c3706d0a3329d"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] -lxml = ["lxml (>=4.0,<5)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] [package.source] type = "legacy" @@ -821,80 +1212,73 @@ reference = "ali" [[package]] name = "greenlet" -version = "2.0.2" +version = "3.0.3" description = "Lightweight in-process concurrent programming" -category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.7" files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [package.source] @@ -904,61 +1288,61 @@ reference = "ali" [[package]] name = "grpcio" -version = "1.57.0" +version = "1.65.4" description = "HTTP/2-based RPC framework" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "grpcio-1.57.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:092fa155b945015754bdf988be47793c377b52b88d546e45c6a9f9579ac7f7b6"}, - {file = "grpcio-1.57.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2f7349786da979a94690cc5c2b804cab4e8774a3cf59be40d037c4342c906649"}, - {file = "grpcio-1.57.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:82640e57fb86ea1d71ea9ab54f7e942502cf98a429a200b2e743d8672171734f"}, - {file = "grpcio-1.57.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40b72effd4c789de94ce1be2b5f88d7b9b5f7379fe9645f198854112a6567d9a"}, - {file = "grpcio-1.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f708a6a17868ad8bf586598bee69abded4996b18adf26fd2d91191383b79019"}, - {file = "grpcio-1.57.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:60fe15288a0a65d5c1cb5b4a62b1850d07336e3ba728257a810317be14f0c527"}, - {file = "grpcio-1.57.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6907b1cf8bb29b058081d2aad677b15757a44ef2d4d8d9130271d2ad5e33efca"}, - {file = "grpcio-1.57.0-cp310-cp310-win32.whl", hash = "sha256:57b183e8b252825c4dd29114d6c13559be95387aafc10a7be645462a0fc98bbb"}, - {file = "grpcio-1.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7b400807fa749a9eb286e2cd893e501b110b4d356a218426cb9c825a0474ca56"}, - {file = "grpcio-1.57.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:c6ebecfb7a31385393203eb04ed8b6a08f5002f53df3d59e5e795edb80999652"}, - {file = "grpcio-1.57.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:00258cbe3f5188629828363ae8ff78477ce976a6f63fb2bb5e90088396faa82e"}, - {file = "grpcio-1.57.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:23e7d8849a0e58b806253fd206ac105b328171e01b8f18c7d5922274958cc87e"}, - {file = "grpcio-1.57.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5371bcd861e679d63b8274f73ac281751d34bd54eccdbfcd6aa00e692a82cd7b"}, - {file = "grpcio-1.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aed90d93b731929e742967e236f842a4a2174dc5db077c8f9ad2c5996f89f63e"}, - {file = "grpcio-1.57.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe752639919aad9ffb0dee0d87f29a6467d1ef764f13c4644d212a9a853a078d"}, - {file = "grpcio-1.57.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fada6b07ec4f0befe05218181f4b85176f11d531911b64c715d1875c4736d73a"}, - {file = "grpcio-1.57.0-cp311-cp311-win32.whl", hash = "sha256:bb396952cfa7ad2f01061fbc7dc1ad91dd9d69243bcb8110cf4e36924785a0fe"}, - {file = "grpcio-1.57.0-cp311-cp311-win_amd64.whl", hash = "sha256:e503cb45ed12b924b5b988ba9576dc9949b2f5283b8e33b21dcb6be74a7c58d0"}, - {file = "grpcio-1.57.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:fd173b4cf02b20f60860dc2ffe30115c18972d7d6d2d69df97ac38dee03be5bf"}, - {file = "grpcio-1.57.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:d7f8df114d6b4cf5a916b98389aeaf1e3132035420a88beea4e3d977e5f267a5"}, - {file = "grpcio-1.57.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:76c44efa4ede1f42a9d5b2fed1fe9377e73a109bef8675fb0728eb80b0b8e8f2"}, - {file = "grpcio-1.57.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4faea2cfdf762a664ab90589b66f416274887641ae17817de510b8178356bf73"}, - {file = "grpcio-1.57.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60b83c43faeb6d0a9831f0351d7787a0753f5087cc6fa218d78fdf38e5acef0"}, - {file = "grpcio-1.57.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b363bbb5253e5f9c23d8a0a034dfdf1b7c9e7f12e602fc788c435171e96daccc"}, - {file = "grpcio-1.57.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f1fb0fd4a1e9b11ac21c30c169d169ef434c6e9344ee0ab27cfa6f605f6387b2"}, - {file = "grpcio-1.57.0-cp37-cp37m-win_amd64.whl", hash = "sha256:34950353539e7d93f61c6796a007c705d663f3be41166358e3d88c45760c7d98"}, - {file = "grpcio-1.57.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:871f9999e0211f9551f368612460442a5436d9444606184652117d6a688c9f51"}, - {file = "grpcio-1.57.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:a8a8e560e8dbbdf29288872e91efd22af71e88b0e5736b0daf7773c1fecd99f0"}, - {file = "grpcio-1.57.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:2313b124e475aa9017a9844bdc5eafb2d5abdda9d456af16fc4535408c7d6da6"}, - {file = "grpcio-1.57.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4098b6b638d9e0ca839a81656a2fd4bc26c9486ea707e8b1437d6f9d61c3941"}, - {file = "grpcio-1.57.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e5b58e32ae14658085c16986d11e99abd002ddbf51c8daae8a0671fffb3467f"}, - {file = "grpcio-1.57.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0f80bf37f09e1caba6a8063e56e2b87fa335add314cf2b78ebf7cb45aa7e3d06"}, - {file = "grpcio-1.57.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5b7a4ce8f862fe32b2a10b57752cf3169f5fe2915acfe7e6a1e155db3da99e79"}, - {file = "grpcio-1.57.0-cp38-cp38-win32.whl", hash = "sha256:9338bacf172e942e62e5889b6364e56657fbf8ac68062e8b25c48843e7b202bb"}, - {file = "grpcio-1.57.0-cp38-cp38-win_amd64.whl", hash = "sha256:e1cb52fa2d67d7f7fab310b600f22ce1ff04d562d46e9e0ac3e3403c2bb4cc16"}, - {file = "grpcio-1.57.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:fee387d2fab144e8a34e0e9c5ca0f45c9376b99de45628265cfa9886b1dbe62b"}, - {file = "grpcio-1.57.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:b53333627283e7241fcc217323f225c37783b5f0472316edcaa4479a213abfa6"}, - {file = "grpcio-1.57.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:f19ac6ac0a256cf77d3cc926ef0b4e64a9725cc612f97228cd5dc4bd9dbab03b"}, - {file = "grpcio-1.57.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fdf04e402f12e1de8074458549337febb3b45f21076cc02ef4ff786aff687e"}, - {file = "grpcio-1.57.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5613a2fecc82f95d6c51d15b9a72705553aa0d7c932fad7aed7afb51dc982ee5"}, - {file = "grpcio-1.57.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b670c2faa92124b7397b42303e4d8eb64a4cd0b7a77e35a9e865a55d61c57ef9"}, - {file = "grpcio-1.57.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a635589201b18510ff988161b7b573f50c6a48fae9cb567657920ca82022b37"}, - {file = "grpcio-1.57.0-cp39-cp39-win32.whl", hash = "sha256:d78d8b86fcdfa1e4c21f8896614b6cc7ee01a2a758ec0c4382d662f2a62cf766"}, - {file = "grpcio-1.57.0-cp39-cp39-win_amd64.whl", hash = "sha256:20ec6fc4ad47d1b6e12deec5045ec3cd5402d9a1597f738263e98f490fe07056"}, - {file = "grpcio-1.57.0.tar.gz", hash = "sha256:4b089f7ad1eb00a104078bab8015b0ed0ebcb3b589e527ab009c53893fd4e613"}, + {file = "grpcio-1.65.4-cp310-cp310-linux_armv7l.whl", hash = "sha256:0e85c8766cf7f004ab01aff6a0393935a30d84388fa3c58d77849fcf27f3e98c"}, + {file = "grpcio-1.65.4-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e4a795c02405c7dfa8affd98c14d980f4acea16ea3b539e7404c645329460e5a"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:d7b984a8dd975d949c2042b9b5ebcf297d6d5af57dcd47f946849ee15d3c2fb8"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644a783ce604a7d7c91412bd51cf9418b942cf71896344b6dc8d55713c71ce82"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5764237d751d3031a36fafd57eb7d36fd2c10c658d2b4057c516ccf114849a3e"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ee40d058cf20e1dd4cacec9c39e9bce13fedd38ce32f9ba00f639464fcb757de"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4482a44ce7cf577a1f8082e807a5b909236bce35b3e3897f839f2fbd9ae6982d"}, + {file = "grpcio-1.65.4-cp310-cp310-win32.whl", hash = "sha256:66bb051881c84aa82e4f22d8ebc9d1704b2e35d7867757f0740c6ef7b902f9b1"}, + {file = "grpcio-1.65.4-cp310-cp310-win_amd64.whl", hash = "sha256:870370524eff3144304da4d1bbe901d39bdd24f858ce849b7197e530c8c8f2ec"}, + {file = "grpcio-1.65.4-cp311-cp311-linux_armv7l.whl", hash = "sha256:85e9c69378af02e483bc626fc19a218451b24a402bdf44c7531e4c9253fb49ef"}, + {file = "grpcio-1.65.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2bd672e005afab8bf0d6aad5ad659e72a06dd713020554182a66d7c0c8f47e18"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:abccc5d73f5988e8f512eb29341ed9ced923b586bb72e785f265131c160231d8"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:886b45b29f3793b0c2576201947258782d7e54a218fe15d4a0468d9a6e00ce17"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be952436571dacc93ccc7796db06b7daf37b3b56bb97e3420e6503dccfe2f1b4"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8dc9ddc4603ec43f6238a5c95400c9a901b6d079feb824e890623da7194ff11e"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ade1256c98cba5a333ef54636095f2c09e6882c35f76acb04412f3b1aa3c29a5"}, + {file = "grpcio-1.65.4-cp311-cp311-win32.whl", hash = "sha256:280e93356fba6058cbbfc6f91a18e958062ef1bdaf5b1caf46c615ba1ae71b5b"}, + {file = "grpcio-1.65.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2b819f9ee27ed4e3e737a4f3920e337e00bc53f9e254377dd26fc7027c4d558"}, + {file = "grpcio-1.65.4-cp312-cp312-linux_armv7l.whl", hash = "sha256:926a0750a5e6fb002542e80f7fa6cab8b1a2ce5513a1c24641da33e088ca4c56"}, + {file = "grpcio-1.65.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2a1d4c84d9e657f72bfbab8bedf31bdfc6bfc4a1efb10b8f2d28241efabfaaf2"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:17de4fda50967679677712eec0a5c13e8904b76ec90ac845d83386b65da0ae1e"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dee50c1b69754a4228e933696408ea87f7e896e8d9797a3ed2aeed8dbd04b74"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c34fc7562bdd169b77966068434a93040bfca990e235f7a67cdf26e1bd5c63"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:24a2246e80a059b9eb981e4c2a6d8111b1b5e03a44421adbf2736cc1d4988a8a"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18c10f0d054d2dce34dd15855fcca7cc44ec3b811139437543226776730c0f28"}, + {file = "grpcio-1.65.4-cp312-cp312-win32.whl", hash = "sha256:d72962788b6c22ddbcdb70b10c11fbb37d60ae598c51eb47ec019db66ccfdff0"}, + {file = "grpcio-1.65.4-cp312-cp312-win_amd64.whl", hash = "sha256:7656376821fed8c89e68206a522522317787a3d9ed66fb5110b1dff736a5e416"}, + {file = "grpcio-1.65.4-cp38-cp38-linux_armv7l.whl", hash = "sha256:4934077b33aa6fe0b451de8b71dabde96bf2d9b4cb2b3187be86e5adebcba021"}, + {file = "grpcio-1.65.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0cef8c919a3359847c357cb4314e50ed1f0cca070f828ee8f878d362fd744d52"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:a925446e6aa12ca37114840d8550f308e29026cdc423a73da3043fd1603a6385"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf53e6247f1e2af93657e62e240e4f12e11ee0b9cef4ddcb37eab03d501ca864"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb34278e4ceb224c89704cd23db0d902e5e3c1c9687ec9d7c5bb4c150f86816"}, + {file = "grpcio-1.65.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e6cbdd107e56bde55c565da5fd16f08e1b4e9b0674851d7749e7f32d8645f524"}, + {file = "grpcio-1.65.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:626319a156b1f19513156a3b0dbfe977f5f93db63ca673a0703238ebd40670d7"}, + {file = "grpcio-1.65.4-cp38-cp38-win32.whl", hash = "sha256:3d1bbf7e1dd1096378bd83c83f554d3b93819b91161deaf63e03b7022a85224a"}, + {file = "grpcio-1.65.4-cp38-cp38-win_amd64.whl", hash = "sha256:a99e6dffefd3027b438116f33ed1261c8d360f0dd4f943cb44541a2782eba72f"}, + {file = "grpcio-1.65.4-cp39-cp39-linux_armv7l.whl", hash = "sha256:874acd010e60a2ec1e30d5e505b0651ab12eb968157cd244f852b27c6dbed733"}, + {file = "grpcio-1.65.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b07f36faf01fca5427d4aa23645e2d492157d56c91fab7e06fe5697d7e171ad4"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:b81711bf4ec08a3710b534e8054c7dcf90f2edc22bebe11c1775a23f145595fe"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88fcabc332a4aef8bcefadc34a02e9ab9407ab975d2c7d981a8e12c1aed92aa1"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ba3e63108a8749994f02c7c0e156afb39ba5bdf755337de8e75eb685be244b"}, + {file = "grpcio-1.65.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8eb485801957a486bf5de15f2c792d9f9c897a86f2f18db8f3f6795a094b4bb2"}, + {file = "grpcio-1.65.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075f3903bc1749ace93f2b0664f72964ee5f2da5c15d4b47e0ab68e4f442c257"}, + {file = "grpcio-1.65.4-cp39-cp39-win32.whl", hash = "sha256:0a0720299bdb2cc7306737295d56e41ce8827d5669d4a3cd870af832e3b17c4d"}, + {file = "grpcio-1.65.4-cp39-cp39-win_amd64.whl", hash = "sha256:a146bc40fa78769f22e1e9ff4f110ef36ad271b79707577bf2a31e3e931141b9"}, + {file = "grpcio-1.65.4.tar.gz", hash = "sha256:2a4f476209acffec056360d3e647ae0e14ae13dcf3dfb130c227ae1c594cbe39"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.57.0)"] +protobuf = ["grpcio-tools (>=1.65.4)"] [package.source] type = "legacy" @@ -969,7 +1353,6 @@ reference = "ali" name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -986,7 +1369,6 @@ reference = "ali" name = "httpcore" version = "0.16.3" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -998,11 +1380,11 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [package.source] type = "legacy" @@ -1011,47 +1393,47 @@ reference = "ali" [[package]] name = "httptools" -version = "0.6.0" +version = "0.6.1" description = "A collection of framework independent HTTP protocol utils." -category = "main" optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.8.0" files = [ - {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:818325afee467d483bfab1647a72054246d29f9053fd17cc4b86cda09cc60339"}, - {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72205730bf1be875003692ca54a4a7c35fac77b4746008966061d9d41a61b0f5"}, - {file = "httptools-0.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33eb1d4e609c835966e969a31b1dedf5ba16b38cab356c2ce4f3e33ffa94cad3"}, - {file = "httptools-0.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdc6675ec6cb79d27e0575750ac6e2b47032742e24eed011b8db73f2da9ed40"}, - {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:463c3bc5ef64b9cf091be9ac0e0556199503f6e80456b790a917774a616aff6e"}, - {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f228b88b0e8c6099a9c4757ce9fdbb8b45548074f8d0b1f0fc071e35655d1c"}, - {file = "httptools-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:0781fedc610293a2716bc7fa142d4c85e6776bc59d617a807ff91246a95dea35"}, - {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:721e503245d591527cddd0f6fd771d156c509e831caa7a57929b55ac91ee2b51"}, - {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:274bf20eeb41b0956e34f6a81f84d26ed57c84dd9253f13dcb7174b27ccd8aaf"}, - {file = "httptools-0.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:259920bbae18740a40236807915def554132ad70af5067e562f4660b62c59b90"}, - {file = "httptools-0.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfd2ae8a2d532952ac54445a2fb2504c804135ed28b53fefaf03d3a93eb1fd"}, - {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f959e4770b3fc8ee4dbc3578fd910fab9003e093f20ac8c621452c4d62e517cb"}, - {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e22896b42b95b3237eccc42278cd72c0df6f23247d886b7ded3163452481e38"}, - {file = "httptools-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:38f3cafedd6aa20ae05f81f2e616ea6f92116c8a0f8dcb79dc798df3356836e2"}, - {file = "httptools-0.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:47043a6e0ea753f006a9d0dd076a8f8c99bc0ecae86a0888448eb3076c43d717"}, - {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a541579bed0270d1ac10245a3e71e5beeb1903b5fbbc8d8b4d4e728d48ff1d"}, - {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d802e7b2538a9756df5acc062300c160907b02e15ed15ba035b02bce43e89c"}, - {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:26326e0a8fe56829f3af483200d914a7cd16d8d398d14e36888b56de30bec81a"}, - {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e41ccac9e77cd045f3e4ee0fc62cbf3d54d7d4b375431eb855561f26ee7a9ec4"}, - {file = "httptools-0.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e748fc0d5c4a629988ef50ac1aef99dfb5e8996583a73a717fc2cac4ab89932"}, - {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cf8169e839a0d740f3d3c9c4fa630ac1a5aaf81641a34575ca6773ed7ce041a1"}, - {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5dcc14c090ab57b35908d4a4585ec5c0715439df07be2913405991dbb37e049d"}, - {file = "httptools-0.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0b0571806a5168013b8c3d180d9f9d6997365a4212cb18ea20df18b938aa0b"}, - {file = "httptools-0.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb4a608c631f7dcbdf986f40af7a030521a10ba6bc3d36b28c1dc9e9035a3c0"}, - {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:93f89975465133619aea8b1952bc6fa0e6bad22a447c6d982fc338fbb4c89649"}, - {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:73e9d66a5a28b2d5d9fbd9e197a31edd02be310186db423b28e6052472dc8201"}, - {file = "httptools-0.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:22c01fcd53648162730a71c42842f73b50f989daae36534c818b3f5050b54589"}, - {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f96d2a351b5625a9fd9133c95744e8ca06f7a4f8f0b8231e4bbaae2c485046a"}, - {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72ec7c70bd9f95ef1083d14a755f321d181f046ca685b6358676737a5fecd26a"}, - {file = "httptools-0.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b703d15dbe082cc23266bf5d9448e764c7cb3fcfe7cb358d79d3fd8248673ef9"}, - {file = "httptools-0.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c723ed5982f8ead00f8e7605c53e55ffe47c47465d878305ebe0082b6a1755"}, - {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0a816bb425c116a160fbc6f34cece097fd22ece15059d68932af686520966bd"}, - {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dea66d94e5a3f68c5e9d86e0894653b87d952e624845e0b0e3ad1c733c6cc75d"}, - {file = "httptools-0.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:23b09537086a5a611fad5696fc8963d67c7e7f98cb329d38ee114d588b0b74cd"}, - {file = "httptools-0.6.0.tar.gz", hash = "sha256:9fc6e409ad38cbd68b177cd5158fc4042c796b82ca88d99ec78f07bed6c6b796"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, ] [package.extras] @@ -1066,7 +1448,6 @@ reference = "ali" name = "httpx" version = "0.23.3" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1082,9 +1463,9 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [package.source] type = "legacy" @@ -1093,14 +1474,13 @@ reference = "ali" [[package]] name = "idna" -version = "3.4" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [package.source] @@ -1112,7 +1492,6 @@ reference = "ali" name = "imagehash" version = "4.3.1" description = "Image Hashing library" -category = "main" optional = false python-versions = "*" files = [ @@ -1133,47 +1512,22 @@ reference = "ali" [[package]] name = "importlib-metadata" -version = "6.8.0" +version = "8.2.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, + {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, + {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "importlib-resources" -version = "6.0.1" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.0.1-py3-none-any.whl", hash = "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf"}, - {file = "importlib_resources-6.0.1.tar.gz", hash = "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [package.source] type = "legacy" @@ -1184,7 +1538,6 @@ reference = "ali" name = "iso8601" version = "1.1.0" description = "Simple module to parse ISO 8601 dates" -category = "main" optional = false python-versions = ">=3.6.2,<4.0" files = [ @@ -1197,32 +1550,15 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "jieba" -version = "0.42.1" -description = "Chinese Words Segmentation Utilities" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.4" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -1236,135 +1572,15 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "kiwisolver" -version = "1.4.5" -description = "A fast implementation of the Cassowary constraint solver" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, - {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "loguru" -version = "0.7.1" +version = "0.7.2" description = "Python logging made (stupidly) simple" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "loguru-0.7.1-py3-none-any.whl", hash = "sha256:046bf970cb3cad77a28d607cbf042ac25a407db987a1e801c7f7e692469982f9"}, - {file = "loguru-0.7.1.tar.gz", hash = "sha256:7ba2a7d81b79a412b0ded69bd921e012335e80fd39937a633570f273a343579e"}, + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, ] [package.dependencies] @@ -1372,7 +1588,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "pre-commit (==3.3.1)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] [package.source] type = "legacy" @@ -1381,79 +1597,157 @@ reference = "ali" [[package]] name = "lxml" -version = "4.6.5" +version = "5.3.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +python-versions = ">=3.6" files = [ - {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, - {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, - {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, - {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, - {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, - {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, - {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, - {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, - {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, - {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, - {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, - {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, - {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, - {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, - {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, - {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, - {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, - {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, - {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, - {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, - {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, - {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, - {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, - {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, - {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, - {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.7)"] +source = ["Cython (>=3.0.11)"] [package.source] type = "legacy" @@ -1462,21 +1756,17 @@ reference = "ali" [[package]] name = "markdown" -version = "3.4.4" +version = "3.6" description = "Python implementation of John Gruber's Markdown." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - [package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] [package.source] @@ -1484,64 +1774,102 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [package.source] @@ -1550,68 +1878,16 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "matplotlib" -version = "3.7.2" -description = "Python plotting package" -category = "main" +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:2699f7e73a76d4c110f4f25be9d2496d6ab4f17345307738557d345f099e07de"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a8035ba590658bae7562786c9cc6ea1a84aa49d3afab157e414c9e2ea74f496d"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f8e4a49493add46ad4a8c92f63e19d548b2b6ebbed75c6b4c7f46f57d36cdd1"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71667eb2ccca4c3537d9414b1bc00554cb7f91527c17ee4ec38027201f8f1603"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:152ee0b569a37630d8628534c628456b28686e085d51394da6b71ef84c4da201"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f8dddd1f5939e60aacb8fa08f19551f4b0140fab16a3669d5cd6e9cb28fc8"}, - {file = "matplotlib-3.7.2-cp310-cp310-win32.whl", hash = "sha256:fdbb46fad4fb47443b5b8ac76904b2e7a66556844f33370861b4788db0f8816a"}, - {file = "matplotlib-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:23fb1750934e5f0128f9423db27c474aa32534cec21f7b2153262b066a581fd1"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:30e1409b857aa8a747c5d4f85f63a79e479835f8dffc52992ac1f3f25837b544"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:50e0a55ec74bf2d7a0ebf50ac580a209582c2dd0f7ab51bc270f1b4a0027454e"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ac60daa1dc83e8821eed155796b0f7888b6b916cf61d620a4ddd8200ac70cd64"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305e3da477dc8607336ba10bac96986d6308d614706cae2efe7d3ffa60465b24"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c308b255efb9b06b23874236ec0f10f026673ad6515f602027cc8ac7805352d"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c521e21031632aa0d87ca5ba0c1c05f3daacadb34c093585a0be6780f698e4"}, - {file = "matplotlib-3.7.2-cp311-cp311-win32.whl", hash = "sha256:26bede320d77e469fdf1bde212de0ec889169b04f7f1179b8930d66f82b30cbc"}, - {file = "matplotlib-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4860132c8c05261a5f5f8467f1b269bf1c7c23902d75f2be57c4a7f2394b3e"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:a1733b8e84e7e40a9853e505fe68cc54339f97273bdfe6f3ed980095f769ddc7"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d9881356dc48e58910c53af82b57183879129fa30492be69058c5b0d9fddf391"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f081c03f413f59390a80b3e351cc2b2ea0205839714dbc364519bcf51f4b56ca"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cd120fca3407a225168238b790bd5c528f0fafde6172b140a2f3ab7a4ea63e9"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c1590b90aa7bd741b54c62b78de05d4186271e34e2377e0289d943b3522273"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d2ff3c984b8a569bc1383cd468fc06b70d7b59d5c2854ca39f1436ae8394117"}, - {file = "matplotlib-3.7.2-cp38-cp38-win32.whl", hash = "sha256:5dea00b62d28654b71ca92463656d80646675628d0828e08a5f3b57e12869e13"}, - {file = "matplotlib-3.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f506a1776ee94f9e131af1ac6efa6e5bc7cb606a3e389b0ccb6e657f60bb676"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:6515e878f91894c2e4340d81f0911857998ccaf04dbc1bba781e3d89cbf70608"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:71f7a8c6b124e904db550f5b9fe483d28b896d4135e45c4ea381ad3b8a0e3256"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12f01b92ecd518e0697da4d97d163b2b3aa55eb3eb4e2c98235b3396d7dad55f"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7e28d6396563955f7af437894a36bf2b279462239a41028323e04b85179058b"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbcf59334ff645e6a67cd5f78b4b2cdb76384cdf587fa0d2dc85f634a72e1a3e"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:318c89edde72ff95d8df67d82aca03861240512994a597a435a1011ba18dbc7f"}, - {file = "matplotlib-3.7.2-cp39-cp39-win32.whl", hash = "sha256:ce55289d5659b5b12b3db4dc9b7075b70cef5631e56530f14b2945e8836f2d20"}, - {file = "matplotlib-3.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:2ecb5be2b2815431c81dc115667e33da0f5a1bcf6143980d180d09a717c4a12e"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fdcd28360dbb6203fb5219b1a5658df226ac9bebc2542a9e8f457de959d713d0"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3cca3e842b11b55b52c6fb8bd6a4088693829acbfcdb3e815fa9b7d5c92c1b"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf577c7a6744e9e1bd3fee45fc74a02710b214f94e2bde344912d85e0c9af7c"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:936bba394682049919dda062d33435b3be211dc3dcaa011e09634f060ec878b2"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bc221ffbc2150458b1cd71cdd9ddd5bb37962b036e41b8be258280b5b01da1dd"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35d74ebdb3f71f112b36c2629cf32323adfbf42679e2751252acd468f5001c07"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717157e61b3a71d3d26ad4e1770dc85156c9af435659a25ee6407dc866cb258d"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:20f844d6be031948148ba49605c8b96dfe7d3711d1b63592830d650622458c11"}, - {file = "matplotlib-3.7.2.tar.gz", hash = "sha256:a8cdb91dddb04436bd2f098b8fdf4b81352e68cf4d2c6756fcc414791076569b"}, + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.3.1,<3.1" -python-dateutil = ">=2.7" - [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -1619,75 +1895,67 @@ reference = "ali" [[package]] name = "msgpack" -version = "1.0.5" +version = "1.0.8" description = "MessagePack serializer" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [package.source] @@ -1697,86 +1965,101 @@ reference = "ali" [[package]] name = "multidict" -version = "6.0.4" +version = "6.0.5" description = "multidict implementation" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] [package.source] @@ -1788,7 +2071,6 @@ reference = "ali" name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1802,20 +2084,130 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "nonebot-adapter-onebot" -version = "2.2.4" -description = "OneBot(CQHTTP) adapter for nonebot2" -category = "main" +name = "nb-cli" +version = "1.4.1" +description = "CLI for nonebot2" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "nb_cli-1.4.1-py3-none-any.whl", hash = "sha256:57b6111773202bce29c0520f4a281edb8a7643fa33692d4afc70ca5b51b10f70"}, + {file = "nb_cli-1.4.1.tar.gz", hash = "sha256:908dd4cbbf66bf46fe879c23ad1377332f63385cebca1912b627aa686d1816f3"}, +] + +[package.dependencies] +anyio = ">=3.6,<5.0" +cashews = ">=6.0,<8.0" +click = ">=8.1,<9.0" +cookiecutter = ">=2.2,<3.0" +httpx = ">=0.18,<1.0" +jinja2 = ">=3.0,<4.0" +noneprompt = ">=0.1.9,<1.0.0" +pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0" +pyfiglet = ">=1.0.1,<2.0.0" +tomlkit = ">=0.10,<1.0" +typing-extensions = ">=4.4,<5.0" +virtualenv = ">=20.21,<21.0" +watchfiles = ">=0.16,<1.0" +wcwidth = ">=0.2,<1.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nepattern" +version = "0.7.6" +description = "a complex pattern, support typing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nepattern-0.7.6-py3-none-any.whl", hash = "sha256:233d0befecc190f228ded3651a85faaf53f1308bba40ab8ddec379d0d3c88051"}, + {file = "nepattern-0.7.6.tar.gz", hash = "sha256:07bd5b2f3b9b9739b703bf723ffd642ca93738a32df7b699d57d6f338d46bad0"}, +] + +[package.dependencies] +tarina = ">=0.5.1" +typing-extensions = ">=4.5.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-adapter-discord" +version = "0.1.8" +description = "Discord adapter for nonebot2" +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "nonebot_adapter_discord-0.1.8-py3-none-any.whl", hash = "sha256:d063bf524f6a75c5c123f2d04227e0ec62c2433f56b28fb92fa5eb2aebef1c16"}, + {file = "nonebot_adapter_discord-0.1.8.tar.gz", hash = "sha256:5d3a7a8e0ab23b7ae84551b479c40c5d09733b15d09538d64765c5af54721781"}, +] + +[package.dependencies] +nonebot2 = ">=2.2.1,<3.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-adapter-dodo" +version = "0.1.4" +description = "Dodo adapter for nonebot2" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot_adapter_onebot-2.2.4-py3-none-any.whl", hash = "sha256:ae9971bb77a2984d6ca097d5565132723b051dafdfd1cef954a62f45684ae62c"}, - {file = "nonebot_adapter_onebot-2.2.4.tar.gz", hash = "sha256:1024b503514f87d6262adf1bde6f160b3a159afc4f6c21987eece3dacb4762dd"}, + {file = "nonebot_adapter_dodo-0.1.4-py3-none-any.whl", hash = "sha256:3bbe8ce1d686923dc7347d49e9e7164a93bc87e79626d6067e77b7c3d41d6861"}, + {file = "nonebot_adapter_dodo-0.1.4.tar.gz", hash = "sha256:21375ee712e97fe546ef24654dcb479f51e972335f13b4208af9ef53cc5fca29"}, +] + +[package.dependencies] +nonebot2 = ">=2.0.0,<3.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-adapter-kaiheila" +version = "0.3.4" +description = "kaiheila adapter for nonebot2" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_adapter_kaiheila-0.3.4-py3-none-any.whl", hash = "sha256:a4cc0e43bd24e015b8312f1753705116274d5b7e9a68be266384dd413ca4f510"}, + {file = "nonebot_adapter_kaiheila-0.3.4.tar.gz", hash = "sha256:1fea823e5bc2bb5dc8e56a4c10a8f6698dac6e4f77d4526768275fa0925340f2"}, +] + +[package.dependencies] +nonebot2 = ">=2.2.0,<3.0.0" +typing-extensions = ">=4.8.0,<5.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-adapter-onebot" +version = "2.4.4" +description = "OneBot(CQHTTP) adapter for nonebot2" +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "nonebot_adapter_onebot-2.4.4-py3-none-any.whl", hash = "sha256:4dceeec7332bb560652c764405e9dd350268303f69b7c0e92b7cfebe876e8d39"}, + {file = "nonebot_adapter_onebot-2.4.4.tar.gz", hash = "sha256:c8a3645f74a3e43c85f092fb670508c662c36831f019a15e4d74eaac686089f0"}, ] [package.dependencies] msgpack = ">=1.0.3,<2.0.0" -nonebot2 = ">=2.0.0-beta.3,<3.0.0" +nonebot2 = ">=2.2.0,<3.0.0" +pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0" typing-extensions = ">=4.0.0,<5.0.0" [package.source] @@ -1823,21 +2215,45 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "nonebot-plugin-alconna" +version = "0.51.1" +description = "Alconna Adapter for Nonebot" +optional = false +python-versions = ">=3.9" +files = [ + {file = "nonebot_plugin_alconna-0.51.1-py3-none-any.whl", hash = "sha256:450a27afa9dcaedb6c82f649d57d42c4ca81596bf6accdf2e163f2dc9befc2c4"}, + {file = "nonebot_plugin_alconna-0.51.1.tar.gz", hash = "sha256:aaec8206adc9892e284d7ad12c8bb03b43586bbc145d439f0a40a055146ed176"}, +] + +[package.dependencies] +arclet-alconna = ">=1.8.23" +arclet-alconna-tools = ">=0.7.9" +importlib-metadata = ">=4.13.0" +nepattern = ">=0.7.4" +nonebot-plugin-waiter = ">=0.6.0" +nonebot2 = ">=2.3.0" +tarina = ">=0.5.5" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "nonebot-plugin-apscheduler" -version = "0.2.0" +version = "0.3.0" description = "APScheduler Support for NoneBot2" -category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot-plugin-apscheduler-0.2.0.tar.gz", hash = "sha256:7b63e99a611b657533b48fcf1f8c6627c18c2eb3fa820a906cd4ec4666c0ceb0"}, - {file = "nonebot_plugin_apscheduler-0.2.0-py3-none-any.whl", hash = "sha256:9285ee84ca1cfa4db73c86cedb5911bbbd25a21ec0cd5f22447cd12f89e48fb4"}, + {file = "nonebot_plugin_apscheduler-0.3.0-py3-none-any.whl", hash = "sha256:ec5e0267293fc9803e543c6086d3e109ac87bf6dccea5473d219cad826238aae"}, + {file = "nonebot_plugin_apscheduler-0.3.0.tar.gz", hash = "sha256:7c41cc1d49ea6af7c4518c72cd15f8c2f549071b8bc8bfc4b21fbdd0a4875cfd"}, ] [package.dependencies] apscheduler = ">=3.7.0,<4.0.0" -nonebot2 = ">=2.0.0-rc.1,<3.0.0" +nonebot2 = ">=2.0.0,<3.0.0" [package.source] type = "legacy" @@ -1846,21 +2262,20 @@ reference = "ali" [[package]] name = "nonebot-plugin-htmlrender" -version = "0.2.2" +version = "0.3.3" description = "通过浏览器渲染图片" -category = "main" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "nonebot_plugin_htmlrender-0.2.2-py3-none-any.whl", hash = "sha256:0415d8123a3c1a8bc321762631e1bfa33611530122ad662b95a7a772c24a0725"}, - {file = "nonebot_plugin_htmlrender-0.2.2.tar.gz", hash = "sha256:d79c4fa1c9bd655ac12caa4289da58d249f67ed45d29754565e05e4efbfa1e85"}, + {file = "nonebot_plugin_htmlrender-0.3.3-py3-none-any.whl", hash = "sha256:2ac871d345c94103aa630153e007caa6319b5f5468491347513d746ba98b70d7"}, + {file = "nonebot_plugin_htmlrender-0.3.3.tar.gz", hash = "sha256:ab46ecc6dbd102628af8f88437fdc24da11839487950d07d0c5fd8db0db98ae8"}, ] [package.dependencies] aiofiles = ">=0.8.0" jinja2 = ">=3.0.3" markdown = ">=3.3.6" -nonebot2 = {version = ">=2.0.0", extras = ["fastapi"]} +nonebot2 = ">=2.2.0" playwright = ">=1.17.2" Pygments = ">=2.10.0" pymdown-extensions = ">=9.1" @@ -1872,30 +2287,92 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "nonebot2" -version = "2.0.1" -description = "An asynchronous python bot framework." -category = "main" +name = "nonebot-plugin-session" +version = "0.2.3" +description = "Nonebot2 会话信息提取与会话id定义" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot2-2.0.1-py3-none-any.whl", hash = "sha256:58111068df7a6c13cca2a412dd0f6f88d7bf2a2af3e92ae770fd913a9421743e"}, - {file = "nonebot2-2.0.1.tar.gz", hash = "sha256:c61294644aef08f2b427301ca1c358d34e6cfaa7025d694a502ad66e9508e7c2"}, + {file = "nonebot_plugin_session-0.2.3-py3-none-any.whl", hash = "sha256:5f652a0c082231c1cea72deb994a81e50f77ba532e14d30fdec09772f69079fd"}, + {file = "nonebot_plugin_session-0.2.3.tar.gz", hash = "sha256:33af37400f5005927c4ff861e593774bedc314fba00cfe06f482e582d9f447b7"}, +] + +[package.dependencies] +nonebot2 = ">=2.0.0,<3.0.0" +strenum = ">=0.4.8,<0.5.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-plugin-userinfo" +version = "0.1.3" +description = "Nonebot2 用户信息获取插件" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_plugin_userinfo-0.1.3-py3-none-any.whl", hash = "sha256:e20b22c81e86e81f7953560bd8ce0a54559a87ad615358c613b78cb5a4918191"}, + {file = "nonebot_plugin_userinfo-0.1.3.tar.gz", hash = "sha256:d0a4d64c612486df63cd16950446072f8dfd2063ea28f15d56305a585a6b0b6e"}, +] + +[package.dependencies] +cachetools = ">=5.0.0,<6.0.0" +emoji = ">=2.0.0,<3.0.0" +httpx = ">=0.20.0,<1.0.0" +nonebot2 = {version = ">=2.0.0,<3.0.0", extras = ["fastapi"]} +strenum = ">=0.4.8,<0.5.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-plugin-waiter" +version = "0.7.1" +description = "An alternative for got-and-reject in Nonebot" +optional = false +python-versions = ">=3.9" +files = [ + {file = "nonebot_plugin_waiter-0.7.1-py3-none-any.whl", hash = "sha256:b9967cc7aeea0db86053ada20929841830aea60bb8c7da26d0483eefda75635c"}, + {file = "nonebot_plugin_waiter-0.7.1.tar.gz", hash = "sha256:8be2adc175e45ca794881e3df449302b8e6e045cd9bae97a809907f4200b4110"}, +] + +[package.dependencies] +nonebot2 = ">=2.3.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot2" +version = "2.3.2" +description = "An asynchronous python bot framework." +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "nonebot2-2.3.2-py3-none-any.whl", hash = "sha256:c51aa3c1f23d8062ce6d13c8423dcb9a8bf0c44f21687916095f825da79a9a55"}, + {file = "nonebot2-2.3.2.tar.gz", hash = "sha256:af52e27e03e7fe147f2b642151eec81f264d058efe53b974eb08b5d90177cd14"}, ] [package.dependencies] fastapi = {version = ">=0.93.0,<1.0.0", optional = true, markers = "extra == \"fastapi\" or extra == \"all\""} loguru = ">=0.6.0,<1.0.0" -pydantic = {version = ">=1.10.0,<2.0.0", extras = ["dotenv"]} +pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0" pygtrie = ">=2.4.1,<3.0.0" +python-dotenv = ">=0.21.0,<2.0.0" tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.0.0,<5.0.0" +typing-extensions = ">=4.4.0,<5.0.0" uvicorn = {version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true, markers = "extra == \"quart\" or extra == \"fastapi\" or extra == \"all\""} yarl = ">=1.7.2,<2.0.0" [package.extras] -aiohttp = ["aiohttp[speedups] (>=3.7.4,<4.0.0)"] -all = ["Quart (>=0.18.0,<1.0.0)", "aiohttp[speedups] (>=3.7.4,<4.0.0)", "fastapi (>=0.93.0,<1.0.0)", "httpx[http2] (>=0.20.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)", "websockets (>=10.0)"] +aiohttp = ["aiohttp[speedups] (>=3.9.0b0,<4.0.0)"] +all = ["Quart (>=0.18.0,<1.0.0)", "aiohttp[speedups] (>=3.9.0b0,<4.0.0)", "fastapi (>=0.93.0,<1.0.0)", "httpx[http2] (>=0.20.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)", "websockets (>=10.0)"] fastapi = ["fastapi (>=0.93.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"] httpx = ["httpx[http2] (>=0.20.0,<1.0.0)"] quart = ["Quart (>=0.18.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"] @@ -1907,41 +2384,76 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "numpy" -version = "1.24.4" -description = "Fundamental package for array computing in Python" -category = "main" +name = "noneprompt" +version = "0.1.9" +description = "Prompt toolkit for console interaction" optional = false -python-versions = ">=3.8" +python-versions = ">=3.8,<4.0" files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, + {file = "noneprompt-0.1.9-py3-none-any.whl", hash = "sha256:a54f1e6a19a3da2dedf7f365f80420e9ae49326a0ffe60a8a9c7afdee6b6eeb3"}, + {file = "noneprompt-0.1.9.tar.gz", hash = "sha256:338b8bb89a8d22ef35f1dedb3aa7c1b228cf139973bdc43c5ffc3eef64457db9"}, +] + +[package.dependencies] +prompt-toolkit = ">=3.0.19,<4.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "numpy" +version = "2.0.1" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, + {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, + {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, + {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, + {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, + {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, + {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, + {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, + {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, + {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, ] [package.source] @@ -1951,30 +2463,26 @@ reference = "ali" [[package]] name = "opencv-python" -version = "4.8.0.76" +version = "4.10.0.84" description = "Wrapper package for OpenCV python bindings." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "opencv-python-4.8.0.76.tar.gz", hash = "sha256:56d84c43ce800938b9b1ec74b33942b2edbcef3f70c2754eb9bfe5dff1ee3ace"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:67bce4b9aad307c98a9a07c6afb7de3a4e823c1f4991d6d8e88e229e7dfeee59"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:48eb3121d809a873086d6677565e3ac963e6946110d13cd115533fa70e2aa2eb"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93871871b1c9d6b125cddd45b0638a2fa01ee9fd37f5e428823f750e404f2f15"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcb4944211acf13742dbfd9d3a11dc4e36353ffa1746f2c7dcd6a01c32d1376"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-win32.whl", hash = "sha256:b2349dc9f97ed6c9ba163d0a7a24bcef9695a3e216cd143e92f1b9659c5d9a49"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-win_amd64.whl", hash = "sha256:ba32cfa75a806abd68249699d34420737d27b5678553387fc5768747a6492147"}, + {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, ] [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, ] [package.source] @@ -1984,14 +2492,13 @@ reference = "ali" [[package]] name = "packaging" -version = "23.1" +version = "24.1" description = "Core utilities for Python packages" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [package.source] @@ -2001,14 +2508,13 @@ reference = "ali" [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [package.source] @@ -2020,7 +2526,6 @@ reference = "ali" name = "pillow" version = "9.5.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2103,19 +2608,19 @@ reference = "ali" [[package]] name = "platformdirs" -version = "3.10.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [package.source] type = "legacy" @@ -2124,25 +2629,23 @@ reference = "ali" [[package]] name = "playwright" -version = "1.37.0" +version = "1.45.1" description = "A high-level API to automate web browsers" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "playwright-1.37.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b476f63251876f1625f490af8d58ec0db90b555c623b7f54105f91d33878c06d"}, - {file = "playwright-1.37.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:68d56efe5ce916bab349177e90726837a6f0cae77ebd6a5200f5333b787b25fb"}, - {file = "playwright-1.37.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:428fdf9bfff586b73f96df53692d50d422afb93ca4650624f61e8181f548fed2"}, - {file = "playwright-1.37.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:41f0280472af94c426e941f6a969ff6a7ea156dc15fd01d09ac4b8f092e2346e"}, - {file = "playwright-1.37.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b574889ef97b7f44a633aa10d72b8966a850a4354d915fd0bc7e8658e825dd63"}, - {file = "playwright-1.37.0-py3-none-win32.whl", hash = "sha256:8b5d96aae54289129ab19d3d0e2e431171ae3e5d88d49a10900dcbe569a27d43"}, - {file = "playwright-1.37.0-py3-none-win_amd64.whl", hash = "sha256:678b9926be2df06321d11a525d4bf08d9f4a5b151354a3b82fe2ac14476322d5"}, + {file = "playwright-1.45.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:360607e37c00cdf97c74317f010e106ac4671aeaec6a192431dd71a30941da9d"}, + {file = "playwright-1.45.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:20adc2abf164c5e8969f9066011b152e12c210549edec78cd05bd0e9cf4135b7"}, + {file = "playwright-1.45.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5f047cdc6accf4c7084dfc7587a2a5ef790cddc44cbb111e471293c5a91119db"}, + {file = "playwright-1.45.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:f06f6659abe0abf263e5f6661d379fbf85c112745dd31d82332ceae914f58df7"}, + {file = "playwright-1.45.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87dc3b3d17e12c68830c29b7fdf5e93315221bbb4c6090e83e967e154e2c1828"}, + {file = "playwright-1.45.1-py3-none-win32.whl", hash = "sha256:2b8f517886ef1e2151982f6e7be84be3ef7d8135bdcf8ee705b4e4e99566e866"}, + {file = "playwright-1.45.1-py3-none-win_amd64.whl", hash = "sha256:0d236cf427784e77de352ba1b7d700693c5fe455b8e5f627f6d84ad5b84b5bf5"}, ] [package.dependencies] -greenlet = "2.0.2" -pyee = "9.0.4" -typing-extensions = {version = "*", markers = "python_version <= \"3.8\""} +greenlet = "3.0.3" +pyee = "11.1.0" [package.source] type = "legacy" @@ -2151,18 +2654,33 @@ reference = "ali" [[package]] name = "proces" -version = "0.1.6" +version = "0.1.7" description = "text preprocess." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "proces-0.1.6-py3-none-any.whl", hash = "sha256:6d6fefcb2a83bf26e1f0d35ac1d1c22b988d5161ccd01d83440544780a294d9e"}, - {file = "proces-0.1.6.tar.gz", hash = "sha256:a319bdf4b1724d080371f87febf4fd511d387dc78185d91687b607fa289cdce2"}, + {file = "proces-0.1.7-py3-none-any.whl", hash = "sha256:308325bbc96877263f06e57e5e9c760c4b42cc722887ad60be6b18fc37d68762"}, + {file = "proces-0.1.7.tar.gz", hash = "sha256:70a05d9e973dd685f7a9092c58be695a8181a411d63796c213232fd3fdc43775"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "prompt-toolkit" +version = "3.0.47" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] [package.dependencies] -"ruamel.yaml" = ">=0.16.5" +wcwidth = "*" [package.source] type = "legacy" @@ -2171,25 +2689,22 @@ reference = "ali" [[package]] name = "protobuf" -version = "4.24.2" +version = "4.25.4" description = "" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "protobuf-4.24.2-cp310-abi3-win32.whl", hash = "sha256:58e12d2c1aa428ece2281cef09bbaa6938b083bcda606db3da4e02e991a0d924"}, - {file = "protobuf-4.24.2-cp310-abi3-win_amd64.whl", hash = "sha256:77700b55ba41144fc64828e02afb41901b42497b8217b558e4a001f18a85f2e3"}, - {file = "protobuf-4.24.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:237b9a50bd3b7307d0d834c1b0eb1a6cd47d3f4c2da840802cd03ea288ae8880"}, - {file = "protobuf-4.24.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:25ae91d21e3ce8d874211110c2f7edd6384816fb44e06b2867afe35139e1fd1c"}, - {file = "protobuf-4.24.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:c00c3c7eb9ad3833806e21e86dca448f46035242a680f81c3fe068ff65e79c74"}, - {file = "protobuf-4.24.2-cp37-cp37m-win32.whl", hash = "sha256:4e69965e7e54de4db989289a9b971a099e626f6167a9351e9d112221fc691bc1"}, - {file = "protobuf-4.24.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c5cdd486af081bf752225b26809d2d0a85e575b80a84cde5172a05bbb1990099"}, - {file = "protobuf-4.24.2-cp38-cp38-win32.whl", hash = "sha256:6bd26c1fa9038b26c5c044ee77e0ecb18463e957fefbaeb81a3feb419313a54e"}, - {file = "protobuf-4.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb7aa97c252279da65584af0456f802bd4b2de429eb945bbc9b3d61a42a8cd16"}, - {file = "protobuf-4.24.2-cp39-cp39-win32.whl", hash = "sha256:2b23bd6e06445699b12f525f3e92a916f2dcf45ffba441026357dea7fa46f42b"}, - {file = "protobuf-4.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:839952e759fc40b5d46be319a265cf94920174d88de31657d5622b5d8d6be5cd"}, - {file = "protobuf-4.24.2-py3-none-any.whl", hash = "sha256:3b7b170d3491ceed33f723bbf2d5a260f8a4e23843799a3906f16ef736ef251e"}, - {file = "protobuf-4.24.2.tar.gz", hash = "sha256:7fda70797ddec31ddfa3576cbdcc3ddbb6b3078b737a1a87ab9136af0570cd6e"}, + {file = "protobuf-4.25.4-cp310-abi3-win32.whl", hash = "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4"}, + {file = "protobuf-4.25.4-cp310-abi3-win_amd64.whl", hash = "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d"}, + {file = "protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040"}, + {file = "protobuf-4.25.4-cp38-cp38-win32.whl", hash = "sha256:7e372cbbda66a63ebca18f8ffaa6948455dfecc4e9c1029312f6c2edcd86c4e1"}, + {file = "protobuf-4.25.4-cp38-cp38-win_amd64.whl", hash = "sha256:051e97ce9fa6067a4546e75cb14f90cf0232dcb3e3d508c448b8d0e4265b61c1"}, + {file = "protobuf-4.25.4-cp39-cp39-win32.whl", hash = "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca"}, + {file = "protobuf-4.25.4-cp39-cp39-win_amd64.whl", hash = "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f"}, + {file = "protobuf-4.25.4-py3-none-any.whl", hash = "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978"}, + {file = "protobuf-4.25.4.tar.gz", hash = "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d"}, ] [package.source] @@ -2199,26 +2714,27 @@ reference = "ali" [[package]] name = "psutil" -version = "5.9.5" +version = "5.9.8" description = "Cross-platform lib for process and system monitoring in Python." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, - {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, - {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, - {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, - {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, - {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, - {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, - {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, ] [package.extras] @@ -2231,14 +2747,29 @@ reference = "ali" [[package]] name = "pyasn1" -version = "0.5.0" +version = "0.6.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, - {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, + {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, + {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [package.source] @@ -2248,52 +2779,57 @@ reference = "ali" [[package]] name = "pydantic" -version = "1.10.12" +version = "1.10.17" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, + {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, + {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, + {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, + {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, + {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, + {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, + {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, + {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, + {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, + {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, + {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, + {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, + {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, + {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, + {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, + {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, + {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, + {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, + {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, + {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, + {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, + {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, + {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, + {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, + {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, + {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, + {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, + {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, + {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, + {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, + {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, + {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, + {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, + {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, + {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, + {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, + {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, + {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, + {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, + {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, + {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, + {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, + {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, ] [package.dependencies] -python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} typing-extensions = ">=4.2.0" [package.extras] @@ -2307,19 +2843,37 @@ reference = "ali" [[package]] name = "pyee" -version = "9.0.4" -description = "A port of node.js's EventEmitter to python." -category = "main" +version = "11.1.0" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pyee-9.0.4-py2.py3-none-any.whl", hash = "sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5"}, - {file = "pyee-9.0.4.tar.gz", hash = "sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32"}, + {file = "pyee-11.1.0-py3-none-any.whl", hash = "sha256:5d346a7d0f861a4b2e6c47960295bd895f816725b27d656181947346be98d7c1"}, + {file = "pyee-11.1.0.tar.gz", hash = "sha256:b53af98f6990c810edd9b56b87791021a8f54fd13db4edd1142438d44ba2263f"}, ] [package.dependencies] typing-extensions = "*" +[package.extras] +dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "pyfiglet" +version = "1.0.2" +description = "Pure-python FIGlet implementation" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyfiglet-1.0.2-py3-none-any.whl", hash = "sha256:889b351d79c99e50a3f619c8f8e6ffdb27fd8c939fc43ecbd7559bd57d5f93ea"}, + {file = "pyfiglet-1.0.2.tar.gz", hash = "sha256:758788018ab8faaddc0984e1ea05ff330d3c64be663c513cc1f105f6a3066dab"}, +] + [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -2327,18 +2881,17 @@ reference = "ali" [[package]] name = "pygments" -version = "2.16.1" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [package.source] type = "legacy" @@ -2349,7 +2902,6 @@ reference = "ali" name = "pygtrie" version = "2.5.0" description = "A pure Python trie data structure implementation." -category = "main" optional = false python-versions = "*" files = [ @@ -2364,18 +2916,17 @@ reference = "ali" [[package]] name = "pymdown-extensions" -version = "10.3" +version = "10.9" description = "Extension pack for Python Markdown." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"}, - {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"}, + {file = "pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626"}, + {file = "pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753"}, ] [package.dependencies] -markdown = ">=3.2" +markdown = ">=3.6" pyyaml = "*" [package.extras] @@ -2386,31 +2937,10 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "pypika-tortoise" version = "0.1.6" description = "Forked from pypika and streamline just for tortoise-orm" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -2425,14 +2955,13 @@ reference = "ali" [[package]] name = "pypinyin" -version = "0.46.0" +version = "0.51.0" description = "汉字拼音转换模块/工具." -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" files = [ - {file = "pypinyin-0.46.0-py2.py3-none-any.whl", hash = "sha256:7251f4fa0b1e43ad91f6121d9a842e8acd72a6a34deea5e87d2a97621eadc11f"}, - {file = "pypinyin-0.46.0.tar.gz", hash = "sha256:0d2e41e95dbc20a232c0f5d3850654eebbfcba303d96358d2c46592725bb989c"}, + {file = "pypinyin-0.51.0-py2.py3-none-any.whl", hash = "sha256:ae8878f08fee15d0c5c11053a737e68a4158c22c63dc632b4de060af5c95bf84"}, + {file = "pypinyin-0.51.0.tar.gz", hash = "sha256:cede34fc35a79ef6c799f161e2c280e7b6755ee072fb741cae5ce2a60c4ae0c5"}, ] [package.source] @@ -2442,14 +2971,13 @@ reference = "ali" [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -2462,14 +2990,13 @@ reference = "ali" [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] @@ -2484,7 +3011,6 @@ reference = "ali" name = "python-jose" version = "3.3.0" description = "JOSE implementation in Python" -category = "main" optional = false python-versions = "*" files = [ @@ -2493,6 +3019,7 @@ files = [ ] [package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} ecdsa = "!=0.15" pyasn1 = "*" rsa = "*" @@ -2511,7 +3038,6 @@ reference = "ali" name = "python-markdown-math" version = "0.8" description = "Math extension for Python-Markdown" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2529,17 +3055,39 @@ reference = "ali" [[package]] name = "python-multipart" -version = "0.0.5" +version = "0.0.9" description = "A streaming multipart parser for Python" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] + +[package.extras] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, ] [package.dependencies] -six = ">=1.4.0" +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] [package.source] type = "legacy" @@ -2548,14 +3096,13 @@ reference = "ali" [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [package.source] @@ -2565,41 +3112,48 @@ reference = "ali" [[package]] name = "pywavelets" -version = "1.4.1" +version = "1.6.0" description = "PyWavelets, wavelet transform module" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win32.whl", hash = "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win32.whl", hash = "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win32.whl", hash = "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win32.whl", hash = "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4"}, - {file = "PyWavelets-1.4.1.tar.gz", hash = "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93"}, + {file = "pywavelets-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ddc1ff5ad706313d930f857f9656f565dfb81b85bbe58a9db16ad8fa7d1537c5"}, + {file = "pywavelets-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78feab4e0c25fa32034b6b64cb854c6ce15663b4f0ffb25d8f0ee58915300f9b"}, + {file = "pywavelets-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be36f08efe9bc3abf40cf40cd2ee0aa0db26e4894e13ce5ac178442864161e8c"}, + {file = "pywavelets-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0595c51472c9c5724fe087cb73e2797053fd25c788d6553fdad6ff61abc60e91"}, + {file = "pywavelets-1.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:058a750477dde633ac53b8806f835af3559d52db6532fb2b93c1f4b5441365b8"}, + {file = "pywavelets-1.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:538795d9c4181152b414285b5a7f72ac52581ecdcdce74b6cca3fa0b8a5ab0aa"}, + {file = "pywavelets-1.6.0-cp310-cp310-win32.whl", hash = "sha256:47de024ba4f9df97e98b5f540340e1a9edd82d2c477450bef8c9b5381487128e"}, + {file = "pywavelets-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2c44760c0906ddf2176920a2613287f6eea947f166ce7eee9546081b06a6835"}, + {file = "pywavelets-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d91aaaf6de53b758bcdc96c81cdb5a8607758602be49f691188c0e108cf1e738"}, + {file = "pywavelets-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b5302edb6d1d1ff6636d37c9ff29c4892f2a3648d736cc1df01f3f36e25c8cf"}, + {file = "pywavelets-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e655446e37a3c87213d5c6386b86f65c4d61736b4432d720171e7dd6523d6a"}, + {file = "pywavelets-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ec7d69b746a0eaa327b829a3252a63619f2345e263177be5dd9bf30d7933c8d"}, + {file = "pywavelets-1.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97ea9613bd6b7108ebb44b709060adc7e2d5fac73be7152342bdd5513d75f84e"}, + {file = "pywavelets-1.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:48b3813c6d1a7a8194f37dbb5dbbdf2fe1112152c91445ea2e54f64ff6350c36"}, + {file = "pywavelets-1.6.0-cp311-cp311-win32.whl", hash = "sha256:4ffb484d096a5eb10af7121e0203546a03e1369328df321a33ef91f67bac40cf"}, + {file = "pywavelets-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:274bc47b289585383aa65519b3fcae5b4dee5e31db3d4198d4fad701a70e59f7"}, + {file = "pywavelets-1.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d6ec113386a432e04103f95e351d2657b42145bd1e1ed26513423391bcb5f011"}, + {file = "pywavelets-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab652112d3932d21f020e281e06926a751354c2b5629fb716f5eb9d0104b84e5"}, + {file = "pywavelets-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47b0314a22616c5f3f08760f0e00b4a15b7c7dadca5e39bb701cf7869a4207c5"}, + {file = "pywavelets-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138471513bc0a4cd2ddc4e50c7ec04e3468c268e101a0d02f698f6aedd1d5e79"}, + {file = "pywavelets-1.6.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67936491ae3e5f957c428e34fdaed21f131535b8d60c7c729a1b539ce8864837"}, + {file = "pywavelets-1.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd798cee3d28fb3d32a26a00d9831a20bf316c36d685e4ced01b4e4a8f36f5ce"}, + {file = "pywavelets-1.6.0-cp312-cp312-win32.whl", hash = "sha256:e772f7f0c16bfc3be8ac3cd10d29a9920bb7a39781358856223c491b899e6e79"}, + {file = "pywavelets-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:4ef15a63a72afa67ae9f4f3b06c95c5382730fb3075e668d49a880e65f2f089c"}, + {file = "pywavelets-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:627df378e63e9c789b6f2e7060cb4264ebae6f6b0efc1da287a2c060de454a1f"}, + {file = "pywavelets-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a413b51dc19e05243fe0b0864a8e8a16b5ca9bf2e4713da00a95b1b5747a5367"}, + {file = "pywavelets-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be615c6c1873e189c265d4a76d1751ec49b17e29725e6dd2e9c74f1868f590b7"}, + {file = "pywavelets-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4021ef69ec9f3862f66580fc4417be728bd78722914394594b48212fd1fcaf21"}, + {file = "pywavelets-1.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8fbf7b61b28b5457693c034e58a01622756d1fd60a80ae13ac5888b1d3e57e80"}, + {file = "pywavelets-1.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f58ddbb0a6cd243928876edfc463b990763a24fb94498607d6fea690e32cca4c"}, + {file = "pywavelets-1.6.0-cp39-cp39-win32.whl", hash = "sha256:42a22e68e345b6de7d387ef752111ab4530c98048d2b4bdac8ceefb078b4ead6"}, + {file = "pywavelets-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:32198de321892743c1a3d1957fe1cd8a8ecc078bfbba6b8f3982518e897271d7"}, + {file = "pywavelets-1.6.0.tar.gz", hash = "sha256:ea027c70977122c5fc27b2510f0a0d9528f9c3df6ea3e4c577ca55fd00325a5b"}, ] [package.dependencies] -numpy = ">=1.17.3" +numpy = ">=1.22.4,<3" [package.source] type = "legacy" @@ -2608,41 +3162,64 @@ reference = "ali" [[package]] name = "pyyaml" -version = "5.4.1" +version = "6.0.2" description = "YAML parser and emitter for Python" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.8" files = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [package.source] @@ -2652,100 +3229,90 @@ reference = "ali" [[package]] name = "regex" -version = "2023.8.8" +version = "2024.7.24" description = "Alternative regular expression module, to replace re." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "regex-2023.8.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88900f521c645f784260a8d346e12a1590f79e96403971241e64c3a265c8ecdb"}, - {file = "regex-2023.8.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3611576aff55918af2697410ff0293d6071b7e00f4b09e005d614686ac4cd57c"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a0ccc8f2698f120e9e5742f4b38dc944c38744d4bdfc427616f3a163dd9de5"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c662a4cbdd6280ee56f841f14620787215a171c4e2d1744c9528bed8f5816c96"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf0633e4a1b667bfe0bb10b5e53fe0d5f34a6243ea2530eb342491f1adf4f739"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551ad543fa19e94943c5b2cebc54c73353ffff08228ee5f3376bd27b3d5b9800"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54de2619f5ea58474f2ac211ceea6b615af2d7e4306220d4f3fe690c91988a61"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ec4b3f0aebbbe2fc0134ee30a791af522a92ad9f164858805a77442d7d18570"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ae646c35cb9f820491760ac62c25b6d6b496757fda2d51be429e0e7b67ae0ab"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca339088839582d01654e6f83a637a4b8194d0960477b9769d2ff2cfa0fa36d2"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d9b6627408021452dcd0d2cdf8da0534e19d93d070bfa8b6b4176f99711e7f90"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:bd3366aceedf274f765a3a4bc95d6cd97b130d1dda524d8f25225d14123c01db"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7aed90a72fc3654fba9bc4b7f851571dcc368120432ad68b226bd593f3f6c0b7"}, - {file = "regex-2023.8.8-cp310-cp310-win32.whl", hash = "sha256:80b80b889cb767cc47f31d2b2f3dec2db8126fbcd0cff31b3925b4dc6609dcdb"}, - {file = "regex-2023.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:b82edc98d107cbc7357da7a5a695901b47d6eb0420e587256ba3ad24b80b7d0b"}, - {file = "regex-2023.8.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e7d84d64c84ad97bf06f3c8cb5e48941f135ace28f450d86af6b6512f1c9a71"}, - {file = "regex-2023.8.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce0f9fbe7d295f9922c0424a3637b88c6c472b75eafeaff6f910494a1fa719ef"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06c57e14ac723b04458df5956cfb7e2d9caa6e9d353c0b4c7d5d54fcb1325c46"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7a9aaa5a1267125eef22cef3b63484c3241aaec6f48949b366d26c7250e0357"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7408511fca48a82a119d78a77c2f5eb1b22fe88b0d2450ed0756d194fe7a9a"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14dc6f2d88192a67d708341f3085df6a4f5a0c7b03dec08d763ca2cd86e9f559"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c640b99213643d141550326f34f0502fedb1798adb3c9eb79650b1ecb2f177"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0085da0f6c6393428bf0d9c08d8b1874d805bb55e17cb1dfa5ddb7cfb11140bf"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:964b16dcc10c79a4a2be9f1273fcc2684a9eedb3906439720598029a797b46e6"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7ce606c14bb195b0e5108544b540e2c5faed6843367e4ab3deb5c6aa5e681208"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:40f029d73b10fac448c73d6eb33d57b34607f40116e9f6e9f0d32e9229b147d7"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3b8e6ea6be6d64104d8e9afc34c151926f8182f84e7ac290a93925c0db004bfd"}, - {file = "regex-2023.8.8-cp311-cp311-win32.whl", hash = "sha256:942f8b1f3b223638b02df7df79140646c03938d488fbfb771824f3d05fc083a8"}, - {file = "regex-2023.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:51d8ea2a3a1a8fe4f67de21b8b93757005213e8ac3917567872f2865185fa7fb"}, - {file = "regex-2023.8.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e951d1a8e9963ea51efd7f150450803e3b95db5939f994ad3d5edac2b6f6e2b4"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704f63b774218207b8ccc6c47fcef5340741e5d839d11d606f70af93ee78e4d4"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22283c769a7b01c8ac355d5be0715bf6929b6267619505e289f792b01304d898"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91129ff1bb0619bc1f4ad19485718cc623a2dc433dff95baadbf89405c7f6b57"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de35342190deb7b866ad6ba5cbcccb2d22c0487ee0cbb251efef0843d705f0d4"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b993b6f524d1e274a5062488a43e3f9f8764ee9745ccd8e8193df743dbe5ee61"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3026cbcf11d79095a32d9a13bbc572a458727bd5b1ca332df4a79faecd45281c"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:293352710172239bf579c90a9864d0df57340b6fd21272345222fb6371bf82b3"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d909b5a3fff619dc7e48b6b1bedc2f30ec43033ba7af32f936c10839e81b9217"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3d370ff652323c5307d9c8e4c62efd1956fb08051b0e9210212bc51168b4ff56"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:b076da1ed19dc37788f6a934c60adf97bd02c7eea461b73730513921a85d4235"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e9941a4ada58f6218694f382e43fdd256e97615db9da135e77359da257a7168b"}, - {file = "regex-2023.8.8-cp36-cp36m-win32.whl", hash = "sha256:a8c65c17aed7e15a0c824cdc63a6b104dfc530f6fa8cb6ac51c437af52b481c7"}, - {file = "regex-2023.8.8-cp36-cp36m-win_amd64.whl", hash = "sha256:aadf28046e77a72f30dcc1ab185639e8de7f4104b8cb5c6dfa5d8ed860e57236"}, - {file = "regex-2023.8.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:423adfa872b4908843ac3e7a30f957f5d5282944b81ca0a3b8a7ccbbfaa06103"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ae594c66f4a7e1ea67232a0846649a7c94c188d6c071ac0210c3e86a5f92109"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e51c80c168074faa793685656c38eb7a06cbad7774c8cbc3ea05552d615393d8"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09b7f4c66aa9d1522b06e31a54f15581c37286237208df1345108fcf4e050c18"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e73e5243af12d9cd6a9d6a45a43570dbe2e5b1cdfc862f5ae2b031e44dd95a8"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941460db8fe3bd613db52f05259c9336f5a47ccae7d7def44cc277184030a116"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f0ccf3e01afeb412a1a9993049cb160d0352dba635bbca7762b2dc722aa5742a"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2e9216e0d2cdce7dbc9be48cb3eacb962740a09b011a116fd7af8c832ab116ca"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cd9cd7170459b9223c5e592ac036e0704bee765706445c353d96f2890e816c8"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4873ef92e03a4309b3ccd8281454801b291b689f6ad45ef8c3658b6fa761d7ac"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:239c3c2a339d3b3ddd51c2daef10874410917cd2b998f043c13e2084cb191684"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1005c60ed7037be0d9dea1f9c53cc42f836188227366370867222bda4c3c6bd7"}, - {file = "regex-2023.8.8-cp37-cp37m-win32.whl", hash = "sha256:e6bd1e9b95bc5614a7a9c9c44fde9539cba1c823b43a9f7bc11266446dd568e3"}, - {file = "regex-2023.8.8-cp37-cp37m-win_amd64.whl", hash = "sha256:9a96edd79661e93327cfeac4edec72a4046e14550a1d22aa0dd2e3ca52aec921"}, - {file = "regex-2023.8.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2181c20ef18747d5f4a7ea513e09ea03bdd50884a11ce46066bb90fe4213675"}, - {file = "regex-2023.8.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a2ad5add903eb7cdde2b7c64aaca405f3957ab34f16594d2b78d53b8b1a6a7d6"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9233ac249b354c54146e392e8a451e465dd2d967fc773690811d3a8c240ac601"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920974009fb37b20d32afcdf0227a2e707eb83fe418713f7a8b7de038b870d0b"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2b6c5dfe0929b6c23dde9624483380b170b6e34ed79054ad131b20203a1a63"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96979d753b1dc3b2169003e1854dc67bfc86edf93c01e84757927f810b8c3c93"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ae54a338191e1356253e7883d9d19f8679b6143703086245fb14d1f20196be9"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2162ae2eb8b079622176a81b65d486ba50b888271302190870b8cc488587d280"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c884d1a59e69e03b93cf0dfee8794c63d7de0ee8f7ffb76e5f75be8131b6400a"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf9273e96f3ee2ac89ffcb17627a78f78e7516b08f94dc435844ae72576a276e"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:83215147121e15d5f3a45d99abeed9cf1fe16869d5c233b08c56cdf75f43a504"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f7454aa427b8ab9101f3787eb178057c5250478e39b99540cfc2b889c7d0586"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0640913d2c1044d97e30d7c41728195fc37e54d190c5385eacb52115127b882"}, - {file = "regex-2023.8.8-cp38-cp38-win32.whl", hash = "sha256:0c59122ceccb905a941fb23b087b8eafc5290bf983ebcb14d2301febcbe199c7"}, - {file = "regex-2023.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:c12f6f67495ea05c3d542d119d270007090bad5b843f642d418eb601ec0fa7be"}, - {file = "regex-2023.8.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82cd0a69cd28f6cc3789cc6adeb1027f79526b1ab50b1f6062bbc3a0ccb2dbc3"}, - {file = "regex-2023.8.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bb34d1605f96a245fc39790a117ac1bac8de84ab7691637b26ab2c5efb8f228c"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:987b9ac04d0b38ef4f89fbc035e84a7efad9cdd5f1e29024f9289182c8d99e09"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dd6082f4e2aec9b6a0927202c85bc1b09dcab113f97265127c1dc20e2e32495"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7eb95fe8222932c10d4436e7a6f7c99991e3fdd9f36c949eff16a69246dee2dc"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7098c524ba9f20717a56a8d551d2ed491ea89cbf37e540759ed3b776a4f8d6eb"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b694430b3f00eb02c594ff5a16db30e054c1b9589a043fe9174584c6efa8033"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2aeab3895d778155054abea5238d0eb9a72e9242bd4b43f42fd911ef9a13470"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:988631b9d78b546e284478c2ec15c8a85960e262e247b35ca5eaf7ee22f6050a"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:67ecd894e56a0c6108ec5ab1d8fa8418ec0cff45844a855966b875d1039a2e34"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:14898830f0a0eb67cae2bbbc787c1a7d6e34ecc06fbd39d3af5fe29a4468e2c9"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f2200e00b62568cfd920127782c61bc1c546062a879cdc741cfcc6976668dfcf"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9691a549c19c22d26a4f3b948071e93517bdf86e41b81d8c6ac8a964bb71e5a6"}, - {file = "regex-2023.8.8-cp39-cp39-win32.whl", hash = "sha256:6ab2ed84bf0137927846b37e882745a827458689eb969028af8032b1b3dac78e"}, - {file = "regex-2023.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5543c055d8ec7801901e1193a51570643d6a6ab8751b1f7dd9af71af467538bb"}, - {file = "regex-2023.8.8.tar.gz", hash = "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, + {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, + {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, + {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, + {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, + {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, + {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, + {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, + {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, + {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, + {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, + {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, ] [package.source] @@ -2753,11 +3320,36 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "retrying" version = "1.3.4" description = "Retrying" -category = "main" optional = false python-versions = "*" files = [ @@ -2777,7 +3369,6 @@ reference = "ali" name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" -category = "main" optional = false python-versions = "*" files = [ @@ -2798,23 +3389,21 @@ reference = "ali" [[package]] name = "rich" -version = "12.6.0" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" optional = false -python-versions = ">=3.6.3,<4.0.0" +python-versions = ">=3.7.0" files = [ - {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, - {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" [package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] +jupyter = ["ipywidgets (>=7.5.1,<9)"] [package.source] type = "legacy" @@ -2825,7 +3414,6 @@ reference = "ali" name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -2843,21 +3431,20 @@ reference = "ali" [[package]] name = "ruamel-yaml" -version = "0.17.32" +version = "0.18.6" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -category = "main" optional = false -python-versions = ">=3" +python-versions = ">=3.7" files = [ - {file = "ruamel.yaml-0.17.32-py3-none-any.whl", hash = "sha256:23cd2ed620231677564646b0c6a89d138b6822a0d78656df7abda5879ec4f447"}, - {file = "ruamel.yaml-0.17.32.tar.gz", hash = "sha256:ec939063761914e14542972a5cba6d33c23b0859ab6342f61cf070cfc600efc2"}, + {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, + {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, ] [package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\""} +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} [package.extras] -docs = ["ryd"] +docs = ["mercurial (>5.7)", "ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [package.source] @@ -2867,49 +3454,61 @@ reference = "ali" [[package]] name = "ruamel-yaml-clib" -version = "0.2.7" +version = "0.2.8" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, - {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, ] [package.source] @@ -2919,42 +3518,45 @@ reference = "ali" [[package]] name = "scipy" -version = "1.9.3" +version = "1.14.0" description = "Fundamental algorithms for scientific computing in Python" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, - {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0"}, + {file = "scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d"}, + {file = "scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74"}, + {file = "scipy-1.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb"}, + {file = "scipy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14"}, + {file = "scipy-1.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159"}, + {file = "scipy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20"}, + {file = "scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b"}, ] [package.dependencies] -numpy = ">=1.18.5,<1.26.0" +numpy = ">=1.23.5,<2.3" [package.extras] -dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] -test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [package.source] type = "legacy" @@ -2963,20 +3565,19 @@ reference = "ali" [[package]] name = "setuptools" -version = "68.1.2" +version = "72.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, - {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, + {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, + {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [package.source] type = "legacy" @@ -2987,7 +3588,6 @@ reference = "ali" name = "sgmllib3k" version = "1.0.0" description = "Py3k port of sgmllib." -category = "main" optional = false python-versions = "*" files = [ @@ -3003,7 +3603,6 @@ reference = "ali" name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3018,14 +3617,13 @@ reference = "ali" [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [package.source] @@ -3037,7 +3635,6 @@ reference = "ali" name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3052,22 +3649,124 @@ reference = "ali" [[package]] name = "starlette" -version = "0.26.1" +version = "0.37.2" description = "The little ASGI library that shines." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, - {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, ] [package.dependencies] anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "strenum" +version = "0.4.15" +description = "An Enum that inherits from str." +optional = false +python-versions = "*" +files = [ + {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"}, + {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"}, +] + +[package.extras] +docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"] +release = ["twine"] +test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "tarina" +version = "0.5.5" +description = "A collection of common utils for Arclet" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tarina-0.5.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fda200701a81ed48e4303ccff10b5d680a7ad3d1772a6830f32995fe04459d6e"}, + {file = "tarina-0.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ffe373da5f9e35179b96e233731e8a7bb83fe6bf8866753f468db53b3ed22e"}, + {file = "tarina-0.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb7474ba9f9d55dc29df9d317c12fdc870ba10582b0c5ce36550e237881c9ea6"}, + {file = "tarina-0.5.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a392ac4d4b94a9a51b7540d8194605be621a129147dc874933a524911a09c94e"}, + {file = "tarina-0.5.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cc131ecab68d7ec31a12dfb8f0ab0638729a9b866043a79b66dcf7022000652"}, + {file = "tarina-0.5.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:724a3d33ed7c48f68af7fc583aa21abff2cd1b60d0c51d3ba043683d715717f8"}, + {file = "tarina-0.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b04897665d96ebd55461c0876407c3e569008ba8efee4d4342bad47c32b64b0f"}, + {file = "tarina-0.5.5-cp310-cp310-win32.whl", hash = "sha256:f58c9eaa087af597cfd7e2885073c9dc93a3f93ba3f6957d55a9dacbcc1270ee"}, + {file = "tarina-0.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:b7dc4a5e0779fd4ee023abf445c2f801069a5861133c3ad04a5e055d5d5071fb"}, + {file = "tarina-0.5.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ffb4ed6bd241809fd76b82bc7df857413cbc4a73a2ac8397374b79cb6e85e9b"}, + {file = "tarina-0.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f5551815a970cd22d6d609a8769eac3e8b499e54ac5283e01169727f9ce0edd0"}, + {file = "tarina-0.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e2c18bcb1a3c59e45dc0fe39880b41d7e4fb5d742ef98a88fb4621aea9da02f"}, + {file = "tarina-0.5.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2db5c4bc285d73bec00b159dde6ec41b74d14371eb6da29d8b14a382e370567e"}, + {file = "tarina-0.5.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74923bc3d6884639e102a6a35bffda9578d934a23c4eb3f2d835e718ac75cee"}, + {file = "tarina-0.5.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e55686cff98c91ed4982226163ac5daeaf85510b4acab0c3d75331e255fbdce0"}, + {file = "tarina-0.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:50572901cd69983cfdc9d5a5823d17c49755f9e071eb287e091df014beaf6e73"}, + {file = "tarina-0.5.5-cp311-cp311-win32.whl", hash = "sha256:9d0a20f8b084af361fab7b070917edad611ede38014bab2cfc4024599586ade0"}, + {file = "tarina-0.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:8e740532d5a9346079c55613adfb77895f596a9c57e46c06d7d6c03640bd4f38"}, + {file = "tarina-0.5.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1bab4762a24d9fcd8eacae4376c8fa2d4a96e1a3c5aadbeaad9e113cd679ee7d"}, + {file = "tarina-0.5.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:05149d5aef6947fcf11a5b6cbbab788202077a734b7a2d184a574283de311725"}, + {file = "tarina-0.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b4ae866721d7b906fb327f847d9f8522f46bbea3b0df61b74d6bcc22dad1a33c"}, + {file = "tarina-0.5.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c687aa0cfef24b1df2c8f044a72d8993d68b4e13ea8967b79105be7a2e4097dd"}, + {file = "tarina-0.5.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e609199df957cd35cee6a942028f4caded21f1db8ac4c300c1dba94d61f0080"}, + {file = "tarina-0.5.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d57033ce9fa1c6c0a3a4851503c7320e7f7eba5dfc77e4e2f98932f1b329ba85"}, + {file = "tarina-0.5.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:986c5c59e30041e2a223c04b429777d3848c40e70b449f395b4b40290b6ff1ef"}, + {file = "tarina-0.5.5-cp312-cp312-win32.whl", hash = "sha256:256cf6a4f6a395b90aa4c1305f69a36c5fa6155124b30157a4c7e7af7c6be9ca"}, + {file = "tarina-0.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:ada4a85937cb7f0c5968ffc1b4914779d35525bff14e451113da94028d6a7a23"}, + {file = "tarina-0.5.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4dc78ecae28f9422cb211268e7741058838d24dbf0714ae68ee3c00da278519d"}, + {file = "tarina-0.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a85e14f1006c4f1cab21535c47819c3aceedd909e9b34c3044cfec584deee9ea"}, + {file = "tarina-0.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0689be4febdb9ba442b44c79d9dd861f6269f3dd62a33d258db6f6f1c40454c7"}, + {file = "tarina-0.5.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:046e441e9598c03d3013693688aa1825ba9f78538f81ba15ab3a0dc31cffb74c"}, + {file = "tarina-0.5.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6af01b231f724aef7233ce85ad99619e0bda81bf7d29863ba624117b5e3a82f9"}, + {file = "tarina-0.5.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:028f156980c0e89bc739d3875bafee82bfb198523a0199dd80b10931b50cda8f"}, + {file = "tarina-0.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7f4738381cb9291918c0f83928a13720879e0cfdcc679389bfa1bef985beed93"}, + {file = "tarina-0.5.5-cp38-cp38-win32.whl", hash = "sha256:30b30d0e3c21d2ab04f11f079d2205faa7320b595d1252c6728e8705781f6171"}, + {file = "tarina-0.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:e1f36c9972fa2e0cf3c1ca3842660531008fa4b6b1b89b31cdf06c56254cc902"}, + {file = "tarina-0.5.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d819c4fa630c78e1d3c1b5fbc72158a84da6404009dc040e675e664fa38c030a"}, + {file = "tarina-0.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a81375dab4b02eacedd2364e2394d0c3d76ac064fb0a9d3af1f0c0ea7740e296"}, + {file = "tarina-0.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:926bf0cd6901091c60460c6ac90ef5ea53ebb5a24d865ab1b9381117e4ba2825"}, + {file = "tarina-0.5.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee3dd8ebe04370915e7b763d39f8faee1bd4e9d2600acc8005da5104a698d9e8"}, + {file = "tarina-0.5.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bccac5a9b5af0c4c4b545d7e37eca55abab0abd779f4554cf69bbe29635e3c5c"}, + {file = "tarina-0.5.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dda57675b259a8b0db6647832c4f6a734ce3acf63b2392b7a45e34bace681230"}, + {file = "tarina-0.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:aeba9af50fba8d270abdcffb9f7ca3390223e7e7b4cf1a6a52c8adb2c98b8726"}, + {file = "tarina-0.5.5-cp39-cp39-win32.whl", hash = "sha256:fb1e3130cb6e35495f5867c54d8f049f06a1d915644afce2138ab915ff78291a"}, + {file = "tarina-0.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:da9ababc95b38037280eaeedbbb80c45179bda08578e2a4254e44ee1ef794ac9"}, + {file = "tarina-0.5.5-py3-none-any.whl", hash = "sha256:4828ace26e49037b2dab624e62ca13a473909b2f535f1b4fd5169dd01e16f6c5"}, + {file = "tarina-0.5.5.tar.gz", hash = "sha256:762a3871906e3dd79fc82d13ff99f14f1af977c4b8e2ce860209b8fa97a8b321"}, +] + +[package.dependencies] +typing-extensions = ">=4.4.0" + +[package.extras] +yaml = ["pyyaml (>=6.0.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] [package.source] type = "legacy" @@ -3078,7 +3777,6 @@ reference = "ali" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3092,15 +3790,30 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "tortoise-orm" -version = "0.19.3" -description = "Easy async ORM for python, built with relations in mind" -category = "main" +name = "tomlkit" +version = "0.13.0" +description = "Style preserving TOML library" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8" files = [ - {file = "tortoise_orm-0.19.3-py3-none-any.whl", hash = "sha256:9e368820c70a0866ef9c521d43aa5503485bd7a20a561edc0933b7b0f7036fbc"}, - {file = "tortoise_orm-0.19.3.tar.gz", hash = "sha256:ca574bca5191f55608f9013314b1f5d1c6ffd4165a1fcc2f60f6c902f529b3b6"}, + {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, + {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "tortoise-orm" +version = "0.20.0" +description = "Easy async ORM for python, built with relations in mind" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "tortoise_orm-0.20.0-py3-none-any.whl", hash = "sha256:1891ad935de689ddf002c5c65c864176d28659ab6069e45f0e2cde32359bb8d9"}, + {file = "tortoise_orm-0.20.0.tar.gz", hash = "sha256:283af584d685dcc58d6cc1da35b9115bb1e41c89075eae2a19c493b39b9b41f7"}, ] [package.dependencies] @@ -3113,10 +3826,26 @@ pytz = "*" [package.extras] accel = ["ciso8601", "orjson", "uvloop"] aiomysql = ["aiomysql"] -asyncmy = ["asyncmy (>=0.2.5,<0.3.0)"] +asyncmy = ["asyncmy (>=0.2.8,<0.3.0)"] asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] asyncpg = ["asyncpg"] -psycopg = ["psycopg[binary,pool] (==3.0.12)"] +psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20240316" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, +] [package.source] type = "legacy" @@ -3125,14 +3854,13 @@ reference = "ali" [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [package.source] @@ -3142,14 +3870,13 @@ reference = "ali" [[package]] name = "tzdata" -version = "2023.3" +version = "2024.1" description = "Provider of IANA time zone data" -category = "main" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [package.source] @@ -3159,22 +3886,20 @@ reference = "ali" [[package]] name = "tzlocal" -version = "5.0.1" +version = "5.2" description = "tzinfo object for the local timezone" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, - {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] [package.source] type = "legacy" @@ -3183,73 +3908,89 @@ reference = "ali" [[package]] name = "ujson" -version = "5.8.0" +version = "5.10.0" description = "Ultra fast JSON encoder and decoder for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "ujson-5.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4511560d75b15ecb367eef561554959b9d49b6ec3b8d5634212f9fed74a6df1"}, - {file = "ujson-5.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9399eaa5d1931a0ead49dce3ffacbea63f3177978588b956036bfe53cdf6af75"}, - {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4e7bb7eba0e1963f8b768f9c458ecb193e5bf6977090182e2b4f4408f35ac76"}, - {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40931d7c08c4ce99adc4b409ddb1bbb01635a950e81239c2382cfe24251b127a"}, - {file = "ujson-5.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d53039d39de65360e924b511c7ca1a67b0975c34c015dd468fca492b11caa8f7"}, - {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bdf04c6af3852161be9613e458a1fb67327910391de8ffedb8332e60800147a2"}, - {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a70f776bda2e5072a086c02792c7863ba5833d565189e09fabbd04c8b4c3abba"}, - {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f26629ac531d712f93192c233a74888bc8b8212558bd7d04c349125f10199fcf"}, - {file = "ujson-5.8.0-cp310-cp310-win32.whl", hash = "sha256:7ecc33b107ae88405aebdb8d82c13d6944be2331ebb04399134c03171509371a"}, - {file = "ujson-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:3b27a8da7a080add559a3b73ec9ebd52e82cc4419f7c6fb7266e62439a055ed0"}, - {file = "ujson-5.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:193349a998cd821483a25f5df30b44e8f495423840ee11b3b28df092ddfd0f7f"}, - {file = "ujson-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ddeabbc78b2aed531f167d1e70387b151900bc856d61e9325fcdfefb2a51ad8"}, - {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ce24909a9c25062e60653073dd6d5e6ec9d6ad7ed6e0069450d5b673c854405"}, - {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a2a3c7620ebe43641e926a1062bc04e92dbe90d3501687957d71b4bdddaec4"}, - {file = "ujson-5.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b852bdf920fe9f84e2a2c210cc45f1b64f763b4f7d01468b33f7791698e455e"}, - {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:20768961a6a706170497129960762ded9c89fb1c10db2989c56956b162e2a8a3"}, - {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e0147d41e9fb5cd174207c4a2895c5e24813204499fd0839951d4c8784a23bf5"}, - {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3673053b036fd161ae7a5a33358ccae6793ee89fd499000204676baafd7b3aa"}, - {file = "ujson-5.8.0-cp311-cp311-win32.whl", hash = "sha256:a89cf3cd8bf33a37600431b7024a7ccf499db25f9f0b332947fbc79043aad879"}, - {file = "ujson-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3659deec9ab9eb19e8646932bfe6fe22730757c4addbe9d7d5544e879dc1b721"}, - {file = "ujson-5.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:102bf31c56f59538cccdfec45649780ae00657e86247c07edac434cb14d5388c"}, - {file = "ujson-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:299a312c3e85edee1178cb6453645217ba23b4e3186412677fa48e9a7f986de6"}, - {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e385a7679b9088d7bc43a64811a7713cc7c33d032d020f757c54e7d41931ae"}, - {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad24ec130855d4430a682c7a60ca0bc158f8253ec81feed4073801f6b6cb681b"}, - {file = "ujson-5.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16fde596d5e45bdf0d7de615346a102510ac8c405098e5595625015b0d4b5296"}, - {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6d230d870d1ce03df915e694dcfa3f4e8714369cce2346686dbe0bc8e3f135e7"}, - {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9571de0c53db5cbc265945e08f093f093af2c5a11e14772c72d8e37fceeedd08"}, - {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7cba16b26efe774c096a5e822e4f27097b7c81ed6fb5264a2b3f5fd8784bab30"}, - {file = "ujson-5.8.0-cp312-cp312-win32.whl", hash = "sha256:48c7d373ff22366eecfa36a52b9b55b0ee5bd44c2b50e16084aa88b9de038916"}, - {file = "ujson-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ac97b1e182d81cf395ded620528c59f4177eee024b4b39a50cdd7b720fdeec6"}, - {file = "ujson-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a64cc32bb4a436e5813b83f5aab0889927e5ea1788bf99b930fad853c5625cb"}, - {file = "ujson-5.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e54578fa8838ddc722539a752adfce9372474114f8c127bb316db5392d942f8b"}, - {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9721cd112b5e4687cb4ade12a7b8af8b048d4991227ae8066d9c4b3a6642a582"}, - {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9707e5aacf63fb919f6237d6490c4e0244c7f8d3dc2a0f84d7dec5db7cb54c"}, - {file = "ujson-5.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0be81bae295f65a6896b0c9030b55a106fb2dec69ef877253a87bc7c9c5308f7"}, - {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae7f4725c344bf437e9b881019c558416fe84ad9c6b67426416c131ad577df67"}, - {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9ab282d67ef3097105552bf151438b551cc4bedb3f24d80fada830f2e132aeb9"}, - {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:94c7bd9880fa33fcf7f6d7f4cc032e2371adee3c5dba2922b918987141d1bf07"}, - {file = "ujson-5.8.0-cp38-cp38-win32.whl", hash = "sha256:bf5737dbcfe0fa0ac8fa599eceafae86b376492c8f1e4b84e3adf765f03fb564"}, - {file = "ujson-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:11da6bed916f9bfacf13f4fc6a9594abd62b2bb115acfb17a77b0f03bee4cfd5"}, - {file = "ujson-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:69b3104a2603bab510497ceabc186ba40fef38ec731c0ccaa662e01ff94a985c"}, - {file = "ujson-5.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9249fdefeb021e00b46025e77feed89cd91ffe9b3a49415239103fc1d5d9c29a"}, - {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2873d196725a8193f56dde527b322c4bc79ed97cd60f1d087826ac3290cf9207"}, - {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4dafa9010c366589f55afb0fd67084acd8added1a51251008f9ff2c3e44042"}, - {file = "ujson-5.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a42baa647a50fa8bed53d4e242be61023bd37b93577f27f90ffe521ac9dc7a3"}, - {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f3554eaadffe416c6f543af442066afa6549edbc34fe6a7719818c3e72ebfe95"}, - {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fb87decf38cc82bcdea1d7511e73629e651bdec3a43ab40985167ab8449b769c"}, - {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:407d60eb942c318482bbfb1e66be093308bb11617d41c613e33b4ce5be789adc"}, - {file = "ujson-5.8.0-cp39-cp39-win32.whl", hash = "sha256:0fe1b7edaf560ca6ab023f81cbeaf9946a240876a993b8c5a21a1c539171d903"}, - {file = "ujson-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f9b63530a5392eb687baff3989d0fb5f45194ae5b1ca8276282fb647f8dcdb3"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:efeddf950fb15a832376c0c01d8d7713479fbeceaed1eaecb2665aa62c305aec"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d8283ac5d03e65f488530c43d6610134309085b71db4f675e9cf5dff96a8282"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb0142f6f10f57598655340a3b2c70ed4646cbe674191da195eb0985a9813b83"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d459aca895eb17eb463b00441986b021b9312c6c8cc1d06880925c7f51009c"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d524a8c15cfc863705991d70bbec998456a42c405c291d0f84a74ad7f35c5109"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d6f84a7a175c75beecde53a624881ff618e9433045a69fcfb5e154b73cdaa377"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b748797131ac7b29826d1524db1cc366d2722ab7afacc2ce1287cdafccddbf1f"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e72ba76313d48a1a3a42e7dc9d1db32ea93fac782ad8dde6f8b13e35c229130"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f504117a39cb98abba4153bf0b46b4954cc5d62f6351a14660201500ba31fe7f"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8c91b6f4bf23f274af9002b128d133b735141e867109487d17e344d38b87d94"}, - {file = "ujson-5.8.0.tar.gz", hash = "sha256:78e318def4ade898a461b3d92a79f9441e7e0e4d2ad5419abed4336d702c7425"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, + {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, + {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, + {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, + {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, + {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, + {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, + {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, + {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, + {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, + {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, + {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, + {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, + {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, ] [package.source] @@ -3258,15 +3999,36 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "uvicorn" -version = "0.23.2" -description = "The lightning-fast ASGI server." -category = "main" +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, - {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "uvicorn" +version = "0.30.5" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"}, + {file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"}, ] [package.dependencies] @@ -3277,7 +4039,7 @@ httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standar python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} @@ -3291,48 +4053,72 @@ reference = "ali" [[package]] name = "uvloop" -version = "0.17.0" +version = "0.19.0" description = "Fast implementation of asyncio event loop on top of libuv" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8.0" files = [ - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"}, - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"}, - {file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"}, - {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, ] [package.extras] -dev = ["Cython (>=0.29.32,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "virtualenv" +version = "20.26.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [package.source] type = "legacy" @@ -3341,34 +4127,98 @@ reference = "ali" [[package]] name = "watchfiles" -version = "0.20.0" +version = "0.23.0" description = "Simple, modern and high performance file watching and code reload in python." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "watchfiles-0.20.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:3796312bd3587e14926013612b23066912cf45a14af71cf2b20db1c12dadf4e9"}, - {file = "watchfiles-0.20.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:d0002d81c89a662b595645fb684a371b98ff90a9c7d8f8630c82f0fde8310458"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:570848706440373b4cd8017f3e850ae17f76dbdf1e9045fc79023b11e1afe490"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a0351d20d03c6f7ad6b2e8a226a5efafb924c7755ee1e34f04c77c3682417fa"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:007dcc4a401093010b389c044e81172c8a2520dba257c88f8828b3d460c6bb38"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d82dbc1832da83e441d112069833eedd4cf583d983fb8dd666fbefbea9d99c0"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99f4c65fd2fce61a571b2a6fcf747d6868db0bef8a934e8ca235cc8533944d95"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5392dd327a05f538c56edb1c6ebba6af91afc81b40822452342f6da54907bbdf"}, - {file = "watchfiles-0.20.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:08dc702529bb06a2b23859110c214db245455532da5eaea602921687cfcd23db"}, - {file = "watchfiles-0.20.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7d4e66a857621584869cfbad87039e65dadd7119f0d9bb9dbc957e089e32c164"}, - {file = "watchfiles-0.20.0-cp37-abi3-win32.whl", hash = "sha256:a03d1e6feb7966b417f43c3e3783188167fd69c2063e86bad31e62c4ea794cc5"}, - {file = "watchfiles-0.20.0-cp37-abi3-win_amd64.whl", hash = "sha256:eccc8942bcdc7d638a01435d915b913255bbd66f018f1af051cd8afddb339ea3"}, - {file = "watchfiles-0.20.0-cp37-abi3-win_arm64.whl", hash = "sha256:b17d4176c49d207865630da5b59a91779468dd3e08692fe943064da260de2c7c"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d97db179f7566dcf145c5179ddb2ae2a4450e3a634eb864b09ea04e68c252e8e"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:835df2da7a5df5464c4a23b2d963e1a9d35afa422c83bf4ff4380b3114603644"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:608cd94a8767f49521901aff9ae0c92cc8f5a24d528db7d6b0295290f9d41193"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89d1de8218874925bce7bb2ae9657efc504411528930d7a83f98b1749864f2ef"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:13f995d5152a8ba4ed7c2bbbaeee4e11a5944defc7cacd0ccb4dcbdcfd78029a"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b5c8d3be7b502f8c43a33c63166ada8828dbb0c6d49c8f9ce990a96de2f5a49"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e43af4464daa08723c04b43cf978ab86cc55c684c16172622bdac64b34e36af0"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d9e1f75c4f86c93d73b5bd1ebe667558357548f11b4f8af4e0e272f79413ce"}, - {file = "watchfiles-0.20.0.tar.gz", hash = "sha256:728575b6b94c90dd531514677201e8851708e6e4b5fe7028ac506a200b622019"}, + {file = "watchfiles-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4"}, + {file = "watchfiles-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb02d41c33be667e6135e6686f1bb76104c88a312a18faa0ef0262b5bf7f1a0f"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf12ac34c444362f3261fb3ff548f0037ddd4c5bb85f66c4be30d2936beb3c5"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0b2c25040a3c0ce0e66c7779cc045fdfbbb8d59e5aabfe033000b42fe44b53e"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf2be4b9eece4f3da8ba5f244b9e51932ebc441c0867bd6af46a3d97eb068d6"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40cb8fa00028908211eb9f8d47744dca21a4be6766672e1ff3280bee320436f1"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f48c917ffd36ff9a5212614c2d0d585fa8b064ca7e66206fb5c095015bc8207"}, + {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d183e3888ada88185ab17064079c0db8c17e32023f5c278d7bf8014713b1b5b"}, + {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9837edf328b2805346f91209b7e660f65fb0e9ca18b7459d075d58db082bf981"}, + {file = "watchfiles-0.23.0-cp310-none-win32.whl", hash = "sha256:296e0b29ab0276ca59d82d2da22cbbdb39a23eed94cca69aed274595fb3dfe42"}, + {file = "watchfiles-0.23.0-cp310-none-win_amd64.whl", hash = "sha256:4ea756e425ab2dfc8ef2a0cb87af8aa7ef7dfc6fc46c6f89bcf382121d4fff75"}, + {file = "watchfiles-0.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e397b64f7aaf26915bf2ad0f1190f75c855d11eb111cc00f12f97430153c2eab"}, + {file = "watchfiles-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4ac73b02ca1824ec0a7351588241fd3953748d3774694aa7ddb5e8e46aef3e3"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a896d53b48a1cecccfa903f37a1d87dbb74295305f865a3e816452f6e49e4"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5e7803a65eb2d563c73230e9d693c6539e3c975ccfe62526cadde69f3fda0cf"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1aa4cc85202956d1a65c88d18c7b687b8319dbe6b1aec8969784ef7a10e7d1a"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87f889f6e58849ddb7c5d2cb19e2e074917ed1c6e3ceca50405775166492cca8"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37fd826dac84c6441615aa3f04077adcc5cac7194a021c9f0d69af20fb9fa788"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7db6e36e7a2c15923072e41ea24d9a0cf39658cb0637ecc9307b09d28827e1"}, + {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2368c5371c17fdcb5a2ea71c5c9d49f9b128821bfee69503cc38eae00feb3220"}, + {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:857af85d445b9ba9178db95658c219dbd77b71b8264e66836a6eba4fbf49c320"}, + {file = "watchfiles-0.23.0-cp311-none-win32.whl", hash = "sha256:1d636c8aeb28cdd04a4aa89030c4b48f8b2954d8483e5f989774fa441c0ed57b"}, + {file = "watchfiles-0.23.0-cp311-none-win_amd64.whl", hash = "sha256:46f1d8069a95885ca529645cdbb05aea5837d799965676e1b2b1f95a4206313e"}, + {file = "watchfiles-0.23.0-cp311-none-win_arm64.whl", hash = "sha256:e495ed2a7943503766c5d1ff05ae9212dc2ce1c0e30a80d4f0d84889298fa304"}, + {file = "watchfiles-0.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1db691bad0243aed27c8354b12d60e8e266b75216ae99d33e927ff5238d270b5"}, + {file = "watchfiles-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62d2b18cb1edaba311fbbfe83fb5e53a858ba37cacb01e69bc20553bb70911b8"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e087e8fdf1270d000913c12e6eca44edd02aad3559b3e6b8ef00f0ce76e0636f"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd41d5c72417b87c00b1b635738f3c283e737d75c5fa5c3e1c60cd03eac3af77"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5f3ca0ff47940ce0a389457b35d6df601c317c1e1a9615981c474452f98de1"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6991e3a78f642368b8b1b669327eb6751439f9f7eaaa625fae67dd6070ecfa0b"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f7252f52a09f8fa5435dc82b6af79483118ce6bd51eb74e6269f05ee22a7b9f"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e01bcb8d767c58865207a6c2f2792ad763a0fe1119fb0a430f444f5b02a5ea0"}, + {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8e56fbcdd27fce061854ddec99e015dd779cae186eb36b14471fc9ae713b118c"}, + {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd3e2d64500a6cad28bcd710ee6269fbeb2e5320525acd0cfab5f269ade68581"}, + {file = "watchfiles-0.23.0-cp312-none-win32.whl", hash = "sha256:eb99c954291b2fad0eff98b490aa641e128fbc4a03b11c8a0086de8b7077fb75"}, + {file = "watchfiles-0.23.0-cp312-none-win_amd64.whl", hash = "sha256:dccc858372a56080332ea89b78cfb18efb945da858fabeb67f5a44fa0bcb4ebb"}, + {file = "watchfiles-0.23.0-cp312-none-win_arm64.whl", hash = "sha256:6c21a5467f35c61eafb4e394303720893066897fca937bade5b4f5877d350ff8"}, + {file = "watchfiles-0.23.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ba31c32f6b4dceeb2be04f717811565159617e28d61a60bb616b6442027fd4b9"}, + {file = "watchfiles-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85042ab91814fca99cec4678fc063fb46df4cbb57b4835a1cc2cb7a51e10250e"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24655e8c1c9c114005c3868a3d432c8aa595a786b8493500071e6a52f3d09217"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1a950ab299a4a78fd6369a97b8763732bfb154fdb433356ec55a5bce9515c1"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8d3c5cd327dd6ce0edfc94374fb5883d254fe78a5e9d9dfc237a1897dc73cd1"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ff785af8bacdf0be863ec0c428e3288b817e82f3d0c1d652cd9c6d509020dd0"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02b7ba9d4557149410747353e7325010d48edcfe9d609a85cb450f17fd50dc3d"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a1b05c0afb2cd2f48c1ed2ae5487b116e34b93b13074ed3c22ad5c743109f0"}, + {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:109a61763e7318d9f821b878589e71229f97366fa6a5c7720687d367f3ab9eef"}, + {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9f8e6bb5ac007d4a4027b25f09827ed78cbbd5b9700fd6c54429278dacce05d1"}, + {file = "watchfiles-0.23.0-cp313-none-win32.whl", hash = "sha256:f46c6f0aec8d02a52d97a583782d9af38c19a29900747eb048af358a9c1d8e5b"}, + {file = "watchfiles-0.23.0-cp313-none-win_amd64.whl", hash = "sha256:f449afbb971df5c6faeb0a27bca0427d7b600dd8f4a068492faec18023f0dcff"}, + {file = "watchfiles-0.23.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:2dddc2487d33e92f8b6222b5fb74ae2cfde5e8e6c44e0248d24ec23befdc5366"}, + {file = "watchfiles-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e75695cc952e825fa3e0684a7f4a302f9128721f13eedd8dbd3af2ba450932b8"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2537ef60596511df79b91613a5bb499b63f46f01a11a81b0a2b0dedf645d0a9c"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20b423b58f5fdde704a226b598a2d78165fe29eb5621358fe57ea63f16f165c4"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b98732ec893975455708d6fc9a6daab527fc8bbe65be354a3861f8c450a632a4"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee1f5fcbf5bc33acc0be9dd31130bcba35d6d2302e4eceafafd7d9018c7755ab"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f195338a5a7b50a058522b39517c50238358d9ad8284fd92943643144c0c03"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524fcb8d59b0dbee2c9b32207084b67b2420f6431ed02c18bd191e6c575f5c48"}, + {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0eff099a4df36afaa0eea7a913aa64dcf2cbd4e7a4f319a73012210af4d23810"}, + {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a8323daae27ea290ba3350c70c836c0d2b0fb47897fa3b0ca6a5375b952b90d3"}, + {file = "watchfiles-0.23.0-cp38-none-win32.whl", hash = "sha256:aafea64a3ae698695975251f4254df2225e2624185a69534e7fe70581066bc1b"}, + {file = "watchfiles-0.23.0-cp38-none-win_amd64.whl", hash = "sha256:c846884b2e690ba62a51048a097acb6b5cd263d8bd91062cd6137e2880578472"}, + {file = "watchfiles-0.23.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a753993635eccf1ecb185dedcc69d220dab41804272f45e4aef0a67e790c3eb3"}, + {file = "watchfiles-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6bb91fa4d0b392f0f7e27c40981e46dda9eb0fbc84162c7fb478fe115944f491"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1f67312efa3902a8e8496bfa9824d3bec096ff83c4669ea555c6bdd213aa516"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ca6b71dcc50d320c88fb2d88ecd63924934a8abc1673683a242a7ca7d39e781"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aec5c29915caf08771d2507da3ac08e8de24a50f746eb1ed295584ba1820330"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1733b9bc2c8098c6bdb0ff7a3d7cb211753fecb7bd99bdd6df995621ee1a574b"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02ff5d7bd066c6a7673b17c8879cd8ee903078d184802a7ee851449c43521bdd"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e2de19801b0eaa4c5292a223effb7cfb43904cb742c5317a0ac686ed604765"}, + {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8ada449e22198c31fb013ae7e9add887e8d2bd2335401abd3cbc55f8c5083647"}, + {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3af1b05361e1cc497bf1be654a664750ae61f5739e4bb094a2be86ec8c6db9b6"}, + {file = "watchfiles-0.23.0-cp39-none-win32.whl", hash = "sha256:486bda18be5d25ab5d932699ceed918f68eb91f45d018b0343e3502e52866e5e"}, + {file = "watchfiles-0.23.0-cp39-none-win_amd64.whl", hash = "sha256:d2d42254b189a346249424fb9bb39182a19289a2409051ee432fb2926bad966a"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9265cf87a5b70147bfb2fec14770ed5b11a5bb83353f0eee1c25a81af5abfe"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f02a259fcbbb5fcfe7a0805b1097ead5ba7a043e318eef1db59f93067f0b49b"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebaebb53b34690da0936c256c1cdb0914f24fb0e03da76d185806df9328abed"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd257f98cff9c6cb39eee1a83c7c3183970d8a8d23e8cf4f47d9a21329285cee"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aba037c1310dd108411d27b3d5815998ef0e83573e47d4219f45753c710f969f"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a96ac14e184aa86dc43b8a22bb53854760a58b2966c2b41580de938e9bf26ed0"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11698bb2ea5e991d10f1f4f83a39a02f91e44e4bd05f01b5c1ec04c9342bf63c"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efadd40fca3a04063d40c4448c9303ce24dd6151dc162cfae4a2a060232ebdcb"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:556347b0abb4224c5ec688fc58214162e92a500323f50182f994f3ad33385dcb"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1cf7f486169986c4b9d34087f08ce56a35126600b6fef3028f19ca16d5889071"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18de0f82c62c4197bea5ecf4389288ac755896aac734bd2cc44004c56e4ac47"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:532e1f2c491274d1333a814e4c5c2e8b92345d41b12dc806cf07aaff786beb66"}, + {file = "watchfiles-0.23.0.tar.gz", hash = "sha256:9338ade39ff24f8086bb005d16c29f8e9f19e55b18dcb04dfa26fcbc09da497b"}, ] [package.dependencies] @@ -3380,83 +4230,100 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "websockets" -version = "11.0.3" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "main" +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, - {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, - {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, - {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, - {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, - {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, - {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, - {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, - {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, - {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, - {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, - {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, - {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, - {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] [package.source] @@ -3468,7 +4335,6 @@ reference = "ali" name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3484,163 +4350,103 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "wordcloud" -version = "1.9.2" -description = "A little word cloud generator" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "wordcloud-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67a83ad05b7a08db64e99cf734ba4394855e78c9e3c6e3a6104f80c64f090d2f"}, - {file = "wordcloud-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d7614f46c8062fe2d3fa0325c747793b463215b0e9d97b4f9a3d6f31a60b7f"}, - {file = "wordcloud-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cec4a76b3cde8a19ec5c7ed7356d92c7337af3f1e11eb8e0d8191459f318bba"}, - {file = "wordcloud-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f37673f17d772907b37d92102cb437dce408b1e12fc3f59a6d3920082318881b"}, - {file = "wordcloud-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1e0b346eb15f6deed7d4cb6caaa4d7abf4d488434cb9cdb91194720b524ea86d"}, - {file = "wordcloud-1.9.2-cp310-cp310-win32.whl", hash = "sha256:61816f1e548a5505789e6e42a7cf7c798312b77a30465c3b8a6049235bcf4649"}, - {file = "wordcloud-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:e03ec307901cab43baeda3416d880d56895ef4ffa560e25c7a9dd74e94ede5c6"}, - {file = "wordcloud-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e744a88bc463ca373472cc831d9a6a6469dceaa6471abdc124828adeb2355df"}, - {file = "wordcloud-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c25014af784bc75741ae6693bbbd98faa4865de397903eed5485dafb2eb356a4"}, - {file = "wordcloud-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f49ff8eb67982b22ed476c48a4b5728c4f638ebed2452b4760b68f15d62931f"}, - {file = "wordcloud-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7101f62967fd912e49cf10e58e09a5af0fc11095d9f8bf170b22d8e0b2ee8ca"}, - {file = "wordcloud-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:73f7571df267ea8f6765b4cb946d6b158c4fd36aca44533716ca634bf2fd75f9"}, - {file = "wordcloud-1.9.2-cp311-cp311-win32.whl", hash = "sha256:10ad4bbed83ad487b3592ad0417426ffff946f131422e9e6bab74129caf7a6e3"}, - {file = "wordcloud-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:76ebbbefd453fa3ea15e03e605437c88a721502e077bae2f49c676d84dd0437d"}, - {file = "wordcloud-1.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a0fefffa6ea013276a36489d28aaceed1ae2f99f884b6a2fa1d4107bd1b2df3a"}, - {file = "wordcloud-1.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da5b4b8ed29fe56039b30f86c1c5ef4b06ce7f2f3ba111af61ede2fc661b0c45"}, - {file = "wordcloud-1.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4210c7f2db7e825d670a8e31a118f15b333e7ace555e03df15c901ae7fa19219"}, - {file = "wordcloud-1.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:030d89ea934472eeda1beabd82a81cfb0f2d0de7c5703f220016742aa6911c3f"}, - {file = "wordcloud-1.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9750c4e641b96e02e7e50ba2cae9441d611ca26310de0628b1e6444dfba93554"}, - {file = "wordcloud-1.9.2-cp36-cp36m-win32.whl", hash = "sha256:4dad68777056a09b5c9d8bb10842fbd5c851ecca35a27ddcbb47f28b1d6b5921"}, - {file = "wordcloud-1.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a70f10c477db98a9850dec1f9fe4f8774c2afd902f29bbff887c625f6c92e5d1"}, - {file = "wordcloud-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2df1e7e74ec97e296bf1a1b0ac2764576b83c6c4d3b1d00e01d095326b89063b"}, - {file = "wordcloud-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cd52aaa52b1a802154e402c2063ee6805c6f7d7ee84b5503b5c11d6b501224"}, - {file = "wordcloud-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ec9ab90114a3afc050b4a3e94c86d6f81e7dbbf517f79291a89e39366459e24"}, - {file = "wordcloud-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1a1d0bac12025031594a76694e0b04b38f82172443ad197224ef6d51f540e28e"}, - {file = "wordcloud-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1e521afd56d86e7f6ced3567ac685c4342c81c47f52c32b414a408c2f00f7884"}, - {file = "wordcloud-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:6dfcf9a54a328c5547a6c08197d5999a8d2444390dcf35dfdba9774d65c992cb"}, - {file = "wordcloud-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9eb2a9469d1981f3d102225ca5b1b2e7022042761e824d803c802ae0ff5d3251"}, - {file = "wordcloud-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:349272c5156e196ff0c05d910595dcc56071a4e622707e12889f2ee21c360a0b"}, - {file = "wordcloud-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad0a2e4509cc620587746280e5cf667ec3db4e713c63f4c21e69adad41041be"}, - {file = "wordcloud-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baebbdd53b1b800c816ffc1c2d71f78feb3a9599f1197422a5995d112069d485"}, - {file = "wordcloud-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad41c4069a636452fe37754e0f3b008417ab3b29e45016caf9113fad911106c7"}, - {file = "wordcloud-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf03002ea15f5ad2ba69d8a68071c96758d88e0c466a599a71324e4297d4d2e0"}, - {file = "wordcloud-1.9.2-cp38-cp38-win32.whl", hash = "sha256:fb25360f2c8d5b52703b8a92dd45cadc84fa901bc322b40c0883dc2f974c29fe"}, - {file = "wordcloud-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:785b8bcc00d953b752a220c9b4b1f8532983f26805f70b46f3c6ce2e6b130f54"}, - {file = "wordcloud-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:139f0dbf6b6aeb32a20ccb383320dde514297c9aeba9b6cc7a82f4bb2502b1f9"}, - {file = "wordcloud-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91e025b5e50d814601703aa4878dbafdceaa0a3e64d42cd1633317d9b829bb64"}, - {file = "wordcloud-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cdf7962555f9fed612c4d060a36851f2aefeb744bd0bf6b45adeaf1a37c43e1"}, - {file = "wordcloud-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce15d884fb2b1087303878b248f449035abe2a545e17016021c2b86fe916ba4a"}, - {file = "wordcloud-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:aed191fb1a7cefc6556a290a26d4986246f0a71e93330f8cbca25341566c312c"}, - {file = "wordcloud-1.9.2-cp39-cp39-win32.whl", hash = "sha256:391b98c88ef2ce79171edee9be60b9a5eb864716452a1fbe47984252550e6a8d"}, - {file = "wordcloud-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:0115a5dd0fd7aafc1ea20738478ac01ba9cf4203bfd7b624e2ab1a311c33f80c"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6ecfde604fbb8a1096d1507edc6d75771f07751409293188e559efb6628fd9f6"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f6ba0cc325d99eb515a60cb89d4b34d546c5f1c9a0595accff4c783d92ce28"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22fb86e0101c46f139d353e8f3575c8c64a6c503f263d90cecc28cd14bc5032"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7dfcb202cd38a094add34261bbb63119e2897dacbde50cac6c7ff3fdb97c555f"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fa46c8d175c0b8dd9a1d599884bfb3dd7f626a9fc553834ae6522544d1ae0eca"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a602e9161b7a8d4e2d92f5a891a9aee92b97402aa75db51918afa6aa4da9e8f"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0625865763b1a6c27860431023ee5f5a402e199301b2dc671bea03734a612ee0"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b98e7ff6fa4277ca40074cb4fac2f2e4dd792d530177cc7494d42777d53e96c6"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0351a0190f2f2abd8422a0f817d1f9689eec24f0563438626f8f9b25ba7d061"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47b6e8948350cfc6f55988f1398ccd405a49a14b2ca6dd6b76fb76089a8b9a03"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9b7524bd647077ea1b095b3f2669dba4e6a6314eb41641914a41b991ae56b2a"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3132bf742fa403616201454ee90f1123e53f546b6e22bddda1b5ba14974457a7"}, - {file = "wordcloud-1.9.2.tar.gz", hash = "sha256:71062ba6bfeaf1a7f8b6f18f6a8a7a810ef10973ebd9aa10c04d9fff690363d3"}, -] - -[package.dependencies] -matplotlib = "*" -numpy = ">=1.6.1" -pillow = "*" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "yarl" -version = "1.9.2" +version = "1.9.4" description = "Yet another URL library" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, - {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, - {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, - {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, - {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, - {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, - {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, - {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, - {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, - {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, - {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, - {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, - {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, ] [package.dependencies] @@ -3654,19 +4460,18 @@ reference = "ali" [[package]] name = "zipp" -version = "3.16.2" +version = "3.19.2" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [package.source] type = "legacy" @@ -3675,5 +4480,5 @@ reference = "ali" [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "d2b5e1b19170350e5f955e1ca454f952e69c2229b06d6631ed05dc8c8531064a" +python-versions = "^3.10" +content-hash = "0c19bc0955bde3a4f368395f6d7117c3ab638e36b9af0d9ac05d08d1922dabbc" diff --git a/pyproject.toml b/pyproject.toml index 75e454ae..dc2d6501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,42 +11,41 @@ default = true url = "https://mirrors.aliyun.com/pypi/simple/" [tool.poetry.dependencies] -python = "^3.8" -nonebot2 = "^2.0.0rc1" -aiofiles = "^0.8.0" -aiohttp = "3.7.4.post0" -beautifulsoup4 = "4.9.3" -feedparser = "^6.0.8" -httpx = "^0.23.0" -ImageHash = "^4.2.1" -jieba = "^0.42.1" -lxml = "4.6.5" -opencv-python = "^4.5.5" -Pillow = "^9.0.1" -playwright = "^1.18.2" -psutil = "^5.9.0" -PyYAML = "5.4.1" -retrying = "^1.3.3" -ujson = "^5.1.0" -"ruamel.yaml" = "^0.17.21" -matplotlib = "^3.5.1" -black = "^22.1.0" -pypinyin = "^0.46.0" -dateparser = "^1.1.0" -cn2an = "^0.5.16" -python-jose = "^3.3.0" -python-multipart = "^0.0.5" -bilireq = "^0.2.6" -emoji = "^1.7.0" -wordcloud = "^1.8.1" -rich = "^12.4.3" -nonebot-adapter-onebot = "^2.1.5" -nonebot-plugin-apscheduler = "^0.2.0" -nonebot-plugin-htmlrender = "^0.2.0" -cachetools = "^5.2.0" -tortoise-orm = {extras = ["asyncpg"], version = "^0.19.3"} -cattrs = "^22.2.0" -starlette = "^0.26.1" +python = "^3.10" +playwright = "^1.41.1" +nonebot-adapter-onebot = "^2.3.1" +nonebot-plugin-apscheduler = "^0.3.0" +tortoise-orm = {extras = ["asyncpg"], version = "^0.20.0"} +cattrs = "^23.2.3" +ruamel-yaml = "^0.18.5" +strenum = "^0.4.15" +nonebot-plugin-session = "^0.2.3" +ujson = "^5.9.0" +nonebot-adapter-kaiheila = "^0.3.0" +nb-cli = "^1.3.0" +nonebot2 = "^2.1.3" +nonebot-adapter-discord = "^0.1.3" +nonebot-adapter-dodo = "^0.1.4" +pillow = "9.5" +retrying = "^1.3.4" +aiofiles = "^23.2.1" +nonebot-plugin-htmlrender = "^0.3.0" +nonebot-plugin-userinfo = "^0.1.3" +pypinyin = "^0.51.0" +beautifulsoup4 = "^4.12.3" +lxml = "^5.1.0" +psutil = "^5.9.8" +feedparser = "^6.0.11" +opencv-python = "^4.9.0.80" +imagehash = "^4.3.1" +black = "^24.4.2" +cn2an = "^0.5.22" +aiohttp = "^3.9.5" +dateparser = "^1.2.0" +bilireq = "0.2.3post0" +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +python-multipart = "^0.0.9" +nonebot-plugin-alconna = "^0.51.1" [tool.poetry.dev-dependencies] diff --git a/resources/image/_base/laugh/0.jpg b/resources/image/_base/laugh/0.jpg new file mode 100644 index 00000000..3ad37672 Binary files /dev/null and b/resources/image/_base/laugh/0.jpg differ diff --git a/resources/image/_base/laugh/1.jpg b/resources/image/_base/laugh/1.jpg new file mode 100644 index 00000000..8e302817 Binary files /dev/null and b/resources/image/_base/laugh/1.jpg differ diff --git a/resources/image/_icon/discode.png b/resources/image/_icon/discode.png new file mode 100644 index 00000000..08e0a937 Binary files /dev/null and b/resources/image/_icon/discode.png differ diff --git a/resources/image/_icon/dodo.png b/resources/image/_icon/dodo.png new file mode 100644 index 00000000..58fa20fc Binary files /dev/null and b/resources/image/_icon/dodo.png differ diff --git a/resources/image/_icon/kook.png b/resources/image/_icon/kook.png new file mode 100644 index 00000000..3c87716f Binary files /dev/null and b/resources/image/_icon/kook.png differ diff --git a/resources/image/_icon/qq.png b/resources/image/_icon/qq.png new file mode 100644 index 00000000..42825456 Binary files /dev/null and b/resources/image/_icon/qq.png differ diff --git a/resources/image/sign/sign_res/bar.png b/resources/image/sign/sign_res/bar.png index 4fc99ec3..18b898d1 100644 Binary files a/resources/image/sign/sign_res/bar.png and b/resources/image/sign/sign_res/bar.png differ diff --git a/resources/image/sign/sign_res/bar_white.png b/resources/image/sign/sign_res/bar_white.png index c09c4635..2f3bcae4 100644 Binary files a/resources/image/sign/sign_res/bar_white.png and b/resources/image/sign/sign_res/bar_white.png differ diff --git a/services/db_context.py b/services/db_context.py deleted file mode 100755 index 20afb41d..00000000 --- a/services/db_context.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import List - -from nonebot.utils import is_coroutine_callable -from tortoise import Tortoise, fields -from tortoise.connection import connections -from tortoise.models import Model as Model_ -from tortoise.queryset import RawSQLQuery - -from configs.config import address, bind, database, password, port, sql_name, user -from utils.text_utils import prompt2cn - -from .log import logger - -MODELS: List[str] = [] - -SCRIPT_METHOD = [] - - -class Model(Model_): - """ - 自动添加模块 - - Args: - Model_ (_type_): _description_ - """ - - def __init_subclass__(cls, **kwargs): - MODELS.append(cls.__module__) - - if func := getattr(cls, "_run_script", None): - SCRIPT_METHOD.append((cls.__module__, func)) - - -class TestSQL(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - - class Meta: - table = "test_sql" - table_description = "执行SQL命令,不记录任何数据" - - -async def init(): - if not bind and not any([user, password, address, port, database]): - raise ValueError("\n" + prompt2cn("数据库配置未填写", 28)) - i_bind = bind - if not i_bind: - i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" - try: - await Tortoise.init( - db_url=i_bind, - modules={"models": MODELS}, - # timezone="Asia/Shanghai" - ) - await Tortoise.generate_schemas() - logger.info(f"Database loaded successfully!") - except Exception as e: - raise Exception(f"数据库连接错误.... {type(e)}: {e}") - if SCRIPT_METHOD: - logger.debug(f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个...") - sql_list = [] - for module, func in SCRIPT_METHOD: - try: - if is_coroutine_callable(func): - sql = await func() - else: - sql = func() - if sql: - sql_list += sql - - except Exception as e: - logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e) - for sql in sql_list: - logger.debug(f"执行SQL: {sql}") - try: - await TestSQL.raw(sql) - except Exception as e: - logger.debug(f"执行SQL: {sql} 错误...", e=e) - - -async def disconnect(): - await connections.close_all() diff --git a/services/log.py b/services/log.py deleted file mode 100755 index c656100d..00000000 --- a/services/log.py +++ /dev/null @@ -1,144 +0,0 @@ -from datetime import datetime, timedelta -from typing import Any, Dict, Optional, Union - -from loguru import logger as logger_ -from nonebot.log import default_filter, default_format - -from configs.path_config import LOG_PATH - -logger_.add( - LOG_PATH / f"{datetime.now().date()}.log", - level="INFO", - rotation="00:00", - format=default_format, - filter=default_filter, - retention=timedelta(days=30), -) - -logger_.add( - LOG_PATH / f"error_{datetime.now().date()}.log", - level="ERROR", - rotation="00:00", - format=default_format, - filter=default_filter, - retention=timedelta(days=30), -) - - -class logger: - - TEMPLATE_A = "{}" - TEMPLATE_B = "[{}]: {}" - TEMPLATE_C = "用户[{}] 触发 [{}]: {}" - TEMPLATE_D = "群聊[{}] 用户[{}] 触发 [{}]: {}" - TEMPLATE_E = "群聊[{}] 用户[{}] 触发 [{}] [Target]({}): {}" - - TEMPLATE_USER = "用户[{}] " - TEMPLATE_GROUP = "群聊[{}] " - TEMPLATE_COMMAND = "CMD[{}] " - TEMPLATE_TARGET = "[Target]([{}]) " - - SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]" - - WARNING_TEMPLATE = "[{}]: {}" - - ERROR_TEMPLATE = "[{}]: {}" - - @classmethod - def info( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - logger_.opt(colors=True).info(template) - - @classmethod - def success( - cls, - info: str, - command: str, - param: Optional[Dict[str, Any]] = None, - result: Optional[str] = "", - ): - param_str = "" - if param: - param_str = ",".join([f"{k}:{v}" for k, v in param.items()]) - logger_.opt(colors=True).success( - cls.SUCCESS_TEMPLATE.format(command, info, param_str, result) - ) - - @classmethod - def warning( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - e: Optional[Exception] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - if e: - template += f" || 错误{type(e)}: {e}" - logger_.opt(colors=True).warning(template) - - @classmethod - def error( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - e: Optional[Exception] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - if e: - template += f" || 错误 {type(e)}: {e}" - logger_.opt(colors=True).error(template) - - @classmethod - def debug( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - e: Optional[Exception] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - if e: - template += f" || 错误 {type(e)}: {e}" - logger_.opt(colors=True).debug(template) - - @classmethod - def __parser_template( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - ) -> str: - arg_list = [] - template = "" - if group_id is not None: - template += cls.TEMPLATE_GROUP - arg_list.append(group_id) - if user_id is not None: - template += cls.TEMPLATE_USER - arg_list.append(user_id) - if command is not None: - template += cls.TEMPLATE_COMMAND - arg_list.append(command) - if target is not None: - template += cls.TEMPLATE_TARGET - arg_list.append(target) - arg_list.append(info) - template += "{}" - return template.format(*arg_list) diff --git a/update_info.json b/update_info.json deleted file mode 100644 index 140e2ea4..00000000 --- a/update_info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "update_file": [ - "plugins", - "models", - "services", - "utils", - "basic_plugins", - "configs/path_config.py", - "configs/utils", - "poetry.lock", - "pyproject.toml" - ], - "add_file": ["resources/image/csgo_cases"], - "delete_file": [] -} \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/utils/data_utils.py b/utils/data_utils.py deleted file mode 100755 index 6c6458c0..00000000 --- a/utils/data_utils.py +++ /dev/null @@ -1,73 +0,0 @@ -import asyncio -import os -from typing import List, Union - -from configs.path_config import IMAGE_PATH -from models.group_member_info import GroupInfoUser -from utils.image_utils import BuildMat - - -async def init_rank( - title: str, - all_user_id: List[str], - all_user_data: List[Union[int, float]], - group_id: int, - total_count: int = 10, -) -> BuildMat: - """ - 说明: - 初始化通用的数据排行榜 - 参数: - :param title: 排行榜标题 - :param all_user_id: 所有用户的qq号 - :param all_user_data: 所有用户需要排行的对应数据 - :param group_id: 群号,用于从数据库中获取该用户在此群的昵称 - :param total_count: 获取人数总数 - """ - _uname_lst = [] - _num_lst = [] - for i in range(min(len(all_user_id), total_count)): - _max = max(all_user_data) - max_user_id = all_user_id[all_user_data.index(_max)] - all_user_id.remove(max_user_id) - all_user_data.remove(_max) - if user := await GroupInfoUser.get_or_none( - user_id=str(max_user_id), group_id=str(group_id) - ): - user_name = user.user_name - else: - user_name = f"{max_user_id}" - _uname_lst.append(user_name) - _num_lst.append(_max) - _uname_lst.reverse() - _num_lst.reverse() - return await asyncio.get_event_loop().run_in_executor( - None, _init_rank_graph, title, _uname_lst, _num_lst - ) - - -def _init_rank_graph( - title: str, _uname_lst: List[str], _num_lst: List[Union[int, float]] -) -> BuildMat: - """ - 生成排行榜统计图 - :param title: 排行榜标题 - :param _uname_lst: 用户名列表 - :param _num_lst: 数值列表 - """ - image = BuildMat( - y=_num_lst, - y_name="* 可以在命令后添加数字来指定排行人数 至多 50 *", - mat_type="barh", - title=title, - x_index=_uname_lst, - display_num=True, - x_rotate=30, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - image.gen_graph() - return image diff --git a/utils/decorator/__init__.py b/utils/decorator/__init__.py deleted file mode 100644 index b91f2a8a..00000000 --- a/utils/decorator/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -class Singleton: - - """ - 单例注解 - """ - - def __init__(self, cls): - self._cls = cls - - def __call__(self, *args, **kw): - if not hasattr(self, "_instance"): - self._instance = self._cls(*args, **kw) - return self._instance diff --git a/utils/decorator/shop.py b/utils/decorator/shop.py deleted file mode 100644 index 5105e4c2..00000000 --- a/utils/decorator/shop.py +++ /dev/null @@ -1,204 +0,0 @@ -from typing import Callable, Union, Tuple, Optional -from nonebot.adapters.onebot.v11 import MessageSegment, Message -from nonebot.plugin import require - - -class ShopRegister(dict): - def __init__(self, *args, **kwargs): - super(ShopRegister, self).__init__(*args, **kwargs) - self._data = {} - self._flag = True - - def before_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): - """ - 说明: - 使用前检查方法 - 参数: - :param name: 道具名称 - :param load_status: 加载状态 - """ - def register_before_handle(name_list: Tuple[str, ...], func: Callable): - if load_status: - for name_ in name_list: - if not self._data[name_]: - self._data[name_] = {} - if not self._data[name_].get('before_handle'): - self._data[name_]['before_handle'] = [] - self._data[name]['before_handle'].append(func) - _name = (name,) if isinstance(name, str) else name - return lambda func: register_before_handle(_name, func) - - def after_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): - """ - 说明: - 使用后执行方法 - 参数: - :param name: 道具名称 - :param load_status: 加载状态 - """ - def register_after_handle(name_list: Tuple[str, ...], func: Callable): - if load_status: - for name_ in name_list: - if not self._data[name_]: - self._data[name_] = {} - if not self._data[name_].get('after_handle'): - self._data[name_]['after_handle'] = [] - self._data[name_]['after_handle'].append(func) - _name = (name,) if isinstance(name, str) else name - return lambda func: register_after_handle(_name, func) - - def register( - self, - name: Tuple[str, ...], - price: Tuple[float, ...], - des: Tuple[str, ...], - discount: Tuple[float, ...], - limit_time: Tuple[int, ...], - load_status: Tuple[bool, ...], - daily_limit: Tuple[int, ...], - is_passive: Tuple[bool, ...], - icon: Tuple[str, ...], - **kwargs, - ): - def add_register_item(func: Callable): - if name in self._data.keys(): - raise ValueError("该商品已注册,请替换其他名称!") - for n, p, d, dd, l, s, dl, pa, i in zip( - name, price, des, discount, limit_time, load_status, daily_limit, is_passive, icon - ): - if s: - _temp_kwargs = {} - for key, value in kwargs.items(): - if key.startswith(f"{n}_"): - _temp_kwargs[key.split("_", maxsplit=1)[-1]] = value - else: - _temp_kwargs[key] = value - temp = self._data.get(n, {}) - temp.update({ - "price": p, - "des": d, - "discount": dd, - "limit_time": l, - "daily_limit": dl, - "icon": i, - "is_passive": pa, - "func": func, - "kwargs": _temp_kwargs, - }) - self._data[n] = temp - return func - - return lambda func: add_register_item(func) - - async def load_register(self): - require("use") - require("shop_handle") - from basic_plugins.shop.use.data_source import register_use, func_manager - from basic_plugins.shop.shop_handle.data_source import register_goods - # 统一进行注册 - if self._flag: - # 只进行一次注册 - self._flag = False - for name in self._data.keys(): - await register_goods( - name, - self._data[name]["price"], - self._data[name]["des"], - self._data[name]["discount"], - self._data[name]["limit_time"], - self._data[name]["daily_limit"], - self._data[name]["is_passive"], - self._data[name]["icon"], - ) - register_use( - name, self._data[name]["func"], **self._data[name]["kwargs"] - ) - func_manager.register_use_before_handle(name, self._data[name].get('before_handle', [])) - func_manager.register_use_after_handle(name, self._data[name].get('after_handle', [])) - - def __call__( - self, - name: Union[str, Tuple[str, ...]], # 名称 - price: Union[float, Tuple[float, ...]], # 价格 - des: Union[str, Tuple[str, ...]], # 简介 - discount: Union[float, Tuple[float, ...]] = 1, # 折扣 - limit_time: Union[int, Tuple[int, ...]] = 0, # 限时 - load_status: Union[bool, Tuple[bool, ...]] = True, # 加载状态 - daily_limit: Union[int, Tuple[int, ...]] = 0, # 每日限购 - is_passive: Union[bool, Tuple[bool, ...]] = False, # 被动道具(无法被'使用道具'命令消耗) - icon: Union[str, Tuple[str, ...]] = False, # 图标 - **kwargs, - ): - _tuple_list = [] - _current_len = -1 - for x in [name, price, des, discount, limit_time, load_status]: - if isinstance(x, tuple): - if _current_len == -1: - _current_len = len(x) - if _current_len != len(x): - raise ValueError( - f"注册商品 {name} 中 name,price,des,discount,limit_time,load_status,daily_limit 数量不符!" - ) - _current_len = _current_len if _current_len > -1 else 1 - _name = self.__get(name, _current_len) - _price = self.__get(price, _current_len) - _discount = self.__get(discount, _current_len) - _limit_time = self.__get(limit_time, _current_len) - _des = self.__get(des, _current_len) - _load_status = self.__get(load_status, _current_len) - _daily_limit = self.__get(daily_limit, _current_len) - _is_passive = self.__get(is_passive, _current_len) - _icon = self.__get(icon, _current_len) - return self.register( - _name, - _price, - _des, - _discount, - _limit_time, - _load_status, - _daily_limit, - _is_passive, - _icon, - **kwargs, - ) - - def __get(self, value, _current_len): - return value if isinstance(value, tuple) else tuple([value for _ in range(_current_len)]) - - def __setitem__(self, key, value): - self._data[key] = value - - def __getitem__(self, key): - return self._data[key] - - def __contains__(self, key): - return key in self._data - - def __str__(self): - return str(self._data) - - def keys(self): - return self._data.keys() - - def values(self): - return self._data.values() - - def items(self): - return self._data.items() - - -class NotMeetUseConditionsException(Exception): - - """ - 不满足条件异常类 - """ - - def __init__(self, info: Optional[Union[str, MessageSegment, Message]]): - super().__init__(self) - self._info = info - - def get_info(self): - return self._info - - -shop_register = ShopRegister() diff --git a/utils/depends/__init__.py b/utils/depends/__init__.py deleted file mode 100644 index 310df213..00000000 --- a/utils/depends/__init__.py +++ /dev/null @@ -1,215 +0,0 @@ -from typing import Any, Callable, List, Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.internal.matcher import Matcher -from nonebot.internal.params import Depends -from nonebot.params import Command - -from configs.config import Config -from models.bag_user import BagUser -# from models.bag_user import BagUser -from models.level_user import LevelUser -from models.user_shop_gold_log import UserShopGoldLog -# from models.user_shop_gold_log import UserShopGoldLog -from utils.manager import admin_manager -from utils.message_builder import at -from utils.utils import ( - get_message_at, - get_message_face, - get_message_img, - get_message_text, -) - - -def OneCommand(): - """ - 获取单个命令Command - """ - - async def dependency( - cmd: Tuple[str, ...] = Command(), - ): - return cmd[0] if cmd else None - - return Depends(dependency) - - -def AdminCheck(level: Optional[int] = None): - """ - 说明: - 权限检查 - 参数: - :param level: 等级 - """ - - async def dependency(matcher: Matcher, event: GroupMessageEvent): - if name := matcher.plugin_name: - plugin_level = admin_manager.get_plugin_level(name) - user_level = await LevelUser.get_user_level(event.user_id, event.group_id) - if level is None: - if user_level < plugin_level: - await matcher.finish( - at(event.user_id) + f"你的权限不足喔,该功能需要的权限等级:{plugin_level}" - ) - else: - if user_level < level: - await matcher.finish( - at(event.user_id) + f"你的权限不足喔,该功能需要的权限等级:{level}" - ) - - return Depends(dependency) - - -def CostGold(gold: int): - """ - 说明: - 插件方法调用使用金币 - 参数: - :param gold: 金币数量 - """ - - async def dependency(matcher: Matcher, event: GroupMessageEvent): - if (await BagUser.get_gold(event.user_id, event.group_id)) < gold: - await matcher.finish(at(event.user_id) + f"金币不足..该功能需要{gold}金币..") - await BagUser.spend_gold(event.user_id, event.group_id, gold) - await UserShopGoldLog.create( - user_id=str(event.user_id), - group_id=str(event.group_id), - type=2, - name=matcher.plugin_name, - num=1, - spend_gold=gold, - ) - - return Depends(dependency) - - -def GetConfig( - module: Optional[str] = None, - config: str = "", - default_value: Any = None, - prompt: Optional[str] = None, -): - """ - 说明: - 获取配置项 - 参数: - :param module: 模块名,为空时默认使用当前插件模块名 - :param config: 配置项名称 - :param default_value: 默认值 - :param prompt: 为空时提示 - """ - - async def dependency(matcher: Matcher): - module_ = module or matcher.plugin_name - if module_: - value = Config.get_config(module_, config, default_value) - if value is None and prompt: - # await matcher.finish(prompt or f"配置项 {config} 未填写!") - await matcher.finish(prompt) - return value - - return Depends(dependency) - - -def CheckConfig( - module: Optional[str] = None, - config: Union[str, List[str]] = "", - prompt: Optional[str] = None, -): - """ - 说明: - 检测配置项在配置文件中是否填写 - 参数: - :param module: 模块名,为空时默认使用当前插件模块名 - :param config: 需要检查的配置项名称 - :param prompt: 为空时提示 - """ - - async def dependency(matcher: Matcher): - module_ = module or matcher.plugin_name - if module_: - config_list = [config] if isinstance(config, str) else config - for c in config_list: - if Config.get_config(module_, c) is None: - await matcher.finish(prompt or f"配置项 {c} 未填写!") - - return Depends(dependency) - - -async def _match( - matcher: Matcher, - event: MessageEvent, - msg: Optional[str], - func: Callable, - contain_reply: bool, -): - _list = func(event.message) - if event.reply and contain_reply: - _list = func(event.reply.message) - if not _list and msg: - await matcher.finish(msg) - return _list - - -def ImageList(msg: Optional[str] = None, contain_reply: bool = True) -> List[str]: - """ - 说明: - 获取图片列表(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return await _match(matcher, event, msg, get_message_img, contain_reply) - - return Depends(dependency) - - -def AtList(msg: Optional[str] = None, contain_reply: bool = True) -> List[int]: - """ - 说明: - 获取at列表(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return [ - int(x) - for x in await _match(matcher, event, msg, get_message_at, contain_reply) - ] - - return Depends(dependency) - - -def FaceList(msg: Optional[str] = None, contain_reply: bool = True) -> List[str]: - """ - 说明: - 获取face列表(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return await _match(matcher, event, msg, get_message_face, contain_reply) - - return Depends(dependency) - - -def PlaintText(msg: Optional[str] = None, contain_reply: bool = True) -> str: - """ - 说明: - 获取纯文本且(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return await _match(matcher, event, msg, get_message_text, contain_reply) - - return Depends(dependency) diff --git a/utils/game_utils.py b/utils/game_utils.py deleted file mode 100644 index 2ce2eb86..00000000 --- a/utils/game_utils.py +++ /dev/null @@ -1,192 +0,0 @@ -from typing import Optional, Dict, Union - -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from pydantic import BaseModel -import time - - -class GameEntry(BaseModel): - game_name: str - module: str - default_msg: str - msg_data: Dict[int, Union[str, Message, MessageSegment]] - timeout: int # 超时时限 - anti_concurrency: bool # 是否阻断 - - -class GroupGameStatus(BaseModel): - game: GameEntry - status: int - time: time.time() # 创建时间 - - -class GameManager: - def __init__(self): - self._data = {} - self._status = {} - - def add_game( - self, - game_name: str, - module: str, - timeout: int, - default_msg: Optional[str] = "游戏还未结束!", - msg_data: Dict[int, Union[str, Message, MessageSegment]] = None, - anti_concurrency: bool = True, - **kwargs, - ): - """ - 参数: - 将游戏添加到游戏管理器 - 说明: - :param game_name: 游戏名称 - :param module: 模块名 - :param timeout: 超时时长 - :param default_msg: 默认回复消息 - :param msg_data: 不同状态回复的消息 - :param anti_concurrency: 是否阻断反并发 - """ - self._data[module] = GameEntry( - game_name=game_name, - module=module, - timeout=timeout, - default_msg=default_msg, - msg_data=msg_data or {}, - anti_concurrency=anti_concurrency, - **kwargs, - ) - - def start(self, group_id: int, module: str): - """ - 说明: - 游戏开始标记 - 参数: - :param group_id: 群号 - :param module: 模块名 - """ - if not self._status.get(group_id): - self._status[group_id] = [] - if module not in [x.game.module for x in self._status[module]]: - self._status[group_id].append( - GroupGameStatus(game=self._data[module], status=0) - ) - - def end(self, group_id: int, module: str): - """ - 说明: - 游戏结束标记 - 参数: - :param group_id: 群号 - :param module: 模块名 - """ - if self._status.get(group_id) and module in [ - x.game.module for x in self._status[group_id] - ]: - for x in self._status[group_id]: - if self._status[group_id][x].game.module == module: - self._status[group_id].remove(x) - break - - def set_status(self, group_id: int, module: str, status: int): - """ - 说明: - 设置游戏状态,根据状态发送不同的提示消息 msg_data - 参数: - :param group_id: 群号 - :param module: 模块名 - :param status: 状态码 - """ - if self._status.get(group_id) and module in [ - x.game.module for x in self._status[group_id] - ]: - [x.game.module for x in self._status[group_id] if x.game.module == module][ - 0 - ].status = status - - def check(self, group_id, module: str) -> Optional[str]: - """ - 说明: - 检查群游戏当前状态并返回提示语句 - 参数: - :param group_id: 群号 - :param module: 模块名 - """ - if module in self._data and self._status.get(group_id): - for x in self._status[group_id]: - if x.game.anti_concurrency: - return f"{x.game.game_name} 还未结束,请等待 {x.game.game_name} 游戏结束!" - if self._status.get(group_id) and module in [ - x.game.module for x in self._status[group_id] - ]: - group_game_status = [ - x.game.module for x in self._status[group_id] if x.game.module == module - ][0] - if time.time() - group_game_status.time > group_game_status.game.timeout: - # 超时结束 - self.end(group_id, module) - else: - return ( - group_game_status.game.msg_data.get(group_game_status.status) - or group_game_status.game.default_msg - ) - - -game_manager = GameManager() - - -class Game: - """ - 反并发,游戏重复开始 - """ - def __init__( - self, - game_name: str, - module: str, - timeout: int = 60, - default_msg: Optional[str] = None, - msg_data: Dict[int, Union[str, Message, MessageSegment]] = None, - anti_concurrency: bool = True, - ): - """ - 参数: - 将游戏添加到游戏管理器 - 说明: - :param game_name: 游戏名称 - :param module: 模块名 - :param timeout: 超时时长 - :param default_msg: 默认回复消息 - :param msg_data: 不同状态回复的消息 - :param anti_concurrency: 是否阻断反并发 - """ - self.module = module - game_manager.add_game( - game_name, module, timeout, default_msg, msg_data, anti_concurrency - ) - - def start(self, group_id: int): - """ - 说明: - 游戏开始标记 - 参数: - :param group_id: 群号 - """ - game_manager.start(group_id, self.module) - - def end(self, group_id: int): - """ - 说明: - 游戏结束标记 - 参数: - :param group_id: 群号 - """ - game_manager.end(group_id, self.module) - - def set_status(self, group_id: int, status: int): - """ - 说明: - 设置游戏状态,根据状态发送不同的提示消息 msg_data - 参数: - :param group_id: 群号 - :param status: 状态码 - """ - game_manager.set_status(group_id, self.module, status) diff --git a/utils/image_template.py b/utils/image_template.py deleted file mode 100644 index caa93698..00000000 --- a/utils/image_template.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Tuple, List, Literal - -from utils.image_utils import BuildImage, text2image - - -async def help_template(title: str, usage: BuildImage) -> BuildImage: - """ - 说明: - 生成单个功能帮助模板 - 参数: - :param title: 标题 - :param usage: 说明图片 - """ - title_image = BuildImage( - 0, - 0, - font_size=35, - plain_text=title, - font_color=(255, 255, 255), - font="CJGaoDeGuo.otf", - ) - background_image = BuildImage( - max(title_image.w, usage.w) + 50, - max(title_image.h, usage.h) + 100, - color=(114, 138, 204), - ) - await background_image.apaste(usage, (25, 80), True) - await background_image.apaste(title_image, (25, 20), True) - await background_image.aline( - (25, title_image.h + 22, 25 + title_image.w, title_image.h + 22), - (204, 196, 151), - 3, - ) - return background_image - diff --git a/utils/image_utils.py b/utils/image_utils.py deleted file mode 100755 index d312f62f..00000000 --- a/utils/image_utils.py +++ /dev/null @@ -1,1778 +0,0 @@ -import asyncio -import base64 -import os -import random -import re -import uuid -from io import BytesIO -from math import ceil -from pathlib import Path -from typing import Any, Awaitable, Callable, List, Literal, Optional, Tuple, Union - -import cv2 -import imagehash -from imagehash import ImageHash -from matplotlib import pyplot as plt -from nonebot.utils import is_coroutine_callable -from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont -from PIL.ImageFont import FreeTypeFont - -from configs.path_config import FONT_PATH, IMAGE_PATH -from services import logger - -ImageFile.LOAD_TRUNCATED_IMAGES = True -Image.MAX_IMAGE_PIXELS = None - - -ModeType = Literal[ - "1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr" -] - - -def compare_image_with_hash( - image_file1: str, image_file2: str, max_dif: float = 1.5 -) -> bool: - """ - 说明: - 比较两张图片的hash值是否相同 - 参数: - :param image_file1: 图片文件路径 - :param image_file2: 图片文件路径 - :param max_dif: 允许最大hash差值, 越小越精确,最小为0 - """ - ImageFile.LOAD_TRUNCATED_IMAGES = True - hash_1 = get_img_hash(image_file1) - hash_2 = get_img_hash(image_file2) - dif = hash_1 - hash_2 - if dif < 0: - dif = -dif - if dif <= max_dif: - return True - else: - return False - - -def get_img_hash(image_file: Union[str, Path]) -> ImageHash: - """ - 说明: - 获取图片的hash值 - 参数: - :param image_file: 图片文件路径 - """ - with open(image_file, "rb") as fp: - hash_value = imagehash.average_hash(Image.open(fp)) - return hash_value - - -def compressed_image( - in_file: Union[str, Path], - out_file: Optional[Union[str, Path]] = None, - ratio: float = 0.9, -): - """ - 说明: - 压缩图片 - 参数: - :param in_file: 被压缩的文件路径 - :param out_file: 压缩后输出的文件路径 - :param ratio: 压缩率,宽高 * 压缩率 - """ - in_file = IMAGE_PATH / in_file if isinstance(in_file, str) else in_file - if out_file: - out_file = IMAGE_PATH / out_file if isinstance(out_file, str) else out_file - else: - out_file = in_file - h, w, d = cv2.imread(str(in_file.absolute())).shape - img = cv2.resize( - cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio)) - ) - cv2.imwrite(str(out_file.absolute()), img) - - -def alpha2white_pil(pic: Image) -> Image: - """ - 说明: - 将图片透明背景转化为白色 - 参数: - :param pic: 通过PIL打开的图片文件 - """ - img = pic.convert("RGBA") - width, height = img.size - for yh in range(height): - for xw in range(width): - dot = (xw, yh) - color_d = img.getpixel(dot) - if color_d[3] == 0: - color_d = (255, 255, 255, 255) - img.putpixel(dot, color_d) - return img - - -def pic2b64(pic: Image) -> str: - """ - 说明: - PIL图片转base64 - 参数: - :param pic: 通过PIL打开的图片文件 - """ - buf = BytesIO() - pic.save(buf, format="PNG") - base64_str = base64.b64encode(buf.getvalue()).decode() - return "base64://" + base64_str - - -def fig2b64(plt_: plt) -> str: - """ - 说明: - matplotlib图片转base64 - 参数: - :param plt_: matplotlib生成的图片 - """ - buf = BytesIO() - plt_.savefig(buf, format="PNG", dpi=100) - base64_str = base64.b64encode(buf.getvalue()).decode() - return "base64://" + base64_str - - -def is_valid(file: Union[str, Path]) -> bool: - """ - 说明: - 判断图片是否损坏 - 参数: - :param file: 图片文件路径 - """ - valid = True - try: - Image.open(file).load() - except OSError: - valid = False - return valid - - -class BuildImage: - """ - 快捷生成图片与操作图片的工具类 - """ - - def __init__( - self, - w: int, - h: int, - paste_image_width: int = 0, - paste_image_height: int = 0, - paste_space: int = 0, - color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] = None, - image_mode: ModeType = "RGBA", - font_size: int = 10, - background: Union[Optional[str], BytesIO, Path] = None, - font: str = "yz.ttf", - ratio: float = 1, - is_alpha: bool = False, - plain_text: Optional[str] = None, - font_color: Optional[Union[str, Tuple[int, int, int]]] = None, - **kwargs, - ): - """ - 参数: - :param w: 自定义图片的宽度,w=0时为图片原本宽度 - :param h: 自定义图片的高度,h=0时为图片原本高度 - :param paste_image_width: 当图片做为背景图时,设置贴图的宽度,用于贴图自动换行 - :param paste_image_height: 当图片做为背景图时,设置贴图的高度,用于贴图自动换行 - :param paste_space: 自动贴图间隔 - :param color: 生成图片的颜色 - :param image_mode: 图片的类型 - :param font_size: 文字大小 - :param background: 打开图片的路径 - :param font: 字体,默认在 resource/ttf/ 路径下 - :param ratio: 倍率压缩 - :param is_alpha: 是否背景透明 - :param plain_text: 纯文字文本 - """ - self.w = int(w) - self.h = int(h) - self.paste_image_width = int(paste_image_width) - self.paste_image_height = int(paste_image_height) - self.paste_space = int(paste_space) - self._current_w = 0 - self._current_h = 0 - self.uid = uuid.uuid1() - self.font_name = font - self.font_size = font_size - self.font = ImageFont.truetype(str(FONT_PATH / font), int(font_size)) - if not plain_text and not color: - color = (255, 255, 255) - self.background = background - if not background: - if plain_text: - if not color: - color = (255, 255, 255, 0) - ttf_w, ttf_h = self.getsize(str(plain_text)) - self.w = self.w if self.w > ttf_w else ttf_w - self.h = self.h if self.h > ttf_h else ttf_h - self.markImg = Image.new(image_mode, (self.w, self.h), color) - self.markImg.convert(image_mode) - else: - if not w and not h: - self.markImg = Image.open(background) - w, h = self.markImg.size - if ratio and ratio > 0 and ratio != 1: - self.w = int(ratio * w) - self.h = int(ratio * h) - self.markImg = self.markImg.resize( - (self.w, self.h), Image.ANTIALIAS - ) - else: - self.w = w - self.h = h - else: - self.markImg = Image.open(background).resize( - (self.w, self.h), Image.ANTIALIAS - ) - if is_alpha: - try: - if array := self.markImg.load(): - for i in range(w): - for j in range(h): - pos = array[i, j] - is_edit = sum([1 for x in pos[0:3] if x > 240]) == 3 - if is_edit: - array[i, j] = (255, 255, 255, 0) - except Exception as e: - logger.warning(f"背景透明化发生错误..{type(e)}:{e}") - self.draw = ImageDraw.Draw(self.markImg) - self.size = self.w, self.h - if plain_text: - fill = font_color if font_color else (0, 0, 0) - self.text((0, 0), str(plain_text), fill) - try: - self.loop = asyncio.get_event_loop() - except RuntimeError: - new_loop = asyncio.new_event_loop() - asyncio.set_event_loop(new_loop) - self.loop = asyncio.get_event_loop() - - @classmethod - def load_font(cls, font: str, font_size: Optional[int]) -> FreeTypeFont: - """ - 说明: - 加载字体 - 参数: - :param font: 字体名称 - :param font_size: 字体大小 - """ - return ImageFont.truetype(str(FONT_PATH / font), font_size or cls.font_size) - - async def apaste( - self, - img: "BuildImage" or Image, - pos: Optional[Tuple[int, int]] = None, - alpha: bool = False, - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - allow_negative: bool = False, - ): - """ - 说明: - 异步 贴图 - 参数: - :param img: 已打开的图片文件,可以为 BuildImage 或 Image - :param pos: 贴图位置(左上角) - :param alpha: 图片背景是否为透明 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param allow_negative: 允许使用负数作为坐标且不超出图片范围,从右侧开始计算 - """ - await self.loop.run_in_executor( - None, self.paste, img, pos, alpha, center_type, allow_negative - ) - - def paste( - self, - img: "BuildImage", - pos: Optional[Tuple[int, int]] = None, - alpha: bool = False, - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - allow_negative: bool = False, - ): - """ - 说明: - 贴图 - 参数: - :param img: 已打开的图片文件,可以为 BuildImage 或 Image - :param pos: 贴图位置(左上角) - :param alpha: 图片背景是否为透明 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param allow_negative: 允许使用负数作为坐标且不超出图片范围,从右侧开始计算 - """ - if center_type: - if center_type not in ["center", "by_height", "by_width"]: - raise ValueError( - "center_type must be 'center', 'by_width' or 'by_height'" - ) - width, height = 0, 0 - if not pos: - pos = (0, 0) - if center_type == "center": - width = int((self.w - img.w) / 2) - height = int((self.h - img.h) / 2) - elif center_type == "by_width": - width = int((self.w - img.w) / 2) - height = pos[1] - elif center_type == "by_height": - width = pos[0] - height = int((self.h - img.h) / 2) - pos = (width, height) - if pos and allow_negative: - if pos[0] < 0: - pos = (self.w + pos[0], pos[1]) - if pos[1] < 0: - pos = (pos[0], self.h + pos[1]) - if isinstance(img, BuildImage): - img = img.markImg - if self._current_w >= self.w: - self._current_w = 0 - self._current_h += self.paste_image_height + self.paste_space - if not pos: - pos = (self._current_w, self._current_h) - if alpha: - try: - self.markImg.paste(img, pos, img) - except ValueError: - img = img.convert("RGBA") - self.markImg.paste(img, pos, img) - else: - self.markImg.paste(img, pos) - self._current_w += self.paste_image_width + self.paste_space - - @classmethod - def get_text_size(cls, msg: str, font: str, font_size: int) -> Tuple[int, int]: - """ - 说明: - 获取文字在该图片 font_size 下所需要的空间 - 参数: - :param msg: 文字内容 - :param font: 字体 - :param font_size: 字体大小 - """ - font_ = cls.load_font(font, font_size) - return font_.getsize(msg) # type: ignore - - def getsize(self, msg: Any) -> Tuple[int, int]: - """ - 说明: - 获取文字在该图片 font_size 下所需要的空间 - 参数: - :param msg: 文字内容 - """ - return self.font.getsize(str(msg)) # type: ignore - - async def apoint( - self, pos: Tuple[int, int], fill: Optional[Tuple[int, int, int]] = None - ): - """ - 说明: - 异步 绘制多个或单独的像素 - 参数: - :param pos: 坐标 - :param fill: 填错颜色 - """ - await self.loop.run_in_executor(None, self.point, pos, fill) - - def point(self, pos: Tuple[int, int], fill: Optional[Tuple[int, int, int]] = None): - """ - 说明: - 绘制多个或单独的像素 - 参数: - :param pos: 坐标 - :param fill: 填错颜色 - """ - self.draw.point(pos, fill=fill) - - async def aellipse( - self, - pos: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[Tuple[int, int, int]] = None, - width: int = 1, - ): - """ - 说明: - 异步 绘制圆 - 参数: - :param pos: 坐标范围 - :param fill: 填充颜色 - :param outline: 描线颜色 - :param width: 描线宽度 - """ - await self.loop.run_in_executor(None, self.ellipse, pos, fill, outline, width) - - def ellipse( - self, - pos: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[Tuple[int, int, int]] = None, - width: int = 1, - ): - """ - 说明: - 绘制圆 - 参数: - :param pos: 坐标范围 - :param fill: 填充颜色 - :param outline: 描线颜色 - :param width: 描线宽度 - """ - self.draw.ellipse(pos, fill, outline, width) - - async def atext( - self, - pos: Union[Tuple[int, int], Tuple[float, float]], - text: str, - fill: Union[str, Tuple[int, int, int]] = (0, 0, 0), - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - font: Optional[Union[FreeTypeFont, str]] = None, - font_size: Optional[int] = None, - **kwargs, - ): - """ - 说明: - 异步 在图片上添加文字 - 参数: - :param pos: 文字位置 - :param text: 文字内容 - :param fill: 文字颜色 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param font: 字体 - :param font_size: 字体大小 - """ - await self.loop.run_in_executor( - None, self.text, pos, text, fill, center_type, font, font_size, **kwargs - ) - - def text( - self, - pos: Union[Tuple[int, int], Tuple[float, float]], - text: str, - fill: Union[str, Tuple[int, int, int]] = (0, 0, 0), - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - font: Optional[Union[FreeTypeFont, str]] = None, - font_size: Optional[int] = None, - **kwargs, - ): - """ - 说明: - 在图片上添加文字 - 参数: - :param pos: 文字位置(使用center_type中的center后会失效,使用by_width后x失效,使用by_height后y失效) - :param text: 文字内容 - :param fill: 文字颜色 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param font: 字体 - :param font_size: 字体大小 - """ - if center_type: - if center_type not in ["center", "by_height", "by_width"]: - raise ValueError( - "center_type must be 'center', 'by_width' or 'by_height'" - ) - w, h = self.w, self.h - longgest_text = "" - sentence = text.split("\n") - for x in sentence: - longgest_text = x if len(x) > len(longgest_text) else longgest_text - ttf_w, ttf_h = self.getsize(longgest_text) - ttf_h = ttf_h * len(sentence) - if center_type == "center": - w = int((w - ttf_w) / 2) - h = int((h - ttf_h) / 2) - elif center_type == "by_width": - w = int((w - ttf_w) / 2) - h = pos[1] - elif center_type == "by_height": - h = int((h - ttf_h) / 2) - w = pos[0] - pos = (w, h) - if font: - if isinstance(font, str): - font = self.load_font(font, font_size) - elif font_size: - font = self.load_font(self.font_name, font_size) - self.draw.text(pos, text, fill=fill, font=font or self.font, **kwargs) - - async def asave(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 异步 保存图片 - 参数: - :param path: 图片路径 - """ - await self.loop.run_in_executor(None, self.save, path) - - def save(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 保存图片 - 参数: - :param path: 图片路径 - """ - self.markImg.save(path or self.background) # type: ignore - - def show(self): - """ - 说明: - 显示图片 - """ - self.markImg.show() - - async def aresize(self, ratio: float = 0, w: int = 0, h: int = 0): - """ - 说明: - 异步 压缩图片 - 参数: - :param ratio: 压缩倍率 - :param w: 压缩图片宽度至 w - :param h: 压缩图片高度至 h - """ - await self.loop.run_in_executor(None, self.resize, ratio, w, h) - - def resize(self, ratio: float = 0, w: int = 0, h: int = 0): - """ - 说明: - 压缩图片 - 参数: - :param ratio: 压缩倍率 - :param w: 压缩图片宽度至 w - :param h: 压缩图片高度至 h - """ - if not w and not h and not ratio: - raise Exception("缺少参数...") - if not w and not h and ratio: - w = int(self.w * ratio) - h = int(self.h * ratio) - self.markImg = self.markImg.resize((w, h), Image.ANTIALIAS) - self.w, self.h = self.markImg.size - self.size = self.w, self.h - self.draw = ImageDraw.Draw(self.markImg) - - async def acrop(self, box: Tuple[int, int, int, int]): - """ - 说明: - 异步 裁剪图片 - 参数: - :param box: 左上角坐标,右下角坐标 (left, upper, right, lower) - """ - await self.loop.run_in_executor(None, self.crop, box) - - def crop(self, box: Tuple[int, int, int, int]): - """ - 说明: - 裁剪图片 - 参数: - :param box: 左上角坐标,右下角坐标 (left, upper, right, lower) - """ - self.markImg = self.markImg.crop(box) - self.w, self.h = self.markImg.size - self.size = self.w, self.h - self.draw = ImageDraw.Draw(self.markImg) - - def check_font_size(self, word: str) -> bool: - """ - 说明: - 检查文本所需宽度是否大于图片宽度 - 参数: - :param word: 文本内容 - """ - return self.font.getsize(word)[0] > self.w - - async def atransparent(self, alpha_ratio: float = 1, n: int = 0): - """ - 说明: - 异步 图片透明化 - 参数: - :param alpha_ratio: 透明化程度 - :param n: 透明化大小内边距 - """ - await self.loop.run_in_executor(None, self.transparent, alpha_ratio, n) - - def transparent(self, alpha_ratio: float = 1, n: int = 0): - """ - 说明: - 图片透明化 - 参数: - :param alpha_ratio: 透明化程度 - :param n: 透明化大小内边距 - """ - self.markImg = self.markImg.convert("RGBA") - x, y = self.markImg.size - for i in range(n, x - n): - for k in range(n, y - n): - color = self.markImg.getpixel((i, k)) - color = color[:-1] + (int(100 * alpha_ratio),) - self.markImg.putpixel((i, k), color) - self.draw = ImageDraw.Draw(self.markImg) - - def pic2bs4(self) -> str: - """ - 说明: - BuildImage 转 base64 - """ - buf = BytesIO() - self.markImg.save(buf, format="PNG") - base64_str = base64.b64encode(buf.getvalue()).decode() - return "base64://" + base64_str - - def convert(self, type_: ModeType): - """ - 说明: - 修改图片类型 - 参数: - :param type_: 类型 - """ - self.markImg = self.markImg.convert(type_) - - async def arectangle( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[str] = None, - width: int = 1, - ): - """ - 说明: - 异步 画框 - 参数: - :param xy: 坐标 - :param fill: 填充颜色 - :param outline: 轮廓颜色 - :param width: 线宽 - """ - await self.loop.run_in_executor(None, self.rectangle, xy, fill, outline, width) - - def rectangle( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[str] = None, - width: int = 1, - ): - """ - 说明: - 画框 - 参数: - :param xy: 坐标 - :param fill: 填充颜色 - :param outline: 轮廓颜色 - :param width: 线宽 - """ - self.draw.rectangle(xy, fill, outline, width) - - async def apolygon( - self, - xy: List[Tuple[int, int]], - fill: Tuple[int, int, int] = (0, 0, 0), - outline: int = 1, - ): - """ - 说明: - 异步 画多边形 - 参数: - :param xy: 坐标 - :param fill: 颜色 - :param outline: 线宽 - """ - await self.loop.run_in_executor(None, self.polygon, xy, fill, outline) - - def polygon( - self, - xy: List[Tuple[int, int]], - fill: Tuple[int, int, int] = (0, 0, 0), - outline: int = 1, - ): - """ - 说明: - 画多边形 - 参数: - :param xy: 坐标 - :param fill: 颜色 - :param outline: 线宽 - """ - self.draw.polygon(xy, fill, outline) - - async def aline( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Union[str, Tuple[int, int, int]]] = None, - width: int = 1, - ): - """ - 说明: - 异步 画线 - 参数: - :param xy: 坐标 - :param fill: 填充 - :param width: 线宽 - """ - await self.loop.run_in_executor(None, self.line, xy, fill, width) - - def line( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Union[Tuple[int, int, int], str]] = None, - width: int = 1, - ): - """ - 说明: - 画线 - 参数: - :param xy: 坐标 - :param fill: 填充 - :param width: 线宽 - """ - self.draw.line(xy, fill, width) - - async def acircle(self): - """ - 说明: - 异步 将 BuildImage 图片变为圆形 - """ - await self.loop.run_in_executor(None, self.circle) - - def circle(self): - """ - 说明: - 使图像变圆 - """ - self.markImg.convert("RGBA") - size = self.markImg.size - r2 = min(size[0], size[1]) - if size[0] != size[1]: - self.markImg = self.markImg.resize((r2, r2), Image.ANTIALIAS) - width = 1 - antialias = 4 - ellipse_box = [0, 0, r2 - 2, r2 - 2] - mask = Image.new( - size=[int(dim * antialias) for dim in self.markImg.size], - mode="L", - color="black", - ) - draw = ImageDraw.Draw(mask) - for offset, fill in (width / -2.0, "black"), (width / 2.0, "white"): - left, top = [(value + offset) * antialias for value in ellipse_box[:2]] - right, bottom = [(value - offset) * antialias for value in ellipse_box[2:]] - draw.ellipse([left, top, right, bottom], fill=fill) - mask = mask.resize(self.markImg.size, Image.LANCZOS) - try: - self.markImg.putalpha(mask) - except ValueError: - pass - - async def acircle_corner( - self, - radii: int = 30, - point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], - ): - """ - 说明: - 异步 矩形四角变圆 - 参数: - :param radii: 半径 - :param point_list: 需要变化的角 - """ - await self.loop.run_in_executor(None, self.circle_corner, radii, point_list) - - def circle_corner( - self, - radii: int = 30, - point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], - ): - """ - 说明: - 矩形四角变圆 - 参数: - :param radii: 半径 - :param point_list: 需要变化的角 - """ - # 画圆(用于分离4个角) - img = self.markImg.convert("RGBA") - alpha = img.split()[-1] - circle = Image.new("L", (radii * 2, radii * 2), 0) - draw = ImageDraw.Draw(circle) - draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 - w, h = img.size - if "lt" in point_list: - alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) - if "rt" in point_list: - alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) - if "lb" in point_list: - alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) - if "rb" in point_list: - alpha.paste( - circle.crop((radii, radii, radii * 2, radii * 2)), - (w - radii, h - radii), - ) - img.putalpha(alpha) - self.markImg = img - self.draw = ImageDraw.Draw(self.markImg) - - async def arotate(self, angle: int, expand: bool = False): - """ - 说明: - 异步 旋转图片 - 参数: - :param angle: 角度 - :param expand: 放大图片适应角度 - """ - await self.loop.run_in_executor(None, self.rotate, angle, expand) - - def rotate(self, angle: int, expand: bool = False): - """ - 说明: - 旋转图片 - 参数: - :param angle: 角度 - :param expand: 放大图片适应角度 - """ - self.markImg = self.markImg.rotate(angle, expand=expand) - - async def atranspose(self, angle: Literal[0, 1, 2, 3, 4, 5, 6]): - """ - 说明: - 异步 旋转图片(包括边框) - 参数: - :param angle: 角度 - """ - await self.loop.run_in_executor(None, self.transpose, angle) - - def transpose(self, angle: Literal[0, 1, 2, 3, 4, 5, 6]): - """ - 说明: - 旋转图片(包括边框) - 参数: - :param angle: 角度 - """ - self.markImg.transpose(angle) - - async def afilter(self, filter_: str, aud: Optional[int] = None): - """ - 说明: - 异步 图片变化 - 参数: - :param filter_: 变化效果 - :param aud: 利率 - """ - await self.loop.run_in_executor(None, self.filter, filter_, aud) - - def filter(self, filter_: str, aud: Optional[int] = None): - """ - 说明: - 图片变化 - 参数: - :param filter_: 变化效果 - :param aud: 利率 - """ - _x = None - if filter_ == "GaussianBlur": # 高斯模糊 - _x = ImageFilter.GaussianBlur - elif filter_ == "EDGE_ENHANCE": # 锐化效果 - _x = ImageFilter.EDGE_ENHANCE - elif filter_ == "BLUR": # 模糊效果 - _x = ImageFilter.BLUR - elif filter_ == "CONTOUR": # 铅笔滤镜 - _x = ImageFilter.CONTOUR - elif filter_ == "FIND_EDGES": # 边缘检测 - _x = ImageFilter.FIND_EDGES - if _x: - if aud: - self.markImg = self.markImg.filter(_x(aud)) - else: - self.markImg = self.markImg.filter(_x) - self.draw = ImageDraw.Draw(self.markImg) - - async def areplace_color_tran( - self, - src_color: Union[ - Tuple[int, int, int], Tuple[Tuple[int, int, int], Tuple[int, int, int]] - ], - replace_color: Tuple[int, int, int], - ): - """ - 说明: - 异步 颜色替换 - 参数: - :param src_color: 目标颜色,或者使用列表,设置阈值 - :param replace_color: 替换颜色 - """ - self.loop.run_in_executor( - None, self.replace_color_tran, src_color, replace_color - ) - - def replace_color_tran( - self, - src_color: Union[ - Tuple[int, int, int], Tuple[Tuple[int, int, int], Tuple[int, int, int]] - ], - replace_color: Tuple[int, int, int], - ): - """ - 说明: - 颜色替换 - 参数: - :param src_color: 目标颜色,或者使用元祖,设置阈值 - :param replace_color: 替换颜色 - """ - if isinstance(src_color, tuple): - start_ = src_color[0] - end_ = src_color[1] - else: - start_ = src_color - end_ = None - for i in range(self.w): - for j in range(self.h): - r, g, b = self.markImg.getpixel((i, j)) - if not end_: - if r == start_[0] and g == start_[1] and b == start_[2]: - self.markImg.putpixel((i, j), replace_color) - else: - if ( - start_[0] <= r <= end_[0] - and start_[1] <= g <= end_[1] - and start_[2] <= b <= end_[2] - ): - self.markImg.putpixel((i, j), replace_color) - - # - def getchannel(self, type_): - self.markImg = self.markImg.getchannel(type_) - - -class BuildMat: - """ - 针对 折线图/柱状图,基于 BuildImage 编写的 非常难用的 自定义画图工具 - 目前仅支持 正整数 - """ - - def __init__( - self, - y: List[int], - mat_type: str = "line", - *, - x_name: Optional[str] = None, - y_name: Optional[str] = None, - x_index: List[Union[str, int, float]] = None, - y_index: List[Union[str, int, float]] = None, - x_min_spacing: Optional[int] = None, - x_rotate: int = 0, - title: Optional[str] = None, - size: Tuple[int, int] = (1000, 1000), - font: str = "msyh.ttf", - font_size: Optional[int] = None, - display_num: bool = False, - is_grid: bool = False, - background: Optional[List[str]] = None, - background_filler_type: Optional[str] = "center", - bar_color: Optional[List[Union[str, Tuple[int, int, int]]]] = None, - ): - """ - 说明: - 初始化 BuildMat - 参数: - :param y: 坐标值 - :param mat_type: 图像类型 可能的值:[line]: 折线图,[bar]: 柱状图,[barh]: 横向柱状图 - :param x_name: 横坐标名称 - :param y_name: 纵坐标名称 - :param x_index: 横坐标值 - :param y_index: 纵坐标值 - :param x_min_spacing: x轴最小间距 - :param x_rotate: 横坐标旋转角度 - :param title: 标题 - :param size: 图像大小,建议默认 - :param font: 字体 - :param font_size: 字体大小,建议默认 - :param display_num: 是否显示数值 - :param is_grid: 是否添加栅格 - :param background: 背景图片 - :param background_filler_type: 图像填充类型 - :param bar_color: 柱状图颜色,位 ['*'] 时替换位彩虹随机色 - """ - self.mat_type = mat_type - self.markImg = None - self._check_value(y, y_index) - self.w = size[0] - self.h = size[1] - self.y = y - self.x_name = x_name - self.y_name = y_name - self.x_index = x_index - self.y_index = y_index - self.x_min_spacing = x_min_spacing - self.x_rotate = x_rotate - self.title = title - self.font = font - self.display_num = display_num - self.is_grid = is_grid - self.background = background - self.background_filler_type = background_filler_type - self.bar_color = bar_color if bar_color else [(0, 0, 0)] - self.size = size - self.padding_w = 120 - self.padding_h = 120 - self.line_length = 760 - self._deviation = 0.905 - self._color = {} - if not font_size: - self.font_size = int(25 * (1 - len(x_index) / 100)) - else: - self.font_size = font_size - if self.bar_color == ["*"]: - self.bar_color = [ - "#FF0000", - "#FF7F00", - "#FFFF00", - "#00FF00", - "#00FFFF", - "#0000FF", - "#8B00FF", - ] - if not x_index: - raise ValueError("缺少 x_index [横坐标值]...") - if x_min_spacing: - self._x_interval = x_min_spacing - else: - self._x_interval = int((self.line_length - 70) / len(x_index)) - self._bar_width = int(30 * (1 - (len(x_index) + 10) / 100)) - # 没有 y_index 时自动生成 - if not y_index: - _y_index = [] - _max_value = int(max(y)) - _max_value = ceil( - _max_value / eval("1" + "0" * (len(str(_max_value)) - 1)) - ) * eval("1" + "0" * (len(str(_max_value)) - 1)) - _max_value = _max_value if _max_value >= 10 else 100 - _step = int(_max_value / 10) - for i in range(_step, _max_value + _step, _step): - _y_index.append(i) - self.y_index = _y_index - self._p = self.line_length / max(self.y_index) - self._y_interval = int((self.line_length - 70) / len(self.y_index)) - - def gen_graph(self): - """ - 说明: - 生成图像 - """ - self.markImg = self._init_graph( - x_name=self.x_name, - y_name=self.y_name, - x_index=self.x_index, - y_index=self.y_index, - font_size=self.font_size, - is_grid=self.is_grid, - ) - if self.mat_type == "line": - self._gen_line_graph(y=self.y, display_num=self.display_num) - elif self.mat_type == "bar": - self._gen_bar_graph(y=self.y, display_num=self.display_num) - elif self.mat_type == "barh": - self._gen_bar_graph(y=self.y, display_num=self.display_num, is_barh=True) - - def set_y(self, y: List[int]): - """ - 说明: - 给坐标点设置新值 - 参数: - :param y: 坐标点 - """ - self._check_value(y, self.y_index) - self.y = y - - def set_y_index(self, y_index: List[Union[str, int, float]]): - """ - 说明: - 设置y轴坐标值 - 参数: - :param y_index: y轴坐标值 - """ - self._check_value(self.y, y_index) - self.y_index = y_index - - def set_title(self, title: str, color: Optional[Union[str, Tuple[int, int, int]]]): - """ - 说明: - 设置标题 - 参数: - :param title: 标题 - :param color: 字体颜色 - """ - self.title = title - if color: - self._color["title"] = color - - def set_background( - self, background: Optional[List[str]], type_: Optional[str] = None - ): - """ - 说明: - 设置背景图片 - 参数: - :param background: 图片路径列表 - :param type_: 填充类型 - """ - self.background = background - self.background_filler_type = type_ if type_ else self.background_filler_type - - def show(self): - """ - 说明: - 展示图像 - """ - self.markImg.show() - - def pic2bs4(self) -> str: - """ - 说明: - 转base64 - """ - return self.markImg.pic2bs4() - - def resize(self, ratio: float = 0.9): - """ - 说明: - 调整图像大小 - 参数: - :param ratio: 比例 - """ - self.markImg.resize(ratio) - - def save(self, path: Union[str, Path]): - """ - 说明: - 保存图片 - 参数: - :param path: 路径 - """ - self.markImg.save(path) - - def _check_value( - self, - y: List[int], - y_index: List[Union[str, int, float]] = None, - x_index: List[Union[str, int, float]] = None, - ): - """ - 说明: - 检查值合法性 - 参数: - :param y: 坐标值 - :param y_index: y轴坐标值 - :param x_index: x轴坐标值 - """ - if y_index: - _value = x_index if self.mat_type == "barh" else y_index - if max(y) > max(y_index): - raise ValueError("坐标点的值必须小于y轴坐标的最大值...") - i = -9999999999 - for y in y_index: - if y > i: - i = y - else: - raise ValueError("y轴坐标值必须有序...") - - def _gen_line_graph( - self, - y: List[Union[int, float]], - display_num: bool = False, - ): - """ - 说明: - 生成折线图 - 参数: - :param y: 坐标点 - :param display_num: 显示该点的值 - """ - _black_point = BuildImage(11, 11, color=random.choice(self.bar_color)) - _black_point.circle() - x_interval = self._x_interval - current_w = self.padding_w + x_interval - current_h = self.padding_h + self.line_length - for i in range(len(y)): - if display_num: - w = int(self.markImg.getsize(str(y[i]))[0] / 2) - self.markImg.text( - ( - current_w - w, - current_h - int(y[i] * self._p * self._deviation) - 25 - 5, - ), - f"{y[i]:.2f}" if isinstance(y[i], float) else f"{y[i]}", - ) - if i != len(y) - 1: - self.markImg.line( - ( - current_w, - current_h - int(y[i] * self._p * self._deviation), - current_w + x_interval, - current_h - int(y[i + 1] * self._p * self._deviation), - ), - fill=(0, 0, 0), - width=2, - ) - self.markImg.paste( - _black_point, - ( - current_w - 3, - current_h - int(y[i] * self._p * self._deviation) - 3, - ), - True, - ) - current_w += x_interval - - def _gen_bar_graph( - self, - y: List[Union[int, float]], - display_num: bool = False, - is_barh: bool = False, - ): - """ - 说明: - 生成柱状图 - 参数: - :param y: 坐标值 - :param display_num: 是否显示数值 - :param is_barh: 横柱状图 - """ - _interval = self._x_interval - if is_barh: - current_h = self.padding_h + self.line_length - _interval - current_w = self.padding_w - else: - current_w = self.padding_w + _interval - current_h = self.padding_h + self.line_length - for i in range(len(y)): - # 画出显示数字 - if display_num: - # 横柱状图 - if is_barh: - font_h = self.markImg.getsize(str(y[i]))[1] - self.markImg.text( - ( - self.padding_w - + int(y[i] * self._p * self._deviation) - + 2 - + 5, - current_h - int(font_h / 2) - 1, - ), - f"{y[i]:.2f}" if isinstance(y[i], float) else f"{y[i]}", - ) - else: - w = int(self.markImg.getsize(str(y[i]))[0] / 2) - self.markImg.text( - ( - current_w - w, - current_h - int(y[i] * self._p * self._deviation) - 25, - ), - f"{y[i]:.2f}" if isinstance(y[i], float) else f"{y[i]}", - ) - if i != len(y): - bar_color = random.choice(self.bar_color) - if is_barh: - A = BuildImage( - int(y[i] * self._p * self._deviation), - self._bar_width, - color=bar_color, - ) - self.markImg.paste( - A, - ( - current_w + 2, - current_h - int(self._bar_width / 2), - ), - ) - else: - A = BuildImage( - self._bar_width, - int(y[i] * self._p * self._deviation), - color=bar_color, - ) - self.markImg.paste( - A, - ( - current_w - int(self._bar_width / 2), - current_h - int(y[i] * self._p * self._deviation), - ), - ) - if is_barh: - current_h -= _interval - else: - current_w += _interval - - def _init_graph( - self, - x_name: Optional[str] = None, - y_name: Optional[str] = None, - x_index: List[Union[str, int, float]] = None, - y_index: List[Union[str, int, float]] = None, - font_size: Optional[int] = None, - is_grid: bool = False, - ) -> BuildImage: - """ - 说明: - 初始化图像,生成xy轴 - 参数: - :param x_name: x轴名称 - :param y_name: y轴名称 - :param x_index: x轴坐标值 - :param y_index: y轴坐标值 - :param is_grid: 添加栅格 - """ - padding_w = self.padding_w - padding_h = self.padding_h - line_length = self.line_length - background = random.choice(self.background) if self.background else None - if self.x_min_spacing: - length = (len(self.x_index) + 1) * self.x_min_spacing - if 2 * padding_w + length > self.w: - self.w = 2 * padding_w + length - background = None - A = BuildImage( - self.w, self.h, font_size=font_size, font=self.font, background=background - ) - if background: - _tmp = BuildImage(self.w, self.h) - _tmp.transparent(2) - A.paste(_tmp, alpha=True) - if self.title: - title = BuildImage( - 0, - 0, - plain_text=self.title, - color=(255, 255, 255, 0), - font_size=35, - font_color=self._color.get("title"), - font=self.font, - ) - A.paste(title, (0, 25), True, "by_width") - A.line( - ( - padding_w, - padding_h + line_length, - self.w - padding_w, - padding_h + line_length, - ), - (0, 0, 0), - 2, - ) - A.line( - ( - padding_w, - padding_h, - padding_w, - padding_h + line_length, - ), - (0, 0, 0), - 2, - ) - _interval = self._x_interval - if self.mat_type == "barh": - tmp = x_index - x_index = y_index - y_index = tmp - _interval = self._y_interval - current_w = padding_w + _interval - _text_font = BuildImage(0, 0, font_size=self.font_size, font=self.font) - _grid = self.line_length if is_grid else 10 - x_rotate_height = 0 - for _x in x_index: - _p = BuildImage(1, _grid, color="#a9a9a9") - A.paste(_p, (current_w, padding_h + line_length - _grid)) - w = int(_text_font.getsize(f"{_x}")[0] / 2) - text = BuildImage( - 0, - 0, - plain_text=f"{_x}", - font_size=self.font_size, - color=(255, 255, 255, 0), - font=self.font, - ) - text.rotate(self.x_rotate, True) - A.paste(text, (current_w - w, padding_h + line_length + 10), alpha=True) - current_w += _interval - x_rotate_height = text.h - _interval = self._x_interval if self.mat_type == "barh" else self._y_interval - current_h = padding_h + line_length - _interval - _text_font = BuildImage(0, 0, font_size=self.font_size, font=self.font) - for _y in y_index: - _p = BuildImage(_grid, 1, color="#a9a9a9") - A.paste(_p, (padding_w + 2, current_h)) - w, h = _text_font.getsize(f"{_y}") - h = int(h / 2) - text = BuildImage( - 0, - 0, - plain_text=f"{_y}", - font_size=self.font_size, - color=(255, 255, 255, 0), - font=self.font, - ) - idx = 0 - while text.size[0] > self.padding_w - 10 and idx < 3: - text = BuildImage( - 0, - 0, - plain_text=f"{_y}", - font_size=int(self.font_size * 0.75), - color=(255, 255, 255, 0), - font=self.font, - ) - w, _ = text.getsize(f"{_y}") - idx += 1 - A.paste(text, (padding_w - w - 10, current_h - h), alpha=True) - current_h -= _interval - if x_name: - A.text((int(padding_w / 2), int(padding_w / 2)), x_name) - if y_name: - A.text( - ( - int(padding_w + line_length + 50 - A.getsize(y_name)[0]), - int(padding_h + line_length + 50 + x_rotate_height), - ), - y_name, - ) - return A - - -async def text2image( - text: str, - auto_parse: bool = True, - font_size: int = 20, - color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] = "white", - font: str = "CJGaoDeGuo.otf", - font_color: Union[str, Tuple[int, int, int]] = "black", - padding: Union[int, Tuple[int, int, int, int]] = 0, - _add_height: float = 0, -) -> BuildImage: - """ - 说明: - 解析文本并转为图片 - 使用标签 - - 可选配置项 - font: str -> 特殊文本字体 - fs / font_size: int -> 特殊文本大小 - fc / font_color: Union[str, Tuple[int, int, int]] -> 特殊文本颜色 - 示例 - 在不在,HibiKi小姐, - 你最近还好吗,我非常想你,这段时间我非常不好过, - 抽卡抽不到金色,这让我很痛苦 - 参数: - :param text: 文本 - :param auto_parse: 是否自动解析,否则原样发送 - :param font_size: 普通字体大小 - :param color: 背景颜色 - :param font: 普通字体 - :param font_color: 普通字体颜色 - :param padding: 文本外边距,元组类型时为 (上,左,下,右) - :param _add_height: 由于get_size无法返回正确的高度,采用手动方式额外添加高度 - """ - pw = ph = top_padding = left_padding = 0 - if padding: - if isinstance(padding, int): - pw = padding * 2 - ph = padding * 2 - top_padding = left_padding = padding - elif isinstance(padding, tuple): - pw = padding[0] + padding[2] - ph = padding[1] + padding[3] - top_padding = padding[0] - left_padding = padding[1] - if auto_parse and re.search(r"(.*)", text): - _data = [] - new_text = "" - placeholder_index = 0 - for s in text.split(""): - r = re.search(r"(.*)", s) - if r: - start, end = r.span() - if start != 0 and (t := s[:start]): - new_text += t - _data.append( - [ - (start, end), - f"[placeholder_{placeholder_index}]", - r.group(1).strip(), - r.group(2), - ] - ) - new_text += f"[placeholder_{placeholder_index}]" - placeholder_index += 1 - new_text += text.split("")[-1] - image_list = [] - current_placeholder_index = 0 - # 切分换行,每行为单张图片 - for s in new_text.split("\n"): - _tmp_text = s - img_height = BuildImage(0, 0, font_size=font_size).getsize("正")[1] - img_width = 0 - _tmp_index = current_placeholder_index - for _ in range(s.count("[placeholder_")): - placeholder = _data[_tmp_index] - if "font_size" in placeholder[2]: - r = re.search(r"font_size=['\"]?(\d+)", placeholder[2]) - if r: - w, h = BuildImage(0, 0, font_size=int(r.group(1))).getsize( - placeholder[3] - ) - img_height = img_height if img_height > h else h - img_width += w - else: - img_width += BuildImage(0, 0, font_size=font_size).getsize( - placeholder[3] - )[0] - _tmp_text = _tmp_text.replace(f"[placeholder_{_tmp_index}]", "") - _tmp_index += 1 - img_width += BuildImage(0, 0, font_size=font_size).getsize(_tmp_text)[0] - # img_width += len(_tmp_text) * font_size - # 开始画图 - A = BuildImage( - img_width, img_height, color=color, font=font, font_size=font_size - ) - basic_font_h = A.getsize("正")[1] - current_width = 0 - # 遍历占位符 - for _ in range(s.count("[placeholder_")): - if not s.startswith(f"[placeholder_{current_placeholder_index}]"): - slice_ = s.split(f"[placeholder_{current_placeholder_index}]") - await A.atext( - (current_width, A.h - basic_font_h - 1), slice_[0], font_color - ) - current_width += A.getsize(slice_[0])[0] - placeholder = _data[current_placeholder_index] - # 解析配置 - _font = font - _font_size = font_size - _font_color = font_color - for e in placeholder[2].split(): - if e.startswith("font="): - _font = e.split("=")[-1] - if e.startswith("font_size=") or e.startswith("fs="): - _font_size = int(e.split("=")[-1]) - if _font_size > 1000: - _font_size = 1000 - if _font_size < 1: - _font_size = 1 - if e.startswith("font_color") or e.startswith("fc="): - _font_color = e.split("=")[-1] - text_img = BuildImage( - 0, - 0, - plain_text=placeholder[3], - font_size=_font_size, - font_color=_font_color, - font=_font, - ) - _img_h = ( - int(A.h / 2 - text_img.h / 2) - if new_text == "[placeholder_0]" - else A.h - text_img.h - ) - await A.apaste(text_img, (current_width, _img_h - 1), True) - current_width += text_img.w - s = s[ - s.index(f"[placeholder_{current_placeholder_index}]") - + len(f"[placeholder_{current_placeholder_index}]") : - ] - current_placeholder_index += 1 - if s: - slice_ = s.split(f"[placeholder_{current_placeholder_index}]") - await A.atext((current_width, A.h - basic_font_h), slice_[0]) - current_width += A.getsize(slice_[0])[0] - A.crop((0, 0, current_width, A.h)) - # A.show() - image_list.append(A) - height = 0 - width = 0 - for img in image_list: - height += img.h - width = width if width > img.w else img.w - width += pw - height += ph - A = BuildImage(width + left_padding, height + top_padding, color=color) - current_height = top_padding - for img in image_list: - await A.apaste(img, (left_padding, current_height), True) - current_height += img.h - else: - width = 0 - height = 0 - _tmp = BuildImage(0, 0, font=font, font_size=font_size) - _, h = _tmp.getsize("正") - line_height = int(font_size / 3) - image_list = [] - for x in text.split("\n"): - w, _ = _tmp.getsize(x.strip() or "正") - height += h + line_height - width = width if width > w else w - image_list.append( - BuildImage( - w, - h, - font=font, - font_size=font_size, - plain_text=x.strip(), - color=color, - ) - ) - width += pw - height += ph - A = BuildImage( - width + left_padding, - height + top_padding + 2, - color=color, - ) - cur_h = ph - for img in image_list: - await A.apaste(img, (pw, cur_h), True) - cur_h += img.h + line_height - return A - - -def group_image(image_list: List[BuildImage]) -> Tuple[List[List[BuildImage]], int]: - """ - 说明: - 根据图片大小进行分组 - 参数: - :param image_list: 排序图片列表 - """ - image_list.sort(key=lambda x: x.h, reverse=True) - max_image = max(image_list, key=lambda x: x.h) - - image_list.remove(max_image) - max_h = max_image.h - total_w = 0 - - # 图片分组 - image_group = [[max_image]] - is_use = [] - surplus_list = image_list[:] - - for image in image_list: - if image.uid not in is_use: - group = [image] - is_use.append(image.uid) - curr_h = image.h - while True: - surplus_list = [x for x in surplus_list if x.uid not in is_use] - for tmp in surplus_list: - temp_h = curr_h + tmp.h + 10 - if temp_h < max_h or abs(max_h - temp_h) < 100: - curr_h += tmp.h + 15 - is_use.append(tmp.uid) - group.append(tmp) - break - else: - break - total_w += max([x.w for x in group]) + 15 - image_group.append(group) - while surplus_list: - surplus_list = [x for x in surplus_list if x.uid not in is_use] - if not surplus_list: - break - surplus_list.sort(key=lambda x: x.h, reverse=True) - for img in surplus_list: - if img.uid not in is_use: - _w = 0 - index = -1 - for i, ig in enumerate(image_group): - if s := sum([x.h for x in ig]) > _w: - _w = s - index = i - if index != -1: - image_group[index].append(img) - is_use.append(img.uid) - - max_h = 0 - max_w = 0 - for ig in image_group: - if (_h := sum([x.h + 15 for x in ig])) > max_h: - max_h = _h - max_w += max([x.w for x in ig]) + 30 - is_use.clear() - while abs(max_h - max_w) > 200 and len(image_group) - 1 >= len(image_group[-1]): - for img in image_group[-1]: - _min_h = 999999 - _min_index = -1 - for i, ig in enumerate(image_group): - # if i not in is_use and (_h := sum([x.h for x in ig]) + img.h) > _min_h: - if (_h := sum([x.h for x in ig]) + img.h) < _min_h: - _min_h = _h - _min_index = i - is_use.append(_min_index) - image_group[_min_index].append(img) - max_w -= max([x.w for x in image_group[-1]]) - 30 - image_group.pop(-1) - max_h = max([sum([x.h + 15 for x in ig]) for ig in image_group]) - return image_group, max(max_h + 250, max_w + 70) - - -async def build_sort_image( - image_group: List[List[BuildImage]], - h: Optional[int] = None, - padding_top: int = 200, - color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] = ( - 255, - 255, - 255, - ), - background_path: Optional[Path] = None, - background_handle: Callable[[BuildImage], Optional[Awaitable]] = None, -) -> BuildImage: - """ - 说明: - 对group_image的图片进行组装 - 参数: - :param image_group: 分组图片列表 - :param h: max(宽,高),一般为group_image的返回值,有值时,图片必定为正方形 - :param padding_top: 图像列表与最顶层间距 - :param color: 背景颜色 - :param background_path: 背景图片文件夹路径(随机) - :param background_handle: 背景图额外操作 - """ - bk_file = None - if background_path: - random_bk = os.listdir(background_path) - if random_bk: - bk_file = random.choice(random_bk) - image_w = 0 - image_h = 0 - if not h: - for ig in image_group: - _w = max([x.w + 30 for x in ig]) - image_w += _w + 30 - _h = sum([x.h + 10 for x in ig]) - if _h > image_h: - image_h = _h - image_h += padding_top - else: - image_w = h - image_h = h - A = BuildImage( - image_w, - image_h, - font_size=24, - font="CJGaoDeGuo.otf", - color=color, - background=(background_path / bk_file) if bk_file else None, - ) - if background_handle: - if is_coroutine_callable(background_handle): - await background_handle(A) - else: - background_handle(A) - curr_w = 50 - for ig in image_group: - curr_h = padding_top - 20 - for img in ig: - await A.apaste(img, (curr_w, curr_h), True) - curr_h += img.h + 10 - curr_w += max([x.w for x in ig]) + 30 - return A - - -if __name__ == "__main__": - pass diff --git a/utils/langconv.py b/utils/langconv.py deleted file mode 100755 index 4977161b..00000000 --- a/utils/langconv.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from copy import deepcopy -import re - -try: - import psyco - psyco.full() -except: - pass - -from .zh_wiki import zh2Hant, zh2Hans - -import sys -py3k = sys.version_info >= (3, 0, 0) - -if py3k: - UEMPTY = '' -else: - _zh2Hant, _zh2Hans = {}, {} - for old, new in ((zh2Hant, _zh2Hant), (zh2Hans, _zh2Hans)): - for k, v in old.items(): - new[k.decode('utf8')] = v.decode('utf8') - zh2Hant = _zh2Hant - zh2Hans = _zh2Hans - UEMPTY = ''.decode('utf8') - -# states -(START, END, FAIL, WAIT_TAIL) = list(range(4)) -# conditions -(TAIL, ERROR, MATCHED_SWITCH, UNMATCHED_SWITCH, CONNECTOR) = list(range(5)) - -MAPS = {} - -class Node(object): - def __init__(self, from_word, to_word=None, is_tail=True, - have_child=False): - self.from_word = from_word - if to_word is None: - self.to_word = from_word - self.data = (is_tail, have_child, from_word) - self.is_original = True - else: - self.to_word = to_word or from_word - self.data = (is_tail, have_child, to_word) - self.is_original = False - self.is_tail = is_tail - self.have_child = have_child - - def is_original_long_word(self): - return self.is_original and len(self.from_word)>1 - - def is_follow(self, chars): - return chars != self.from_word[:-1] - - def __str__(self): - return '' % (repr(self.from_word), - repr(self.to_word), self.is_tail, self.have_child) - - __repr__ = __str__ - -class ConvertMap(object): - def __init__(self, name, mapping=None): - self.name = name - self._map = {} - if mapping: - self.set_convert_map(mapping) - - def set_convert_map(self, mapping): - convert_map = {} - have_child = {} - max_key_length = 0 - for key in sorted(mapping.keys()): - if len(key)>1: - for i in range(1, len(key)): - parent_key = key[:i] - have_child[parent_key] = True - have_child[key] = False - max_key_length = max(max_key_length, len(key)) - for key in sorted(have_child.keys()): - convert_map[key] = (key in mapping, have_child[key], - mapping.get(key, UEMPTY)) - self._map = convert_map - self.max_key_length = max_key_length - - def __getitem__(self, k): - try: - is_tail, have_child, to_word = self._map[k] - return Node(k, to_word, is_tail, have_child) - except: - return Node(k) - - def __contains__(self, k): - return k in self._map - - def __len__(self): - return len(self._map) - -class StatesMachineException(Exception): pass - -class StatesMachine(object): - def __init__(self): - self.state = START - self.final = UEMPTY - self.len = 0 - self.pool = UEMPTY - - def clone(self, pool): - new = deepcopy(self) - new.state = WAIT_TAIL - new.pool = pool - return new - - def feed(self, char, map): - node = map[self.pool+char] - - if node.have_child: - if node.is_tail: - if node.is_original: - cond = UNMATCHED_SWITCH - else: - cond = MATCHED_SWITCH - else: - cond = CONNECTOR - else: - if node.is_tail: - cond = TAIL - else: - cond = ERROR - - new = None - if cond == ERROR: - self.state = FAIL - elif cond == TAIL: - if self.state == WAIT_TAIL and node.is_original_long_word(): - self.state = FAIL - else: - self.final += node.to_word - self.len += 1 - self.pool = UEMPTY - self.state = END - elif self.state == START or self.state == WAIT_TAIL: - if cond == MATCHED_SWITCH: - new = self.clone(node.from_word) - self.final += node.to_word - self.len += 1 - self.state = END - self.pool = UEMPTY - elif cond == UNMATCHED_SWITCH or cond == CONNECTOR: - if self.state == START: - new = self.clone(node.from_word) - self.final += node.to_word - self.len += 1 - self.state = END - else: - if node.is_follow(self.pool): - self.state = FAIL - else: - self.pool = node.from_word - elif self.state == END: - # END is a new START - self.state = START - new = self.feed(char, map) - elif self.state == FAIL: - raise StatesMachineException('Translate States Machine ' - 'have error with input data %s' % node) - return new - - def __len__(self): - return self.len + 1 - - def __str__(self): - return '' % ( - id(self), self.pool, self.state, self.final) - __repr__ = __str__ - -class Converter(object): - def __init__(self, to_encoding): - self.to_encoding = to_encoding - self.map = MAPS[to_encoding] - self.start() - - def feed(self, char): - branches = [] - for fsm in self.machines: - new = fsm.feed(char, self.map) - if new: - branches.append(new) - if branches: - self.machines.extend(branches) - self.machines = [fsm for fsm in self.machines if fsm.state != FAIL] - all_ok = True - for fsm in self.machines: - if fsm.state != END: - all_ok = False - if all_ok: - self._clean() - return self.get_result() - - def _clean(self): - if len(self.machines): - self.machines.sort(key=lambda x: len(x)) - # self.machines.sort(cmp=lambda x,y: cmp(len(x), len(y))) - self.final += self.machines[0].final - self.machines = [StatesMachine()] - - def start(self): - self.machines = [StatesMachine()] - self.final = UEMPTY - - def end(self): - self.machines = [fsm for fsm in self.machines - if fsm.state == FAIL or fsm.state == END] - self._clean() - - def convert(self, string): - self.start() - for char in string: - self.feed(char) - self.end() - return self.get_result() - - def get_result(self): - return self.final - - -def registery(name, mapping): - global MAPS - MAPS[name] = ConvertMap(name, mapping) - -registery('zh-hant', zh2Hant) -registery('zh-hans', zh2Hans) -del zh2Hant, zh2Hans - - -def run(): - import sys - from optparse import OptionParser - parser = OptionParser() - parser.add_option('-e', type='string', dest='encoding', - help='encoding') - parser.add_option('-f', type='string', dest='file_in', - help='input file (- for stdin)') - parser.add_option('-t', type='string', dest='file_out', - help='output file') - (options, args) = parser.parse_args() - if not options.encoding: - parser.error('encoding must be set') - if options.file_in: - if options.file_in == '-': - file_in = sys.stdin - else: - file_in = open(options.file_in) - else: - file_in = sys.stdin - if options.file_out: - if options.file_out == '-': - file_out = sys.stdout - else: - file_out = open(options.file_out, 'wb') - else: - file_out = sys.stdout - - c = Converter(options.encoding) - for line in file_in: - # print >> file_out, c.convert(line.rstrip('\n').decode( - file_out.write(c.convert(line.rstrip('\n').decode( - 'utf8')).encode('utf8')) - - -if __name__ == '__main__': - run() - diff --git a/utils/manager/__init__.py b/utils/manager/__init__.py deleted file mode 100755 index a3dc5ab0..00000000 --- a/utils/manager/__init__.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Optional -from .group_manager import GroupManager -from .data_class import StaticData -from .plugin_data_manager import PluginDataManager -from .withdraw_message_manager import WithdrawMessageManager -from .plugins2cd_manager import Plugins2cdManager -from .plugins2block_manager import Plugins2blockManager -from .plugins2count_manager import Plugins2countManager -from .plugins2settings_manager import Plugins2settingsManager -from .plugins_manager import PluginsManager -from .resources_manager import ResourcesManager -from .admin_manager import AdminManager -from .none_plugin_count_manager import NonePluginCountManager -from .requests_manager import RequestManager -from configs.path_config import DATA_PATH - - -# 管理员命令管理器 -admin_manager = AdminManager() - -# 群功能开关 | 群被动技能 | 群权限 管理 -group_manager: GroupManager = GroupManager( - DATA_PATH / "manager" / "group_manager.json" -) - -# 撤回消息管理 -withdraw_message_manager: WithdrawMessageManager = WithdrawMessageManager() - -# 插件管理 -plugins_manager: PluginsManager = PluginsManager( - DATA_PATH / "manager" / "plugins_manager.json" -) - -# 插件基本设置管理 -plugins2settings_manager: Plugins2settingsManager = Plugins2settingsManager( - DATA_PATH / "configs" / "plugins2settings.yaml" -) - -# 插件命令 cd 管理 -plugins2cd_manager: Plugins2cdManager = Plugins2cdManager( - DATA_PATH / "configs" / "plugins2cd.yaml" -) - -# 插件命令 阻塞 管理 -plugins2block_manager: Plugins2blockManager = Plugins2blockManager( - DATA_PATH / "configs" / "plugins2block.yaml" -) - -# 插件命令 每次次数限制 管理 -plugins2count_manager: Plugins2countManager = Plugins2countManager( - DATA_PATH / "configs" / "plugins2count.yaml" -) - -# 资源管理 -resources_manager: ResourcesManager = ResourcesManager( - DATA_PATH / "manager" / "resources_manager.json" -) - -# 插件加载容忍管理 -none_plugin_count_manager: NonePluginCountManager = NonePluginCountManager( - DATA_PATH / "manager" / "none_plugin_count_manager.json" -) - -# 好友请求/群聊邀请 管理 -requests_manager: RequestManager = RequestManager( - DATA_PATH / "manager" / "requests_manager.json" -) - -# 全局插件数据 -plugin_data_manager: PluginDataManager = PluginDataManager() - diff --git a/utils/manager/admin_manager.py b/utils/manager/admin_manager.py deleted file mode 100644 index 9576582d..00000000 --- a/utils/manager/admin_manager.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Dict, List, Optional - -from utils.manager.data_class import StaticData - -from .models import AdminSetting - - -class AdminManager(StaticData): - """ - 管理员命令 管理器 - """ - - def __init__(self): - super().__init__(None) - self._data: Dict[str, AdminSetting] = {} - - def add_admin_plugin_settings(self, plugin: str, cmd: List[str], level: int): - """ - 说明: - 添加一个管理员命令 - 参数: - :param plugin: 模块 - :param cmd: 别名 - :param level: 等级 - """ - self._data[plugin] = AdminSetting(level=level, cmd=cmd) - - def set_admin_level(self, plugin: str, level: int): - """ - 说明: - 设置管理员命令等级 - 参数: - :param plugin: 模块名 - :param level: 权限等级 - """ - if plugin in self._data.keys(): - self._data[plugin].level = level - - def remove_admin_plugin_settings(self, plugin: str): - """ - 说明: - 删除一个管理员命令 - 参数: - :param plugin: 模块名 - """ - if plugin in self._data.keys(): - del self._data[plugin] - - def check(self, plugin: str, level: int) -> bool: - """ - 说明: - 检查是否满足权限 - 参数: - :param plugin: 模块名 - :param level: 权限等级 - """ - if plugin in self._data.keys(): - return level >= self._data[plugin].level - return True - - def get_plugin_level(self, plugin: str) -> int: - """ - 说明: - 获取插件权限 - 参数: - :param plugin: 模块名 - """ - if plugin in self._data.keys(): - return self._data[plugin].level - return 0 - - def get_plugin_module(self, cmd: str) -> Optional[str]: - """ - 说明: - 根据 cmd 获取功能 modules - 参数: - :param cmd: 命令 - """ - for key in self._data.keys(): - if data := self._data.get(key): - if data.cmd and cmd in data.cmd: - return key - return None diff --git a/utils/manager/configs_manager.py b/utils/manager/configs_manager.py deleted file mode 100644 index b6f106c2..00000000 --- a/utils/manager/configs_manager.py +++ /dev/null @@ -1,65 +0,0 @@ -# from typing import Optional, Any -# from .data_class import StaticData -# from pathlib import Path -# from ruamel.yaml import YAML -# -# yaml = YAML(typ="safe") -# -# -# class ConfigsManager(StaticData): -# """ -# 插件配置 与 资源 管理器 -# """ -# -# def __init__(self, file: Path): -# self.file = file -# super().__init__(file) -# self._resource_data = {} -# -# def add_plugin_config( -# self, -# modules: str, -# key: str, -# value: str, -# help_: Optional[str] = None, -# default_value: Optional[str] = None, -# ): -# """ -# 为插件添加一个配置 -# :param modules: 模块 -# :param key: 键 -# :param value: 值 -# :param help_: 配置注解 -# :param default_value: 默认值 -# """ -# if self._data.get(modules) is None: -# self._data[modules] = {} -# self._data[modules][key] = { -# "value": value, -# "help": help_, -# "default_value": default_value, -# } -# -# def remove_plugin_config(self, modules: str): -# """ -# 为插件删除一个配置 -# :param modules: 模块名 -# """ -# if modules in self._data.keys(): -# del self._data[modules] -# -# def get_config(self, modules: str, key: str) -> Optional[Any]: -# """ -# 获取指定配置值 -# :param modules: 模块名 -# :param key: 配置名称 -# """ -# if modules in self._data.keys(): -# if self._data[modules].get(key): -# if self._data[modules][key]["value"] is None: -# return self._data[modules][key]["default_value"] -# return self._data[modules][key]["value"] -# return None -# -# -# diff --git a/utils/manager/data_class.py b/utils/manager/data_class.py deleted file mode 100755 index b93a0ef5..00000000 --- a/utils/manager/data_class.py +++ /dev/null @@ -1,110 +0,0 @@ -import copy -from pathlib import Path -from typing import Any, Dict, Generic, NoReturn, Optional, TypeVar, Union - -import ujson as json -from ruamel import yaml -from ruamel.yaml import YAML - -from .models import * - -_yaml = YAML(typ="safe") - - -T = TypeVar("T") - - -class StaticData(Generic[T]): - """ - 静态数据共享类 - """ - - def __init__(self, file: Optional[Path], load_file: bool = True): - self._data: dict = {} - if file: - file.parent.mkdir(exist_ok=True, parents=True) - self.file = file - if file.exists() and load_file: - with open(file, "r", encoding="utf8") as f: - if file.name.endswith("json"): - try: - self._data: dict = json.load(f) - except ValueError: - if not f.read().strip(): - raise ValueError(f"{file} 文件加载错误,请检查文件内容格式.") - elif file.name.endswith("yaml"): - self._data = _yaml.load(f) - - def set(self, key, value): - self._data[key] = value - self.save() - - def set_module_data(self, module, key, value, auto_save: bool = True): - if module in self._data.keys(): - self._data[module][key] = value - if auto_save: - self.save() - - def get(self, key) -> T: - return self._data.get(key) - - def keys(self) -> List[str]: - return self._data.keys() - - def delete(self, key): - if self._data.get(key) is not None: - del self._data[key] - - def get_data(self) -> Dict[str, T]: - return copy.deepcopy(self._data) - - def dict(self) -> Dict[str, Any]: - temp = {} - for k, v in self._data.items(): - try: - temp[k] = v.dict() - except AttributeError: - temp[k] = copy.deepcopy(v) - return temp - - def save(self, path: Optional[Union[str, Path]] = None): - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - if path.name.endswith("yaml"): - yaml.dump( - self._data, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - else: - json.dump(self.dict(), f, ensure_ascii=False, indent=4) - - def reload(self): - if self.file.exists(): - if self.file.name.endswith("json"): - self._data: dict = json.load(open(self.file, "r", encoding="utf8")) - elif self.file.name.endswith("yaml"): - self._data: dict = _yaml.load(open(self.file, "r", encoding="utf8")) - - def is_exists(self) -> bool: - return self.file.exists() - - def is_empty(self) -> bool: - return bool(len(self._data)) - - def __str__(self) -> str: - return str(self._data) - - def __setitem__(self, key, value): - self._data[key] = value - - def __getitem__(self, key) -> T: - return self._data[key] - - def __len__(self) -> int: - return len(self._data) diff --git a/utils/manager/group_manager.py b/utils/manager/group_manager.py deleted file mode 100644 index 80f37337..00000000 --- a/utils/manager/group_manager.py +++ /dev/null @@ -1,443 +0,0 @@ -import copy -from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union - -import nonebot -import ujson as json - -from configs.config import Config -from utils.manager.data_class import StaticData -from utils.utils import get_matchers, is_number - -from .models import BaseData, BaseGroup - -Config.add_plugin_config( - "group_manager", "DEFAULT_GROUP_LEVEL", 5, help_="默认群权限", default_value=5, type=int -) - -Config.add_plugin_config( - "group_manager", - "DEFAULT_GROUP_BOT_STATUS", - True, - help_="默认进群总开关状态", - default_value=True, - type=bool, -) - - -def init_group(func: Callable): - """ - 说明: - 初始化群数据 - 参数: - :param func: func - """ - - def wrapper(*args, **kwargs): - self = args[0] - if arg_list := list(filter(lambda x: is_number(x), args[1:])): - group_id = str(arg_list[0]) - if self is not None and group_id and not self._data.group_manager.get(group_id): - self._data.group_manager[group_id] = BaseGroup() - self.save() - return func(*args, **kwargs) - - return wrapper - - -def init_task(func: Callable): - """ - 说明: - 初始化群被动 - 参数: - :param func: func - """ - - def wrapper(*args, **kwargs): - self = args[0] - group_id = str(args[1]) - task = args[2] if len(args) > 1 else None - if ( - group_id - and task - and self._data.group_manager[group_id].group_task_status.get(task) is None - ): - for task in self._data.task: - if ( - self._data.group_manager[group_id].group_task_status.get(task) - is None - ): - self._data.group_manager[group_id].group_task_status[ - task - ] = Config.get_config("_task", f"DEFAULT_{task}", default=True) - for task in list(self._data.group_manager[group_id].group_task_status): - if task not in self._data.task: - del self._data.group_manager[group_id].group_task_status[task] - self.save() - return func(*args, **kwargs) - - return wrapper - - -class GroupManager(StaticData[BaseData]): - """ - 群权限 | 功能 | 总开关 | 聊天时间 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._data: BaseData = ( - BaseData.parse_file(file) if file.exists() else BaseData() - ) - - def get_data(self) -> BaseData: - return copy.deepcopy(self._data) - - def block_plugin( - self, module: str, group_id: Union[str, int], is_save: bool = True - ): - """ - 说明: - 锁定插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组,None时为超级用户禁用 - :param is_save: 是否保存文件 - """ - self._set_plugin_status(module, "block", group_id, is_save) - - def unblock_plugin( - self, module: str, group_id: Union[str, int], is_save: bool = True - ): - """ - 说明: - 解锁插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - :param is_save: 是否保存文件 - """ - self._set_plugin_status(module, "unblock", group_id, is_save) - - def turn_on_group_bot_status(self, group_id: Union[str, int]): - """ - 说明: - 开启群bot开关 - 参数: - :param group_id: 群号 - """ - self._set_group_bot_status(group_id, True) - - def shutdown_group_bot_status(self, group_id: Union[str, int]): - """ - 说明: - 关闭群bot开关 - 参数: - :param group_id: 群号 - """ - self._set_group_bot_status(group_id, False) - - @init_group - def check_group_bot_status(self, group_id: Union[str, int]) -> bool: - """ - 说明: - 检查群聊bot总开关状态 - 参数: - :param group_id: 说明 - """ - return self._data.group_manager[str(group_id)].status - - @init_group - def set_group_level(self, group_id: Union[str, int], level: int): - """ - 说明: - 设置群权限 - 参数: - :param group_id: 群组 - :param level: 权限等级 - """ - self._data.group_manager[str(group_id)].level = level - self.save() - - @init_group - def get_plugin_status(self, module: str, group_id: Union[str, int]) -> bool: - """ - 说明: - 获取插件状态 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - """ - return module not in self._data.group_manager[str(group_id)].close_plugins - - def get_plugin_super_status(self, module: str, group_id: Union[str, int]) -> bool: - """ - 说明: - 获取插件是否被超级用户关闭 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - """ - return ( - f"{module}:super" - not in self._data.group_manager[str(group_id)].close_plugins - ) - - @init_group - def get_group_level(self, group_id: Union[str, int]) -> int: - """ - 说明: - 获取群等级 - 参数: - :param group_id: 群号 - """ - return self._data.group_manager[str(group_id)].level - - def check_group_is_white(self, group_id: Union[str, int]) -> bool: - """ - 说明: - 检测群聊是否在白名单 - 参数: - :param group_id: 群号 - """ - return str(group_id) in self._data.white_group - - def add_group_white_list(self, group_id: Union[str, int]): - """ - 说明: - 将群聊加入白名单 - 参数: - :param group_id: 群号 - """ - group_id = str(group_id) - if group_id not in self._data.white_group: - self._data.white_group.append(group_id) - - def delete_group_white_list(self, group_id: Union[str, int]): - """ - 说明: - 将群聊从白名单中删除 - 参数: - :param group_id: 群号 - """ - group_id = str(group_id) - if group_id in self._data.white_group: - self._data.white_group.remove(group_id) - - def get_group_white_list(self) -> List[str]: - """ - 说明: - 获取所有群白名单 - """ - return self._data.white_group - - def load_task(self): - """ - 说明: - 加载被动技能 - """ - for matcher in get_matchers(True): - _plugin = nonebot.plugin.get_plugin(matcher.plugin_name) # type: ignore - try: - _module = _plugin.module - plugin_task = _module.__getattribute__("__plugin_task__") - for key in plugin_task.keys(): - if key in self._data.task.keys(): - raise ValueError(f"plugin_task:{key} 已存在!") - self._data.task[key] = plugin_task[key] - except AttributeError: - pass - - @init_group - def delete_group(self, group_id: Union[str, int]): - """ - 说明: - 删除群配置 - 参数: - :param group_id: 群号 - """ - group_id = str(group_id) - if group_id in self._data.white_group: - self._data.white_group.remove(group_id) - self.save() - - def open_group_task(self, group_id: Union[str, int], task: str): - """ - 说明: - 开启群被动技能 - 参数: - :param group_id: 群号 - :param task: 被动技能名称 - """ - self._set_group_group_task_status(group_id, task, True) - - def close_global_task(self, task: str): - """ - 说明: - 关闭全局被动技能 - 参数: - :param task: 被动技能名称 - """ - if task not in self._data.close_task: - self._data.close_task.append(task) - - def open_global_task(self, task: str): - """ - 说明: - 开启全局被动技能 - 参数: - :param task: 被动技能名称 - """ - if task in self._data.close_task: - self._data.close_task.remove(task) - - def close_group_task(self, group_id: Union[str, int], task: str): - """ - 说明: - 关闭群被动技能 - 参数: - :param group_id: 群号 - :param task: 被动技能名称 - """ - self._set_group_group_task_status(group_id, task, False) - - def check_task_status(self, task: str, group_id: Optional[str] = None) -> bool: - """ - 说明: - 检查该被动状态 - 参数: - :param task: 被动技能名称 - :param group_id: 群号 - """ - if group_id: - return self.check_group_task_status( - group_id, task - ) and self.check_task_super_status(task) - return self.check_task_super_status(task) - - @init_group - @init_task - def check_group_task_status(self, group_id: Union[str, int], task: str) -> bool: - """ - 说明: - 查看群被动技能状态 - 参数: - :param group_id: 群号 - :param task: 被动技能名称 - """ - return self._data.group_manager[str(group_id)].group_task_status.get( - task, False - ) - - def check_task_super_status(self, task: str) -> bool: - """ - 说明: - 查看群被动技能状态(超级用户设置的状态) - 参数: - :param task: 被动技能名称 - """ - return task not in self._data.close_task - - def get_task_data(self) -> Dict[str, str]: - """ - 说明: - 获取所有被动任务 - """ - return self._data.task - - @init_group - @init_task - def group_group_task_status(self, group_id: Union[str, int]) -> str: - """ - 说明: - 查看群被全部动技能状态 - 参数: - :param group_id: 群号 - """ - x = "[群被动技能]:\n" - group_id = str(group_id) - for key in self._data.group_manager[group_id].group_task_status.keys(): - x += f'{self._data.task[key]}:{"√" if self.check_group_task_status(group_id, key) else "×"}\n' - return x[:-1] - - @init_group - @init_task - def _set_group_group_task_status( - self, group_id: Union[str, int], task: str, status: bool - ): - """ - 说明: - 管理群被动技能状态 - 参数: - :param group_id: 群号 - :param task: 被动技能 - :param status: 状态 - """ - self._data.group_manager[str(group_id)].group_task_status[task] = status - self.save() - - @init_group - def _set_plugin_status( - self, module: str, status: str, group_id: Union[str, int], is_save: bool - ): - """ - 说明: - 设置功能开关状态 - 参数: - :param module: 功能模块名 - :param status: 功能状态 - :param group_id: 群组 - :param is_save: 是否保存 - """ - group_id = str(group_id) - if status == "block": - if module not in self._data.group_manager[group_id].close_plugins: - self._data.group_manager[group_id].close_plugins.append(module) - else: - if module in self._data.group_manager[group_id].close_plugins: - self._data.group_manager[group_id].close_plugins.remove(module) - if is_save: - self.save() - - @init_group - def _set_group_bot_status(self, group_id: Union[int, str], status: bool): - """ - 说明: - 设置群聊bot总开关 - 参数: - :param group_id: 群号 - :param status: 开关状态 - """ - self._data.group_manager[str(group_id)].status = status - self.save() - - def reload(self): - if self.file.exists(): - t = self._data.task - self._data = BaseData.parse_file(self.file) - self._data.task = t - - def save(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 路径文件 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - dict_data = self._data.dict() - del dict_data["task"] - with open(path, "w", encoding="utf8") as f: - json.dump(dict_data, f, ensure_ascii=False, indent=4) - - def get(self, key: str, default: Any = None) -> BaseGroup: - return self._data.group_manager.get(key, default) - - def __setitem__(self, key, value): - self._data.group_manager[key] = value - - def __getitem__(self, key) -> BaseGroup: - return self._data.group_manager[key] diff --git a/utils/manager/models.py b/utils/manager/models.py deleted file mode 100644 index 60b93661..00000000 --- a/utils/manager/models.py +++ /dev/null @@ -1,150 +0,0 @@ -from enum import Enum -from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Tuple, Union - -from pydantic import BaseModel - -from configs.config import Config -from configs.utils import Config as zConfig - - -class AdminSetting(BaseModel): - """ - 管理员设置 - """ - - level: int = 5 - cmd: Optional[List[str]] - - -class BaseGroup(BaseModel): - """ - 基础群聊信息 - """ - - level: int = Config.get_config("group_manager", "DEFAULT_GROUP_LEVEL") # 群等级 - status: bool = Config.get_config( - "group_manager", "DEFAULT_GROUP_BOT_STATUS" - ) # 总开关状态 - close_plugins: List[str] = [] # 已关闭插件 - group_task_status: Dict[str, bool] = {} # 被动状态 - - -class BaseData(BaseModel): - """ - 群基本信息 - """ - - white_group: List[str] = [] - """白名单""" - close_task: List[str] = [] - """全局关闭的被动任务""" - group_manager: Dict[str, BaseGroup] = {} - """群组管理""" - task: Dict[str, str] = {} - """被动任务 英文:中文名""" - - def __len__(self) -> int: - return len(self.group_manager) - - -class PluginBlock(BaseModel): - """ - 插件阻断 - """ - - status: bool = True # 限制状态 - check_type: Literal["private", "group", "all"] = "all" # 检查类型 - limit_type: Literal["user", "group"] = "user" # 监听对象 - rst: Optional[str] # 阻断时回复 - - -class PluginCd(BaseModel): - """ - 插件阻断 - """ - - cd: int = 5 # cd - status: bool = True # 限制状态 - check_type: Literal["private", "group", "all"] = "all" # 检查类型 - limit_type: Literal["user", "group"] = "user" # 监听对象 - rst: Optional[str] # 阻断时回复 - - -class PluginCount(BaseModel): - """ - 插件阻断 - """ - - max_count: int # 次数 - status: bool = True # 限制状态 - limit_type: Literal["user", "group"] = "user" # 监听对象 - rst: Optional[str] # 阻断时回复 - - -class PluginSetting(BaseModel): - """ - 插件设置 - """ - - cmd: List[str] = [] # 命令 或 命令别名 - default_status: bool = True # 默认开关状态 - level: int = 5 # 功能权限等级 - limit_superuser: bool = False # 功能状态是否限制超级用户 - plugin_type: Tuple[Union[str, int], ...] = ("normal",) # 插件类型 - cost_gold: int = 0 # 需要消费的金币 - - -class Plugin(BaseModel): - """ - 插件数据 - """ - - plugin_name: str # 模块名 - status: Optional[bool] = True # 开关状态 - error: Optional[bool] = False # 是否加载报错 - block_type: Optional[str] = None # 关闭类型 - author: Optional[str] = None # 作者 - version: Optional[Union[int, str]] = None # 版本 - - -class PluginType(Enum): - """ - 插件类型 - """ - - NORMAL = "normal" - ADMIN = "admin" - HIDDEN = "hidden" - SUPERUSER = "superuser" - - -class PluginData(BaseModel): - model: str - name: str - plugin_type: PluginType # 插件内部类型,根据name [Hidden] [Admin] [SUPERUSER] - usage: Optional[str] - superuser_usage: Optional[str] - des: Optional[str] - task: Optional[Dict[str, str]] - menu_type: Tuple[Union[str, int], ...] = ("normal",) # 菜单类型 - plugin_setting: Optional[PluginSetting] - plugin_cd: Optional[PluginCd] - plugin_block: Optional[PluginBlock] - plugin_count: Optional[PluginCount] - plugin_resources: Optional[Dict[str, Union[str, Path]]] - plugin_configs: Optional[Dict[str, zConfig]] - plugin_status: Plugin - - class Config: - arbitrary_types_allowed = True - - def __eq__(self, other: "PluginData"): - return ( - isinstance(other, PluginData) - and self.name == other.name - and self.menu_type == other.menu_type - ) - - def __hash__(self): - return hash(self.name + str(self.menu_type[0])) diff --git a/utils/manager/none_plugin_count_manager.py b/utils/manager/none_plugin_count_manager.py deleted file mode 100644 index 9aab831f..00000000 --- a/utils/manager/none_plugin_count_manager.py +++ /dev/null @@ -1,47 +0,0 @@ -from utils.manager.data_class import StaticData -from typing import Optional -from pathlib import Path - - -class NonePluginCountManager(StaticData): - """ - 插件加载容忍管理器,当连续 max_count 次插件加载,视为删除插件,清楚数据 - """ - - def __init__(self, file: Optional[Path], max_count: int = 5): - """ - :param file: 存储路径 - :param max_count: 容忍最大次数 - """ - super().__init__(file) - if not self._data: - self._data = {} - self._max_count = max_count - - def add_count(self, module: str, count: int = 1): - """ - 添加次数 - :param module: 模块 - :param count: 次数,无特殊情况均为 1 - """ - if module not in self._data.keys(): - self._data[module] = count - else: - self._data[module] += count - - def reset(self, module: str): - """ - 重置次数 - :param module: 模块 - """ - if module in self._data.keys(): - self._data[module] = 0 - - def check(self, module: str): - """ - 检查容忍次数是否到达最大值 - :param module: 模块 - """ - if module in self._data.keys(): - return self._data[module] >= self._max_count - return False diff --git a/utils/manager/plugin_data_manager.py b/utils/manager/plugin_data_manager.py deleted file mode 100644 index f94307dc..00000000 --- a/utils/manager/plugin_data_manager.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any, Dict, Optional - -from . import StaticData -from .models import PluginData - - -class PluginDataManager(StaticData[PluginData]): - """ - 插件所有信息管理 - """ - - def __init__(self): - super().__init__(None) - self._data: Dict[str, PluginData] = {} - - def add_plugin_info(self, info: PluginData): - """ - 说明: - 添加插件信息 - 参数: - :param info: PluginInfo - """ - if info.model in self._data.keys() and self._data[info.model] == info: - raise ValueError(f"PluginInfoManager {info.model}:{info.name} 插件名称及类型已存在") - self._data[info.model] = info - - def get(self, item: str, default: Any = None) -> Optional[PluginData]: - return self._data.get(item, default) - - def __getitem__(self, item) -> Optional[PluginData]: - return self._data.get(item) - - def reload(self): - pass diff --git a/utils/manager/plugins2block_manager.py b/utils/manager/plugins2block_manager.py deleted file mode 100644 index 92259ffc..00000000 --- a/utils/manager/plugins2block_manager.py +++ /dev/null @@ -1,192 +0,0 @@ -from typing import Optional, Dict, Literal, Union, overload -from utils.manager.data_class import StaticData -from services.log import logger -from utils.utils import UserBlockLimiter -from pathlib import Path -from ruamel import yaml -from .models import PluginBlock - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2blockManager(StaticData[PluginBlock]): - """ - 插件命令阻塞 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._block_limiter: Dict[str, UserBlockLimiter] = {} - self.__load_file() - - @overload - def add_block_limit(self, plugin: str, plugin_block: PluginBlock): - ... - - @overload - def add_block_limit( - self, - plugin: str, - status: bool = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - ... - - def add_block_limit( - self, - plugin: str, - status: Union[bool, PluginBlock] = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - """ - 说明: - 添加插件调用 block 限制 - 参数: - :param plugin: 插件模块名称 - :param status: 默认开关状态 - :param check_type: 检查类型 'private'/'group'/'all',限制私聊/群聊/全部 - :param limit_type: 限制类型 监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id - :param rst: 回复的话,为空则不回复 - """ - if isinstance(status, PluginBlock): - self._data[plugin] = status - else: - if check_type not in ["all", "group", "private"]: - raise ValueError( - f"{plugin} 添加block限制错误,‘check_type‘ 必须为 'private'/'group'/'all'" - ) - if limit_type not in ["user", "group"]: - raise ValueError(f"{plugin} 添加block限制错误,‘limit_type‘ 必须为 'user'/'group'") - self._data[plugin] = PluginBlock( - status=status, check_type=check_type, limit_type=limit_type, rst=rst - ) - - def get_plugin_block_data(self, plugin: str) -> Optional[PluginBlock]: - """ - 说明: - 获取插件block数据 - 参数: - :param plugin: 模块名 - """ - if self.check_plugin_block_status(plugin): - return self._data[plugin] - return None - - def check_plugin_block_status(self, plugin: str) -> bool: - """ - 说明: - 检测插件是否有 block - 参数: - :param plugin: 模块名 - """ - return plugin in self._data.keys() and self._data[plugin].status - - def check(self, id_: int, plugin: str) -> bool: - """ - 说明: - 检查 block - 参数: - :param id_: 限制 id - :param plugin: 模块名 - """ - if self._block_limiter.get(plugin): - return self._block_limiter[plugin].check(id_) - return False - - def set_true(self, id_: int, plugin: str): - """ - 说明: - 对插件 block - 参数: - :param id_: 限制 id - :param plugin: 模块名 - """ - if self._block_limiter.get(plugin): - self._block_limiter[plugin].set_true(id_) - - def set_false(self, id_: int, plugin: str): - """ - 说明: - 对插件 unblock - 参数: - :param plugin: 模块名 - :param id_: 限制 id - """ - if self._block_limiter.get(plugin): - self._block_limiter[plugin].set_false(id_) - - def reload_block_limit(self): - """ - 说明: - 加载 block 限制器 - """ - for plugin in self._data: - if self.check_plugin_block_status(plugin): - self._block_limiter[plugin] = UserBlockLimiter() - logger.info(f"已成功加载 {len(self._block_limiter)} 个Block限制.") - - def reload(self): - """ - 说明: - 重载本地数据 - """ - self.__load_file() - self.reload_block_limit() - - def save(self, path: Union[str, Path] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - yaml.dump( - {"PluginBlockLimit": self.dict()}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginBlockLimit"].yaml_set_start_comment( - """# 用户调用阻塞 -# 即 当用户调用此功能还未结束时 -# 用发送消息阻止用户重复调用此命令直到该命令结束 -# key:模块名称 -# status:此限制的开关状态 -# check_type:'private'/'group'/'all',限制私聊/群聊/全部 -# limit_type:监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id -# 示例:'user':阻塞用户,'group':阻塞群聊 -# rst:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称 -# rst 为 "" 或 None 时则不回复 -# rst示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]" -# rst回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" -# 用户昵称↑ 昵称系统的昵称↑ 艾特用户↑""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginBlock] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - temp = yaml.round_trip_load(f) - if "PluginBlockLimit" in temp.keys(): - for k, v in temp["PluginBlockLimit"].items(): - self._data[k] = PluginBlock.parse_obj(v) diff --git a/utils/manager/plugins2cd_manager.py b/utils/manager/plugins2cd_manager.py deleted file mode 100644 index 40aa668a..00000000 --- a/utils/manager/plugins2cd_manager.py +++ /dev/null @@ -1,199 +0,0 @@ -from typing import Optional, Dict, Literal, Union, overload -from utils.manager.data_class import StaticData -from utils.utils import FreqLimiter -from services.log import logger -from pathlib import Path -from ruamel import yaml -from .models import PluginCd - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2cdManager(StaticData[PluginCd]): - """ - 插件命令 cd 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._freq_limiter: Dict[str, FreqLimiter] = {} - self.__load_file() - - @overload - def add_cd_limit(self, plugin: str, plugin_cd: PluginCd): - ... - - @overload - def add_cd_limit( - self, - plugin: str, - cd: Union[int, PluginCd] = 5, - status: Optional[bool] = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - ... - - def add_cd_limit( - self, - plugin: str, - cd: Union[int, PluginCd] = 5, - status: Optional[bool] = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - """ - 说明: - 添加插件调用 cd 限制 - 参数: - :param plugin: 插件模块名称 - :param cd: cd 时长 - :param status: 默认开关状态 - :param check_type: 检查类型 'private'/'group'/'all',限制私聊/群聊/全部 - :param limit_type: 限制类型 监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id - :param rst: 回复的话,为空则不回复 - """ - if isinstance(cd, PluginCd): - self._data[plugin] = cd - else: - if check_type not in ["all", "group", "private"]: - raise ValueError( - f"{plugin} 添加cd限制错误,‘check_type‘ 必须为 'private'/'group'/'all'" - ) - if limit_type not in ["user", "group"]: - raise ValueError(f"{plugin} 添加cd限制错误,‘limit_type‘ 必须为 'user'/'group'") - self._data[plugin] = PluginCd(cd=cd, status=status, check_type=check_type, limit_type=limit_type, rst=rst) - - def get_plugin_cd_data(self, plugin: str) -> Optional[PluginCd]: - """ - 说明: - 获取插件cd数据 - 参数: - :param plugin: 模块名 - """ - if self.check_plugin_cd_status(plugin): - return self._data[plugin] - return None - - def check_plugin_cd_status(self, plugin: str) -> bool: - """ - 说明: - 检测插件是否有 cd - 参数: - :param plugin: 模块名 - """ - return ( - plugin in self._data.keys() - and self._data[plugin].cd > 0 - and self._data[plugin].status - ) - - def check(self, plugin: str, id_: int) -> bool: - """ - 说明: - 检查 cd - 参数: - :param plugin: 模块名 - :param id_: 限制 id - """ - if self._freq_limiter.get(plugin): - return self._freq_limiter[plugin].check(id_) - return False - - def start_cd(self, plugin: str, id_: int, cd: int = 0): - """ - 说明: - 开始cd - 参数: - :param plugin: 模块名 - :param id_: cd 限制类型 - :param cd: cd 时长 - """ - if self._freq_limiter.get(plugin): - self._freq_limiter[plugin].start_cd(id_, cd) - - def get_plugin_data(self, plugin: str) -> Optional[PluginCd]: - """ - 说明: - 获取单个模块限制数据 - 参数: - :param plugin: 模块名 - """ - if self._data.get(plugin): - return self._data.get(plugin) - - def reload_cd_limit(self): - """ - 说明: - 加载 cd 限制器 - """ - for plugin in self._data: - if self.check_plugin_cd_status(plugin): - self._freq_limiter[plugin] = FreqLimiter( - self.get_plugin_cd_data(plugin).cd - ) - logger.info(f"已成功加载 {len(self._freq_limiter)} 个Cd限制.") - - def reload(self): - """ - 说明: - 重载本地数据 - """ - self.__load_file() - self.reload_cd_limit() - - def save(self, path: Union[str, Path] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - yaml.dump( - {"PluginCdLimit": self.dict()}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginCdLimit"].yaml_set_start_comment( - """# 需要cd的功能 -# 自定义的功能需要cd也可以在此配置 -# key:模块名称 -# cd:cd 时长(秒) -# status:此限制的开关状态 -# check_type:'private'/'group'/'all',限制私聊/群聊/全部 -# limit_type:监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id -# 示例:'user':用户N秒内触发1次,'group':群N秒内触发1次 -# rst:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称 -# rst 为 "" 或 None 时则不回复 -# rst示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]" -# rst回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" -# 用户昵称↑ 昵称系统的昵称↑ 艾特用户↑""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginCd] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - temp = _yaml.load(f) - if "PluginCdLimit" in temp.keys(): - for k, v in temp["PluginCdLimit"].items(): - self._data[k] = PluginCd.parse_obj(v) diff --git a/utils/manager/plugins2count_manager.py b/utils/manager/plugins2count_manager.py deleted file mode 100644 index 09b305df..00000000 --- a/utils/manager/plugins2count_manager.py +++ /dev/null @@ -1,191 +0,0 @@ -from typing import Optional, Dict, Literal, Union, overload -from utils.manager.data_class import StaticData -from utils.utils import DailyNumberLimiter -from services.log import logger -from pathlib import Path -from ruamel import yaml -from .models import PluginCount - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2countManager(StaticData[PluginCount]): - """ - 插件命令 次数 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._daily_limiter: Dict[str, DailyNumberLimiter] = {} - self.__load_file() - - @overload - def add_count_limit(self, plugin: str, plugin_count: PluginCount): - ... - - @overload - def add_count_limit( - self, - plugin: str, - max_count: int = 5, - status: Optional[bool] = True, - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - ... - - def add_count_limit( - self, - plugin: str, - max_count: Union[int, PluginCount] = 5, - status: Optional[bool] = True, - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - """ - 说明: - 添加插件调用 次数 限制 - 参数: - :param plugin: 插件模块名称 - :param max_count: 最大次数限制 - :param status: 默认开关状态 - :param limit_type: 限制类型 监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id - :param rst: 回复的话,为空则不回复 - """ - if isinstance(max_count, PluginCount): - self._data[plugin] = max_count - else: - if limit_type not in ["user", "group"]: - raise ValueError(f"{plugin} 添加count限制错误,‘limit_type‘ 必须为 'user'/'group'") - self._data[plugin] = PluginCount(max_count=max_count, status=status, limit_type=limit_type, rst=rst) - - def get_plugin_count_data(self, plugin: str) -> Optional[PluginCount]: - """ - 说明: - 获取插件次数数据 - 参数: - :param plugin: 模块名 - """ - if self.check_plugin_count_status(plugin): - return self._data[plugin] - return None - - def get_plugin_data(self, plugin: str) -> Optional[PluginCount]: - """ - 说明: - 获取单个模块限制数据 - 参数: - :param plugin: 模块名 - """ - if self._data.get(plugin) is not None: - return self._data.get(plugin) - - def check_plugin_count_status(self, plugin: str) -> bool: - """ - 说明: - 检测插件是否有 次数 限制 - 参数: - :param plugin: 模块名 - """ - return ( - plugin in self._data.keys() - and self._data[plugin].status - and self._data[plugin].max_count > 0 - ) - - def check(self, plugin: str, id_: int) -> bool: - """ - 说明: - 检查 count - 参数: - :param plugin: 模块名 - :param id_: 限制 id - """ - if self._daily_limiter.get(plugin): - return self._daily_limiter[plugin].check(id_) - return True - - def increase(self, plugin: str, id_: int, num: int = 1): - """ - 说明: - 增加次数 - 参数: - :param plugin: 模块名 - :param id_: cd 限制类型 - :param num: 增加次数 - """ - if self._daily_limiter.get(plugin): - self._daily_limiter[plugin].increase(id_, num) - - def reload_count_limit(self): - """ - 说明: - 加载 cd 限制器 - """ - for plugin in self._data: - if self.check_plugin_count_status(plugin): - self._daily_limiter[plugin] = DailyNumberLimiter( - self.get_plugin_count_data(plugin).max_count - ) - logger.info(f"已成功加载 {len(self._daily_limiter)} 个Count限制.") - - def reload(self): - """ - 重载本地数据 - """ - self.__load_file() - self.reload_count_limit() - - def save(self, path: Union[str, Path] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - yaml.dump( - {"PluginCountLimit": self.dict()}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginCountLimit"].yaml_set_start_comment( - """# 命令每日次数限制 -# 即 用户/群聊 每日可调用命令的次数 [数据内存存储,重启将会重置] -# 每日调用直到 00:00 刷新 -# key:模块名称 -# max_count: 每日调用上限 -# status:此限制的开关状态 -# limit_type:监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id -# 示例:'user':用户上限,'group':群聊上限 -# rst:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称 -# rst 为 "" 或 None 时则不回复 -# rst示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]" -# rst回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" -# 用户昵称↑ 昵称系统的昵称↑ 艾特用户↑""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginCount] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - temp = _yaml.load(f) - if "PluginCountLimit" in temp.keys(): - for k, v in temp["PluginCountLimit"].items(): - self._data[k] = PluginCount.parse_obj(v) diff --git a/utils/manager/plugins2settings_manager.py b/utils/manager/plugins2settings_manager.py deleted file mode 100644 index b58c4644..00000000 --- a/utils/manager/plugins2settings_manager.py +++ /dev/null @@ -1,171 +0,0 @@ -from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union, overload - -from ruamel import yaml - -from utils.manager.data_class import StaticData - -from .models import PluginSetting, PluginType - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2settingsManager(StaticData[PluginSetting]): - """ - 插件命令阻塞 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self.__load_file() - - @overload - def add_plugin_settings(self, plugin: str, plugin_settings: PluginSetting): - ... - - @overload - def add_plugin_settings( - self, - plugin: str, - cmd: Optional[List[str]] = None, - default_status: bool = True, - level: int = 5, - limit_superuser: bool = False, - plugin_type: Tuple[Union[str, int]] = ("normal",), - cost_gold: int = 0, - ): - ... - - def add_plugin_settings( - self, - plugin: str, - cmd: Optional[Union[List[str], PluginSetting]] = None, - default_status: bool = True, - level: int = 5, - limit_superuser: bool = False, - plugin_type: Tuple[Union[str, int]] = ("normal",), - cost_gold: int = 0, - ): - """ - 说明: - 添加一个插件设置 - 参数: - :param plugin: 插件模块名称 - :param cmd: 命令 或 命令别名 - :param default_status: 默认开关状态 - :param level: 功能权限等级 - :param limit_superuser: 功能状态是否限制超级用户 - :param plugin_type: 插件类型 - :param cost_gold: 需要消费的金币 - """ - if isinstance(cmd, PluginSetting): - self._data[plugin] = cmd - else: - self._data[plugin] = PluginSetting( - cmd=cmd, - level=level, - default_status=default_status, - limit_superuser=limit_superuser, - plugin_type=plugin_type, - cost_gold=cost_gold, - ) - - def get_plugin_data(self, module: str) -> Optional[PluginSetting]: - """ - 说明: - 通过模块名获取数据 - 参数: - :param module: 模块名称 - """ - return self._data.get(module) - - @overload - def get_plugin_module(self, cmd: str) -> str: - ... - - @overload - def get_plugin_module(self, cmd: str, is_all: bool = True) -> List[str]: - ... - - def get_plugin_module( - self, cmd: str, is_all: bool = False - ) -> Union[str, List[str]]: - """ - 说明: - 根据 cmd 获取功能 modules - 参数: - :param cmd: 命令 - :param is_all: 获取全部包含cmd的模块 - """ - keys = [] - for key in self._data.keys(): - if cmd in self._data[key].cmd: - if is_all: - keys.append(key) - else: - return key - return keys - - def reload(self): - """ - 说明: - 重载本地数据 - """ - self.__load_file() - - def save(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - self_dict = self.dict() - for key in self_dict.keys(): - if self_dict[key].get("plugin_type") and isinstance( - self_dict[key].get("plugin_type"), PluginType - ): - self_dict[key]["plugin_type"] = self_dict[key][ - "plugin_type" - ].value - yaml.dump( - {"PluginSettings": self_dict}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginSettings"].yaml_set_start_comment( - """# 模块与对应命令和对应群权限 -# 用于生成帮助图片 和 开关功能 -# key:模块名称 -# level:需要的群等级 -# default_status:加入群时功能的默认开关状态 -# limit_superuser: 功能状态是否限制超级用户 -# cmd: 关闭[cmd] 都会触发命令 关闭对应功能,cmd列表第一个词为统计的功能名称 -# plugin_type: 帮助类别 示例:('原神相关',) 或 ('原神相关', 1),1代表帮助命令列向排列,否则为横向排列""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginSetting] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - if temp := _yaml.load(f): - if "PluginSettings" in temp.keys(): - for k, v in temp["PluginSettings"].items(): - self._data[k] = PluginSetting.parse_obj(v) diff --git a/utils/manager/plugins_manager.py b/utils/manager/plugins_manager.py deleted file mode 100644 index 51aa725a..00000000 --- a/utils/manager/plugins_manager.py +++ /dev/null @@ -1,169 +0,0 @@ -from pathlib import Path -from typing import Callable, Dict, Optional, Union - -from utils.manager import group_manager -from utils.manager.data_class import StaticData - -from .models import Plugin - - -def init_plugin(func: Callable): - """ - 说明: - 初始化群数据 - 参数: - :param func: func - """ - - def wrapper(*args, **kwargs): - try: - self = args[0] - module = args[1] - if module not in self._data.keys(): - self._data[module] = Plugin( - plugin_name=module, - status=True, - error=False, - block_type=None, - author=None, - version=None, - ) - except Exception as e: - pass - return func(*args, **kwargs) - - return wrapper - - -class PluginsManager(StaticData[Plugin]): - """ - 插件 管理器 - """ - - def __init__(self, file: Path): - self._data: Dict[str, Plugin] - super().__init__(file) - for k, v in self._data.items(): - self._data[k] = Plugin.parse_obj(v) - - def add_plugin_data( - self, - module: str, - plugin_name: str, - *, - status: Optional[bool] = True, - error: Optional[bool] = False, - block_type: Optional[str] = None, - author: Optional[str] = None, - version: Optional[int] = None, - ): - """ - 说明: - 添加插件数据 - 参数: - :param module: 模块名称 - :param plugin_name: 插件名称 - :param status: 插件开关状态 - :param error: 加载状态 - :param block_type: 限制类型 - :param author: 作者 - :param version: 版本 - """ - self._data[module] = Plugin( - plugin_name=plugin_name, - status=status, - error=error, - block_type=block_type, - author=author, - version=version, - ) - - def block_plugin( - self, module: str, group_id: Optional[str] = None, block_type: str = "all" - ): - """ - 说明: - 锁定插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组,None时为超级用户禁用 - :param block_type: 限制类型 - """ - self._set_plugin_status(module, "block", group_id, block_type) - - def unblock_plugin(self, module: str, group_id: Optional[str] = None): - """ - 说明: - 解锁插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - """ - self._set_plugin_status(module, "unblock", group_id) - - def get_plugin_status(self, module: str, block_type: str = "all") -> bool: - """ - 说明: - 获取插件状态 - 参数: - :param module: 功能模块名 - :param block_type: 限制类型 - """ - if module in self._data.keys(): - if self._data[module].block_type == "all" and block_type == "all": - return False - return not self._data[module].block_type == block_type - return True - - def get_plugin_block_type(self, module: str) -> Optional[str]: - """ - 说明: - 获取功能限制类型 - 参数: - :param module: 模块名称 - """ - if module in self._data.keys(): - return self._data[module].block_type - - @init_plugin - def get_plugin_error_status(self, module: str) -> Optional[bool]: - """ - 说明: - 插件是否成功加载 - 参数: - :param module: 模块名称 - """ - return self._data[module].error - - @init_plugin - def _set_plugin_status( - self, - module: str, - status: str, - group_id: Optional[str], - block_type: str = "all", - ): - """ - 说明: - 设置功能开关状态 - 参数: - :param module: 功能模块名 - :param status: 功能状态 - :param group_id: 群组 - :param block_type: 限制类型 - """ - if module: - if group_id: - if status == "block": - group_manager.block_plugin(f"{module}:super", group_id) - else: - group_manager.unblock_plugin(f"{module}:super", group_id) - else: - if status == "block": - self._data[module].status = False - self._data[module].block_type = block_type - else: - if module in self._data.keys(): - self._data[module].status = True - self._data[module].block_type = None - self.save() diff --git a/utils/manager/requests_manager.py b/utils/manager/requests_manager.py deleted file mode 100644 index d9b5175c..00000000 --- a/utils/manager/requests_manager.py +++ /dev/null @@ -1,347 +0,0 @@ -from io import BytesIO -from pathlib import Path -from typing import Optional, Union, overload - -from nonebot.adapters.onebot.v11 import ActionFailed, Bot - -from services.log import logger -from utils.image_utils import BuildImage -from utils.manager.data_class import StaticData -from utils.utils import get_user_avatar - - -class RequestManager(StaticData): - - """ - 好友请求/邀请请求 管理 - """ - - def __init__(self, file: Optional[Path]): - super().__init__(file) - if not self._data: - self._data = {"private": {}, "group": {}} - - def add_request( - self, - bot_id: str, - id_: int, - type_: str, - flag: str, - *, - nickname: Optional[str] = None, - level: Optional[int] = None, - sex: Optional[str] = None, - age: Optional[str] = None, - from_: Optional[str] = "", - comment: Optional[str] = None, - invite_group: Optional[int] = None, - group_name: Optional[str] = None, - ): - """ - 添加一个请求 - :param bot_id: bot_id - :param id_: id,用户id或群id - :param type_: 类型,private 或 group - :param flag: event.flag - :param nickname: 用户昵称 - :param level: 等级 - :param sex: 性别 - :param age: 年龄 - :param from_: 请求来自 - :param comment: 附加消息 - :param invite_group: 邀请群聊 - :param group_name: 群聊名称 - """ - self._data[type_][str(len(self._data[type_].keys()))] = { - "bot_id": bot_id, - "id": id_, - "flag": flag, - "nickname": nickname, - "level": level, - "sex": sex, - "age": age, - "from": from_, - "comment": comment, - "invite_group": invite_group, - "group_name": group_name, - } - self.save() - - @overload - def remove_request(self, type_: str, flag: str): - ... - - @overload - def remove_request(self, type_: str, id_: int): - ... - - def remove_request(self, type_: str, id_: Union[int, str]): - """ - 删除一个请求数据 - :param type_: 类型 - :param id_: id,user_id 或 group_id - """ - for x in self._data[type_].keys(): - a_id = self._data[type_][x].get("id") - a_flag = self._data[type_][x].get("flag") - if a_id == id_ or a_flag == id_: - del self._data[type_][x] - break - self.save() - - def get_group_id(self, id_: int) -> Optional[int]: - """ - 通过id获取群号 - :param id_: id - """ - data = self._data["group"].get(str(id_)) - if data: - return data["invite_group"] - return None - - @overload - async def approve(self, bot: Bot, id_: int, type_: str) -> int: - ... - - @overload - async def approve(self, bot: Bot, flag: str, type_: str) -> int: - ... - - - async def approve(self, bot: Bot, id_: Union[int, str], type_: str) -> int: - """ - 同意请求 - :param bot: Bot - :param id_: id - :param type_: 类型,private 或 group - """ - return await self._set_add_request(bot, id_, type_, True) - - @overload - async def refused(self, bot: Bot, id_: int, type_: str) -> int: - ... - - @overload - async def refused(self, bot: Bot, flag: str, type_: str) -> int: - ... - - async def refused(self, bot: Bot, id_: Union[int, str], type_: str) -> Optional[int]: - """ - 拒绝请求 - :param bot: Bot - :param id_: id - :param type_: 类型,private 或 group - """ - return await self._set_add_request(bot, id_, type_, False) - - def clear( - self, type_: Optional[str] = None - ): # type_: Optional[Literal["group", "private"]] = None - """ - 清空所有请求信息,无视请求 - :param type_: 类型 - """ - if type_: - self._data[type_] = {} - else: - self._data = {"private": {}, "group": {}} - self.save() - - @overload - async def delete_request(self, id_: int, type_: str) -> int: - ... - - @overload - async def delete_request(self, flag: str, type_: str) -> int: - ... - - def delete_request( - self, id_: Union[str, int], type_: str - ): # type_: Literal["group", "private"] - """ - 删除请求 - :param id_: id - :param type_: 类型 - """ - if type(id_) == int: - if self._data[type_].get(id_): - del self._data[type_][id_] - self.save() - else: - for k, item in self._data[type_].items(): - if item['flag'] == id_: - del self._data[type_][k] - self.save() - break - - def set_group_name(self, group_name: str, group_id: int): - """ - 设置群聊名称 - :param group_name: 名称 - :param group_id: id - """ - for id_ in self._data["group"].keys(): - if self._data["group"][id_]["invite_group"] == group_id: - self._data["group"][id_]["group_name"] = group_name - break - self.save() - - async def show(self, type_: str) -> Optional[str]: - """ - 请求可视化 - """ - data = self._data[type_] - if not data: - return None - img_list = [] - id_list = list(data.keys()) - id_list.reverse() - for id_ in id_list: - age = data[id_]["age"] - nickname = data[id_]["nickname"] - comment = data[id_]["comment"] if type_ == "private" else "" - from_ = data[id_]["from"] - sex = data[id_]["sex"] - ava = BuildImage( - 80, 80, background=BytesIO(await get_user_avatar(data[id_]["id"])) - ) - ava.circle() - age_bk = BuildImage( - len(str(age)) * 6 + 6, - 15, - color="#04CAF7" if sex == "male" else "#F983C1", - ) - age_bk.text((3, 1), f"{age}", fill=(255, 255, 255)) - x = BuildImage( - 90, 32, font_size=15, color="#EEEFF4", font="HYWenHei-85W.ttf" - ) - x.text((0, 0), "同意/拒绝", center_type="center") - x.circle_corner(10) - A = BuildImage(500, 100, font_size=24, font="msyh.ttf") - A.paste(ava, (15, 0), alpha=True, center_type="by_height") - A.text((120, 15), nickname) - A.paste(age_bk, (120, 50), True) - A.paste( - BuildImage( - 200, - 0, - font_size=12, - plain_text=f"对方留言:{comment}", - font_color=(140, 140, 143), - ), - (120 + age_bk.w + 10, 49), - True, - ) - if type_ == "private": - A.paste( - BuildImage( - 200, - 0, - font_size=12, - plain_text=f"来源:{from_}", - font_color=(140, 140, 143), - ), - (120, 70), - True, - ) - else: - A.paste( - BuildImage( - 200, - 0, - font_size=12, - plain_text=f"邀请你加入:{data[id_]['group_name']}({data[id_]['invite_group']})", - font_color=(140, 140, 143), - ), - (120, 70), - True, - ) - A.paste(x, (380, 35), True) - A.paste( - BuildImage( - 0, - 0, - plain_text=f"id:{id_}", - font_size=13, - font_color=(140, 140, 143), - ), - (400, 10), - True, - ) - img_list.append(A) - A = BuildImage(500, len(img_list) * 100, 500, 100) - for img in img_list: - A.paste(img) - bk = BuildImage(A.w, A.h + 50, color="#F8F9FB", font_size=20) - bk.paste(A, (0, 50)) - bk.text( - (15, 13), "好友请求" if type_ == "private" else "群聊请求", fill=(140, 140, 143) - ) - return bk.pic2bs4() - - async def _set_add_request( - self, bot: Bot, idx: Union[str, int], type_: str, approve: bool - ) -> int: - """ - 处理请求 - :param bot: Bot - :param id_: id - :param type_: 类型,private 或 group - :param approve: 是否同意 - """ - flag = None - id_ = None - if type(idx) == str: - flag = idx - else: - id_ = str(idx) - if id_ and id_ in self._data[type_].keys(): - try: - if type_ == "private": - await bot.set_friend_add_request( - flag=self._data[type_][id_]["flag"], approve=approve - ) - rid = self._data[type_][id_]["id"] - else: - await bot.set_group_add_request( - flag=self._data[type_][id_]["flag"], - sub_type="invite", - approve=approve, - ) - rid = self._data[type_][id_]["invite_group"] - except ActionFailed: - logger.info( - f"同意{self._data[type_][id_]['nickname']}({self._data[type_][id_]['id']})" - f"的{'好友' if type_ == 'private' else '入群'}请求失败了..." - ) - return 1 # flag失效 - else: - logger.info( - f"{'同意' if approve else '拒绝'}{self._data[type_][id_]['nickname']}({self._data[type_][id_]['id']})" - f"的{'好友' if type_ == 'private' else '入群'}请求..." - ) - del self._data[type_][id_] - self.save() - return rid - if flag: - rm_id = None - for k, item in self._data[type_].items(): - if item['flag'] == flag: - rm_id = k - if type_ == 'private': - await bot.set_friend_add_request( - flag=item['flag'], approve=approve - ) - rid = item["id"] - else: - await bot.set_group_add_request( - flag=item['flag'], - sub_type="invite", - approve=approve, - ) - rid = item["invite_group"] - if rm_id is not None: - del self._data[type_][rm_id] - self.save() - return rid - return 2 # 未找到id diff --git a/utils/manager/resources_manager.py b/utils/manager/resources_manager.py deleted file mode 100644 index 649223fc..00000000 --- a/utils/manager/resources_manager.py +++ /dev/null @@ -1,113 +0,0 @@ -from typing import Union, List, Optional - -from configs.path_config import IMAGE_PATH, DATA_PATH, RECORD_PATH, TEXT_PATH, FONT_PATH, LOG_PATH -from utils.manager.data_class import StaticData -from pathlib import Path -from ruamel.yaml import YAML -from services.log import logger -import shutil - -yaml = YAML(typ="safe") - - -class ResourcesManager(StaticData): - """ - 插件配置 与 资源 管理器 - """ - - def __init__(self, file: Path): - self.file = file - super().__init__(file) - self._temp_dir = [] - self._abspath = Path() - - def add_resource( - self, module: str, source_file: Union[str, Path], move_file: Union[str, Path] - ): - """ - 添加一个资源移动路劲 - :param module: 模块名 - :param source_file: 源文件路径 - :param move_file: 移动路径 - """ - if isinstance(source_file, Path): - source_file = str(source_file.absolute()) - if isinstance(move_file, Path): - move_file = str(move_file.absolute()) - if module not in self._data.keys(): - self._data[module] = {source_file: move_file} - else: - self._data[module][source_file] = move_file - - def remove_resource(self, module: str, source_file: Optional[Union[str, Path]] = None): - """ - 删除一个资源路径 - :param module: 模块 - :param source_file: 源文件路径 - """ - if not source_file: - if module in self._data.keys(): - for x in self._data[module].keys(): - move_file = Path(self._data[module][x]) - if move_file not in [IMAGE_PATH, DATA_PATH, RECORD_PATH, TEXT_PATH, FONT_PATH, LOG_PATH]: - if move_file.exists(): - shutil.rmtree(move_file.absolute(), ignore_errors=True) - logger.info(f"已清除插件 {module} 资源路径:{self._data[module][x]}") - del self._data[module][x] - else: - if isinstance(source_file, Path): - source_file = str(source_file.absolute()) - if source_file: - if module in self._data.keys() and source_file in self._data[module].keys(): - move_file = Path(self._data[module][source_file]) - if move_file not in [IMAGE_PATH, DATA_PATH, RECORD_PATH, TEXT_PATH, FONT_PATH, LOG_PATH]: - if move_file.exists(): - shutil.rmtree(move_file.absolute(), ignore_errors=True) - del self._data[module][source_file] - self.save() - - def start_move(self): - """ - 开始移动路径 - """ - for module in self._data.keys(): - for source_path in self._data[module].keys(): - move_path = Path(self._data[module][source_path]) - try: - source_path = Path(source_path) - file_name = source_path.name - move_path = move_path / file_name - move_path.mkdir(exist_ok=True, parents=True) - if source_path.exists(): - if move_path.exists(): - shutil.rmtree(str(move_path.absolute()), ignore_errors=True) - shutil.move(str(source_path.absolute()), str(move_path.absolute())) - logger.info( - f"移动资源文件路径 {source_path.absolute()} >>> {move_path.absolute()}" - ) - elif not move_path.exists(): - logger.warning( - f"移动资源路径文件{source_path.absolute()} >>>" - f" {move_path.absolute()} 失败,源文件不存在.." - ) - except Exception as e: - logger.error( - f"移动资源路径文件{source_path.absolute()} >>>" - f" {move_path.absolute()}失败,{type(e)}:{e}" - ) - self.save() - - def add_temp_dir(self, path: Union[str, Path]): - """ - 添加临时清理文件夹 - :param path: 路径 - """ - if isinstance(path, str): - path = Path(path) - self._temp_dir.append(path) - - def get_temp_data_dir(self) -> List[Path]: - """ - 获取临时文件文件夹 - """ - return self._temp_dir diff --git a/utils/manager/withdraw_message_manager.py b/utils/manager/withdraw_message_manager.py deleted file mode 100644 index 59548f4f..00000000 --- a/utils/manager/withdraw_message_manager.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Dict, Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import ( - GroupMessageEvent, - MessageEvent, - PrivateMessageEvent, -) - - -class WithdrawMessageManager: - def __init__(self): - self.data = [] - - def append(self, message_data: Tuple[Union[int, Dict[str, int]], int]): - """ - 说明: - 添加一个撤回消息id和时间 - 参数: - :param message_data: 撤回消息id和时间 - """ - if isinstance(message_data[0], dict): - message_data = (message_data[0]["message_id"], message_data[1]) - self.data.append(message_data) - - def remove(self, message_data: Tuple[int, int]): - """ - 说明: - 删除一个数据 - 参数: - :param message_data: 消息id和时间 - """ - self.data.remove(message_data) - - def withdraw_message( - self, - event: MessageEvent, - id_: Union[int, Dict[str, int]], - conditions: Optional[Tuple[int, int]], - ): - """ - 便捷判断消息撤回 - :param event: event - :param id_: 消息id 或 send 返回的字典 - :param conditions: 判断条件 - """ - if conditions and conditions[0]: - if ( - (conditions[1] == 0 and isinstance(event, PrivateMessageEvent)) - or (conditions[1] == 1 and isinstance(event, GroupMessageEvent)) - or conditions[1] == 2 - ): - self.append((id_, conditions[0])) diff --git a/utils/message_builder.py b/utils/message_builder.py deleted file mode 100755 index f19876da..00000000 --- a/utils/message_builder.py +++ /dev/null @@ -1,212 +0,0 @@ -import io -from pathlib import Path -from typing import List, Optional, Union - -from nonebot.adapters.onebot.v11.message import Message, MessageSegment - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH, RECORD_PATH -from services.log import logger -from utils.image_utils import BuildImage, BuildMat - - -def image( - file: Optional[Union[str, Path, bytes, BuildImage, io.BytesIO, BuildMat]] = None, - b64: Optional[str] = None, -) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.image 消息 - 生成顺序:绝对路径(abspath) > base64(b64) > img_name - 参数: - :param file: 图片文件 - :param b64: 图片base64(兼容旧方法) - """ - if b64: - file = b64 if b64.startswith("base64://") else ("base64://" + b64) - if isinstance(file, str): - if file.startswith(("http", "base64://")): - return MessageSegment.image(file) - else: - if (IMAGE_PATH / file).exists(): - return MessageSegment.image(IMAGE_PATH / file) - logger.warning(f"图片 {(IMAGE_PATH / file).absolute()}缺失...") - return "" - if isinstance(file, Path): - if file.exists(): - return MessageSegment.image(file) - logger.warning(f"图片 {file.absolute()}缺失...") - if isinstance(file, (bytes, io.BytesIO)): - return MessageSegment.image(file) - if isinstance(file, (BuildImage, BuildMat)): - return MessageSegment.image(file.pic2bs4()) - return MessageSegment.image("") - - -def at(qq: Union[int, str]) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.at 消息 - 参数: - :param qq: qq号 - """ - return MessageSegment.at(qq) - - -def record(file: Union[Path, str, bytes, io.BytesIO]) -> Union[MessageSegment, str]: - """ - 说明: - 生成一个 MessageSegment.record 消息 - 参数: - :param file: 音频文件名称,默认在 resource/voice 目录下 - """ - if isinstance(file, Path): - if file.exists(): - return MessageSegment.record(file) - logger.warning(f"音频 {file.absolute()}缺失...") - if isinstance(file, (bytes, io.BytesIO)): - return MessageSegment.record(file) - if isinstance(file, str): - if "http" in file: - return MessageSegment.record(file) - else: - return MessageSegment.record(RECORD_PATH / file) - return "" - - -def text(msg: str) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.text 消息 - 参数: - :param msg: 消息文本 - """ - return MessageSegment.text(msg) - - -def contact_user(qq: int) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.contact_user 消息 - 参数: - :param qq: qq号 - """ - return MessageSegment.contact_user(qq) - - -def share( - url: str, title: str, content: Optional[str] = None, image_url: Optional[str] = None -) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.share 消息 - 参数: - :param url: 自定义分享的链接 - :param title: 自定义分享的包体 - :param content: 自定义分享的内容 - :param image_url: 自定义分享的展示图片 - """ - return MessageSegment.share(url, title, content, image_url) - - -def xml(data: str) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.xml 消息 - 参数: - :param data: 数据文本 - """ - return MessageSegment.xml(data) - - -def json(data: str) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.json 消息 - 参数: - :param data: 消息数据 - """ - return MessageSegment.json(data) - - -def face(id_: int) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.face 消息 - 参数: - :param id_: 表情id - """ - return MessageSegment.face(id_) - - -def poke(qq: int) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.poke 消息 - 参数: - :param qq: qq号 - """ - return MessageSegment("poke", {"qq": qq}) - - -def music(type_: str, id_: int) -> MessageSegment: - return MessageSegment.music(type_, id_) - - -def custom_forward_msg( - msg_list: List[Union[str, Message]], - uin: Union[int, str], - name: str = f"这里是{NICKNAME}", -) -> List[dict]: - """ - 说明: - 生成自定义合并消息 - 参数: - :param msg_list: 消息列表 - :param uin: 发送者 QQ - :param name: 自定义名称 - """ - uin = int(uin) - mes_list = [] - for _message in msg_list: - data = { - "type": "node", - "data": { - "name": name, - "uin": f"{uin}", - "content": _message, - }, - } - mes_list.append(data) - return mes_list - - -class MessageBuilder: - """ - MessageSegment构建工具 - """ - - def __init__(self, msg: Union[str, MessageSegment, Message]): - if msg: - if isinstance(msg, str): - self._msg = text(msg) - else: - self._msg = msg - else: - self._msg = text("") - - def text(self, msg: str): - return MessageBuilder(self._msg + text(msg)) - - def image( - self, - file: Optional[Union[str, Path, bytes]] = None, - b64: Optional[str] = None, - ): - return MessageBuilder(self._msg + image(file, b64)) - - def at(self, qq: int): - return MessageBuilder(self._msg + at(qq)) - - def face(self, id_: int): - return MessageBuilder(self._msg + face(id_)) diff --git a/utils/models/__init__.py b/utils/models/__init__.py deleted file mode 100644 index 0b217fcf..00000000 --- a/utils/models/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Any - -from nonebot.adapters.onebot.v11 import Message, MessageEvent -from pydantic import BaseModel - - -class ShopParam(BaseModel): - - - goods_name: str - """商品名称""" - user_id: int - """用户id""" - group_id: int - """群聊id""" - bot: Any - """bot""" - event: MessageEvent - """event""" - num: int - """道具单次使用数量""" - message: Message - """message""" - text: str - """text""" - send_success_msg: bool = True - """是否发送使用成功信息""" - max_num_limit: int = 1 - """单次使用最大次数""" - - -class CommonSql(BaseModel): - - sql: str - """sql语句""" - remark: str - """备注""" \ No newline at end of file diff --git a/utils/text_utils.py b/utils/text_utils.py deleted file mode 100644 index 01e46bf5..00000000 --- a/utils/text_utils.py +++ /dev/null @@ -1,12 +0,0 @@ - - -def prompt2cn(text: str, count: int, s: str = "#") -> str: - """ - 格式化中文提示 - :param text: 文本 - :param count: 个数 - :param s: # - """ - return s * count + "\n" + s * 6 + " " + text + " " + s * 6 + "\n" + s * count - - diff --git a/utils/typing.py b/utils/typing.py deleted file mode 100644 index cac8ca09..00000000 --- a/utils/typing.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Literal - -BLOCK_TYPE = Literal["all", "private", "group"] -"""禁用类型""" diff --git a/utils/utils.py b/utils/utils.py deleted file mode 100755 index aca0abbc..00000000 --- a/utils/utils.py +++ /dev/null @@ -1,629 +0,0 @@ -import time -from collections import defaultdict -from datetime import datetime -from pathlib import Path -from typing import Any, Callable, List, Optional, Set, Type, Union - -import httpx -import nonebot -import pypinyin -import pytz -from nonebot import require -from nonebot.adapters import Bot -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.matcher import Matcher, matchers - -from configs.config import SYSTEM_PROXY, Config -from services.log import logger - -try: - import ujson as json -except ModuleNotFoundError: - import json - -require("nonebot_plugin_apscheduler") -from nonebot_plugin_apscheduler import scheduler - -scheduler = scheduler - -# 全局字典 -GDict = { - "run_sql": [], # 需要启动前运行的sql语句 - "_shop_before_handle": {}, # 商品使用前函数 - "_shop_after_handle": {}, # 商品使用后函数 -} - - -CN2NUM = { - "一": 1, - "二": 2, - "三": 3, - "四": 4, - "五": 5, - "六": 6, - "七": 7, - "八": 8, - "九": 9, - "十": 10, - "十一": 11, - "十二": 12, - "十三": 13, - "十四": 14, - "十五": 15, - "十六": 16, - "十七": 17, - "十八": 18, - "十九": 19, - "二十": 20, - "二十一": 21, - "二十二": 22, - "二十三": 23, - "二十四": 24, - "二十五": 25, - "二十六": 26, - "二十七": 27, - "二十八": 28, - "二十九": 29, - "三十": 30, -} - - -class CountLimiter: - """ - 次数检测工具,检测调用次数是否超过设定值 - """ - - def __init__(self, max_count: int): - self.count = defaultdict(int) - self.max_count = max_count - - def add(self, key: Any): - self.count[key] += 1 - - def check(self, key: Any) -> bool: - if self.count[key] >= self.max_count: - self.count[key] = 0 - return True - return False - - -class UserBlockLimiter: - """ - 检测用户是否正在调用命令 - """ - - def __init__(self): - self.flag_data = defaultdict(bool) - self.time = time.time() - - def set_true(self, key: Any): - self.time = time.time() - self.flag_data[key] = True - - def set_false(self, key: Any): - self.flag_data[key] = False - - def check(self, key: Any) -> bool: - if time.time() - self.time > 30: - self.set_false(key) - return False - return self.flag_data[key] - - -class FreqLimiter: - """ - 命令冷却,检测用户是否处于冷却状态 - """ - - def __init__(self, default_cd_seconds: int): - self.next_time = defaultdict(float) - self.default_cd = default_cd_seconds - - def check(self, key: Any) -> bool: - return time.time() >= self.next_time[key] - - def start_cd(self, key: Any, cd_time: int = 0): - self.next_time[key] = time.time() + ( - cd_time if cd_time > 0 else self.default_cd - ) - - def left_time(self, key: Any) -> float: - return self.next_time[key] - time.time() - - -static_flmt = FreqLimiter(15) - - -class BanCheckLimiter: - """ - 恶意命令触发检测 - """ - - def __init__(self, default_check_time: float = 5, default_count: int = 4): - self.mint = defaultdict(int) - self.mtime = defaultdict(float) - self.default_check_time = default_check_time - self.default_count = default_count - - def add(self, key: Union[str, int, float]): - if self.mint[key] == 1: - self.mtime[key] = time.time() - self.mint[key] += 1 - - def check(self, key: Union[str, int, float]) -> bool: - if time.time() - self.mtime[key] > self.default_check_time: - self.mtime[key] = time.time() - self.mint[key] = 0 - return False - if ( - self.mint[key] >= self.default_count - and time.time() - self.mtime[key] < self.default_check_time - ): - self.mtime[key] = time.time() - self.mint[key] = 0 - return True - return False - - -class DailyNumberLimiter: - """ - 每日调用命令次数限制 - """ - - tz = pytz.timezone("Asia/Shanghai") - - def __init__(self, max_num): - self.today = -1 - self.count = defaultdict(int) - self.max = max_num - - def check(self, key) -> bool: - day = datetime.now(self.tz).day - if day != self.today: - self.today = day - self.count.clear() - return bool(self.count[key] < self.max) - - def get_num(self, key): - return self.count[key] - - def increase(self, key, num=1): - self.count[key] += num - - def reset(self, key): - self.count[key] = 0 - - -def is_number(s: Union[int, str]) -> bool: - """ - 说明: - 检测 s 是否为数字 - 参数: - :param s: 文本 - """ - if isinstance(s, int): - return True - try: - float(s) - return True - except ValueError: - pass - try: - import unicodedata - - unicodedata.numeric(s) - return True - except (TypeError, ValueError): - pass - return False - - -def get_bot(id_: Optional[str] = None) -> Optional[Bot]: - """ - 说明: - 获取 bot 对象 - """ - try: - return nonebot.get_bot(id_) - except ValueError: - return None - - -def get_matchers(distinct: bool = False) -> List[Type[Matcher]]: - """ - 说明: - 获取所有matcher - 参数: - distinct: 去重 - """ - _matchers = [] - temp = [] - for i in matchers.keys(): - for matcher in matchers[i]: - if distinct and matcher.plugin_name in temp: - continue - temp.append(matcher.plugin_name) - _matchers.append(matcher) - return _matchers - - -def get_message_at(data: Union[str, Message]) -> List[int]: - """ - 说明: - 获取消息中所有的 at 对象的 qq - 参数: - :param data: event.json(), event.message - """ - qq_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg and msg.get("type") == "at": - qq_list.append(int(msg["data"]["qq"])) - else: - for seg in data: - if seg.type == "at": - qq_list.append(seg.data["qq"]) - return qq_list - - -def get_message_img(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有的 图片 的链接 - 参数: - :param data: event.json() - """ - img_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "image": - img_list.append(msg["data"]["url"]) - else: - for seg in data["image"]: - img_list.append(seg.data["url"]) - return img_list - - -def get_message_face(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有的 face Id - 参数: - :param data: event.json() - """ - face_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "face": - face_list.append(msg["data"]["id"]) - else: - for seg in data["face"]: - face_list.append(seg.data["id"]) - return face_list - - -def get_message_img_file(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有的 图片file - 参数: - :param data: event.json() - """ - file_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "image": - file_list.append(msg["data"]["file"]) - else: - for seg in data["image"]: - file_list.append(seg.data["file"]) - return file_list - - -def get_message_text(data: Union[str, Message]) -> str: - """ - 说明: - 获取消息中 纯文本 的信息 - 参数: - :param data: event.json() - """ - result = "" - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - if isinstance(message, str): - return message.strip() - for msg in message: - if msg["type"] == "text": - result += msg["data"]["text"].strip() + " " - return result.strip() - else: - for seg in data["text"]: - result += seg.data["text"] + " " - return result.strip() - - -def get_message_record(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有 语音 的链接 - 参数: - :param data: event.json() - """ - record_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "record": - record_list.append(msg["data"]["url"]) - else: - for seg in data["record"]: - record_list.append(seg.data["url"]) - return record_list - - -def get_message_json(data: str) -> List[dict]: - """ - 说明: - 获取消息中所有 json - 参数: - :param data: event.json() - """ - try: - json_list = [] - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "json": - json_list.append(msg["data"]) - return json_list - except KeyError: - return [] - - -def get_local_proxy() -> Optional[str]: - """ - 说明: - 获取 config.py 中设置的代理 - """ - return SYSTEM_PROXY or None - - -def is_chinese(word: str) -> bool: - """ - 说明: - 判断字符串是否为纯中文 - 参数: - :param word: 文本 - """ - for ch in word: - if not "\u4e00" <= ch <= "\u9fff": - return False - return True - - -async def get_user_avatar(qq: Union[int, str]) -> Optional[bytes]: - """ - 说明: - 快捷获取用户头像 - 参数: - :param qq: qq号 - """ - url = f"http://q1.qlogo.cn/g?b=qq&nk={qq}&s=160" - async with httpx.AsyncClient() as client: - for _ in range(3): - try: - return (await client.get(url)).content - except Exception as e: - logger.error("获取用户头像错误", "Util", target=qq) - return None - - -async def get_group_avatar(group_id: int) -> Optional[bytes]: - """ - 说明: - 快捷获取用群头像 - 参数: - :param group_id: 群号 - """ - url = f"http://p.qlogo.cn/gh/{group_id}/{group_id}/640/" - async with httpx.AsyncClient() as client: - for _ in range(3): - try: - return (await client.get(url)).content - except Exception as e: - logger.error("获取群头像错误", "Util", target=group_id) - return None - - -def cn2py(word: str) -> str: - """ - 说明: - 将字符串转化为拼音 - 参数: - :param word: 文本 - """ - temp = "" - for i in pypinyin.pinyin(word, style=pypinyin.NORMAL): - temp += "".join(i) - return temp - - -def change_pixiv_image_links( - url: str, size: Optional[str] = None, nginx_url: Optional[str] = None -): - """ - 说明: - 根据配置改变图片大小和反代链接 - 参数: - :param url: 图片原图链接 - :param size: 模式 - :param nginx_url: 反代 - """ - if size == "master": - img_sp = url.rsplit(".", maxsplit=1) - url = img_sp[0] - img_type = img_sp[1] - url = url.replace("original", "master") + f"_master1200.{img_type}" - if not nginx_url: - nginx_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") - if nginx_url: - url = ( - url.replace("i.pximg.net", nginx_url) - .replace("i.pixiv.cat", nginx_url) - .replace("_webp", "") - ) - return url - - -def change_img_md5(path_file: Union[str, Path]) -> bool: - """ - 说明: - 改变图片MD5 - 参数: - :param path_file: 图片路径 - """ - try: - with open(path_file, "a") as f: - f.write(str(int(time.time() * 1000))) - return True - except Exception as e: - logger.warning(f"改变图片MD5错误 Path:{path_file}", e=e) - return False - - -async def broadcast_group( - message: Union[str, Message, MessageSegment], - bot: Optional[Union[Bot, List[Bot]]] = None, - bot_id: Optional[Union[str, Set[str]]] = None, - ignore_group: Optional[Set[int]] = None, - check_func: Optional[Callable[[int], bool]] = None, - log_cmd: Optional[str] = None, -): - """获取所有Bot或指定Bot对象广播群聊 - - Args: - message (Any): 广播消息内容 - bot (Optional[Bot], optional): 指定bot对象. Defaults to None. - bot_id (Optional[str], optional): 指定bot id. Defaults to None. - ignore_group (Optional[List[int]], optional): 忽略群聊列表. Defaults to None. - check_func (Optional[Callable[[int], bool]], optional): 发送前对群聊检测方法,判断是否发送. Defaults to None. - log_cmd (Optional[str], optional): 日志标记. Defaults to None. - """ - if not message: - raise ValueError("群聊广播消息不能为空") - bot_dict = nonebot.get_bots() - bot_list: List[Bot] = [] - if bot: - if isinstance(bot, list): - bot_list = bot - else: - bot_list.append(bot) - elif bot_id: - _bot_id_list = bot_id - if isinstance(bot_id, str): - _bot_id_list = [bot_id] - for id_ in _bot_id_list: - if bot_id in bot_dict: - bot_list.append(bot_dict[bot_id]) - else: - logger.warning(f"Bot:{id_} 对象未连接或不存在") - else: - bot_list = list(bot_dict.values()) - _used_group = [] - for _bot in bot_list: - try: - if _group_list := await _bot.get_group_list(): - group_id_list = [g["group_id"] for g in _group_list] - for group_id in set(group_id_list): - try: - if ( - ignore_group and group_id in ignore_group - ) or group_id in _used_group: - continue - if check_func and not check_func(group_id): - continue - _used_group.append(group_id) - await _bot.send_group_msg(group_id=group_id, message=message) - except Exception as e: - logger.error( - f"广播群发消息失败: {message}", - command=log_cmd, - group_id=group_id, - e=e, - ) - except Exception as e: - logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e) - - -async def broadcast_superuser( - message: Union[str, Message, MessageSegment], - bot: Optional[Union[Bot, List[Bot]]] = None, - bot_id: Optional[Union[str, Set[str]]] = None, - ignore_superuser: Optional[Set[int]] = None, - check_func: Optional[Callable[[int], bool]] = None, - log_cmd: Optional[str] = None, -): - """获取所有Bot或指定Bot对象广播超级用户 - - Args: - message (Any): 广播消息内容 - bot (Optional[Bot], optional): 指定bot对象. Defaults to None. - bot_id (Optional[str], optional): 指定bot id. Defaults to None. - ignore_superuser (Optional[List[int]], optional): 忽略的超级用户id. Defaults to None. - check_func (Optional[Callable[[int], bool]], optional): 发送前对群聊检测方法,判断是否发送. Defaults to None. - log_cmd (Optional[str], optional): 日志标记. Defaults to None. - """ - if not message: - raise ValueError("超级用户广播消息不能为空") - bot_dict = nonebot.get_bots() - bot_list: List[Bot] = [] - if bot: - if isinstance(bot, list): - bot_list = bot - else: - bot_list.append(bot) - elif bot_id: - _bot_id_list = bot_id - if isinstance(bot_id, str): - _bot_id_list = [bot_id] - for id_ in _bot_id_list: - if bot_id in bot_dict: - bot_list.append(bot_dict[bot_id]) - else: - logger.warning(f"Bot:{id_} 对象未连接或不存在") - else: - bot_list = list(bot_dict.values()) - _used_user = [] - for _bot in bot_list: - try: - for user_id in _bot.config.superusers: - try: - if ( - ignore_superuser and int(user_id) in ignore_superuser - ) or user_id in _used_user: - continue - if check_func and not check_func(int(user_id)): - continue - _used_user.append(user_id) - await _bot.send_private_message( - user_id=int(user_id), message=message - ) - except Exception as e: - logger.error( - f"广播超级用户发消息失败: {message}", - command=log_cmd, - user_id=user_id, - e=e, - ) - except Exception as e: - logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e) diff --git a/utils/zh_wiki.py b/utils/zh_wiki.py deleted file mode 100755 index 4f5720f7..00000000 --- a/utils/zh_wiki.py +++ /dev/null @@ -1,8275 +0,0 @@ -# -*- coding: utf-8 -*- -# copy fom wikipedia - -zh2Hant = { -'呆': '獃', -"打印机": "印表機", -'帮助文件': '說明檔案', -"画": "畫", -"龙": "竜", -"板": "板", -"表": "表", -"才": "才", -"丑": "醜", -"出": "出", -"淀": "澱", -"冬": "冬", -"范": "範", -"丰": "豐", -"刮": "刮", -"后": "後", -"胡": "胡", -"回": "回", -"伙": "夥", -"姜": "薑", -"借": "借", -"克": "克", -"困": "困", -"漓": "漓", -"里": "里", -"帘": "簾", -"霉": "霉", -"面": "面", -"蔑": "蔑", -"千": "千", -"秋": "秋", -"松": "松", -"咸": "咸", -"向": "向", -"余": "餘", -"郁": "鬱", -"御": "御", -"愿": "願", -"云": "雲", -"芸": "芸", -"沄": "沄", -"致": "致", -"制": "制", -"朱": "朱", -"筑": "築", -"准": "準", -"厂": "廠", -"广": "廣", -"辟": "闢", -"别": "別", -"卜": "卜", -"沈": "沈", -"冲": "沖", -"种": "種", -"虫": "蟲", -"担": "擔", -"党": "黨", -"斗": "鬥", -"儿": "兒", -"干": "乾", -"谷": "谷", -"柜": "櫃", -"合": "合", -"划": "劃", -"坏": "壞", -"几": "幾", -"系": "系", -"家": "家", -"价": "價", -"据": "據", -"卷": "捲", -"适": "適", -"蜡": "蠟", -"腊": "臘", -"了": "了", -"累": "累", -"么": "麽", -"蒙": "蒙", -"万": "萬", -"宁": "寧", -"朴": "樸", -"苹": "蘋", -"仆": "僕", -"曲": "曲", -"确": "確", -"舍": "舍", -"胜": "勝", -"术": "術", -"台": "台", -"体": "體", -"涂": "塗", -"叶": "葉", -"吁": "吁", -"旋": "旋", -"佣": "傭", -"与": "與", -"折": "折", -"征": "徵", -"症": "症", -"恶": "惡", -"发": "發", -"复": "復", -"汇": "匯", -"获": "獲", -"饥": "飢", -"尽": "盡", -"历": "歷", -"卤": "滷", -"弥": "彌", -"签": "簽", -"纤": "纖", -"苏": "蘇", -"坛": "壇", -"团": "團", -"须": "須", -"脏": "臟", -"只": "只", -"钟": "鐘", -"药": "藥", -"同": "同", -"志": "志", -"杯": "杯", -"岳": "岳", -"布": "布", -"当": "當", -"吊": "弔", -"仇": "仇", -"蕴": "蘊", -"线": "線", -"为": "為", -"产": "產", -"众": "眾", -"伪": "偽", -"凫": "鳧", -"厕": "廁", -"启": "啟", -"墙": "牆", -"壳": "殼", -"奖": "獎", -"妫": "媯", -"并": "並", -"录": "錄", -"悫": "愨", -"极": "極", -"沩": "溈", -"瘘": "瘺", -"硷": "鹼", -"竖": "豎", -"绝": "絕", -"绣": "繡", -"绦": "絛", -"绱": "緔", -"绷": "綳", -"绿": "綠", -"缰": "韁", -"苧": "苎", -"莼": "蒓", -"说": "說", -"谣": "謠", -"谫": "譾", -"赃": "贓", -"赍": "齎", -"赝": "贗", -"酝": "醞", -"采": "採", -"钩": "鉤", -"钵": "缽", -"锈": "銹", -"锐": "銳", -"锨": "杴", -"镌": "鐫", -"镢": "钁", -"阅": "閱", -"颓": "頹", -"颜": "顏", -"骂": "罵", -"鲇": "鯰", -"鲞": "鯗", -"鳄": "鱷", -"鸡": "雞", -"鹚": "鶿", -"荡": "盪", -"锤": "錘", -"㟆": "㠏", -"㛟": "𡞵", -"专": "專", -"业": "業", -"丛": "叢", -"东": "東", -"丝": "絲", -"丢": "丟", -"两": "兩", -"严": "嚴", -"丧": "喪", -"个": "個", -"临": "臨", -"丽": "麗", -"举": "舉", -"义": "義", -"乌": "烏", -"乐": "樂", -"乔": "喬", -"习": "習", -"乡": "鄉", -"书": "書", -"买": "買", -"乱": "亂", -"争": "爭", -"于": "於", -"亏": "虧", -"亚": "亞", -"亩": "畝", -"亲": "親", -"亵": "褻", -"亸": "嚲", -"亿": "億", -"仅": "僅", -"从": "從", -"仑": "侖", -"仓": "倉", -"仪": "儀", -"们": "們", -"优": "優", -"会": "會", -"伛": "傴", -"伞": "傘", -"伟": "偉", -"传": "傳", -"伣": "俔", -"伤": "傷", -"伥": "倀", -"伦": "倫", -"伧": "傖", -"伫": "佇", -"佥": "僉", -"侠": "俠", -"侣": "侶", -"侥": "僥", -"侦": "偵", -"侧": "側", -"侨": "僑", -"侩": "儈", -"侪": "儕", -"侬": "儂", -"俣": "俁", -"俦": "儔", -"俨": "儼", -"俩": "倆", -"俪": "儷", -"俫": "倈", -"俭": "儉", -"债": "債", -"倾": "傾", -"偬": "傯", -"偻": "僂", -"偾": "僨", -"偿": "償", -"傥": "儻", -"傧": "儐", -"储": "儲", -"傩": "儺", -"㑩": "儸", -"兑": "兌", -"兖": "兗", -"兰": "蘭", -"关": "關", -"兴": "興", -"兹": "茲", -"养": "養", -"兽": "獸", -"冁": "囅", -"内": "內", -"冈": "岡", -"册": "冊", -"写": "寫", -"军": "軍", -"农": "農", -"冯": "馮", -"决": "決", -"况": "況", -"冻": "凍", -"净": "凈", -"凉": "涼", -"减": "減", -"凑": "湊", -"凛": "凜", -"凤": "鳳", -"凭": "憑", -"凯": "凱", -"击": "擊", -"凿": "鑿", -"刍": "芻", -"刘": "劉", -"则": "則", -"刚": "剛", -"创": "創", -"删": "刪", -"刬": "剗", -"刭": "剄", -"刹": "剎", -"刽": "劊", -"刿": "劌", -"剀": "剴", -"剂": "劑", -"剐": "剮", -"剑": "劍", -"剥": "剝", -"剧": "劇", -"㓥": "劏", -"㔉": "劚", -"劝": "勸", -"办": "辦", -"务": "務", -"劢": "勱", -"动": "動", -"励": "勵", -"劲": "勁", -"劳": "勞", -"势": "勢", -"勋": "勛", -"勚": "勩", -"匀": "勻", -"匦": "匭", -"匮": "匱", -"区": "區", -"医": "醫", -"华": "華", -"协": "協", -"单": "單", -"卖": "賣", -"卢": "盧", -"卫": "衛", -"却": "卻", -"厅": "廳", -"厉": "厲", -"压": "壓", -"厌": "厭", -"厍": "厙", -"厐": "龎", -"厘": "釐", -"厢": "廂", -"厣": "厴", -"厦": "廈", -"厨": "廚", -"厩": "廄", -"厮": "廝", -"县": "縣", -"叁": "叄", -"参": "參", -"双": "雙", -"变": "變", -"叙": "敘", -"叠": "疊", -"号": "號", -"叹": "嘆", -"叽": "嘰", -"吓": "嚇", -"吕": "呂", -"吗": "嗎", -"吣": "唚", -"吨": "噸", -"听": "聽", -"吴": "吳", -"呐": "吶", -"呒": "嘸", -"呓": "囈", -"呕": "嘔", -"呖": "嚦", -"呗": "唄", -"员": "員", -"呙": "咼", -"呛": "嗆", -"呜": "嗚", -"咏": "詠", -"咙": "嚨", -"咛": "嚀", -"咝": "噝", -"响": "響", -"哑": "啞", -"哒": "噠", -"哓": "嘵", -"哔": "嗶", -"哕": "噦", -"哗": "嘩", -"哙": "噲", -"哜": "嚌", -"哝": "噥", -"哟": "喲", -"唛": "嘜", -"唝": "嗊", -"唠": "嘮", -"唡": "啢", -"唢": "嗩", -"唤": "喚", -"啧": "嘖", -"啬": "嗇", -"啭": "囀", -"啮": "嚙", -"啴": "嘽", -"啸": "嘯", -"㖞": "喎", -"喷": "噴", -"喽": "嘍", -"喾": "嚳", -"嗫": "囁", -"嗳": "噯", -"嘘": "噓", -"嘤": "嚶", -"嘱": "囑", -"㖊": "噚", -"噜": "嚕", -"嚣": "囂", -"园": "園", -"囱": "囪", -"围": "圍", -"囵": "圇", -"国": "國", -"图": "圖", -"圆": "圓", -"圣": "聖", -"圹": "壙", -"场": "場", -"坂": "阪", -"块": "塊", -"坚": "堅", -"坜": "壢", -"坝": "壩", -"坞": "塢", -"坟": "墳", -"坠": "墜", -"垄": "壟", -"垅": "壠", -"垆": "壚", -"垒": "壘", -"垦": "墾", -"垩": "堊", -"垫": "墊", -"垭": "埡", -"垱": "壋", -"垲": "塏", -"垴": "堖", -"埘": "塒", -"埙": "塤", -"埚": "堝", -"埯": "垵", -"堑": "塹", -"堕": "墮", -"𡒄": "壈", -"壮": "壯", -"声": "聲", -"壶": "壺", -"壸": "壼", -"处": "處", -"备": "備", -"够": "夠", -"头": "頭", -"夸": "誇", -"夹": "夾", -"夺": "奪", -"奁": "奩", -"奂": "奐", -"奋": "奮", -"奥": "奧", -"奸": "姦", -"妆": "妝", -"妇": "婦", -"妈": "媽", -"妩": "嫵", -"妪": "嫗", -"姗": "姍", -"姹": "奼", -"娄": "婁", -"娅": "婭", -"娆": "嬈", -"娇": "嬌", -"娈": "孌", -"娱": "娛", -"娲": "媧", -"娴": "嫻", -"婳": "嫿", -"婴": "嬰", -"婵": "嬋", -"婶": "嬸", -"媪": "媼", -"嫒": "嬡", -"嫔": "嬪", -"嫱": "嬙", -"嬷": "嬤", -"孙": "孫", -"学": "學", -"孪": "孿", -"宝": "寶", -"实": "實", -"宠": "寵", -"审": "審", -"宪": "憲", -"宫": "宮", -"宽": "寬", -"宾": "賓", -"寝": "寢", -"对": "對", -"寻": "尋", -"导": "導", -"寿": "壽", -"将": "將", -"尔": "爾", -"尘": "塵", -"尝": "嘗", -"尧": "堯", -"尴": "尷", -"尸": "屍", -"层": "層", -"屃": "屓", -"屉": "屜", -"届": "屆", -"属": "屬", -"屡": "屢", -"屦": "屨", -"屿": "嶼", -"岁": "歲", -"岂": "豈", -"岖": "嶇", -"岗": "崗", -"岘": "峴", -"岙": "嶴", -"岚": "嵐", -"岛": "島", -"岭": "嶺", -"岽": "崬", -"岿": "巋", -"峄": "嶧", -"峡": "峽", -"峣": "嶢", -"峤": "嶠", -"峥": "崢", -"峦": "巒", -"崂": "嶗", -"崃": "崍", -"崄": "嶮", -"崭": "嶄", -"嵘": "嶸", -"嵚": "嶔", -"嵝": "嶁", -"巅": "巔", -"巩": "鞏", -"巯": "巰", -"币": "幣", -"帅": "帥", -"师": "師", -"帏": "幃", -"帐": "帳", -"帜": "幟", -"带": "帶", -"帧": "幀", -"帮": "幫", -"帱": "幬", -"帻": "幘", -"帼": "幗", -"幂": "冪", -"庄": "莊", -"庆": "慶", -"庐": "廬", -"庑": "廡", -"库": "庫", -"应": "應", -"庙": "廟", -"庞": "龐", -"废": "廢", -"廪": "廩", -"开": "開", -"异": "異", -"弃": "棄", -"弑": "弒", -"张": "張", -"弪": "弳", -"弯": "彎", -"弹": "彈", -"强": "強", -"归": "歸", -"彝": "彞", -"彦": "彥", -"彻": "徹", -"径": "徑", -"徕": "徠", -"忆": "憶", -"忏": "懺", -"忧": "憂", -"忾": "愾", -"怀": "懷", -"态": "態", -"怂": "慫", -"怃": "憮", -"怄": "慪", -"怅": "悵", -"怆": "愴", -"怜": "憐", -"总": "總", -"怼": "懟", -"怿": "懌", -"恋": "戀", -"恒": "恆", -"恳": "懇", -"恸": "慟", -"恹": "懨", -"恺": "愷", -"恻": "惻", -"恼": "惱", -"恽": "惲", -"悦": "悅", -"悬": "懸", -"悭": "慳", -"悮": "悞", -"悯": "憫", -"惊": "驚", -"惧": "懼", -"惨": "慘", -"惩": "懲", -"惫": "憊", -"惬": "愜", -"惭": "慚", -"惮": "憚", -"惯": "慣", -"愠": "慍", -"愤": "憤", -"愦": "憒", -"慑": "懾", -"懑": "懣", -"懒": "懶", -"懔": "懍", -"戆": "戇", -"戋": "戔", -"戏": "戲", -"戗": "戧", -"战": "戰", -"戬": "戩", -"戯": "戱", -"户": "戶", -"扑": "撲", -"执": "執", -"扩": "擴", -"扪": "捫", -"扫": "掃", -"扬": "揚", -"扰": "擾", -"抚": "撫", -"抛": "拋", -"抟": "摶", -"抠": "摳", -"抡": "掄", -"抢": "搶", -"护": "護", -"报": "報", -"拟": "擬", -"拢": "攏", -"拣": "揀", -"拥": "擁", -"拦": "攔", -"拧": "擰", -"拨": "撥", -"择": "擇", -"挂": "掛", -"挚": "摯", -"挛": "攣", -"挜": "掗", -"挝": "撾", -"挞": "撻", -"挟": "挾", -"挠": "撓", -"挡": "擋", -"挢": "撟", -"挣": "掙", -"挤": "擠", -"挥": "揮", -"挦": "撏", -"挽": "輓", -"捝": "挩", -"捞": "撈", -"损": "損", -"捡": "撿", -"换": "換", -"捣": "搗", -"掳": "擄", -"掴": "摑", -"掷": "擲", -"掸": "撣", -"掺": "摻", -"掼": "摜", -"揽": "攬", -"揾": "搵", -"揿": "撳", -"搀": "攙", -"搁": "擱", -"搂": "摟", -"搅": "攪", -"携": "攜", -"摄": "攝", -"摅": "攄", -"摆": "擺", -"摇": "搖", -"摈": "擯", -"摊": "攤", -"撄": "攖", -"撑": "撐", -"㧑": "撝", -"撵": "攆", -"撷": "擷", -"撸": "擼", -"撺": "攛", -"㧟": "擓", -"擞": "擻", -"攒": "攢", -"敌": "敵", -"敛": "斂", -"数": "數", -"斋": "齋", -"斓": "斕", -"斩": "斬", -"断": "斷", -"无": "無", -"旧": "舊", -"时": "時", -"旷": "曠", -"旸": "暘", -"昙": "曇", -"昼": "晝", -"昽": "曨", -"显": "顯", -"晋": "晉", -"晒": "曬", -"晓": "曉", -"晔": "曄", -"晕": "暈", -"晖": "暉", -"暂": "暫", -"暧": "曖", -"机": "機", -"杀": "殺", -"杂": "雜", -"权": "權", -"杆": "桿", -"条": "條", -"来": "來", -"杨": "楊", -"杩": "榪", -"杰": "傑", -"构": "構", -"枞": "樅", -"枢": "樞", -"枣": "棗", -"枥": "櫪", -"枧": "梘", -"枨": "棖", -"枪": "槍", -"枫": "楓", -"枭": "梟", -"柠": "檸", -"柽": "檉", -"栀": "梔", -"栅": "柵", -"标": "標", -"栈": "棧", -"栉": "櫛", -"栊": "櫳", -"栋": "棟", -"栌": "櫨", -"栎": "櫟", -"栏": "欄", -"树": "樹", -"栖": "棲", -"栗": "慄", -"样": "樣", -"栾": "欒", -"桠": "椏", -"桡": "橈", -"桢": "楨", -"档": "檔", -"桤": "榿", -"桥": "橋", -"桦": "樺", -"桧": "檜", -"桨": "槳", -"桩": "樁", -"梦": "夢", -"梼": "檮", -"梾": "棶", -"梿": "槤", -"检": "檢", -"棁": "梲", -"棂": "欞", -"椁": "槨", -"椟": "櫝", -"椠": "槧", -"椤": "欏", -"椭": "橢", -"楼": "樓", -"榄": "欖", -"榅": "榲", -"榇": "櫬", -"榈": "櫚", -"榉": "櫸", -"槚": "檟", -"槛": "檻", -"槟": "檳", -"槠": "櫧", -"横": "橫", -"樯": "檣", -"樱": "櫻", -"橥": "櫫", -"橱": "櫥", -"橹": "櫓", -"橼": "櫞", -"檩": "檁", -"欢": "歡", -"欤": "歟", -"欧": "歐", -"歼": "殲", -"殁": "歿", -"殇": "殤", -"残": "殘", -"殒": "殞", -"殓": "殮", -"殚": "殫", -"殡": "殯", -"㱮": "殨", -"㱩": "殰", -"殴": "毆", -"毁": "毀", -"毂": "轂", -"毕": "畢", -"毙": "斃", -"毡": "氈", -"毵": "毿", -"氇": "氌", -"气": "氣", -"氢": "氫", -"氩": "氬", -"氲": "氳", -"汉": "漢", -"汤": "湯", -"汹": "洶", -"沟": "溝", -"没": "沒", -"沣": "灃", -"沤": "漚", -"沥": "瀝", -"沦": "淪", -"沧": "滄", -"沪": "滬", -"泞": "濘", -"注": "註", -"泪": "淚", -"泶": "澩", -"泷": "瀧", -"泸": "瀘", -"泺": "濼", -"泻": "瀉", -"泼": "潑", -"泽": "澤", -"泾": "涇", -"洁": "潔", -"洒": "灑", -"洼": "窪", -"浃": "浹", -"浅": "淺", -"浆": "漿", -"浇": "澆", -"浈": "湞", -"浊": "濁", -"测": "測", -"浍": "澮", -"济": "濟", -"浏": "瀏", -"浐": "滻", -"浑": "渾", -"浒": "滸", -"浓": "濃", -"浔": "潯", -"涛": "濤", -"涝": "澇", -"涞": "淶", -"涟": "漣", -"涠": "潿", -"涡": "渦", -"涣": "渙", -"涤": "滌", -"润": "潤", -"涧": "澗", -"涨": "漲", -"涩": "澀", -"渊": "淵", -"渌": "淥", -"渍": "漬", -"渎": "瀆", -"渐": "漸", -"渑": "澠", -"渔": "漁", -"渖": "瀋", -"渗": "滲", -"温": "溫", -"湾": "灣", -"湿": "濕", -"溃": "潰", -"溅": "濺", -"溆": "漵", -"滗": "潷", -"滚": "滾", -"滞": "滯", -"滟": "灧", -"滠": "灄", -"满": "滿", -"滢": "瀅", -"滤": "濾", -"滥": "濫", -"滦": "灤", -"滨": "濱", -"滩": "灘", -"滪": "澦", -"漤": "灠", -"潆": "瀠", -"潇": "瀟", -"潋": "瀲", -"潍": "濰", -"潜": "潛", -"潴": "瀦", -"澜": "瀾", -"濑": "瀨", -"濒": "瀕", -"㲿": "瀇", -"灏": "灝", -"灭": "滅", -"灯": "燈", -"灵": "靈", -"灶": "竈", -"灾": "災", -"灿": "燦", -"炀": "煬", -"炉": "爐", -"炖": "燉", -"炜": "煒", -"炝": "熗", -"点": "點", -"炼": "煉", -"炽": "熾", -"烁": "爍", -"烂": "爛", -"烃": "烴", -"烛": "燭", -"烟": "煙", -"烦": "煩", -"烧": "燒", -"烨": "燁", -"烩": "燴", -"烫": "燙", -"烬": "燼", -"热": "熱", -"焕": "煥", -"焖": "燜", -"焘": "燾", -"㶽": "煱", -"煴": "熅", -"㶶": "燶", -"爱": "愛", -"爷": "爺", -"牍": "牘", -"牦": "氂", -"牵": "牽", -"牺": "犧", -"犊": "犢", -"状": "狀", -"犷": "獷", -"犸": "獁", -"犹": "猶", -"狈": "狽", -"狝": "獮", -"狞": "獰", -"独": "獨", -"狭": "狹", -"狮": "獅", -"狯": "獪", -"狰": "猙", -"狱": "獄", -"狲": "猻", -"猃": "獫", -"猎": "獵", -"猕": "獼", -"猡": "玀", -"猪": "豬", -"猫": "貓", -"猬": "蝟", -"献": "獻", -"獭": "獺", -"㺍": "獱", -"玑": "璣", -"玚": "瑒", -"玛": "瑪", -"玮": "瑋", -"环": "環", -"现": "現", -"玱": "瑲", -"玺": "璽", -"珐": "琺", -"珑": "瓏", -"珰": "璫", -"珲": "琿", -"琏": "璉", -"琐": "瑣", -"琼": "瓊", -"瑶": "瑤", -"瑷": "璦", -"璎": "瓔", -"瓒": "瓚", -"瓯": "甌", -"电": "電", -"画": "畫", -"畅": "暢", -"畴": "疇", -"疖": "癤", -"疗": "療", -"疟": "瘧", -"疠": "癘", -"疡": "瘍", -"疬": "癧", -"疭": "瘲", -"疮": "瘡", -"疯": "瘋", -"疱": "皰", -"疴": "痾", -"痈": "癰", -"痉": "痙", -"痒": "癢", -"痖": "瘂", -"痨": "癆", -"痪": "瘓", -"痫": "癇", -"瘅": "癉", -"瘆": "瘮", -"瘗": "瘞", -"瘪": "癟", -"瘫": "癱", -"瘾": "癮", -"瘿": "癭", -"癞": "癩", -"癣": "癬", -"癫": "癲", -"皑": "皚", -"皱": "皺", -"皲": "皸", -"盏": "盞", -"盐": "鹽", -"监": "監", -"盖": "蓋", -"盗": "盜", -"盘": "盤", -"眍": "瞘", -"眦": "眥", -"眬": "矓", -"睁": "睜", -"睐": "睞", -"睑": "瞼", -"瞆": "瞶", -"瞒": "瞞", -"䁖": "瞜", -"瞩": "矚", -"矫": "矯", -"矶": "磯", -"矾": "礬", -"矿": "礦", -"砀": "碭", -"码": "碼", -"砖": "磚", -"砗": "硨", -"砚": "硯", -"砜": "碸", -"砺": "礪", -"砻": "礱", -"砾": "礫", -"础": "礎", -"硁": "硜", -"硕": "碩", -"硖": "硤", -"硗": "磽", -"硙": "磑", -"碍": "礙", -"碛": "磧", -"碜": "磣", -"碱": "鹼", -"礼": "禮", -"祃": "禡", -"祎": "禕", -"祢": "禰", -"祯": "禎", -"祷": "禱", -"祸": "禍", -"禀": "稟", -"禄": "祿", -"禅": "禪", -"离": "離", -"秃": "禿", -"秆": "稈", -"积": "積", -"称": "稱", -"秽": "穢", -"秾": "穠", -"稆": "穭", -"税": "稅", -"䅉": "稏", -"稣": "穌", -"稳": "穩", -"穑": "穡", -"穷": "窮", -"窃": "竊", -"窍": "竅", -"窎": "窵", -"窑": "窯", -"窜": "竄", -"窝": "窩", -"窥": "窺", -"窦": "竇", -"窭": "窶", -"竞": "競", -"笃": "篤", -"笋": "筍", -"笔": "筆", -"笕": "筧", -"笺": "箋", -"笼": "籠", -"笾": "籩", -"筚": "篳", -"筛": "篩", -"筜": "簹", -"筝": "箏", -"䇲": "筴", -"筹": "籌", -"筼": "篔", -"简": "簡", -"箓": "籙", -"箦": "簀", -"箧": "篋", -"箨": "籜", -"箩": "籮", -"箪": "簞", -"箫": "簫", -"篑": "簣", -"篓": "簍", -"篮": "籃", -"篱": "籬", -"簖": "籪", -"籁": "籟", -"籴": "糴", -"类": "類", -"籼": "秈", -"粜": "糶", -"粝": "糲", -"粤": "粵", -"粪": "糞", -"粮": "糧", -"糁": "糝", -"糇": "餱", -"紧": "緊", -"䌷": "紬", -"䌹": "絅", -"絷": "縶", -"䌼": "綐", -"䌽": "綵", -"䌸": "縳", -"䍁": "繸", -"䍀": "繿", -"纟": "糹", -"纠": "糾", -"纡": "紆", -"红": "紅", -"纣": "紂", -"纥": "紇", -"约": "約", -"级": "級", -"纨": "紈", -"纩": "纊", -"纪": "紀", -"纫": "紉", -"纬": "緯", -"纭": "紜", -"纮": "紘", -"纯": "純", -"纰": "紕", -"纱": "紗", -"纲": "綱", -"纳": "納", -"纴": "紝", -"纵": "縱", -"纶": "綸", -"纷": "紛", -"纸": "紙", -"纹": "紋", -"纺": "紡", -"纻": "紵", -"纼": "紖", -"纽": "紐", -"纾": "紓", -"绀": "紺", -"绁": "紲", -"绂": "紱", -"练": "練", -"组": "組", -"绅": "紳", -"细": "細", -"织": "織", -"终": "終", -"绉": "縐", -"绊": "絆", -"绋": "紼", -"绌": "絀", -"绍": "紹", -"绎": "繹", -"经": "經", -"绐": "紿", -"绑": "綁", -"绒": "絨", -"结": "結", -"绔": "絝", -"绕": "繞", -"绖": "絰", -"绗": "絎", -"绘": "繪", -"给": "給", -"绚": "絢", -"绛": "絳", -"络": "絡", -"绞": "絞", -"统": "統", -"绠": "綆", -"绡": "綃", -"绢": "絹", -"绤": "綌", -"绥": "綏", -"继": "繼", -"绨": "綈", -"绩": "績", -"绪": "緒", -"绫": "綾", -"绬": "緓", -"续": "續", -"绮": "綺", -"绯": "緋", -"绰": "綽", -"绲": "緄", -"绳": "繩", -"维": "維", -"绵": "綿", -"绶": "綬", -"绸": "綢", -"绹": "綯", -"绺": "綹", -"绻": "綣", -"综": "綜", -"绽": "綻", -"绾": "綰", -"缀": "綴", -"缁": "緇", -"缂": "緙", -"缃": "緗", -"缄": "緘", -"缅": "緬", -"缆": "纜", -"缇": "緹", -"缈": "緲", -"缉": "緝", -"缊": "縕", -"缋": "繢", -"缌": "緦", -"缍": "綞", -"缎": "緞", -"缏": "緶", -"缑": "緱", -"缒": "縋", -"缓": "緩", -"缔": "締", -"缕": "縷", -"编": "編", -"缗": "緡", -"缘": "緣", -"缙": "縉", -"缚": "縛", -"缛": "縟", -"缜": "縝", -"缝": "縫", -"缞": "縗", -"缟": "縞", -"缠": "纏", -"缡": "縭", -"缢": "縊", -"缣": "縑", -"缤": "繽", -"缥": "縹", -"缦": "縵", -"缧": "縲", -"缨": "纓", -"缩": "縮", -"缪": "繆", -"缫": "繅", -"缬": "纈", -"缭": "繚", -"缮": "繕", -"缯": "繒", -"缱": "繾", -"缲": "繰", -"缳": "繯", -"缴": "繳", -"缵": "纘", -"罂": "罌", -"网": "網", -"罗": "羅", -"罚": "罰", -"罢": "罷", -"罴": "羆", -"羁": "羈", -"羟": "羥", -"翘": "翹", -"耢": "耮", -"耧": "耬", -"耸": "聳", -"耻": "恥", -"聂": "聶", -"聋": "聾", -"职": "職", -"聍": "聹", -"联": "聯", -"聩": "聵", -"聪": "聰", -"肃": "肅", -"肠": "腸", -"肤": "膚", -"肮": "骯", -"肴": "餚", -"肾": "腎", -"肿": "腫", -"胀": "脹", -"胁": "脅", -"胆": "膽", -"胧": "朧", -"胨": "腖", -"胪": "臚", -"胫": "脛", -"胶": "膠", -"脉": "脈", -"脍": "膾", -"脐": "臍", -"脑": "腦", -"脓": "膿", -"脔": "臠", -"脚": "腳", -"脱": "脫", -"脶": "腡", -"脸": "臉", -"腭": "齶", -"腻": "膩", -"腼": "靦", -"腽": "膃", -"腾": "騰", -"膑": "臏", -"臜": "臢", -"舆": "輿", -"舣": "艤", -"舰": "艦", -"舱": "艙", -"舻": "艫", -"艰": "艱", -"艳": "艷", -"艺": "藝", -"节": "節", -"芈": "羋", -"芗": "薌", -"芜": "蕪", -"芦": "蘆", -"苁": "蓯", -"苇": "葦", -"苈": "藶", -"苋": "莧", -"苌": "萇", -"苍": "蒼", -"苎": "苧", -"茎": "莖", -"茏": "蘢", -"茑": "蔦", -"茔": "塋", -"茕": "煢", -"茧": "繭", -"荆": "荊", -"荐": "薦", -"荙": "薘", -"荚": "莢", -"荛": "蕘", -"荜": "蓽", -"荞": "蕎", -"荟": "薈", -"荠": "薺", -"荣": "榮", -"荤": "葷", -"荥": "滎", -"荦": "犖", -"荧": "熒", -"荨": "蕁", -"荩": "藎", -"荪": "蓀", -"荫": "蔭", -"荬": "蕒", -"荭": "葒", -"荮": "葤", -"莅": "蒞", -"莱": "萊", -"莲": "蓮", -"莳": "蒔", -"莴": "萵", -"莶": "薟", -"莸": "蕕", -"莹": "瑩", -"莺": "鶯", -"萝": "蘿", -"萤": "螢", -"营": "營", -"萦": "縈", -"萧": "蕭", -"萨": "薩", -"葱": "蔥", -"蒇": "蕆", -"蒉": "蕢", -"蒋": "蔣", -"蒌": "蔞", -"蓝": "藍", -"蓟": "薊", -"蓠": "蘺", -"蓣": "蕷", -"蓥": "鎣", -"蓦": "驀", -"蔂": "虆", -"蔷": "薔", -"蔹": "蘞", -"蔺": "藺", -"蔼": "藹", -"蕰": "薀", -"蕲": "蘄", -"薮": "藪", -"䓕": "薳", -"藓": "蘚", -"蘖": "櫱", -"虏": "虜", -"虑": "慮", -"虚": "虛", -"虬": "虯", -"虮": "蟣", -"虽": "雖", -"虾": "蝦", -"虿": "蠆", -"蚀": "蝕", -"蚁": "蟻", -"蚂": "螞", -"蚕": "蠶", -"蚬": "蜆", -"蛊": "蠱", -"蛎": "蠣", -"蛏": "蟶", -"蛮": "蠻", -"蛰": "蟄", -"蛱": "蛺", -"蛲": "蟯", -"蛳": "螄", -"蛴": "蠐", -"蜕": "蛻", -"蜗": "蝸", -"蝇": "蠅", -"蝈": "蟈", -"蝉": "蟬", -"蝼": "螻", -"蝾": "蠑", -"螀": "螿", -"螨": "蟎", -"䗖": "螮", -"蟏": "蠨", -"衅": "釁", -"衔": "銜", -"补": "補", -"衬": "襯", -"衮": "袞", -"袄": "襖", -"袅": "裊", -"袆": "褘", -"袜": "襪", -"袭": "襲", -"袯": "襏", -"装": "裝", -"裆": "襠", -"裈": "褌", -"裢": "褳", -"裣": "襝", -"裤": "褲", -"裥": "襇", -"褛": "褸", -"褴": "襤", -"䙓": "襬", -"见": "見", -"观": "觀", -"觃": "覎", -"规": "規", -"觅": "覓", -"视": "視", -"觇": "覘", -"览": "覽", -"觉": "覺", -"觊": "覬", -"觋": "覡", -"觌": "覿", -"觍": "覥", -"觎": "覦", -"觏": "覯", -"觐": "覲", -"觑": "覷", -"觞": "觴", -"触": "觸", -"觯": "觶", -"訚": "誾", -"䜣": "訢", -"誉": "譽", -"誊": "謄", -"䜧": "譅", -"讠": "訁", -"计": "計", -"订": "訂", -"讣": "訃", -"认": "認", -"讥": "譏", -"讦": "訐", -"讧": "訌", -"讨": "討", -"让": "讓", -"讪": "訕", -"讫": "訖", -"讬": "託", -"训": "訓", -"议": "議", -"讯": "訊", -"记": "記", -"讱": "訒", -"讲": "講", -"讳": "諱", -"讴": "謳", -"讵": "詎", -"讶": "訝", -"讷": "訥", -"许": "許", -"讹": "訛", -"论": "論", -"讻": "訩", -"讼": "訟", -"讽": "諷", -"设": "設", -"访": "訪", -"诀": "訣", -"证": "證", -"诂": "詁", -"诃": "訶", -"评": "評", -"诅": "詛", -"识": "識", -"诇": "詗", -"诈": "詐", -"诉": "訴", -"诊": "診", -"诋": "詆", -"诌": "謅", -"词": "詞", -"诎": "詘", -"诏": "詔", -"诐": "詖", -"译": "譯", -"诒": "詒", -"诓": "誆", -"诔": "誄", -"试": "試", -"诖": "詿", -"诗": "詩", -"诘": "詰", -"诙": "詼", -"诚": "誠", -"诛": "誅", -"诜": "詵", -"话": "話", -"诞": "誕", -"诟": "詬", -"诠": "詮", -"诡": "詭", -"询": "詢", -"诣": "詣", -"诤": "諍", -"该": "該", -"详": "詳", -"诧": "詫", -"诨": "諢", -"诩": "詡", -"诪": "譸", -"诫": "誡", -"诬": "誣", -"语": "語", -"诮": "誚", -"误": "誤", -"诰": "誥", -"诱": "誘", -"诲": "誨", -"诳": "誑", -"诵": "誦", -"诶": "誒", -"请": "請", -"诸": "諸", -"诹": "諏", -"诺": "諾", -"读": "讀", -"诼": "諑", -"诽": "誹", -"课": "課", -"诿": "諉", -"谀": "諛", -"谁": "誰", -"谂": "諗", -"调": "調", -"谄": "諂", -"谅": "諒", -"谆": "諄", -"谇": "誶", -"谈": "談", -"谊": "誼", -"谋": "謀", -"谌": "諶", -"谍": "諜", -"谎": "謊", -"谏": "諫", -"谐": "諧", -"谑": "謔", -"谒": "謁", -"谓": "謂", -"谔": "諤", -"谕": "諭", -"谖": "諼", -"谗": "讒", -"谘": "諮", -"谙": "諳", -"谚": "諺", -"谛": "諦", -"谜": "謎", -"谝": "諞", -"谞": "諝", -"谟": "謨", -"谠": "讜", -"谡": "謖", -"谢": "謝", -"谤": "謗", -"谥": "謚", -"谦": "謙", -"谧": "謐", -"谨": "謹", -"谩": "謾", -"谪": "謫", -"谬": "謬", -"谭": "譚", -"谮": "譖", -"谯": "譙", -"谰": "讕", -"谱": "譜", -"谲": "譎", -"谳": "讞", -"谴": "譴", -"谵": "譫", -"谶": "讖", -"豮": "豶", -"䝙": "貙", -"䞐": "賰", -"贝": "貝", -"贞": "貞", -"负": "負", -"贠": "貟", -"贡": "貢", -"财": "財", -"责": "責", -"贤": "賢", -"败": "敗", -"账": "賬", -"货": "貨", -"质": "質", -"贩": "販", -"贪": "貪", -"贫": "貧", -"贬": "貶", -"购": "購", -"贮": "貯", -"贯": "貫", -"贰": "貳", -"贱": "賤", -"贲": "賁", -"贳": "貰", -"贴": "貼", -"贵": "貴", -"贶": "貺", -"贷": "貸", -"贸": "貿", -"费": "費", -"贺": "賀", -"贻": "貽", -"贼": "賊", -"贽": "贄", -"贾": "賈", -"贿": "賄", -"赀": "貲", -"赁": "賃", -"赂": "賂", -"资": "資", -"赅": "賅", -"赆": "贐", -"赇": "賕", -"赈": "賑", -"赉": "賚", -"赊": "賒", -"赋": "賦", -"赌": "賭", -"赎": "贖", -"赏": "賞", -"赐": "賜", -"赑": "贔", -"赒": "賙", -"赓": "賡", -"赔": "賠", -"赕": "賧", -"赖": "賴", -"赗": "賵", -"赘": "贅", -"赙": "賻", -"赚": "賺", -"赛": "賽", -"赜": "賾", -"赞": "贊", -"赟": "贇", -"赠": "贈", -"赡": "贍", -"赢": "贏", -"赣": "贛", -"赪": "赬", -"赵": "趙", -"赶": "趕", -"趋": "趨", -"趱": "趲", -"趸": "躉", -"跃": "躍", -"跄": "蹌", -"跞": "躒", -"践": "踐", -"跶": "躂", -"跷": "蹺", -"跸": "蹕", -"跹": "躚", -"跻": "躋", -"踊": "踴", -"踌": "躊", -"踪": "蹤", -"踬": "躓", -"踯": "躑", -"蹑": "躡", -"蹒": "蹣", -"蹰": "躕", -"蹿": "躥", -"躏": "躪", -"躜": "躦", -"躯": "軀", -"车": "車", -"轧": "軋", -"轨": "軌", -"轩": "軒", -"轪": "軑", -"轫": "軔", -"转": "轉", -"轭": "軛", -"轮": "輪", -"软": "軟", -"轰": "轟", -"轱": "軲", -"轲": "軻", -"轳": "轤", -"轴": "軸", -"轵": "軹", -"轶": "軼", -"轷": "軤", -"轸": "軫", -"轹": "轢", -"轺": "軺", -"轻": "輕", -"轼": "軾", -"载": "載", -"轾": "輊", -"轿": "轎", -"辀": "輈", -"辁": "輇", -"辂": "輅", -"较": "較", -"辄": "輒", -"辅": "輔", -"辆": "輛", -"辇": "輦", -"辈": "輩", -"辉": "輝", -"辊": "輥", -"辋": "輞", -"辌": "輬", -"辍": "輟", -"辎": "輜", -"辏": "輳", -"辐": "輻", -"辑": "輯", -"辒": "轀", -"输": "輸", -"辔": "轡", -"辕": "轅", -"辖": "轄", -"辗": "輾", -"辘": "轆", -"辙": "轍", -"辚": "轔", -"辞": "辭", -"辩": "辯", -"辫": "辮", -"边": "邊", -"辽": "遼", -"达": "達", -"迁": "遷", -"过": "過", -"迈": "邁", -"运": "運", -"还": "還", -"这": "這", -"进": "進", -"远": "遠", -"违": "違", -"连": "連", -"迟": "遲", -"迩": "邇", -"迳": "逕", -"迹": "跡", -"选": "選", -"逊": "遜", -"递": "遞", -"逦": "邐", -"逻": "邏", -"遗": "遺", -"遥": "遙", -"邓": "鄧", -"邝": "鄺", -"邬": "鄔", -"邮": "郵", -"邹": "鄒", -"邺": "鄴", -"邻": "鄰", -"郏": "郟", -"郐": "鄶", -"郑": "鄭", -"郓": "鄆", -"郦": "酈", -"郧": "鄖", -"郸": "鄲", -"酂": "酇", -"酦": "醱", -"酱": "醬", -"酽": "釅", -"酾": "釃", -"酿": "釀", -"释": "釋", -"鉴": "鑒", -"銮": "鑾", -"錾": "鏨", -"𨱏": "鎝", -"钅": "釒", -"钆": "釓", -"钇": "釔", -"针": "針", -"钉": "釘", -"钊": "釗", -"钋": "釙", -"钌": "釕", -"钍": "釷", -"钎": "釺", -"钏": "釧", -"钐": "釤", -"钑": "鈒", -"钒": "釩", -"钓": "釣", -"钔": "鍆", -"钕": "釹", -"钖": "鍚", -"钗": "釵", -"钘": "鈃", -"钙": "鈣", -"钚": "鈈", -"钛": "鈦", -"钜": "鉅", -"钝": "鈍", -"钞": "鈔", -"钠": "鈉", -"钡": "鋇", -"钢": "鋼", -"钣": "鈑", -"钤": "鈐", -"钥": "鑰", -"钦": "欽", -"钧": "鈞", -"钨": "鎢", -"钪": "鈧", -"钫": "鈁", -"钬": "鈥", -"钭": "鈄", -"钮": "鈕", -"钯": "鈀", -"钰": "鈺", -"钱": "錢", -"钲": "鉦", -"钳": "鉗", -"钴": "鈷", -"钶": "鈳", -"钷": "鉕", -"钸": "鈽", -"钹": "鈸", -"钺": "鉞", -"钻": "鑽", -"钼": "鉬", -"钽": "鉭", -"钾": "鉀", -"钿": "鈿", -"铀": "鈾", -"铁": "鐵", -"铂": "鉑", -"铃": "鈴", -"铄": "鑠", -"铅": "鉛", -"铆": "鉚", -"铇": "鉋", -"铈": "鈰", -"铉": "鉉", -"铊": "鉈", -"铋": "鉍", -"铌": "鈮", -"铍": "鈹", -"铎": "鐸", -"铏": "鉶", -"铐": "銬", -"铑": "銠", -"铒": "鉺", -"铓": "鋩", -"铔": "錏", -"铕": "銪", -"铖": "鋮", -"铗": "鋏", -"铘": "鋣", -"铙": "鐃", -"铚": "銍", -"铛": "鐺", -"铜": "銅", -"铝": "鋁", -"铞": "銱", -"铟": "銦", -"铠": "鎧", -"铡": "鍘", -"铢": "銖", -"铣": "銑", -"铤": "鋌", -"铥": "銩", -"铦": "銛", -"铧": "鏵", -"铨": "銓", -"铩": "鎩", -"铪": "鉿", -"铫": "銚", -"铬": "鉻", -"铭": "銘", -"铮": "錚", -"铯": "銫", -"铰": "鉸", -"铱": "銥", -"铲": "鏟", -"铳": "銃", -"铴": "鐋", -"铵": "銨", -"银": "銀", -"铷": "銣", -"铸": "鑄", -"铹": "鐒", -"铺": "鋪", -"铻": "鋙", -"铼": "錸", -"铽": "鋱", -"链": "鏈", -"铿": "鏗", -"销": "銷", -"锁": "鎖", -"锂": "鋰", -"锃": "鋥", -"锄": "鋤", -"锅": "鍋", -"锆": "鋯", -"锇": "鋨", -"锉": "銼", -"锊": "鋝", -"锋": "鋒", -"锌": "鋅", -"锍": "鋶", -"锎": "鐦", -"锏": "鐧", -"锑": "銻", -"锒": "鋃", -"锓": "鋟", -"锔": "鋦", -"锕": "錒", -"锖": "錆", -"锗": "鍺", -"锘": "鍩", -"错": "錯", -"锚": "錨", -"锛": "錛", -"锜": "錡", -"锝": "鍀", -"锞": "錁", -"锟": "錕", -"锠": "錩", -"锡": "錫", -"锢": "錮", -"锣": "鑼", -"锥": "錐", -"锦": "錦", -"锧": "鑕", -"锩": "錈", -"锪": "鍃", -"锫": "錇", -"锬": "錟", -"锭": "錠", -"键": "鍵", -"锯": "鋸", -"锰": "錳", -"锱": "錙", -"锲": "鍥", -"锳": "鍈", -"锴": "鍇", -"锵": "鏘", -"锶": "鍶", -"锷": "鍔", -"锸": "鍤", -"锹": "鍬", -"锺": "鍾", -"锻": "鍛", -"锼": "鎪", -"锽": "鍠", -"锾": "鍰", -"锿": "鎄", -"镀": "鍍", -"镁": "鎂", -"镂": "鏤", -"镃": "鎡", -"镄": "鐨", -"镅": "鎇", -"镆": "鏌", -"镇": "鎮", -"镈": "鎛", -"镉": "鎘", -"镊": "鑷", -"镋": "鎲", -"镍": "鎳", -"镎": "鎿", -"镏": "鎦", -"镐": "鎬", -"镑": "鎊", -"镒": "鎰", -"镓": "鎵", -"镔": "鑌", -"镕": "鎔", -"镖": "鏢", -"镗": "鏜", -"镘": "鏝", -"镙": "鏍", -"镚": "鏰", -"镛": "鏞", -"镜": "鏡", -"镝": "鏑", -"镞": "鏃", -"镟": "鏇", -"镠": "鏐", -"镡": "鐔", -"镣": "鐐", -"镤": "鏷", -"镥": "鑥", -"镦": "鐓", -"镧": "鑭", -"镨": "鐠", -"镩": "鑹", -"镪": "鏹", -"镫": "鐙", -"镬": "鑊", -"镭": "鐳", -"镮": "鐶", -"镯": "鐲", -"镰": "鐮", -"镱": "鐿", -"镲": "鑔", -"镳": "鑣", -"镴": "鑞", -"镵": "鑱", -"镶": "鑲", -"长": "長", -"门": "門", -"闩": "閂", -"闪": "閃", -"闫": "閆", -"闬": "閈", -"闭": "閉", -"问": "問", -"闯": "闖", -"闰": "閏", -"闱": "闈", -"闲": "閑", -"闳": "閎", -"间": "間", -"闵": "閔", -"闶": "閌", -"闷": "悶", -"闸": "閘", -"闹": "鬧", -"闺": "閨", -"闻": "聞", -"闼": "闥", -"闽": "閩", -"闾": "閭", -"闿": "闓", -"阀": "閥", -"阁": "閣", -"阂": "閡", -"阃": "閫", -"阄": "鬮", -"阆": "閬", -"阇": "闍", -"阈": "閾", -"阉": "閹", -"阊": "閶", -"阋": "鬩", -"阌": "閿", -"阍": "閽", -"阎": "閻", -"阏": "閼", -"阐": "闡", -"阑": "闌", -"阒": "闃", -"阓": "闠", -"阔": "闊", -"阕": "闋", -"阖": "闔", -"阗": "闐", -"阘": "闒", -"阙": "闕", -"阚": "闞", -"阛": "闤", -"队": "隊", -"阳": "陽", -"阴": "陰", -"阵": "陣", -"阶": "階", -"际": "際", -"陆": "陸", -"陇": "隴", -"陈": "陳", -"陉": "陘", -"陕": "陝", -"陧": "隉", -"陨": "隕", -"险": "險", -"随": "隨", -"隐": "隱", -"隶": "隸", -"隽": "雋", -"难": "難", -"雏": "雛", -"雠": "讎", -"雳": "靂", -"雾": "霧", -"霁": "霽", -"霡": "霢", -"霭": "靄", -"靓": "靚", -"静": "靜", -"靥": "靨", -"䩄": "靦", -"鞑": "韃", -"鞒": "鞽", -"鞯": "韉", -"韦": "韋", -"韧": "韌", -"韨": "韍", -"韩": "韓", -"韪": "韙", -"韫": "韞", -"韬": "韜", -"韵": "韻", -"页": "頁", -"顶": "頂", -"顷": "頃", -"顸": "頇", -"项": "項", -"顺": "順", -"顼": "頊", -"顽": "頑", -"顾": "顧", -"顿": "頓", -"颀": "頎", -"颁": "頒", -"颂": "頌", -"颃": "頏", -"预": "預", -"颅": "顱", -"领": "領", -"颇": "頗", -"颈": "頸", -"颉": "頡", -"颊": "頰", -"颋": "頲", -"颌": "頜", -"颍": "潁", -"颎": "熲", -"颏": "頦", -"颐": "頤", -"频": "頻", -"颒": "頮", -"颔": "頷", -"颕": "頴", -"颖": "穎", -"颗": "顆", -"题": "題", -"颙": "顒", -"颚": "顎", -"颛": "顓", -"额": "額", -"颞": "顳", -"颟": "顢", -"颠": "顛", -"颡": "顙", -"颢": "顥", -"颤": "顫", -"颥": "顬", -"颦": "顰", -"颧": "顴", -"风": "風", -"飏": "颺", -"飐": "颭", -"飑": "颮", -"飒": "颯", -"飓": "颶", -"飔": "颸", -"飕": "颼", -"飖": "颻", -"飗": "飀", -"飘": "飄", -"飙": "飆", -"飚": "飈", -"飞": "飛", -"飨": "饗", -"餍": "饜", -"饣": "飠", -"饤": "飣", -"饦": "飥", -"饧": "餳", -"饨": "飩", -"饩": "餼", -"饪": "飪", -"饫": "飫", -"饬": "飭", -"饭": "飯", -"饮": "飲", -"饯": "餞", -"饰": "飾", -"饱": "飽", -"饲": "飼", -"饳": "飿", -"饴": "飴", -"饵": "餌", -"饶": "饒", -"饷": "餉", -"饸": "餄", -"饹": "餎", -"饺": "餃", -"饻": "餏", -"饼": "餅", -"饽": "餑", -"饾": "餖", -"饿": "餓", -"馀": "餘", -"馁": "餒", -"馂": "餕", -"馃": "餜", -"馄": "餛", -"馅": "餡", -"馆": "館", -"馇": "餷", -"馈": "饋", -"馉": "餶", -"馊": "餿", -"馋": "饞", -"馌": "饁", -"馍": "饃", -"馎": "餺", -"馏": "餾", -"馐": "饈", -"馑": "饉", -"馒": "饅", -"馓": "饊", -"馔": "饌", -"馕": "饢", -"䯄": "騧", -"马": "馬", -"驭": "馭", -"驮": "馱", -"驯": "馴", -"驰": "馳", -"驱": "驅", -"驲": "馹", -"驳": "駁", -"驴": "驢", -"驵": "駔", -"驶": "駛", -"驷": "駟", -"驸": "駙", -"驹": "駒", -"驺": "騶", -"驻": "駐", -"驼": "駝", -"驽": "駑", -"驾": "駕", -"驿": "驛", -"骀": "駘", -"骁": "驍", -"骃": "駰", -"骄": "驕", -"骅": "驊", -"骆": "駱", -"骇": "駭", -"骈": "駢", -"骉": "驫", -"骊": "驪", -"骋": "騁", -"验": "驗", -"骍": "騂", -"骎": "駸", -"骏": "駿", -"骐": "騏", -"骑": "騎", -"骒": "騍", -"骓": "騅", -"骔": "騌", -"骕": "驌", -"骖": "驂", -"骗": "騙", -"骘": "騭", -"骙": "騤", -"骚": "騷", -"骛": "騖", -"骜": "驁", -"骝": "騮", -"骞": "騫", -"骟": "騸", -"骠": "驃", -"骡": "騾", -"骢": "驄", -"骣": "驏", -"骤": "驟", -"骥": "驥", -"骦": "驦", -"骧": "驤", -"髅": "髏", -"髋": "髖", -"髌": "髕", -"鬓": "鬢", -"魇": "魘", -"魉": "魎", -"鱼": "魚", -"鱽": "魛", -"鱾": "魢", -"鱿": "魷", -"鲀": "魨", -"鲁": "魯", -"鲂": "魴", -"鲃": "䰾", -"鲄": "魺", -"鲅": "鮁", -"鲆": "鮃", -"鲈": "鱸", -"鲉": "鮋", -"鲊": "鮓", -"鲋": "鮒", -"鲌": "鮊", -"鲍": "鮑", -"鲎": "鱟", -"鲏": "鮍", -"鲐": "鮐", -"鲑": "鮭", -"鲒": "鮚", -"鲓": "鮳", -"鲔": "鮪", -"鲕": "鮞", -"鲖": "鮦", -"鲗": "鰂", -"鲘": "鮜", -"鲙": "鱠", -"鲚": "鱭", -"鲛": "鮫", -"鲜": "鮮", -"鲝": "鮺", -"鲟": "鱘", -"鲠": "鯁", -"鲡": "鱺", -"鲢": "鰱", -"鲣": "鰹", -"鲤": "鯉", -"鲥": "鰣", -"鲦": "鰷", -"鲧": "鯀", -"鲨": "鯊", -"鲩": "鯇", -"鲪": "鮶", -"鲫": "鯽", -"鲬": "鯒", -"鲭": "鯖", -"鲮": "鯪", -"鲯": "鯕", -"鲰": "鯫", -"鲱": "鯡", -"鲲": "鯤", -"鲳": "鯧", -"鲴": "鯝", -"鲵": "鯢", -"鲶": "鯰", -"鲷": "鯛", -"鲸": "鯨", -"鲹": "鰺", -"鲺": "鯴", -"鲻": "鯔", -"鲼": "鱝", -"鲽": "鰈", -"鲾": "鰏", -"鲿": "鱨", -"鳀": "鯷", -"鳁": "鰮", -"鳂": "鰃", -"鳃": "鰓", -"鳅": "鰍", -"鳆": "鰒", -"鳇": "鰉", -"鳈": "鰁", -"鳉": "鱂", -"鳊": "鯿", -"鳋": "鰠", -"鳌": "鰲", -"鳍": "鰭", -"鳎": "鰨", -"鳏": "鰥", -"鳐": "鰩", -"鳑": "鰟", -"鳒": "鰜", -"鳓": "鰳", -"鳔": "鰾", -"鳕": "鱈", -"鳖": "鱉", -"鳗": "鰻", -"鳘": "鰵", -"鳙": "鱅", -"鳚": "䲁", -"鳛": "鰼", -"鳜": "鱖", -"鳝": "鱔", -"鳞": "鱗", -"鳟": "鱒", -"鳠": "鱯", -"鳡": "鱤", -"鳢": "鱧", -"鳣": "鱣", -"䴓": "鳾", -"䴕": "鴷", -"䴔": "鵁", -"䴖": "鶄", -"䴗": "鶪", -"䴘": "鷈", -"䴙": "鷿", -"㶉": "鸂", -"鸟": "鳥", -"鸠": "鳩", -"鸢": "鳶", -"鸣": "鳴", -"鸤": "鳲", -"鸥": "鷗", -"鸦": "鴉", -"鸧": "鶬", -"鸨": "鴇", -"鸩": "鴆", -"鸪": "鴣", -"鸫": "鶇", -"鸬": "鸕", -"鸭": "鴨", -"鸮": "鴞", -"鸯": "鴦", -"鸰": "鴒", -"鸱": "鴟", -"鸲": "鴝", -"鸳": "鴛", -"鸴": "鷽", -"鸵": "鴕", -"鸶": "鷥", -"鸷": "鷙", -"鸸": "鴯", -"鸹": "鴰", -"鸺": "鵂", -"鸻": "鴴", -"鸼": "鵃", -"鸽": "鴿", -"鸾": "鸞", -"鸿": "鴻", -"鹀": "鵐", -"鹁": "鵓", -"鹂": "鸝", -"鹃": "鵑", -"鹄": "鵠", -"鹅": "鵝", -"鹆": "鵒", -"鹇": "鷳", -"鹈": "鵜", -"鹉": "鵡", -"鹊": "鵲", -"鹋": "鶓", -"鹌": "鵪", -"鹍": "鵾", -"鹎": "鵯", -"鹏": "鵬", -"鹐": "鵮", -"鹑": "鶉", -"鹒": "鶊", -"鹓": "鵷", -"鹔": "鷫", -"鹕": "鶘", -"鹖": "鶡", -"鹗": "鶚", -"鹘": "鶻", -"鹙": "鶖", -"鹛": "鶥", -"鹜": "鶩", -"鹝": "鷊", -"鹞": "鷂", -"鹟": "鶲", -"鹠": "鶹", -"鹡": "鶺", -"鹢": "鷁", -"鹣": "鶼", -"鹤": "鶴", -"鹥": "鷖", -"鹦": "鸚", -"鹧": "鷓", -"鹨": "鷚", -"鹩": "鷯", -"鹪": "鷦", -"鹫": "鷲", -"鹬": "鷸", -"鹭": "鷺", -"鹯": "鸇", -"鹰": "鷹", -"鹱": "鸌", -"鹲": "鸏", -"鹳": "鸛", -"鹴": "鸘", -"鹾": "鹺", -"麦": "麥", -"麸": "麩", -"黄": "黃", -"黉": "黌", -"黡": "黶", -"黩": "黷", -"黪": "黲", -"黾": "黽", -"鼋": "黿", -"鼍": "鼉", -"鼗": "鞀", -"鼹": "鼴", -"齐": "齊", -"齑": "齏", -"齿": "齒", -"龀": "齔", -"龁": "齕", -"龂": "齗", -"龃": "齟", -"龄": "齡", -"龅": "齙", -"龆": "齠", -"龇": "齜", -"龈": "齦", -"龉": "齬", -"龊": "齪", -"龋": "齲", -"龌": "齷", -"龙": "龍", -"龚": "龔", -"龛": "龕", -"龟": "龜", -"一伙": "一伙", -"一并": "一併", -"一准": "一准", -"一划": "一划", -"一地里": "一地裡", -"一干": "一干", -"一树百获": "一樹百穫", -"一台": "一臺", -"一冲": "一衝", -"一只": "一隻", -"一发千钧": "一髮千鈞", -"一出": "一齣", -"七只": "七隻", -"三元里": "三元裡", -"三国志": "三國誌", -"三复": "三複", -"三只": "三隻", -"上吊": "上吊", -"上台": "上臺", -"下不了台": "下不了臺", -"下台": "下臺", -"下面": "下麵", -"不准": "不准", -"不吊": "不吊", -"不知就里": "不知就裡", -"不知所云": "不知所云", -"不锈钢": "不鏽鋼", -"丑剧": "丑劇", -"丑旦": "丑旦", -"丑角": "丑角", -"并存着": "並存著", -"中岳": "中嶽", -"中台医专": "中臺醫專", -"丰南": "丰南", -"丰台": "丰台", -"丰姿": "丰姿", -"丰采": "丰采", -"丰韵": "丰韻", -"主干": "主幹", -"么么唱唱": "么么唱唱", -"么儿": "么兒", -"么喝": "么喝", -"么妹": "么妹", -"么弟": "么弟", -"么爷": "么爺", -"九世之雠": "九世之讎", -"九只": "九隻", -"干丝": "乾絲", -"干着急": "乾著急", -"乱发": "亂髮", -"云云": "云云", -"云尔": "云爾", -"五岳": "五嶽", -"五斗柜": "五斗櫃", -"五斗橱": "五斗櫥", -"五谷": "五穀", -"五行生克": "五行生剋", -"五只": "五隻", -"五出": "五齣", -"交卷": "交卷", -"人云亦云": "人云亦云", -"人物志": "人物誌", -"什锦面": "什錦麵", -"什么": "什麼", -"仆倒": "仆倒", -"介系词": "介係詞", -"介系词": "介繫詞", -"仿制": "仿製", -"伙伕": "伙伕", -"伙伴": "伙伴", -"伙同": "伙同", -"伙夫": "伙夫", -"伙房": "伙房", -"伙计": "伙計", -"伙食": "伙食", -"布下": "佈下", -"布告": "佈告", -"布哨": "佈哨", -"布局": "佈局", -"布岗": "佈崗", -"布施": "佈施", -"布景": "佈景", -"布满": "佈滿", -"布线": "佈線", -"布置": "佈置", -"布署": "佈署", -"布道": "佈道", -"布达": "佈達", -"布防": "佈防", -"布阵": "佈陣", -"布雷": "佈雷", -"体育锻鍊": "体育鍛鍊", -"何干": "何干", -"作准": "作准", -"佣人": "佣人", -"佣工": "佣工", -"佣金": "佣金", -"并入": "併入", -"并列": "併列", -"并到": "併到", -"并合": "併合", -"并吞": "併吞", -"并在": "併在", -"并成": "併成", -"并排": "併排", -"并拢": "併攏", -"并案": "併案", -"并为": "併為", -"并发": "併發", -"并科": "併科", -"并购": "併購", -"并进": "併進", -"来复": "來複", -"供制": "供製", -"依依不舍": "依依不捨", -"侵并": "侵併", -"便辟": "便辟", -"系数": "係數", -"系为": "係為", -"保险柜": "保險柜", -"信号台": "信號臺", -"修复": "修複", -"修胡刀": "修鬍刀", -"俯冲": "俯衝", -"个里": "個裡", -"借着": "借著", -"假发": "假髮", -"停制": "停製", -"偷鸡不着": "偷雞不著", -"家伙": "傢伙", -"家俱": "傢俱", -"家具": "傢具", -"传布": "傳佈", -"债台高筑": "債臺高築", -"傻里傻气": "傻裡傻氣", -"倾家荡产": "傾家蕩產", -"倾覆": "傾複", -"倾覆": "傾覆", -"僱佣": "僱佣", -"仪表": "儀錶", -"亿只": "億隻", -"尽尽": "儘儘", -"尽先": "儘先", -"尽其所有": "儘其所有", -"尽力": "儘力", -"尽快": "儘快", -"尽早": "儘早", -"尽是": "儘是", -"尽管": "儘管", -"尽速": "儘速", -"尽量": "儘量", -"允准": "允准", -"兄台": "兄臺", -"充饥": "充饑", -"光采": "光采", -"克里": "克裡", -"克复": "克複", -"入伙": "入伙", -"内制": "內製", -"两只": "兩隻", -"八字胡": "八字鬍", -"八只": "八隻", -"公布": "公佈", -"公干": "公幹", -"公斗": "公斗", -"公历": "公曆", -"六只": "六隻", -"六出": "六齣", -"兼并": "兼併", -"冤雠": "冤讎", -"准予": "准予", -"准假": "准假", -"准将": "准將", -"准考证": "准考證", -"准许": "准許", -"几几": "几几", -"几案": "几案", -"几丝": "几絲", -"凹洞里": "凹洞裡", -"出征": "出征", -"出锤": "出鎚", -"刀削面": "刀削麵", -"刁斗": "刁斗", -"分布": "分佈", -"切面": "切麵", -"刊布": "刊佈", -"划上": "划上", -"划下": "划下", -"划不来": "划不來", -"划了": "划了", -"划具": "划具", -"划出": "划出", -"划到": "划到", -"划动": "划動", -"划去": "划去", -"划子": "划子", -"划得来": "划得來", -"划拳": "划拳", -"划桨": "划槳", -"划水": "划水", -"划算": "划算", -"划船": "划船", -"划艇": "划艇", -"划着": "划著", -"划着走": "划著走", -"划行": "划行", -"划走": "划走", -"划起": "划起", -"划进": "划進", -"划过": "划過", -"初征": "初征", -"别致": "別緻", -"别着": "別著", -"别只": "別隻", -"利比里亚": "利比裡亞", -"刮着": "刮著", -"刮胡刀": "刮鬍刀", -"剃发": "剃髮", -"剃须": "剃鬚", -"削发": "削髮", -"克制": "剋制", -"克星": "剋星", -"克服": "剋服", -"克死": "剋死", -"克薄": "剋薄", -"前仆后继": "前仆後繼", -"前台": "前臺", -"前车之复": "前車之覆", -"刚才": "剛纔", -"剪发": "剪髮", -"割舍": "割捨", -"创制": "創製", -"加里宁": "加裡寧", -"动荡": "動蕩", -"劳力士表": "勞力士錶", -"包准": "包准", -"包谷": "包穀", -"北斗": "北斗", -"北回": "北迴", -"匡复": "匡複", -"匪干": "匪幹", -"十卷": "十卷", -"十台": "十臺", -"十只": "十隻", -"十出": "十齣", -"千丝万缕": "千絲萬縷", -"千回百折": "千迴百折", -"千回百转": "千迴百轉", -"千钧一发": "千鈞一髮", -"千只": "千隻", -"升斗小民": "升斗小民", -"半只": "半隻", -"南岳": "南嶽", -"南征": "南征", -"南台": "南臺", -"南回": "南迴", -"卡里": "卡裡", -"印制": "印製", -"卷入": "卷入", -"卷取": "卷取", -"卷土重来": "卷土重來", -"卷子": "卷子", -"卷宗": "卷宗", -"卷尺": "卷尺", -"卷层云": "卷層雲", -"卷帙": "卷帙", -"卷扬机": "卷揚機", -"卷曲": "卷曲", -"卷染": "卷染", -"卷烟": "卷煙", -"卷筒": "卷筒", -"卷纬": "卷緯", -"卷绕": "卷繞", -"卷装": "卷裝", -"卷轴": "卷軸", -"卷云": "卷雲", -"卷领": "卷領", -"卷发": "卷髮", -"卷须": "卷鬚", -"参与": "參与", -"参与者": "參与者", -"参合": "參合", -"参考价值": "參考價值", -"参与": "參與", -"参与人员": "參與人員", -"参与制": "參與制", -"参与感": "參與感", -"参与者": "參與者", -"参观团": "參觀團", -"参观团体": "參觀團體", -"参阅": "參閱", -"反冲": "反衝", -"反复": "反複", -"反复": "反覆", -"取舍": "取捨", -"口里": "口裡", -"只准": "只准", -"只冲": "只衝", -"叮当": "叮噹", -"可怜虫": "可憐虫", -"可紧可松": "可緊可鬆", -"台制": "台製", -"司令台": "司令臺", -"吃着不尽": "吃著不盡", -"吃里扒外": "吃裡扒外", -"吃里爬外": "吃裡爬外", -"各吊": "各吊", -"合伙": "合伙", -"合并": "合併", -"吊上": "吊上", -"吊下": "吊下", -"吊了": "吊了", -"吊个": "吊個", -"吊儿郎当": "吊兒郎當", -"吊到": "吊到", -"吊去": "吊去", -"吊取": "吊取", -"吊吊": "吊吊", -"吊嗓": "吊嗓", -"吊好": "吊好", -"吊子": "吊子", -"吊带": "吊帶", -"吊带裤": "吊帶褲", -"吊床": "吊床", -"吊得": "吊得", -"吊挂": "吊掛", -"吊挂着": "吊掛著", -"吊杆": "吊杆", -"吊架": "吊架", -"吊桶": "吊桶", -"吊杆": "吊桿", -"吊桥": "吊橋", -"吊死": "吊死", -"吊灯": "吊燈", -"吊环": "吊環", -"吊盘": "吊盤", -"吊索": "吊索", -"吊着": "吊著", -"吊装": "吊裝", -"吊裤": "吊褲", -"吊裤带": "吊褲帶", -"吊袜": "吊襪", -"吊走": "吊走", -"吊起": "吊起", -"吊车": "吊車", -"吊钩": "吊鉤", -"吊销": "吊銷", -"吊钟": "吊鐘", -"同伙": "同伙", -"名表": "名錶", -"后冠": "后冠", -"后土": "后土", -"后妃": "后妃", -"后座": "后座", -"后稷": "后稷", -"后羿": "后羿", -"后里": "后里", -"向着": "向著", -"吞并": "吞併", -"吹发": "吹髮", -"吕后": "呂后", -"獃里獃气": "呆裡呆氣", -"周而复始": "周而複始", -"呼吁": "呼籲", -"和面": "和麵", -"哪里": "哪裡", -"哭脏": "哭髒", -"问卷": "問卷", -"喝采": "喝采", -"单干": "單干", -"单只": "單隻", -"嘴里": "嘴裡", -"恶心": "噁心", -"当啷": "噹啷", -"当当": "噹噹", -"噜苏": "嚕囌", -"向导": "嚮導", -"向往": "嚮往", -"向应": "嚮應", -"向日": "嚮日", -"向迩": "嚮邇", -"严丝合缝": "嚴絲合縫", -"严复": "嚴複", -"四舍五入": "四捨五入", -"四只": "四隻", -"四出": "四齣", -"回丝": "回絲", -"回着": "回著", -"回荡": "回蕩", -"回复": "回覆", -"回采": "回采", -"圈子里": "圈子裡", -"圈里": "圈裡", -"国历": "國曆", -"国雠": "國讎", -"园里": "園裡", -"图里": "圖裡", -"土里": "土裡", -"土制": "土製", -"地志": "地誌", -"坍台": "坍臺", -"坑里": "坑裡", -"坦荡": "坦蕩", -"垂发": "垂髮", -"垮台": "垮臺", -"埋布": "埋佈", -"城里": "城裡", -"基干": "基幹", -"报复": "報複", -"塌台": "塌臺", -"塔台": "塔臺", -"涂着": "塗著", -"墓志": "墓誌", -"墨斗": "墨斗", -"墨索里尼": "墨索裡尼", -"垦复": "墾複", -"垄断价格": "壟斷價格", -"垄断资产": "壟斷資產", -"垄断集团": "壟斷集團", -"壶里": "壺裡", -"寿面": "壽麵", -"夏天里": "夏天裡", -"夏历": "夏曆", -"外制": "外製", -"多冲": "多衝", -"多采多姿": "多采多姿", -"多么": "多麼", -"夜光表": "夜光錶", -"夜里": "夜裡", -"梦里": "夢裡", -"大伙": "大伙", -"大卷": "大卷", -"大干": "大干", -"大干": "大幹", -"大锤": "大鎚", -"大只": "大隻", -"天后": "天后", -"天干": "天干", -"天文台": "天文臺", -"太后": "太后", -"奏折": "奏摺", -"女丑": "女丑", -"女佣": "女佣", -"好家夥": "好傢夥", -"好戏连台": "好戲連臺", -"如法泡制": "如法泡製", -"妆台": "妝臺", -"姜太公": "姜太公", -"姜子牙": "姜子牙", -"姜丝": "姜絲", -"字汇": "字彙", -"字里行间": "字裡行間", -"存折": "存摺", -"孟姜女": "孟姜女", -"宇宙志": "宇宙誌", -"定准": "定准", -"定制": "定製", -"宣布": "宣佈", -"宫里": "宮裡", -"家伙": "家伙", -"家里": "家裡", -"密布": "密佈", -"寇雠": "寇讎", -"实干": "實幹", -"写字台": "寫字檯", -"写字台": "寫字臺", -"宽松": "寬鬆", -"封面里": "封面裡", -"射干": "射干", -"对表": "對錶", -"小丑": "小丑", -"小伙": "小伙", -"小只": "小隻", -"少吊": "少吊", -"尺布斗粟": "尺布斗粟", -"尼克松": "尼克鬆", -"尼采": "尼采", -"尿斗": "尿斗", -"局里": "局裡", -"居里": "居裡", -"屋子里": "屋子裡", -"屋里": "屋裡", -"展布": "展佈", -"屡仆屡起": "屢仆屢起", -"屯里": "屯裡", -"山岳": "山嶽", -"山里": "山裡", -"峰回": "峰迴", -"巡回": "巡迴", -"巧干": "巧幹", -"巴尔干": "巴爾幹", -"巴里": "巴裡", -"巷里": "巷裡", -"市里": "市裡", -"布谷": "布穀", -"希腊": "希腊", -"帘子": "帘子", -"帘布": "帘布", -"席卷": "席卷", -"带团参加": "帶團參加", -"带发修行": "帶髮修行", -"干休": "干休", -"干系": "干係", -"干卿何事": "干卿何事", -"干将": "干將", -"干戈": "干戈", -"干挠": "干撓", -"干扰": "干擾", -"干支": "干支", -"干政": "干政", -"干时": "干時", -"干涉": "干涉", -"干犯": "干犯", -"干与": "干與", -"干着急": "干著急", -"干贝": "干貝", -"干预": "干預", -"平台": "平臺", -"年历": "年曆", -"年里": "年裡", -"干上": "幹上", -"干下去": "幹下去", -"干了": "幹了", -"干事": "幹事", -"干些": "幹些", -"干个": "幹個", -"干劲": "幹勁", -"干员": "幹員", -"干吗": "幹嗎", -"干嘛": "幹嘛", -"干坏事": "幹壞事", -"干完": "幹完", -"干得": "幹得", -"干性油": "幹性油", -"干才": "幹才", -"干掉": "幹掉", -"干校": "幹校", -"干活": "幹活", -"干流": "幹流", -"干球温度": "幹球溫度", -"干线": "幹線", -"干练": "幹練", -"干警": "幹警", -"干起来": "幹起來", -"干路": "幹路", -"干道": "幹道", -"干部": "幹部", -"干么": "幹麼", -"几丝": "幾絲", -"几只": "幾隻", -"几出": "幾齣", -"底里": "底裡", -"康采恩": "康采恩", -"庙里": "廟裡", -"建台": "建臺", -"弄脏": "弄髒", -"弔卷": "弔卷", -"弘历": "弘曆", -"别扭": "彆扭", -"别拗": "彆拗", -"别气": "彆氣", -"别脚": "彆腳", -"别着": "彆著", -"弹子台": "彈子檯", -"弹药": "彈葯", -"汇报": "彙報", -"汇整": "彙整", -"汇编": "彙編", -"汇总": "彙總", -"汇纂": "彙纂", -"汇辑": "彙輯", -"汇集": "彙集", -"形单影只": "形單影隻", -"影后": "影后", -"往里": "往裡", -"往复": "往複", -"征伐": "征伐", -"征兵": "征兵", -"征尘": "征塵", -"征夫": "征夫", -"征战": "征戰", -"征收": "征收", -"征服": "征服", -"征求": "征求", -"征发": "征發", -"征衣": "征衣", -"征讨": "征討", -"征途": "征途", -"后台": "後臺", -"从里到外": "從裡到外", -"从里向外": "從裡向外", -"复雠": "復讎", -"复辟": "復辟", -"德干高原": "德干高原", -"心愿": "心愿", -"心荡神驰": "心蕩神馳", -"心里": "心裡", -"忙里": "忙裡", -"快干": "快幹", -"快冲": "快衝", -"怎么": "怎麼", -"怎么着": "怎麼著", -"怒发冲冠": "怒髮衝冠", -"急冲而下": "急衝而下", -"怪里怪气": "怪裡怪氣", -"恩准": "恩准", -"情有所钟": "情有所鍾", -"意面": "意麵", -"慌里慌张": "慌裡慌張", -"慰借": "慰藉", -"忧郁": "憂郁", -"凭吊": "憑吊", -"凭借": "憑藉", -"凭借着": "憑藉著", -"蒙懂": "懞懂", -"怀里": "懷裡", -"怀表": "懷錶", -"悬吊": "懸吊", -"恋恋不舍": "戀戀不捨", -"戏台": "戲臺", -"戴表": "戴錶", -"戽斗": "戽斗", -"房里": "房裡", -"手不释卷": "手不釋卷", -"手卷": "手卷", -"手折": "手摺", -"手里": "手裡", -"手表": "手錶", -"手松": "手鬆", -"才干": "才幹", -"才高八斗": "才高八斗", -"打谷": "打穀", -"扞御": "扞禦", -"批准": "批准", -"批复": "批複", -"批复": "批覆", -"承制": "承製", -"抗御": "抗禦", -"折冲": "折衝", -"披复": "披覆", -"披发": "披髮", -"抱朴": "抱朴", -"抵御": "抵禦", -"拆伙": "拆伙", -"拆台": "拆臺", -"拈须": "拈鬚", -"拉纤": "拉縴", -"拉面": "拉麵", -"拖吊": "拖吊", -"拗别": "拗彆", -"拮据": "拮据", -"振荡": "振蕩", -"捍御": "捍禦", -"舍不得": "捨不得", -"舍出": "捨出", -"舍去": "捨去", -"舍命": "捨命", -"舍己从人": "捨己從人", -"舍己救人": "捨己救人", -"舍己为人": "捨己為人", -"舍己为公": "捨己為公", -"舍己为国": "捨己為國", -"舍得": "捨得", -"舍我其谁": "捨我其誰", -"舍本逐末": "捨本逐末", -"舍弃": "捨棄", -"舍死忘生": "捨死忘生", -"舍生": "捨生", -"舍短取长": "捨短取長", -"舍身": "捨身", -"舍车保帅": "捨車保帥", -"舍近求远": "捨近求遠", -"捲发": "捲髮", -"捵面": "捵麵", -"扫荡": "掃蕩", -"掌柜": "掌柜", -"排骨面": "排骨麵", -"挂帘": "掛帘", -"挂面": "掛麵", -"接着说": "接著說", -"提心吊胆": "提心吊膽", -"插图卷": "插圖卷", -"换吊": "換吊", -"换只": "換隻", -"换发": "換髮", -"摇荡": "搖蕩", -"搭伙": "搭伙", -"折合": "摺合", -"折奏": "摺奏", -"折子": "摺子", -"折尺": "摺尺", -"折扇": "摺扇", -"折梯": "摺梯", -"折椅": "摺椅", -"折叠": "摺疊", -"折痕": "摺痕", -"折篷": "摺篷", -"折纸": "摺紙", -"折裙": "摺裙", -"撒布": "撒佈", -"撚须": "撚鬚", -"撞球台": "撞球檯", -"擂台": "擂臺", -"担仔面": "擔仔麵", -"担担面": "擔擔麵", -"担着": "擔著", -"担负着": "擔負著", -"据云": "據云", -"擢发难数": "擢髮難數", -"摆布": "擺佈", -"摄制": "攝製", -"支干": "支幹", -"收获": "收穫", -"改制": "改製", -"攻克": "攻剋", -"放荡": "放蕩", -"放松": "放鬆", -"叙说着": "敘說著", -"散伙": "散伙", -"散布": "散佈", -"散荡": "散蕩", -"散发": "散髮", -"整只": "整隻", -"整出": "整齣", -"文采": "文采", -"斗六": "斗六", -"斗南": "斗南", -"斗大": "斗大", -"斗子": "斗子", -"斗室": "斗室", -"斗方": "斗方", -"斗栱": "斗栱", -"斗笠": "斗笠", -"斗箕": "斗箕", -"斗篷": "斗篷", -"斗胆": "斗膽", -"斗转参横": "斗轉參橫", -"斗量": "斗量", -"斗门": "斗門", -"料斗": "料斗", -"斯里兰卡": "斯裡蘭卡", -"新历": "新曆", -"断头台": "斷頭臺", -"方才": "方纔", -"施舍": "施捨", -"旋绕着": "旋繞著", -"旋回": "旋迴", -"族里": "族裡", -"日历": "日曆", -"日志": "日誌", -"日进斗金": "日進斗金", -"明了": "明瞭", -"明窗净几": "明窗淨几", -"明里": "明裡", -"星斗": "星斗", -"星历": "星曆", -"星移斗换": "星移斗換", -"星移斗转": "星移斗轉", -"星罗棋布": "星羅棋佈", -"星辰表": "星辰錶", -"春假里": "春假裡", -"春天里": "春天裡", -"晃荡": "晃蕩", -"景致": "景緻", -"暗地里": "暗地裡", -"暗沟里": "暗溝裡", -"暗里": "暗裡", -"历数": "曆數", -"历书": "曆書", -"历法": "曆法", -"书卷": "書卷", -"会干": "會幹", -"会里": "會裡", -"月历": "月曆", -"月台": "月臺", -"有只": "有隻", -"木制": "木製", -"本台": "本臺", -"朴子": "朴子", -"朴实": "朴實", -"朴硝": "朴硝", -"朴素": "朴素", -"朴资茅斯": "朴資茅斯", -"村里": "村裡", -"束发": "束髮", -"东岳": "東嶽", -"东征": "東征", -"松赞干布": "松贊干布", -"板着脸": "板著臉", -"板荡": "板蕩", -"枕借": "枕藉", -"林宏岳": "林宏嶽", -"枝干": "枝幹", -"枯干": "枯幹", -"某只": "某隻", -"染发": "染髮", -"柜上": "柜上", -"柜台": "柜台", -"柜子": "柜子", -"查卷": "查卷", -"查号台": "查號臺", -"校雠学": "校讎學", -"核准": "核准", -"核复": "核覆", -"格里": "格裡", -"案卷": "案卷", -"条干": "條幹", -"棉卷": "棉卷", -"棉制": "棉製", -"植发": "植髮", -"楼台": "樓臺", -"标志着": "標志著", -"标致": "標緻", -"标志": "標誌", -"模制": "模製", -"树干": "樹幹", -"横征暴敛": "橫征暴斂", -"横冲": "橫衝", -"档卷": "檔卷", -"检复": "檢覆", -"台子": "檯子", -"台布": "檯布", -"台灯": "檯燈", -"台球": "檯球", -"台面": "檯面", -"柜台": "櫃檯", -"柜台": "櫃臺", -"栏干": "欄干", -"欺蒙": "欺矇", -"歌后": "歌后", -"欧几里得": "歐幾裡得", -"正当着": "正當著", -"武后": "武后", -"武松": "武鬆", -"归并": "歸併", -"死里求生": "死裡求生", -"死里逃生": "死裡逃生", -"残卷": "殘卷", -"杀虫药": "殺虫藥", -"壳里": "殼裡", -"母后": "母后", -"每只": "每隻", -"比干": "比干", -"毛卷": "毛卷", -"毛发": "毛髮", -"毫发": "毫髮", -"气冲牛斗": "氣沖牛斗", -"气象台": "氣象臺", -"氯霉素": "氯黴素", -"水斗": "水斗", -"水里": "水裡", -"水表": "水錶", -"永历": "永曆", -"污蔑": "汙衊", -"池里": "池裡", -"污蔑": "污衊", -"沈着": "沈著", -"没事干": "沒事幹", -"没精打采": "沒精打采", -"冲着": "沖著", -"沙里淘金": "沙裡淘金", -"河里": "河裡", -"油面": "油麵", -"泡面": "泡麵", -"泰斗": "泰斗", -"洗手不干": "洗手不幹", -"洗发精": "洗髮精", -"派团参加": "派團參加", -"流荡": "流蕩", -"浩荡": "浩蕩", -"浪琴表": "浪琴錶", -"浪荡": "浪蕩", -"浮荡": "浮蕩", -"海里": "海裡", -"涂着": "涂著", -"液晶表": "液晶錶", -"凉面": "涼麵", -"淡朱": "淡硃", -"淫荡": "淫蕩", -"测验卷": "測驗卷", -"港制": "港製", -"游荡": "游蕩", -"凑合着": "湊合著", -"湖里": "湖裡", -"汤团": "湯糰", -"汤面": "湯麵", -"卤制": "滷製", -"卤面": "滷麵", -"满布": "滿佈", -"漂荡": "漂蕩", -"漏斗": "漏斗", -"演奏台": "演奏臺", -"潭里": "潭裡", -"激荡": "激蕩", -"浓郁": "濃郁", -"浓发": "濃髮", -"湿地松": "濕地鬆", -"蒙蒙": "濛濛", -"蒙雾": "濛霧", -"瀛台": "瀛臺", -"弥漫": "瀰漫", -"弥漫着": "瀰漫著", -"火并": "火併", -"灰蒙": "灰濛", -"炒面": "炒麵", -"炮制": "炮製", -"炸药": "炸葯", -"炸酱面": "炸醬麵", -"为着": "為著", -"乌干达": "烏干達", -"乌苏里江": "烏蘇裡江", -"乌发": "烏髮", -"乌龙面": "烏龍麵", -"烘制": "烘製", -"烽火台": "烽火臺", -"无干": "無干", -"无精打采": "無精打采", -"炼制": "煉製", -"烟卷儿": "煙卷兒", -"烟斗": "煙斗", -"烟斗丝": "煙斗絲", -"烟台": "煙臺", -"照准": "照准", -"熨斗": "熨斗", -"灯台": "燈臺", -"燎发": "燎髮", -"烫发": "燙髮", -"烫面": "燙麵", -"烛台": "燭臺", -"炉台": "爐臺", -"爽荡": "爽蕩", -"片言只语": "片言隻語", -"牛肉面": "牛肉麵", -"牛只": "牛隻", -"特准": "特准", -"特征": "特征", -"特里": "特裡", -"特制": "特製", -"牵系": "牽繫", -"狼借": "狼藉", -"猛冲": "猛衝", -"奖杯": "獎盃", -"获准": "獲准", -"率团参加": "率團參加", -"王侯后": "王侯后", -"王后": "王后", -"班里": "班裡", -"理发": "理髮", -"瑶台": "瑤臺", -"甚么": "甚麼", -"甜面酱": "甜麵醬", -"生力面": "生力麵", -"生锈": "生鏽", -"生发": "生髮", -"田里": "田裡", -"由馀": "由余", -"男佣": "男佣", -"男用表": "男用錶", -"留发": "留髮", -"畚斗": "畚斗", -"当着": "當著", -"疏松": "疏鬆", -"疲困": "疲睏", -"病症": "病癥", -"症候": "癥候", -"症状": "癥狀", -"症结": "癥結", -"登台": "登臺", -"发布": "發佈", -"发着": "發著", -"发面": "發麵", -"发霉": "發黴", -"白卷": "白卷", -"白干儿": "白干兒", -"白发": "白髮", -"白面": "白麵", -"百里": "百裡", -"百只": "百隻", -"皇后": "皇后", -"皇历": "皇曆", -"皓发": "皓髮", -"皮里阳秋": "皮裏陽秋", -"皮里春秋": "皮裡春秋", -"皮制": "皮製", -"皱折": "皺摺", -"盒里": "盒裡", -"监制": "監製", -"盘里": "盤裡", -"盘回": "盤迴", -"直接参与": "直接參与", -"直冲": "直衝", -"相克": "相剋", -"相干": "相干", -"相冲": "相衝", -"看台": "看臺", -"眼帘": "眼帘", -"眼眶里": "眼眶裡", -"眼里": "眼裡", -"困乏": "睏乏", -"睡着了": "睡著了", -"了如": "瞭如", -"了望": "瞭望", -"了然": "瞭然", -"了若指掌": "瞭若指掌", -"了解": "瞭解", -"蒙住": "矇住", -"蒙昧无知": "矇昧無知", -"蒙混": "矇混", -"蒙蒙": "矇矇", -"蒙眬": "矇矓", -"蒙蔽": "矇蔽", -"蒙骗": "矇騙", -"短发": "短髮", -"石英表": "石英錶", -"研制": "研製", -"砰当": "砰噹", -"砲台": "砲臺", -"朱唇皓齿": "硃唇皓齒", -"朱批": "硃批", -"朱砂": "硃砂", -"朱笔": "硃筆", -"朱红色": "硃紅色", -"朱色": "硃色", -"硬干": "硬幹", -"砚台": "硯臺", -"碑志": "碑誌", -"磁制": "磁製", -"磨制": "磨製", -"示复": "示覆", -"社里": "社裡", -"神采": "神采", -"御侮": "禦侮", -"御寇": "禦寇", -"御寒": "禦寒", -"御敌": "禦敵", -"秃发": "禿髮", -"秀发": "秀髮", -"私下里": "私下裡", -"秋天里": "秋天裡", -"秋裤": "秋褲", -"秒表": "秒錶", -"稀松": "稀鬆", -"禀复": "稟覆", -"稻谷": "稻穀", -"稽征": "稽征", -"谷仓": "穀倉", -"谷场": "穀場", -"谷子": "穀子", -"谷壳": "穀殼", -"谷物": "穀物", -"谷皮": "穀皮", -"谷神": "穀神", -"谷粒": "穀粒", -"谷舱": "穀艙", -"谷苗": "穀苗", -"谷草": "穀草", -"谷贱伤农": "穀賤傷農", -"谷道": "穀道", -"谷雨": "穀雨", -"谷类": "穀類", -"积极参与": "積极參与", -"积极参加": "積极參加", -"空荡": "空蕩", -"窗帘": "窗帘", -"窗明几净": "窗明几淨", -"窗台": "窗檯", -"窗台": "窗臺", -"窝里": "窩裡", -"窝阔台": "窩闊臺", -"穷追不舍": "窮追不捨", -"笆斗": "笆斗", -"笑里藏刀": "笑裡藏刀", -"第一卷": "第一卷", -"筋斗": "筋斗", -"答卷": "答卷", -"答复": "答複", -"答复": "答覆", -"筵几": "筵几", -"箕斗": "箕斗", -"签着": "簽著", -"吁求": "籲求", -"吁请": "籲請", -"粗制": "粗製", -"粗卤": "粗鹵", -"精干": "精幹", -"精明强干": "精明強幹", -"精致": "精緻", -"精制": "精製", -"精辟": "精辟", -"精采": "精采", -"糊里糊涂": "糊裡糊塗", -"团子": "糰子", -"系着": "系著", -"纪历": "紀曆", -"红发": "紅髮", -"红霉素": "紅黴素", -"纡回": "紆迴", -"纳采": "納采", -"素食面": "素食麵", -"素面": "素麵", -"紫微斗数": "紫微斗數", -"细致": "細緻", -"组里": "組裡", -"结发": "結髮", -"绝对参照": "絕對參照", -"丝来线去": "絲來線去", -"丝布": "絲布", -"丝板": "絲板", -"丝瓜布": "絲瓜布", -"丝绒布": "絲絨布", -"丝线": "絲線", -"丝织厂": "絲織廠", -"丝虫": "絲蟲", -"綑吊": "綑吊", -"经卷": "經卷", -"绿霉素": "綠黴素", -"维系": "維繫", -"绾发": "綰髮", -"网里": "網裡", -"紧绷": "緊繃", -"紧绷着": "緊繃著", -"紧追不舍": "緊追不捨", -"编制": "編製", -"编发": "編髮", -"缓冲": "緩衝", -"致密": "緻密", -"萦回": "縈迴", -"县里": "縣裡", -"县志": "縣誌", -"缝里": "縫裡", -"缝制": "縫製", -"纤夫": "縴夫", -"繁复": "繁複", -"绷住": "繃住", -"绷子": "繃子", -"绷带": "繃帶", -"绷紧": "繃緊", -"绷脸": "繃臉", -"绷着": "繃著", -"绷着脸": "繃著臉", -"绷着脸儿": "繃著臉兒", -"绷开": "繃開", -"绘制": "繪製", -"系上": "繫上", -"系到": "繫到", -"系囚": "繫囚", -"系心": "繫心", -"系念": "繫念", -"系怀": "繫懷", -"系数": "繫數", -"系于": "繫於", -"系系": "繫系", -"系紧": "繫緊", -"系绳": "繫繩", -"系着": "繫著", -"系辞": "繫辭", -"缴卷": "繳卷", -"累囚": "纍囚", -"累累": "纍纍", -"坛子": "罈子", -"坛坛罐罐": "罈罈罐罐", -"骂着": "罵著", -"美制": "美製", -"美发": "美髮", -"翻来覆去": "翻來覆去", -"翻云覆雨": "翻雲覆雨", -"老么": "老么", -"老板": "老闆", -"考卷": "考卷", -"耕获": "耕穫", -"聊斋志异": "聊齋誌異", -"联系": "聯係", -"联系": "聯繫", -"肉丝面": "肉絲麵", -"肉羹面": "肉羹麵", -"肉松": "肉鬆", -"肢体": "肢体", -"背向着": "背向著", -"背地里": "背地裡", -"胡里胡涂": "胡裡胡塗", -"能干": "能幹", -"脉冲": "脈衝", -"脱发": "脫髮", -"腊味": "腊味", -"腊笔": "腊筆", -"腊肉": "腊肉", -"脑子里": "腦子裡", -"腰里": "腰裡", -"胶卷": "膠卷", -"自制": "自製", -"自觉自愿": "自覺自愿", -"台上": "臺上", -"台下": "臺下", -"台中": "臺中", -"台北": "臺北", -"台南": "臺南", -"台地": "臺地", -"台塑": "臺塑", -"台大": "臺大", -"台币": "臺幣", -"台座": "臺座", -"台东": "臺東", -"台柱": "臺柱", -"台榭": "臺榭", -"台汽": "臺汽", -"台海": "臺海", -"台澎金马": "臺澎金馬", -"台湾": "臺灣", -"台灯": "臺燈", -"台球": "臺球", -"台省": "臺省", -"台端": "臺端", -"台糖": "臺糖", -"台肥": "臺肥", -"台航": "臺航", -"台视": "臺視", -"台词": "臺詞", -"台车": "臺車", -"台铁": "臺鐵", -"台阶": "臺階", -"台电": "臺電", -"台面": "臺面", -"舂谷": "舂穀", -"兴致": "興緻", -"兴高采烈": "興高采烈", -"旧历": "舊曆", -"舒卷": "舒卷", -"舞台": "舞臺", -"航海历": "航海曆", -"船只": "船隻", -"舰只": "艦隻", -"芬郁": "芬郁", -"花卷": "花卷", -"花盆里": "花盆裡", -"花采": "花采", -"苑里": "苑裡", -"若干": "若干", -"苦干": "苦幹", -"苦里": "苦裏", -"苦卤": "苦鹵", -"范仲淹": "范仲淹", -"范蠡": "范蠡", -"范阳": "范陽", -"茅台": "茅臺", -"茶几": "茶几", -"草丛里": "草叢裡", -"庄里": "莊裡", -"茎干": "莖幹", -"莽荡": "莽蕩", -"菌丝体": "菌絲体", -"菌丝体": "菌絲體", -"华里": "華裡", -"华发": "華髮", -"万卷": "萬卷", -"万历": "萬曆", -"万只": "萬隻", -"落发": "落髮", -"着儿": "著兒", -"着书立说": "著書立說", -"着色软体": "著色軟體", -"着重指出": "著重指出", -"着录": "著錄", -"着录规则": "著錄規則", -"蓄发": "蓄髮", -"蓄须": "蓄鬚", -"蓬发": "蓬髮", -"蓬松": "蓬鬆", -"莲台": "蓮臺", -"荡来荡去": "蕩來蕩去", -"荡女": "蕩女", -"荡妇": "蕩婦", -"荡寇": "蕩寇", -"荡平": "蕩平", -"荡涤": "蕩滌", -"荡漾": "蕩漾", -"荡然": "蕩然", -"荡舟": "蕩舟", -"荡船": "蕩船", -"荡荡": "蕩蕩", -"薑丝": "薑絲", -"薙发": "薙髮", -"借以": "藉以", -"借口": "藉口", -"借故": "藉故", -"借机": "藉機", -"借此": "藉此", -"借由": "藉由", -"借端": "藉端", -"借着": "藉著", -"借借": "藉藉", -"借词": "藉詞", -"借资": "藉資", -"借酒浇愁": "藉酒澆愁", -"藤制": "藤製", -"蕴含着": "蘊含著", -"蕴涵着": "蘊涵著", -"蕴借": "蘊藉", -"萝卜": "蘿蔔", -"虎须": "虎鬚", -"号志": "號誌", -"蜂后": "蜂后", -"蛮干": "蠻幹", -"行事历": "行事曆", -"胡同": "衚衕", -"冲上": "衝上", -"冲下": "衝下", -"冲来": "衝來", -"冲倒": "衝倒", -"冲出": "衝出", -"冲到": "衝到", -"冲刺": "衝刺", -"冲克": "衝剋", -"冲力": "衝力", -"冲劲": "衝勁", -"冲动": "衝動", -"冲去": "衝去", -"冲口": "衝口", -"冲垮": "衝垮", -"冲堂": "衝堂", -"冲压": "衝壓", -"冲天": "衝天", -"冲掉": "衝掉", -"冲撞": "衝撞", -"冲击": "衝擊", -"冲散": "衝散", -"冲决": "衝決", -"冲浪": "衝浪", -"冲激": "衝激", -"冲破": "衝破", -"冲程": "衝程", -"冲突": "衝突", -"冲线": "衝線", -"冲着": "衝著", -"冲冲": "衝衝", -"冲要": "衝要", -"冲起": "衝起", -"冲进": "衝進", -"冲过": "衝過", -"冲锋": "衝鋒", -"表里": "表裡", -"袖里": "袖裡", -"被里": "被裡", -"被复": "被複", -"被复": "被覆", -"被复着": "被覆著", -"被发": "被髮", -"裁并": "裁併", -"裁制": "裁製", -"里面": "裏面", -"里人": "裡人", -"里加": "裡加", -"里外": "裡外", -"里子": "裡子", -"里屋": "裡屋", -"里层": "裡層", -"里布": "裡布", -"里带": "裡帶", -"里弦": "裡弦", -"里应外合": "裡應外合", -"里拉": "裡拉", -"里斯": "裡斯", -"里海": "裡海", -"里脊": "裡脊", -"里衣": "裡衣", -"里里": "裡裡", -"里通外国": "裡通外國", -"里通外敌": "裡通外敵", -"里边": "裡邊", -"里间": "裡間", -"里面": "裡面", -"里头": "裡頭", -"制件": "製件", -"制作": "製作", -"制做": "製做", -"制备": "製備", -"制冰": "製冰", -"制冷": "製冷", -"制剂": "製劑", -"制品": "製品", -"制图": "製圖", -"制成": "製成", -"制法": "製法", -"制为": "製為", -"制片": "製片", -"制版": "製版", -"制程": "製程", -"制糖": "製糖", -"制纸": "製紙", -"制药": "製藥", -"制表": "製表", -"制裁": "製裁", -"制造": "製造", -"制革": "製革", -"制鞋": "製鞋", -"制盐": "製鹽", -"复仞年如": "複仞年如", -"复以百万": "複以百萬", -"复位": "複位", -"复信": "複信", -"复分数": "複分數", -"复列": "複列", -"复利": "複利", -"复印": "複印", -"复原": "複原", -"复句": "複句", -"复合": "複合", -"复名": "複名", -"复员": "複員", -"复壁": "複壁", -"复壮": "複壯", -"复姓": "複姓", -"复字键": "複字鍵", -"复审": "複審", -"复写": "複寫", -"复式": "複式", -"复复": "複復", -"复数": "複數", -"复本": "複本", -"复查": "複查", -"复核": "複核", -"复检": "複檢", -"复次": "複次", -"复比": "複比", -"复决": "複決", -"复活": "複活", -"复测": "複測", -"复亩珍": "複畝珍", -"复发": "複發", -"复目": "複目", -"复眼": "複眼", -"复种": "複種", -"复线": "複線", -"复习": "複習", -"复兴社": "複興社", -"复旧": "複舊", -"复色": "複色", -"复叶": "複葉", -"复盖": "複蓋", -"复苏": "複蘇", -"复制": "複製", -"复诊": "複診", -"复词": "複詞", -"复试": "複試", -"复课": "複課", -"复议": "複議", -"复变函数": "複變函數", -"复赛": "複賽", -"复述": "複述", -"复选": "複選", -"复钱": "複錢", -"复杂": "複雜", -"复电": "複電", -"复音": "複音", -"复韵": "複韻", -"衬里": "襯裡", -"西岳": "西嶽", -"西征": "西征", -"西历": "西曆", -"要冲": "要衝", -"要么": "要麼", -"复上": "覆上", -"复亡": "覆亡", -"复住": "覆住", -"复信": "覆信", -"复命": "覆命", -"复在": "覆在", -"复审": "覆審", -"复成": "覆成", -"复败": "覆敗", -"复文": "覆文", -"复校": "覆校", -"复核": "覆核", -"覆水难收": "覆水難收", -"复没": "覆沒", -"复灭": "覆滅", -"复盆": "覆盆", -"复舟": "覆舟", -"复着": "覆著", -"复盖": "覆蓋", -"复盖着": "覆蓋著", -"复试": "覆試", -"复议": "覆議", -"复车": "覆車", -"复载": "覆載", -"复辙": "覆轍", -"复电": "覆電", -"见复": "見覆", -"亲征": "親征", -"观众台": "觀眾臺", -"观台": "觀臺", -"观象台": "觀象臺", -"角落里": "角落裡", -"觔斗": "觔斗", -"触须": "觸鬚", -"订制": "訂製", -"诉说着": "訴說著", -"词汇": "詞彙", -"试卷": "試卷", -"诗卷": "詩卷", -"话里有话": "話裡有話", -"志哀": "誌哀", -"志喜": "誌喜", -"志庆": "誌慶", -"语云": "語云", -"语汇": "語彙", -"诬蔑": "誣衊", -"诵经台": "誦經臺", -"说着": "說著", -"课征": "課征", -"调制": "調製", -"调频台": "調頻臺", -"请参阅": "請參閱", -"讲台": "講臺", -"谢绝参观": "謝絕參觀", -"护发": "護髮", -"雠隙": "讎隙", -"豆腐干": "豆腐干", -"竖着": "豎著", -"丰富多采": "豐富多采", -"丰滨": "豐濱", -"丰滨乡": "豐濱鄉", -"丰采": "豐采", -"象征着": "象徵著", -"贵干": "貴幹", -"贾后": "賈后", -"赈饥": "賑饑", -"贤后": "賢后", -"质朴": "質朴", -"赌台": "賭檯", -"购并": "購併", -"赤松": "赤鬆", -"起吊": "起吊", -"起复": "起複", -"赶制": "趕製", -"跌荡": "跌蕩", -"跟斗": "跟斗", -"跳荡": "跳蕩", -"跳表": "跳錶", -"踬仆": "躓仆", -"躯干": "軀幹", -"车库里": "車庫裡", -"车站里": "車站裡", -"车里": "車裡", -"轻松": "輕鬆", -"轮回": "輪迴", -"转台": "轉檯", -"辛丑": "辛丑", -"辟邪": "辟邪", -"办伙": "辦伙", -"办公台": "辦公檯", -"辞汇": "辭彙", -"农历": "農曆", -"迂回": "迂迴", -"近日里": "近日裡", -"迥然回异": "迥然迴異", -"回光返照": "迴光返照", -"回向": "迴向", -"回圈": "迴圈", -"回廊": "迴廊", -"回形夹": "迴形夾", -"回文": "迴文", -"回旋": "迴旋", -"回流": "迴流", -"回环": "迴環", -"回荡": "迴盪", -"回纹针": "迴紋針", -"回绕": "迴繞", -"回肠": "迴腸", -"回荡": "迴蕩", -"回诵": "迴誦", -"回路": "迴路", -"回转": "迴轉", -"回递性": "迴遞性", -"回避": "迴避", -"回响": "迴響", -"回风": "迴風", -"回首": "迴首", -"迷蒙": "迷濛", -"退伙": "退伙", -"这么着": "這么著", -"这里": "這裏", -"这里": "這裡", -"这只": "這隻", -"这么": "這麼", -"这么着": "這麼著", -"通心面": "通心麵", -"速食面": "速食麵", -"连系": "連繫", -"连台好戏": "連臺好戲", -"游荡": "遊蕩", -"遍布": "遍佈", -"递回": "遞迴", -"远征": "遠征", -"适才": "適纔", -"遮复": "遮覆", -"还冲": "還衝", -"邋里邋遢": "邋裡邋遢", -"那里": "那裡", -"那只": "那隻", -"那么": "那麼", -"那么着": "那麼著", -"邪辟": "邪辟", -"郁烈": "郁烈", -"郁穆": "郁穆", -"郁郁": "郁郁", -"郁闭": "郁閉", -"郁馥": "郁馥", -"乡愿": "鄉愿", -"乡里": "鄉裡", -"邻里": "鄰裡", -"配合着": "配合著", -"配制": "配製", -"酒杯": "酒盃", -"酒坛": "酒罈", -"酥松": "酥鬆", -"醋坛": "醋罈", -"酝借": "醞藉", -"酝酿着": "醞釀著", -"医药": "醫葯", -"醲郁": "醲郁", -"酿制": "釀製", -"采地": "采地", -"采女": "采女", -"采声": "采聲", -"采色": "采色", -"采邑": "采邑", -"里程表": "里程錶", -"重折": "重摺", -"重复": "重複", -"重复": "重覆", -"重锤": "重鎚", -"野台戏": "野臺戲", -"金斗": "金斗", -"金表": "金錶", -"金发": "金髮", -"金霉素": "金黴素", -"钉锤": "釘鎚", -"银朱": "銀硃", -"银发": "銀髮", -"铜制": "銅製", -"铝制": "鋁製", -"钢制": "鋼製", -"录着": "錄著", -"录制": "錄製", -"表带": "錶帶", -"表店": "錶店", -"表厂": "錶廠", -"表壳": "錶殼", -"表链": "錶鏈", -"表面": "錶面", -"锅台": "鍋臺", -"锻鍊出": "鍛鍊出", -"锻鍊身体": "鍛鍊身体", -"锲而不舍": "鍥而不捨", -"锤儿": "鎚兒", -"锤子": "鎚子", -"锤头": "鎚頭", -"链霉素": "鏈黴素", -"镜台": "鏡臺", -"锈病": "鏽病", -"锈菌": "鏽菌", -"锈蚀": "鏽蝕", -"钟表": "鐘錶", -"铁锤": "鐵鎚", -"铁锈": "鐵鏽", -"长征": "長征", -"长发": "長髮", -"长须鲸": "長鬚鯨", -"门帘": "門帘", -"门斗": "門斗", -"门里": "門裡", -"开伙": "開伙", -"开卷": "開卷", -"开诚布公": "開誠佈公", -"开采": "開采", -"閒情逸致": "閒情逸緻", -"閒荡": "閒蕩", -"间不容发": "間不容髮", -"闵采尔": "閔采爾", -"阅卷": "閱卷", -"阑干": "闌干", -"关系": "關係", -"关系着": "關係著", -"防御": "防禦", -"防锈": "防鏽", -"防台": "防颱", -"阿斗": "阿斗", -"阿里": "阿裡", -"除旧布新": "除舊佈新", -"阴干": "陰干", -"阴历": "陰曆", -"阴郁": "陰郁", -"陆征祥": "陸征祥", -"阳春面": "陽春麵", -"阳历": "陽曆", -"阳台": "陽臺", -"只字": "隻字", -"只影": "隻影", -"只手遮天": "隻手遮天", -"只眼": "隻眼", -"只言片语": "隻言片語", -"只身": "隻身", -"雅致": "雅緻", -"雇佣": "雇佣", -"双折": "雙摺", -"杂志": "雜誌", -"鸡丝": "雞絲", -"鸡丝面": "雞絲麵", -"鸡腿面": "雞腿麵", -"鸡只": "雞隻", -"难舍": "難捨", -"雪里": "雪裡", -"云须": "雲鬚", -"电子表": "電子錶", -"电台": "電臺", -"电冲": "電衝", -"电复": "電覆", -"电视台": "電視臺", -"电表": "電錶", -"震荡": "震蕩", -"雾里": "霧裡", -"露台": "露臺", -"灵台": "靈臺", -"青瓦台": "青瓦臺", -"青霉": "青黴", -"面朝着": "面朝著", -"面临着": "面臨著", -"鞋里": "鞋裡", -"鞣制": "鞣製", -"秋千": "鞦韆", -"鞭辟入里": "鞭辟入裡", -"韩国制": "韓國製", -"韩制": "韓製", -"预制": "預製", -"颁布": "頒佈", -"头里": "頭裡", -"头发": "頭髮", -"颊须": "頰鬚", -"颠仆": "顛仆", -"颠复": "顛複", -"颠复": "顛覆", -"显着标志": "顯著標志", -"风土志": "風土誌", -"风斗": "風斗", -"风物志": "風物誌", -"风里": "風裡", -"风采": "風采", -"台风": "颱風", -"刮了": "颳了", -"刮倒": "颳倒", -"刮去": "颳去", -"刮得": "颳得", -"刮着": "颳著", -"刮走": "颳走", -"刮起": "颳起", -"刮风": "颳風", -"飘荡": "飄蕩", -"饭团": "飯糰", -"饼干": "餅干", -"馄饨面": "餛飩麵", -"饥不择食": "饑不擇食", -"饥寒": "饑寒", -"饥民": "饑民", -"饥渴": "饑渴", -"饥溺": "饑溺", -"饥荒": "饑荒", -"饥饱": "饑飽", -"饥饿": "饑餓", -"饥馑": "饑饉", -"首当其冲": "首當其衝", -"香郁": "香郁", -"馥郁": "馥郁", -"马里": "馬裡", -"马表": "馬錶", -"骀荡": "駘蕩", -"腾冲": "騰衝", -"骨子里": "骨子裡", -"骨干": "骨幹", -"骨灰坛": "骨灰罈", -"肮脏": "骯髒", -"脏乱": "髒亂", -"脏兮兮": "髒兮兮", -"脏字": "髒字", -"脏得": "髒得", -"脏东西": "髒東西", -"脏水": "髒水", -"脏的": "髒的", -"脏话": "髒話", -"脏钱": "髒錢", -"高干": "高幹", -"高台": "高臺", -"髭须": "髭鬚", -"发型": "髮型", -"发夹": "髮夾", -"发妻": "髮妻", -"发姐": "髮姐", -"发带": "髮帶", -"发廊": "髮廊", -"发式": "髮式", -"发指": "髮指", -"发捲": "髮捲", -"发根": "髮根", -"发毛": "髮毛", -"发油": "髮油", -"发状": "髮狀", -"发短心长": "髮短心長", -"发端": "髮端", -"发结": "髮結", -"发丝": "髮絲", -"发网": "髮網", -"发肤": "髮膚", -"发胶": "髮膠", -"发菜": "髮菜", -"发蜡": "髮蠟", -"发辫": "髮辮", -"发针": "髮針", -"发长": "髮長", -"发际": "髮際", -"发霜": "髮霜", -"发髻": "髮髻", -"发鬓": "髮鬢", -"鬅松": "鬅鬆", -"松了": "鬆了", -"松些": "鬆些", -"松劲": "鬆勁", -"松动": "鬆動", -"松口": "鬆口", -"松土": "鬆土", -"松弛": "鬆弛", -"松快": "鬆快", -"松懈": "鬆懈", -"松手": "鬆手", -"松散": "鬆散", -"松林": "鬆林", -"松柔": "鬆柔", -"松毛虫": "鬆毛蟲", -"松浮": "鬆浮", -"松涛": "鬆濤", -"松科": "鬆科", -"松节油": "鬆節油", -"松绑": "鬆綁", -"松紧": "鬆緊", -"松缓": "鬆緩", -"松脆": "鬆脆", -"松脱": "鬆脫", -"松起": "鬆起", -"松软": "鬆軟", -"松通": "鬆通", -"松开": "鬆開", -"松饼": "鬆餅", -"松松": "鬆鬆", -"鬈发": "鬈髮", -"胡子": "鬍子", -"胡梢": "鬍梢", -"胡渣": "鬍渣", -"胡髭": "鬍髭", -"胡须": "鬍鬚", -"须根": "鬚根", -"须毛": "鬚毛", -"须生": "鬚生", -"须眉": "鬚眉", -"须发": "鬚髮", -"须须": "鬚鬚", -"鬓发": "鬢髮", -"斗着": "鬥著", -"闹着玩儿": "鬧著玩儿", -"闹着玩儿": "鬧著玩兒", -"郁郁": "鬱郁", -"鱼松": "魚鬆", -"鲸须": "鯨鬚", -"鲇鱼": "鯰魚", -"鹤发": "鶴髮", -"卤化": "鹵化", -"卤味": "鹵味", -"卤族": "鹵族", -"卤水": "鹵水", -"卤汁": "鹵汁", -"卤簿": "鹵簿", -"卤素": "鹵素", -"卤莽": "鹵莽", -"卤钝": "鹵鈍", -"咸味": "鹹味", -"咸土": "鹹土", -"咸度": "鹹度", -"咸得": "鹹得", -"咸水": "鹹水", -"咸海": "鹹海", -"咸淡": "鹹淡", -"咸湖": "鹹湖", -"咸汤": "鹹湯", -"咸的": "鹹的", -"咸肉": "鹹肉", -"咸菜": "鹹菜", -"咸蛋": "鹹蛋", -"咸猪肉": "鹹豬肉", -"咸类": "鹹類", -"咸鱼": "鹹魚", -"咸鸭蛋": "鹹鴨蛋", -"咸卤": "鹹鹵", -"咸咸": "鹹鹹", -"盐卤": "鹽鹵", -"面价": "麵價", -"面包": "麵包", -"面团": "麵團", -"面店": "麵店", -"面厂": "麵廠", -"面杖": "麵杖", -"面条": "麵條", -"面灰": "麵灰", -"面皮": "麵皮", -"面筋": "麵筋", -"面粉": "麵粉", -"面糊": "麵糊", -"面线": "麵線", -"面茶": "麵茶", -"面食": "麵食", -"面饺": "麵餃", -"面饼": "麵餅", -"麻酱面": "麻醬麵", -"黄历": "黃曆", -"黄发垂髫": "黃髮垂髫", -"黑发": "黑髮", -"黑松": "黑鬆", -"霉毒": "黴毒", -"霉菌": "黴菌", -"鼓里": "鼓裡", -"冬冬": "鼕鼕", -"龙卷": "龍卷", -"龙须": "龍鬚", -} - -zh2Hans = { -'顯著': '显著', -'土著': '土著', -'印表機': '打印机', -'說明檔案': '帮助文件', -"瀋": "沈", -"畫": "划", -"鍾": "钟", -"靦": "腼", -"餘": "余", -"鯰": "鲇", -"鹼": "碱", -"㠏": "㟆", -"𡞵": "㛟", -"万": "万", -"与": "与", -"丑": "丑", -"丟": "丢", -"並": "并", -"丰": "丰", -"么": "么", -"乾": "干", -"乾坤": "乾坤", -"乾隆": "乾隆", -"亂": "乱", -"云": "云", -"亙": "亘", -"亞": "亚", -"仆": "仆", -"价": "价", -"伙": "伙", -"佇": "伫", -"佈": "布", -"体": "体", -"余": "余", -"佣": "佣", -"併": "并", -"來": "来", -"侖": "仑", -"侶": "侣", -"俁": "俣", -"係": "系", -"俔": "伣", -"俠": "侠", -"倀": "伥", -"倆": "俩", -"倈": "俫", -"倉": "仓", -"個": "个", -"們": "们", -"倫": "伦", -"偉": "伟", -"側": "侧", -"偵": "侦", -"偽": "伪", -"傑": "杰", -"傖": "伧", -"傘": "伞", -"備": "备", -"傢": "家", -"傭": "佣", -"傯": "偬", -"傳": "传", -"傴": "伛", -"債": "债", -"傷": "伤", -"傾": "倾", -"僂": "偻", -"僅": "仅", -"僉": "佥", -"僑": "侨", -"僕": "仆", -"僞": "伪", -"僥": "侥", -"僨": "偾", -"價": "价", -"儀": "仪", -"儂": "侬", -"億": "亿", -"儈": "侩", -"儉": "俭", -"儐": "傧", -"儔": "俦", -"儕": "侪", -"儘": "尽", -"償": "偿", -"優": "优", -"儲": "储", -"儷": "俪", -"儸": "㑩", -"儺": "傩", -"儻": "傥", -"儼": "俨", -"儿": "儿", -"兇": "凶", -"兌": "兑", -"兒": "儿", -"兗": "兖", -"党": "党", -"內": "内", -"兩": "两", -"冊": "册", -"冪": "幂", -"准": "准", -"凈": "净", -"凍": "冻", -"凜": "凛", -"几": "几", -"凱": "凯", -"划": "划", -"別": "别", -"刪": "删", -"剄": "刭", -"則": "则", -"剋": "克", -"剎": "刹", -"剗": "刬", -"剛": "刚", -"剝": "剥", -"剮": "剐", -"剴": "剀", -"創": "创", -"劃": "划", -"劇": "剧", -"劉": "刘", -"劊": "刽", -"劌": "刿", -"劍": "剑", -"劏": "㓥", -"劑": "剂", -"劚": "㔉", -"勁": "劲", -"動": "动", -"務": "务", -"勛": "勋", -"勝": "胜", -"勞": "劳", -"勢": "势", -"勩": "勚", -"勱": "劢", -"勵": "励", -"勸": "劝", -"勻": "匀", -"匭": "匦", -"匯": "汇", -"匱": "匮", -"區": "区", -"協": "协", -"卷": "卷", -"卻": "却", -"厂": "厂", -"厙": "厍", -"厠": "厕", -"厭": "厌", -"厲": "厉", -"厴": "厣", -"參": "参", -"叄": "叁", -"叢": "丛", -"台": "台", -"叶": "叶", -"吊": "吊", -"后": "后", -"吳": "吴", -"吶": "呐", -"呂": "吕", -"獃": "呆", -"咼": "呙", -"員": "员", -"唄": "呗", -"唚": "吣", -"問": "问", -"啓": "启", -"啞": "哑", -"啟": "启", -"啢": "唡", -"喎": "㖞", -"喚": "唤", -"喪": "丧", -"喬": "乔", -"單": "单", -"喲": "哟", -"嗆": "呛", -"嗇": "啬", -"嗊": "唝", -"嗎": "吗", -"嗚": "呜", -"嗩": "唢", -"嗶": "哔", -"嘆": "叹", -"嘍": "喽", -"嘔": "呕", -"嘖": "啧", -"嘗": "尝", -"嘜": "唛", -"嘩": "哗", -"嘮": "唠", -"嘯": "啸", -"嘰": "叽", -"嘵": "哓", -"嘸": "呒", -"嘽": "啴", -"噁": "恶", -"噓": "嘘", -"噚": "㖊", -"噝": "咝", -"噠": "哒", -"噥": "哝", -"噦": "哕", -"噯": "嗳", -"噲": "哙", -"噴": "喷", -"噸": "吨", -"噹": "当", -"嚀": "咛", -"嚇": "吓", -"嚌": "哜", -"嚕": "噜", -"嚙": "啮", -"嚥": "咽", -"嚦": "呖", -"嚨": "咙", -"嚮": "向", -"嚲": "亸", -"嚳": "喾", -"嚴": "严", -"嚶": "嘤", -"囀": "啭", -"囁": "嗫", -"囂": "嚣", -"囅": "冁", -"囈": "呓", -"囌": "苏", -"囑": "嘱", -"囪": "囱", -"圇": "囵", -"國": "国", -"圍": "围", -"園": "园", -"圓": "圆", -"圖": "图", -"團": "团", -"坏": "坏", -"垵": "埯", -"埡": "垭", -"埰": "采", -"執": "执", -"堅": "坚", -"堊": "垩", -"堖": "垴", -"堝": "埚", -"堯": "尧", -"報": "报", -"場": "场", -"塊": "块", -"塋": "茔", -"塏": "垲", -"塒": "埘", -"塗": "涂", -"塚": "冢", -"塢": "坞", -"塤": "埙", -"塵": "尘", -"塹": "堑", -"墊": "垫", -"墜": "坠", -"墮": "堕", -"墳": "坟", -"墻": "墙", -"墾": "垦", -"壇": "坛", -"壈": "𡒄", -"壋": "垱", -"壓": "压", -"壘": "垒", -"壙": "圹", -"壚": "垆", -"壞": "坏", -"壟": "垄", -"壠": "垅", -"壢": "坜", -"壩": "坝", -"壯": "壮", -"壺": "壶", -"壼": "壸", -"壽": "寿", -"夠": "够", -"夢": "梦", -"夾": "夹", -"奐": "奂", -"奧": "奥", -"奩": "奁", -"奪": "夺", -"奬": "奖", -"奮": "奋", -"奼": "姹", -"妝": "妆", -"姍": "姗", -"姜": "姜", -"姦": "奸", -"娛": "娱", -"婁": "娄", -"婦": "妇", -"婭": "娅", -"媧": "娲", -"媯": "妫", -"媼": "媪", -"媽": "妈", -"嫗": "妪", -"嫵": "妩", -"嫻": "娴", -"嫿": "婳", -"嬀": "妫", -"嬈": "娆", -"嬋": "婵", -"嬌": "娇", -"嬙": "嫱", -"嬡": "嫒", -"嬤": "嬷", -"嬪": "嫔", -"嬰": "婴", -"嬸": "婶", -"孌": "娈", -"孫": "孙", -"學": "学", -"孿": "孪", -"宁": "宁", -"宮": "宫", -"寢": "寝", -"實": "实", -"寧": "宁", -"審": "审", -"寫": "写", -"寬": "宽", -"寵": "宠", -"寶": "宝", -"將": "将", -"專": "专", -"尋": "寻", -"對": "对", -"導": "导", -"尷": "尴", -"屆": "届", -"屍": "尸", -"屓": "屃", -"屜": "屉", -"屢": "屡", -"層": "层", -"屨": "屦", -"屬": "属", -"岡": "冈", -"峴": "岘", -"島": "岛", -"峽": "峡", -"崍": "崃", -"崗": "岗", -"崢": "峥", -"崬": "岽", -"嵐": "岚", -"嶁": "嵝", -"嶄": "崭", -"嶇": "岖", -"嶔": "嵚", -"嶗": "崂", -"嶠": "峤", -"嶢": "峣", -"嶧": "峄", -"嶮": "崄", -"嶴": "岙", -"嶸": "嵘", -"嶺": "岭", -"嶼": "屿", -"嶽": "岳", -"巋": "岿", -"巒": "峦", -"巔": "巅", -"巰": "巯", -"帘": "帘", -"帥": "帅", -"師": "师", -"帳": "帐", -"帶": "带", -"幀": "帧", -"幃": "帏", -"幗": "帼", -"幘": "帻", -"幟": "帜", -"幣": "币", -"幫": "帮", -"幬": "帱", -"幹": "干", -"幺": "么", -"幾": "几", -"广": "广", -"庫": "库", -"廁": "厕", -"廂": "厢", -"廄": "厩", -"廈": "厦", -"廚": "厨", -"廝": "厮", -"廟": "庙", -"廠": "厂", -"廡": "庑", -"廢": "废", -"廣": "广", -"廩": "廪", -"廬": "庐", -"廳": "厅", -"弒": "弑", -"弳": "弪", -"張": "张", -"強": "强", -"彆": "别", -"彈": "弹", -"彌": "弥", -"彎": "弯", -"彙": "汇", -"彞": "彝", -"彥": "彦", -"征": "征", -"後": "后", -"徑": "径", -"從": "从", -"徠": "徕", -"復": "复", -"徵": "征", -"徹": "彻", -"志": "志", -"恆": "恒", -"恥": "耻", -"悅": "悦", -"悞": "悮", -"悵": "怅", -"悶": "闷", -"惡": "恶", -"惱": "恼", -"惲": "恽", -"惻": "恻", -"愛": "爱", -"愜": "惬", -"愨": "悫", -"愴": "怆", -"愷": "恺", -"愾": "忾", -"愿": "愿", -"慄": "栗", -"態": "态", -"慍": "愠", -"慘": "惨", -"慚": "惭", -"慟": "恸", -"慣": "惯", -"慤": "悫", -"慪": "怄", -"慫": "怂", -"慮": "虑", -"慳": "悭", -"慶": "庆", -"憂": "忧", -"憊": "惫", -"憐": "怜", -"憑": "凭", -"憒": "愦", -"憚": "惮", -"憤": "愤", -"憫": "悯", -"憮": "怃", -"憲": "宪", -"憶": "忆", -"懇": "恳", -"應": "应", -"懌": "怿", -"懍": "懔", -"懞": "蒙", -"懟": "怼", -"懣": "懑", -"懨": "恹", -"懲": "惩", -"懶": "懒", -"懷": "怀", -"懸": "悬", -"懺": "忏", -"懼": "惧", -"懾": "慑", -"戀": "恋", -"戇": "戆", -"戔": "戋", -"戧": "戗", -"戩": "戬", -"戰": "战", -"戱": "戯", -"戲": "戏", -"戶": "户", -"担": "担", -"拋": "抛", -"挩": "捝", -"挾": "挟", -"捨": "舍", -"捫": "扪", -"据": "据", -"掃": "扫", -"掄": "抡", -"掗": "挜", -"掙": "挣", -"掛": "挂", -"採": "采", -"揀": "拣", -"揚": "扬", -"換": "换", -"揮": "挥", -"損": "损", -"搖": "摇", -"搗": "捣", -"搵": "揾", -"搶": "抢", -"摑": "掴", -"摜": "掼", -"摟": "搂", -"摯": "挚", -"摳": "抠", -"摶": "抟", -"摺": "折", -"摻": "掺", -"撈": "捞", -"撏": "挦", -"撐": "撑", -"撓": "挠", -"撝": "㧑", -"撟": "挢", -"撣": "掸", -"撥": "拨", -"撫": "抚", -"撲": "扑", -"撳": "揿", -"撻": "挞", -"撾": "挝", -"撿": "捡", -"擁": "拥", -"擄": "掳", -"擇": "择", -"擊": "击", -"擋": "挡", -"擓": "㧟", -"擔": "担", -"據": "据", -"擠": "挤", -"擬": "拟", -"擯": "摈", -"擰": "拧", -"擱": "搁", -"擲": "掷", -"擴": "扩", -"擷": "撷", -"擺": "摆", -"擻": "擞", -"擼": "撸", -"擾": "扰", -"攄": "摅", -"攆": "撵", -"攏": "拢", -"攔": "拦", -"攖": "撄", -"攙": "搀", -"攛": "撺", -"攜": "携", -"攝": "摄", -"攢": "攒", -"攣": "挛", -"攤": "摊", -"攪": "搅", -"攬": "揽", -"敗": "败", -"敘": "叙", -"敵": "敌", -"數": "数", -"斂": "敛", -"斃": "毙", -"斕": "斓", -"斗": "斗", -"斬": "斩", -"斷": "断", -"於": "于", -"時": "时", -"晉": "晋", -"晝": "昼", -"暈": "晕", -"暉": "晖", -"暘": "旸", -"暢": "畅", -"暫": "暂", -"曄": "晔", -"曆": "历", -"曇": "昙", -"曉": "晓", -"曏": "向", -"曖": "暧", -"曠": "旷", -"曨": "昽", -"曬": "晒", -"書": "书", -"會": "会", -"朧": "胧", -"朮": "术", -"术": "术", -"朴": "朴", -"東": "东", -"杴": "锨", -"极": "极", -"柜": "柜", -"柵": "栅", -"桿": "杆", -"梔": "栀", -"梘": "枧", -"條": "条", -"梟": "枭", -"梲": "棁", -"棄": "弃", -"棖": "枨", -"棗": "枣", -"棟": "栋", -"棧": "栈", -"棲": "栖", -"棶": "梾", -"椏": "桠", -"楊": "杨", -"楓": "枫", -"楨": "桢", -"業": "业", -"極": "极", -"榪": "杩", -"榮": "荣", -"榲": "榅", -"榿": "桤", -"構": "构", -"槍": "枪", -"槤": "梿", -"槧": "椠", -"槨": "椁", -"槳": "桨", -"樁": "桩", -"樂": "乐", -"樅": "枞", -"樓": "楼", -"標": "标", -"樞": "枢", -"樣": "样", -"樸": "朴", -"樹": "树", -"樺": "桦", -"橈": "桡", -"橋": "桥", -"機": "机", -"橢": "椭", -"橫": "横", -"檁": "檩", -"檉": "柽", -"檔": "档", -"檜": "桧", -"檟": "槚", -"檢": "检", -"檣": "樯", -"檮": "梼", -"檯": "台", -"檳": "槟", -"檸": "柠", -"檻": "槛", -"櫃": "柜", -"櫓": "橹", -"櫚": "榈", -"櫛": "栉", -"櫝": "椟", -"櫞": "橼", -"櫟": "栎", -"櫥": "橱", -"櫧": "槠", -"櫨": "栌", -"櫪": "枥", -"櫫": "橥", -"櫬": "榇", -"櫱": "蘖", -"櫳": "栊", -"櫸": "榉", -"櫻": "樱", -"欄": "栏", -"權": "权", -"欏": "椤", -"欒": "栾", -"欖": "榄", -"欞": "棂", -"欽": "钦", -"歐": "欧", -"歟": "欤", -"歡": "欢", -"歲": "岁", -"歷": "历", -"歸": "归", -"歿": "殁", -"殘": "残", -"殞": "殒", -"殤": "殇", -"殨": "㱮", -"殫": "殚", -"殮": "殓", -"殯": "殡", -"殰": "㱩", -"殲": "歼", -"殺": "杀", -"殻": "壳", -"殼": "壳", -"毀": "毁", -"毆": "殴", -"毿": "毵", -"氂": "牦", -"氈": "毡", -"氌": "氇", -"氣": "气", -"氫": "氢", -"氬": "氩", -"氳": "氲", -"汙": "污", -"決": "决", -"沒": "没", -"沖": "冲", -"況": "况", -"洶": "汹", -"浹": "浃", -"涂": "涂", -"涇": "泾", -"涼": "凉", -"淀": "淀", -"淒": "凄", -"淚": "泪", -"淥": "渌", -"淨": "净", -"淩": "凌", -"淪": "沦", -"淵": "渊", -"淶": "涞", -"淺": "浅", -"渙": "涣", -"減": "减", -"渦": "涡", -"測": "测", -"渾": "浑", -"湊": "凑", -"湞": "浈", -"湯": "汤", -"溈": "沩", -"準": "准", -"溝": "沟", -"溫": "温", -"滄": "沧", -"滅": "灭", -"滌": "涤", -"滎": "荥", -"滬": "沪", -"滯": "滞", -"滲": "渗", -"滷": "卤", -"滸": "浒", -"滻": "浐", -"滾": "滚", -"滿": "满", -"漁": "渔", -"漚": "沤", -"漢": "汉", -"漣": "涟", -"漬": "渍", -"漲": "涨", -"漵": "溆", -"漸": "渐", -"漿": "浆", -"潁": "颍", -"潑": "泼", -"潔": "洁", -"潙": "沩", -"潛": "潜", -"潤": "润", -"潯": "浔", -"潰": "溃", -"潷": "滗", -"潿": "涠", -"澀": "涩", -"澆": "浇", -"澇": "涝", -"澐": "沄", -"澗": "涧", -"澠": "渑", -"澤": "泽", -"澦": "滪", -"澩": "泶", -"澮": "浍", -"澱": "淀", -"濁": "浊", -"濃": "浓", -"濕": "湿", -"濘": "泞", -"濛": "蒙", -"濟": "济", -"濤": "涛", -"濫": "滥", -"濰": "潍", -"濱": "滨", -"濺": "溅", -"濼": "泺", -"濾": "滤", -"瀅": "滢", -"瀆": "渎", -"瀇": "㲿", -"瀉": "泻", -"瀋": "沈", -"瀏": "浏", -"瀕": "濒", -"瀘": "泸", -"瀝": "沥", -"瀟": "潇", -"瀠": "潆", -"瀦": "潴", -"瀧": "泷", -"瀨": "濑", -"瀰": "弥", -"瀲": "潋", -"瀾": "澜", -"灃": "沣", -"灄": "滠", -"灑": "洒", -"灕": "漓", -"灘": "滩", -"灝": "灏", -"灠": "漤", -"灣": "湾", -"灤": "滦", -"灧": "滟", -"災": "灾", -"為": "为", -"烏": "乌", -"烴": "烃", -"無": "无", -"煉": "炼", -"煒": "炜", -"煙": "烟", -"煢": "茕", -"煥": "焕", -"煩": "烦", -"煬": "炀", -"煱": "㶽", -"熅": "煴", -"熒": "荧", -"熗": "炝", -"熱": "热", -"熲": "颎", -"熾": "炽", -"燁": "烨", -"燈": "灯", -"燉": "炖", -"燒": "烧", -"燙": "烫", -"燜": "焖", -"營": "营", -"燦": "灿", -"燭": "烛", -"燴": "烩", -"燶": "㶶", -"燼": "烬", -"燾": "焘", -"爍": "烁", -"爐": "炉", -"爛": "烂", -"爭": "争", -"爲": "为", -"爺": "爷", -"爾": "尔", -"牆": "墙", -"牘": "牍", -"牽": "牵", -"犖": "荦", -"犢": "犊", -"犧": "牺", -"狀": "状", -"狹": "狭", -"狽": "狈", -"猙": "狰", -"猶": "犹", -"猻": "狲", -"獁": "犸", -"獄": "狱", -"獅": "狮", -"獎": "奖", -"獨": "独", -"獪": "狯", -"獫": "猃", -"獮": "狝", -"獰": "狞", -"獱": "㺍", -"獲": "获", -"獵": "猎", -"獷": "犷", -"獸": "兽", -"獺": "獭", -"獻": "献", -"獼": "猕", -"玀": "猡", -"現": "现", -"琺": "珐", -"琿": "珲", -"瑋": "玮", -"瑒": "玚", -"瑣": "琐", -"瑤": "瑶", -"瑩": "莹", -"瑪": "玛", -"瑲": "玱", -"璉": "琏", -"璣": "玑", -"璦": "瑷", -"璫": "珰", -"環": "环", -"璽": "玺", -"瓊": "琼", -"瓏": "珑", -"瓔": "璎", -"瓚": "瓒", -"甌": "瓯", -"產": "产", -"産": "产", -"畝": "亩", -"畢": "毕", -"異": "异", -"畵": "画", -"當": "当", -"疇": "畴", -"疊": "叠", -"痙": "痉", -"痾": "疴", -"瘂": "痖", -"瘋": "疯", -"瘍": "疡", -"瘓": "痪", -"瘞": "瘗", -"瘡": "疮", -"瘧": "疟", -"瘮": "瘆", -"瘲": "疭", -"瘺": "瘘", -"瘻": "瘘", -"療": "疗", -"癆": "痨", -"癇": "痫", -"癉": "瘅", -"癘": "疠", -"癟": "瘪", -"癢": "痒", -"癤": "疖", -"癥": "症", -"癧": "疬", -"癩": "癞", -"癬": "癣", -"癭": "瘿", -"癮": "瘾", -"癰": "痈", -"癱": "瘫", -"癲": "癫", -"發": "发", -"皚": "皑", -"皰": "疱", -"皸": "皲", -"皺": "皱", -"盃": "杯", -"盜": "盗", -"盞": "盏", -"盡": "尽", -"監": "监", -"盤": "盘", -"盧": "卢", -"盪": "荡", -"眥": "眦", -"眾": "众", -"睏": "困", -"睜": "睁", -"睞": "睐", -"瞘": "眍", -"瞜": "䁖", -"瞞": "瞒", -"瞭": "了", -"瞶": "瞆", -"瞼": "睑", -"矇": "蒙", -"矓": "眬", -"矚": "瞩", -"矯": "矫", -"硃": "朱", -"硜": "硁", -"硤": "硖", -"硨": "砗", -"确": "确", -"硯": "砚", -"碩": "硕", -"碭": "砀", -"碸": "砜", -"確": "确", -"碼": "码", -"磑": "硙", -"磚": "砖", -"磣": "碜", -"磧": "碛", -"磯": "矶", -"磽": "硗", -"礆": "硷", -"礎": "础", -"礙": "碍", -"礦": "矿", -"礪": "砺", -"礫": "砾", -"礬": "矾", -"礱": "砻", -"祿": "禄", -"禍": "祸", -"禎": "祯", -"禕": "祎", -"禡": "祃", -"禦": "御", -"禪": "禅", -"禮": "礼", -"禰": "祢", -"禱": "祷", -"禿": "秃", -"秈": "籼", -"种": "种", -"稅": "税", -"稈": "秆", -"稏": "䅉", -"稟": "禀", -"種": "种", -"稱": "称", -"穀": "谷", -"穌": "稣", -"積": "积", -"穎": "颖", -"穠": "秾", -"穡": "穑", -"穢": "秽", -"穩": "稳", -"穫": "获", -"穭": "稆", -"窩": "窝", -"窪": "洼", -"窮": "穷", -"窯": "窑", -"窵": "窎", -"窶": "窭", -"窺": "窥", -"竄": "窜", -"竅": "窍", -"竇": "窦", -"竈": "灶", -"竊": "窃", -"竪": "竖", -"競": "竞", -"筆": "笔", -"筍": "笋", -"筑": "筑", -"筧": "笕", -"筴": "䇲", -"箋": "笺", -"箏": "筝", -"節": "节", -"範": "范", -"築": "筑", -"篋": "箧", -"篔": "筼", -"篤": "笃", -"篩": "筛", -"篳": "筚", -"簀": "箦", -"簍": "篓", -"簞": "箪", -"簡": "简", -"簣": "篑", -"簫": "箫", -"簹": "筜", -"簽": "签", -"簾": "帘", -"籃": "篮", -"籌": "筹", -"籖": "签", -"籙": "箓", -"籜": "箨", -"籟": "籁", -"籠": "笼", -"籩": "笾", -"籪": "簖", -"籬": "篱", -"籮": "箩", -"籲": "吁", -"粵": "粤", -"糝": "糁", -"糞": "粪", -"糧": "粮", -"糰": "团", -"糲": "粝", -"糴": "籴", -"糶": "粜", -"糹": "纟", -"糾": "纠", -"紀": "纪", -"紂": "纣", -"約": "约", -"紅": "红", -"紆": "纡", -"紇": "纥", -"紈": "纨", -"紉": "纫", -"紋": "纹", -"納": "纳", -"紐": "纽", -"紓": "纾", -"純": "纯", -"紕": "纰", -"紖": "纼", -"紗": "纱", -"紘": "纮", -"紙": "纸", -"級": "级", -"紛": "纷", -"紜": "纭", -"紝": "纴", -"紡": "纺", -"紬": "䌷", -"細": "细", -"紱": "绂", -"紲": "绁", -"紳": "绅", -"紵": "纻", -"紹": "绍", -"紺": "绀", -"紼": "绋", -"紿": "绐", -"絀": "绌", -"終": "终", -"組": "组", -"絅": "䌹", -"絆": "绊", -"絎": "绗", -"結": "结", -"絕": "绝", -"絛": "绦", -"絝": "绔", -"絞": "绞", -"絡": "络", -"絢": "绚", -"給": "给", -"絨": "绒", -"絰": "绖", -"統": "统", -"絲": "丝", -"絳": "绛", -"絶": "绝", -"絹": "绢", -"綁": "绑", -"綃": "绡", -"綆": "绠", -"綈": "绨", -"綉": "绣", -"綌": "绤", -"綏": "绥", -"綐": "䌼", -"經": "经", -"綜": "综", -"綞": "缍", -"綠": "绿", -"綢": "绸", -"綣": "绻", -"綫": "线", -"綬": "绶", -"維": "维", -"綯": "绹", -"綰": "绾", -"綱": "纲", -"網": "网", -"綳": "绷", -"綴": "缀", -"綵": "䌽", -"綸": "纶", -"綹": "绺", -"綺": "绮", -"綻": "绽", -"綽": "绰", -"綾": "绫", -"綿": "绵", -"緄": "绲", -"緇": "缁", -"緊": "紧", -"緋": "绯", -"緑": "绿", -"緒": "绪", -"緓": "绬", -"緔": "绱", -"緗": "缃", -"緘": "缄", -"緙": "缂", -"線": "线", -"緝": "缉", -"緞": "缎", -"締": "缔", -"緡": "缗", -"緣": "缘", -"緦": "缌", -"編": "编", -"緩": "缓", -"緬": "缅", -"緯": "纬", -"緱": "缑", -"緲": "缈", -"練": "练", -"緶": "缏", -"緹": "缇", -"緻": "致", -"縈": "萦", -"縉": "缙", -"縊": "缢", -"縋": "缒", -"縐": "绉", -"縑": "缣", -"縕": "缊", -"縗": "缞", -"縛": "缚", -"縝": "缜", -"縞": "缟", -"縟": "缛", -"縣": "县", -"縧": "绦", -"縫": "缝", -"縭": "缡", -"縮": "缩", -"縱": "纵", -"縲": "缧", -"縳": "䌸", -"縴": "纤", -"縵": "缦", -"縶": "絷", -"縷": "缕", -"縹": "缥", -"總": "总", -"績": "绩", -"繃": "绷", -"繅": "缫", -"繆": "缪", -"繒": "缯", -"織": "织", -"繕": "缮", -"繚": "缭", -"繞": "绕", -"繡": "绣", -"繢": "缋", -"繩": "绳", -"繪": "绘", -"繫": "系", -"繭": "茧", -"繮": "缰", -"繯": "缳", -"繰": "缲", -"繳": "缴", -"繸": "䍁", -"繹": "绎", -"繼": "继", -"繽": "缤", -"繾": "缱", -"繿": "䍀", -"纈": "缬", -"纊": "纩", -"續": "续", -"纍": "累", -"纏": "缠", -"纓": "缨", -"纔": "才", -"纖": "纤", -"纘": "缵", -"纜": "缆", -"缽": "钵", -"罈": "坛", -"罌": "罂", -"罰": "罚", -"罵": "骂", -"罷": "罢", -"羅": "罗", -"羆": "罴", -"羈": "羁", -"羋": "芈", -"羥": "羟", -"義": "义", -"習": "习", -"翹": "翘", -"耬": "耧", -"耮": "耢", -"聖": "圣", -"聞": "闻", -"聯": "联", -"聰": "聪", -"聲": "声", -"聳": "耸", -"聵": "聩", -"聶": "聂", -"職": "职", -"聹": "聍", -"聽": "听", -"聾": "聋", -"肅": "肃", -"胜": "胜", -"脅": "胁", -"脈": "脉", -"脛": "胫", -"脫": "脱", -"脹": "胀", -"腊": "腊", -"腎": "肾", -"腖": "胨", -"腡": "脶", -"腦": "脑", -"腫": "肿", -"腳": "脚", -"腸": "肠", -"膃": "腽", -"膚": "肤", -"膠": "胶", -"膩": "腻", -"膽": "胆", -"膾": "脍", -"膿": "脓", -"臉": "脸", -"臍": "脐", -"臏": "膑", -"臘": "腊", -"臚": "胪", -"臟": "脏", -"臠": "脔", -"臢": "臜", -"臥": "卧", -"臨": "临", -"臺": "台", -"與": "与", -"興": "兴", -"舉": "举", -"舊": "旧", -"艙": "舱", -"艤": "舣", -"艦": "舰", -"艫": "舻", -"艱": "艰", -"艷": "艳", -"芻": "刍", -"苧": "苎", -"苹": "苹", -"范": "范", -"茲": "兹", -"荊": "荆", -"莊": "庄", -"莖": "茎", -"莢": "荚", -"莧": "苋", -"華": "华", -"萇": "苌", -"萊": "莱", -"萬": "万", -"萵": "莴", -"葉": "叶", -"葒": "荭", -"著名": "著名", -"葤": "荮", -"葦": "苇", -"葯": "药", -"葷": "荤", -"蒓": "莼", -"蒔": "莳", -"蒞": "莅", -"蒼": "苍", -"蓀": "荪", -"蓋": "盖", -"蓮": "莲", -"蓯": "苁", -"蓴": "莼", -"蓽": "荜", -"蔔": "卜", -"蔞": "蒌", -"蔣": "蒋", -"蔥": "葱", -"蔦": "茑", -"蔭": "荫", -"蕁": "荨", -"蕆": "蒇", -"蕎": "荞", -"蕒": "荬", -"蕓": "芸", -"蕕": "莸", -"蕘": "荛", -"蕢": "蒉", -"蕩": "荡", -"蕪": "芜", -"蕭": "萧", -"蕷": "蓣", -"薀": "蕰", -"薈": "荟", -"薊": "蓟", -"薌": "芗", -"薔": "蔷", -"薘": "荙", -"薟": "莶", -"薦": "荐", -"薩": "萨", -"薳": "䓕", -"薴": "苧", -"薺": "荠", -"藉": "借", -"藍": "蓝", -"藎": "荩", -"藝": "艺", -"藥": "药", -"藪": "薮", -"藴": "蕴", -"藶": "苈", -"藹": "蔼", -"藺": "蔺", -"蘄": "蕲", -"蘆": "芦", -"蘇": "苏", -"蘊": "蕴", -"蘋": "苹", -"蘚": "藓", -"蘞": "蔹", -"蘢": "茏", -"蘭": "兰", -"蘺": "蓠", -"蘿": "萝", -"虆": "蔂", -"處": "处", -"虛": "虚", -"虜": "虏", -"號": "号", -"虧": "亏", -"虫": "虫", -"虯": "虬", -"蛺": "蛱", -"蛻": "蜕", -"蜆": "蚬", -"蜡": "蜡", -"蝕": "蚀", -"蝟": "猬", -"蝦": "虾", -"蝸": "蜗", -"螄": "蛳", -"螞": "蚂", -"螢": "萤", -"螮": "䗖", -"螻": "蝼", -"螿": "螀", -"蟄": "蛰", -"蟈": "蝈", -"蟎": "螨", -"蟣": "虮", -"蟬": "蝉", -"蟯": "蛲", -"蟲": "虫", -"蟶": "蛏", -"蟻": "蚁", -"蠅": "蝇", -"蠆": "虿", -"蠐": "蛴", -"蠑": "蝾", -"蠟": "蜡", -"蠣": "蛎", -"蠨": "蟏", -"蠱": "蛊", -"蠶": "蚕", -"蠻": "蛮", -"衆": "众", -"衊": "蔑", -"術": "术", -"衕": "同", -"衚": "胡", -"衛": "卫", -"衝": "冲", -"衹": "只", -"袞": "衮", -"裊": "袅", -"裏": "里", -"補": "补", -"裝": "装", -"裡": "里", -"製": "制", -"複": "复", -"褌": "裈", -"褘": "袆", -"褲": "裤", -"褳": "裢", -"褸": "褛", -"褻": "亵", -"襇": "裥", -"襏": "袯", -"襖": "袄", -"襝": "裣", -"襠": "裆", -"襤": "褴", -"襪": "袜", -"襬": "䙓", -"襯": "衬", -"襲": "袭", -"覆蓋": "覆盖", -"翻來覆去": "翻来覆去", -"見": "见", -"覎": "觃", -"規": "规", -"覓": "觅", -"視": "视", -"覘": "觇", -"覡": "觋", -"覥": "觍", -"覦": "觎", -"親": "亲", -"覬": "觊", -"覯": "觏", -"覲": "觐", -"覷": "觑", -"覺": "觉", -"覽": "览", -"覿": "觌", -"觀": "观", -"觴": "觞", -"觶": "觯", -"觸": "触", -"訁": "讠", -"訂": "订", -"訃": "讣", -"計": "计", -"訊": "讯", -"訌": "讧", -"討": "讨", -"訐": "讦", -"訒": "讱", -"訓": "训", -"訕": "讪", -"訖": "讫", -"託": "讬", -"記": "记", -"訛": "讹", -"訝": "讶", -"訟": "讼", -"訢": "䜣", -"訣": "诀", -"訥": "讷", -"訩": "讻", -"訪": "访", -"設": "设", -"許": "许", -"訴": "诉", -"訶": "诃", -"診": "诊", -"註": "注", -"詁": "诂", -"詆": "诋", -"詎": "讵", -"詐": "诈", -"詒": "诒", -"詔": "诏", -"評": "评", -"詖": "诐", -"詗": "诇", -"詘": "诎", -"詛": "诅", -"詞": "词", -"詠": "咏", -"詡": "诩", -"詢": "询", -"詣": "诣", -"試": "试", -"詩": "诗", -"詫": "诧", -"詬": "诟", -"詭": "诡", -"詮": "诠", -"詰": "诘", -"話": "话", -"該": "该", -"詳": "详", -"詵": "诜", -"詼": "诙", -"詿": "诖", -"誄": "诔", -"誅": "诛", -"誆": "诓", -"誇": "夸", -"誌": "志", -"認": "认", -"誑": "诳", -"誒": "诶", -"誕": "诞", -"誘": "诱", -"誚": "诮", -"語": "语", -"誠": "诚", -"誡": "诫", -"誣": "诬", -"誤": "误", -"誥": "诰", -"誦": "诵", -"誨": "诲", -"說": "说", -"説": "说", -"誰": "谁", -"課": "课", -"誶": "谇", -"誹": "诽", -"誼": "谊", -"誾": "訚", -"調": "调", -"諂": "谄", -"諄": "谆", -"談": "谈", -"諉": "诿", -"請": "请", -"諍": "诤", -"諏": "诹", -"諑": "诼", -"諒": "谅", -"論": "论", -"諗": "谂", -"諛": "谀", -"諜": "谍", -"諝": "谞", -"諞": "谝", -"諢": "诨", -"諤": "谔", -"諦": "谛", -"諧": "谐", -"諫": "谏", -"諭": "谕", -"諮": "谘", -"諱": "讳", -"諳": "谙", -"諶": "谌", -"諷": "讽", -"諸": "诸", -"諺": "谚", -"諼": "谖", -"諾": "诺", -"謀": "谋", -"謁": "谒", -"謂": "谓", -"謄": "誊", -"謅": "诌", -"謊": "谎", -"謎": "谜", -"謐": "谧", -"謔": "谑", -"謖": "谡", -"謗": "谤", -"謙": "谦", -"謚": "谥", -"講": "讲", -"謝": "谢", -"謠": "谣", -"謡": "谣", -"謨": "谟", -"謫": "谪", -"謬": "谬", -"謭": "谫", -"謳": "讴", -"謹": "谨", -"謾": "谩", -"譅": "䜧", -"證": "证", -"譎": "谲", -"譏": "讥", -"譖": "谮", -"識": "识", -"譙": "谯", -"譚": "谭", -"譜": "谱", -"譫": "谵", -"譯": "译", -"議": "议", -"譴": "谴", -"護": "护", -"譸": "诪", -"譽": "誉", -"譾": "谫", -"讀": "读", -"變": "变", -"讎": "仇", -"讎": "雠", -"讒": "谗", -"讓": "让", -"讕": "谰", -"讖": "谶", -"讜": "谠", -"讞": "谳", -"豈": "岂", -"豎": "竖", -"豐": "丰", -"豬": "猪", -"豶": "豮", -"貓": "猫", -"貙": "䝙", -"貝": "贝", -"貞": "贞", -"貟": "贠", -"負": "负", -"財": "财", -"貢": "贡", -"貧": "贫", -"貨": "货", -"販": "贩", -"貪": "贪", -"貫": "贯", -"責": "责", -"貯": "贮", -"貰": "贳", -"貲": "赀", -"貳": "贰", -"貴": "贵", -"貶": "贬", -"買": "买", -"貸": "贷", -"貺": "贶", -"費": "费", -"貼": "贴", -"貽": "贻", -"貿": "贸", -"賀": "贺", -"賁": "贲", -"賂": "赂", -"賃": "赁", -"賄": "贿", -"賅": "赅", -"資": "资", -"賈": "贾", -"賊": "贼", -"賑": "赈", -"賒": "赊", -"賓": "宾", -"賕": "赇", -"賙": "赒", -"賚": "赉", -"賜": "赐", -"賞": "赏", -"賠": "赔", -"賡": "赓", -"賢": "贤", -"賣": "卖", -"賤": "贱", -"賦": "赋", -"賧": "赕", -"質": "质", -"賫": "赍", -"賬": "账", -"賭": "赌", -"賰": "䞐", -"賴": "赖", -"賵": "赗", -"賺": "赚", -"賻": "赙", -"購": "购", -"賽": "赛", -"賾": "赜", -"贄": "贽", -"贅": "赘", -"贇": "赟", -"贈": "赠", -"贊": "赞", -"贋": "赝", -"贍": "赡", -"贏": "赢", -"贐": "赆", -"贓": "赃", -"贔": "赑", -"贖": "赎", -"贗": "赝", -"贛": "赣", -"贜": "赃", -"赬": "赪", -"趕": "赶", -"趙": "赵", -"趨": "趋", -"趲": "趱", -"跡": "迹", -"踐": "践", -"踴": "踊", -"蹌": "跄", -"蹕": "跸", -"蹣": "蹒", -"蹤": "踪", -"蹺": "跷", -"躂": "跶", -"躉": "趸", -"躊": "踌", -"躋": "跻", -"躍": "跃", -"躑": "踯", -"躒": "跞", -"躓": "踬", -"躕": "蹰", -"躚": "跹", -"躡": "蹑", -"躥": "蹿", -"躦": "躜", -"躪": "躏", -"軀": "躯", -"車": "车", -"軋": "轧", -"軌": "轨", -"軍": "军", -"軑": "轪", -"軒": "轩", -"軔": "轫", -"軛": "轭", -"軟": "软", -"軤": "轷", -"軫": "轸", -"軲": "轱", -"軸": "轴", -"軹": "轵", -"軺": "轺", -"軻": "轲", -"軼": "轶", -"軾": "轼", -"較": "较", -"輅": "辂", -"輇": "辁", -"輈": "辀", -"載": "载", -"輊": "轾", -"輒": "辄", -"輓": "挽", -"輔": "辅", -"輕": "轻", -"輛": "辆", -"輜": "辎", -"輝": "辉", -"輞": "辋", -"輟": "辍", -"輥": "辊", -"輦": "辇", -"輩": "辈", -"輪": "轮", -"輬": "辌", -"輯": "辑", -"輳": "辏", -"輸": "输", -"輻": "辐", -"輾": "辗", -"輿": "舆", -"轀": "辒", -"轂": "毂", -"轄": "辖", -"轅": "辕", -"轆": "辘", -"轉": "转", -"轍": "辙", -"轎": "轿", -"轔": "辚", -"轟": "轰", -"轡": "辔", -"轢": "轹", -"轤": "轳", -"辟": "辟", -"辦": "办", -"辭": "辞", -"辮": "辫", -"辯": "辩", -"農": "农", -"迴": "回", -"适": "适", -"逕": "迳", -"這": "这", -"連": "连", -"週": "周", -"進": "进", -"遊": "游", -"運": "运", -"過": "过", -"達": "达", -"違": "违", -"遙": "遥", -"遜": "逊", -"遞": "递", -"遠": "远", -"適": "适", -"遲": "迟", -"遷": "迁", -"選": "选", -"遺": "遗", -"遼": "辽", -"邁": "迈", -"還": "还", -"邇": "迩", -"邊": "边", -"邏": "逻", -"邐": "逦", -"郁": "郁", -"郟": "郏", -"郵": "邮", -"鄆": "郓", -"鄉": "乡", -"鄒": "邹", -"鄔": "邬", -"鄖": "郧", -"鄧": "邓", -"鄭": "郑", -"鄰": "邻", -"鄲": "郸", -"鄴": "邺", -"鄶": "郐", -"鄺": "邝", -"酇": "酂", -"酈": "郦", -"醖": "酝", -"醜": "丑", -"醞": "酝", -"醫": "医", -"醬": "酱", -"醱": "酦", -"釀": "酿", -"釁": "衅", -"釃": "酾", -"釅": "酽", -"采": "采", -"釋": "释", -"釐": "厘", -"釒": "钅", -"釓": "钆", -"釔": "钇", -"釕": "钌", -"釗": "钊", -"釘": "钉", -"釙": "钋", -"針": "针", -"釣": "钓", -"釤": "钐", -"釧": "钏", -"釩": "钒", -"釵": "钗", -"釷": "钍", -"釹": "钕", -"釺": "钎", -"鈀": "钯", -"鈁": "钫", -"鈃": "钘", -"鈄": "钭", -"鈈": "钚", -"鈉": "钠", -"鈍": "钝", -"鈎": "钩", -"鈐": "钤", -"鈑": "钣", -"鈒": "钑", -"鈔": "钞", -"鈕": "钮", -"鈞": "钧", -"鈣": "钙", -"鈥": "钬", -"鈦": "钛", -"鈧": "钪", -"鈮": "铌", -"鈰": "铈", -"鈳": "钶", -"鈴": "铃", -"鈷": "钴", -"鈸": "钹", -"鈹": "铍", -"鈺": "钰", -"鈽": "钸", -"鈾": "铀", -"鈿": "钿", -"鉀": "钾", -"鉅": "钜", -"鉈": "铊", -"鉉": "铉", -"鉋": "铇", -"鉍": "铋", -"鉑": "铂", -"鉕": "钷", -"鉗": "钳", -"鉚": "铆", -"鉛": "铅", -"鉞": "钺", -"鉢": "钵", -"鉤": "钩", -"鉦": "钲", -"鉬": "钼", -"鉭": "钽", -"鉶": "铏", -"鉸": "铰", -"鉺": "铒", -"鉻": "铬", -"鉿": "铪", -"銀": "银", -"銃": "铳", -"銅": "铜", -"銍": "铚", -"銑": "铣", -"銓": "铨", -"銖": "铢", -"銘": "铭", -"銚": "铫", -"銛": "铦", -"銜": "衔", -"銠": "铑", -"銣": "铷", -"銥": "铱", -"銦": "铟", -"銨": "铵", -"銩": "铥", -"銪": "铕", -"銫": "铯", -"銬": "铐", -"銱": "铞", -"銳": "锐", -"銷": "销", -"銹": "锈", -"銻": "锑", -"銼": "锉", -"鋁": "铝", -"鋃": "锒", -"鋅": "锌", -"鋇": "钡", -"鋌": "铤", -"鋏": "铗", -"鋒": "锋", -"鋙": "铻", -"鋝": "锊", -"鋟": "锓", -"鋣": "铘", -"鋤": "锄", -"鋥": "锃", -"鋦": "锔", -"鋨": "锇", -"鋩": "铓", -"鋪": "铺", -"鋭": "锐", -"鋮": "铖", -"鋯": "锆", -"鋰": "锂", -"鋱": "铽", -"鋶": "锍", -"鋸": "锯", -"鋼": "钢", -"錁": "锞", -"錄": "录", -"錆": "锖", -"錇": "锫", -"錈": "锩", -"錏": "铔", -"錐": "锥", -"錒": "锕", -"錕": "锟", -"錘": "锤", -"錙": "锱", -"錚": "铮", -"錛": "锛", -"錟": "锬", -"錠": "锭", -"錡": "锜", -"錢": "钱", -"錦": "锦", -"錨": "锚", -"錩": "锠", -"錫": "锡", -"錮": "锢", -"錯": "错", -"録": "录", -"錳": "锰", -"錶": "表", -"錸": "铼", -"鍀": "锝", -"鍁": "锨", -"鍃": "锪", -"鍆": "钔", -"鍇": "锴", -"鍈": "锳", -"鍋": "锅", -"鍍": "镀", -"鍔": "锷", -"鍘": "铡", -"鍚": "钖", -"鍛": "锻", -"鍠": "锽", -"鍤": "锸", -"鍥": "锲", -"鍩": "锘", -"鍬": "锹", -"鍰": "锾", -"鍵": "键", -"鍶": "锶", -"鍺": "锗", -"鍾": "钟", -"鎂": "镁", -"鎄": "锿", -"鎇": "镅", -"鎊": "镑", -"鎔": "镕", -"鎖": "锁", -"鎘": "镉", -"鎚": "锤", -"鎛": "镈", -"鎝": "𨱏", -"鎡": "镃", -"鎢": "钨", -"鎣": "蓥", -"鎦": "镏", -"鎧": "铠", -"鎩": "铩", -"鎪": "锼", -"鎬": "镐", -"鎮": "镇", -"鎰": "镒", -"鎲": "镋", -"鎳": "镍", -"鎵": "镓", -"鎸": "镌", -"鎿": "镎", -"鏃": "镞", -"鏇": "镟", -"鏈": "链", -"鏌": "镆", -"鏍": "镙", -"鏐": "镠", -"鏑": "镝", -"鏗": "铿", -"鏘": "锵", -"鏜": "镗", -"鏝": "镘", -"鏞": "镛", -"鏟": "铲", -"鏡": "镜", -"鏢": "镖", -"鏤": "镂", -"鏨": "錾", -"鏰": "镚", -"鏵": "铧", -"鏷": "镤", -"鏹": "镪", -"鏽": "锈", -"鐃": "铙", -"鐋": "铴", -"鐐": "镣", -"鐒": "铹", -"鐓": "镦", -"鐔": "镡", -"鐘": "钟", -"鐙": "镫", -"鐝": "镢", -"鐠": "镨", -"鐦": "锎", -"鐧": "锏", -"鐨": "镄", -"鐫": "镌", -"鐮": "镰", -"鐲": "镯", -"鐳": "镭", -"鐵": "铁", -"鐶": "镮", -"鐸": "铎", -"鐺": "铛", -"鐿": "镱", -"鑄": "铸", -"鑊": "镬", -"鑌": "镔", -"鑒": "鉴", -"鑔": "镲", -"鑕": "锧", -"鑞": "镴", -"鑠": "铄", -"鑣": "镳", -"鑥": "镥", -"鑭": "镧", -"鑰": "钥", -"鑱": "镵", -"鑲": "镶", -"鑷": "镊", -"鑹": "镩", -"鑼": "锣", -"鑽": "钻", -"鑾": "銮", -"鑿": "凿", -"钁": "镢", -"镟": "旋", -"長": "长", -"門": "门", -"閂": "闩", -"閃": "闪", -"閆": "闫", -"閈": "闬", -"閉": "闭", -"開": "开", -"閌": "闶", -"閎": "闳", -"閏": "闰", -"閑": "闲", -"間": "间", -"閔": "闵", -"閘": "闸", -"閡": "阂", -"閣": "阁", -"閤": "合", -"閥": "阀", -"閨": "闺", -"閩": "闽", -"閫": "阃", -"閬": "阆", -"閭": "闾", -"閱": "阅", -"閲": "阅", -"閶": "阊", -"閹": "阉", -"閻": "阎", -"閼": "阏", -"閽": "阍", -"閾": "阈", -"閿": "阌", -"闃": "阒", -"闆": "板", -"闈": "闱", -"闊": "阔", -"闋": "阕", -"闌": "阑", -"闍": "阇", -"闐": "阗", -"闒": "阘", -"闓": "闿", -"闔": "阖", -"闕": "阙", -"闖": "闯", -"關": "关", -"闞": "阚", -"闠": "阓", -"闡": "阐", -"闤": "阛", -"闥": "闼", -"阪": "坂", -"陘": "陉", -"陝": "陕", -"陣": "阵", -"陰": "阴", -"陳": "陈", -"陸": "陆", -"陽": "阳", -"隉": "陧", -"隊": "队", -"階": "阶", -"隕": "陨", -"際": "际", -"隨": "随", -"險": "险", -"隱": "隐", -"隴": "陇", -"隸": "隶", -"隻": "只", -"雋": "隽", -"雖": "虽", -"雙": "双", -"雛": "雏", -"雜": "杂", -"雞": "鸡", -"離": "离", -"難": "难", -"雲": "云", -"電": "电", -"霢": "霡", -"霧": "雾", -"霽": "霁", -"靂": "雳", -"靄": "霭", -"靈": "灵", -"靚": "靓", -"靜": "静", -"靨": "靥", -"鞀": "鼗", -"鞏": "巩", -"鞝": "绱", -"鞦": "秋", -"鞽": "鞒", -"韁": "缰", -"韃": "鞑", -"韆": "千", -"韉": "鞯", -"韋": "韦", -"韌": "韧", -"韍": "韨", -"韓": "韩", -"韙": "韪", -"韜": "韬", -"韞": "韫", -"韻": "韵", -"響": "响", -"頁": "页", -"頂": "顶", -"頃": "顷", -"項": "项", -"順": "顺", -"頇": "顸", -"須": "须", -"頊": "顼", -"頌": "颂", -"頎": "颀", -"頏": "颃", -"預": "预", -"頑": "顽", -"頒": "颁", -"頓": "顿", -"頗": "颇", -"領": "领", -"頜": "颌", -"頡": "颉", -"頤": "颐", -"頦": "颏", -"頭": "头", -"頮": "颒", -"頰": "颊", -"頲": "颋", -"頴": "颕", -"頷": "颔", -"頸": "颈", -"頹": "颓", -"頻": "频", -"頽": "颓", -"顆": "颗", -"題": "题", -"額": "额", -"顎": "颚", -"顏": "颜", -"顒": "颙", -"顓": "颛", -"顔": "颜", -"願": "愿", -"顙": "颡", -"顛": "颠", -"類": "类", -"顢": "颟", -"顥": "颢", -"顧": "顾", -"顫": "颤", -"顬": "颥", -"顯": "显", -"顰": "颦", -"顱": "颅", -"顳": "颞", -"顴": "颧", -"風": "风", -"颭": "飐", -"颮": "飑", -"颯": "飒", -"颱": "台", -"颳": "刮", -"颶": "飓", -"颸": "飔", -"颺": "飏", -"颻": "飖", -"颼": "飕", -"飀": "飗", -"飄": "飘", -"飆": "飙", -"飈": "飚", -"飛": "飞", -"飠": "饣", -"飢": "饥", -"飣": "饤", -"飥": "饦", -"飩": "饨", -"飪": "饪", -"飫": "饫", -"飭": "饬", -"飯": "饭", -"飲": "饮", -"飴": "饴", -"飼": "饲", -"飽": "饱", -"飾": "饰", -"飿": "饳", -"餃": "饺", -"餄": "饸", -"餅": "饼", -"餉": "饷", -"養": "养", -"餌": "饵", -"餎": "饹", -"餏": "饻", -"餑": "饽", -"餒": "馁", -"餓": "饿", -"餕": "馂", -"餖": "饾", -"餚": "肴", -"餛": "馄", -"餜": "馃", -"餞": "饯", -"餡": "馅", -"館": "馆", -"餱": "糇", -"餳": "饧", -"餶": "馉", -"餷": "馇", -"餺": "馎", -"餼": "饩", -"餾": "馏", -"餿": "馊", -"饁": "馌", -"饃": "馍", -"饅": "馒", -"饈": "馐", -"饉": "馑", -"饊": "馓", -"饋": "馈", -"饌": "馔", -"饑": "饥", -"饒": "饶", -"饗": "飨", -"饜": "餍", -"饞": "馋", -"饢": "馕", -"馬": "马", -"馭": "驭", -"馮": "冯", -"馱": "驮", -"馳": "驰", -"馴": "驯", -"馹": "驲", -"駁": "驳", -"駐": "驻", -"駑": "驽", -"駒": "驹", -"駔": "驵", -"駕": "驾", -"駘": "骀", -"駙": "驸", -"駛": "驶", -"駝": "驼", -"駟": "驷", -"駡": "骂", -"駢": "骈", -"駭": "骇", -"駰": "骃", -"駱": "骆", -"駸": "骎", -"駿": "骏", -"騁": "骋", -"騂": "骍", -"騅": "骓", -"騌": "骔", -"騍": "骒", -"騎": "骑", -"騏": "骐", -"騖": "骛", -"騙": "骗", -"騤": "骙", -"騧": "䯄", -"騫": "骞", -"騭": "骘", -"騮": "骝", -"騰": "腾", -"騶": "驺", -"騷": "骚", -"騸": "骟", -"騾": "骡", -"驀": "蓦", -"驁": "骜", -"驂": "骖", -"驃": "骠", -"驄": "骢", -"驅": "驱", -"驊": "骅", -"驌": "骕", -"驍": "骁", -"驏": "骣", -"驕": "骄", -"驗": "验", -"驚": "惊", -"驛": "驿", -"驟": "骤", -"驢": "驴", -"驤": "骧", -"驥": "骥", -"驦": "骦", -"驪": "骊", -"驫": "骉", -"骯": "肮", -"髏": "髅", -"髒": "脏", -"體": "体", -"髕": "髌", -"髖": "髋", -"髮": "发", -"鬆": "松", -"鬍": "胡", -"鬚": "须", -"鬢": "鬓", -"鬥": "斗", -"鬧": "闹", -"鬩": "阋", -"鬮": "阄", -"鬱": "郁", -"魎": "魉", -"魘": "魇", -"魚": "鱼", -"魛": "鱽", -"魢": "鱾", -"魨": "鲀", -"魯": "鲁", -"魴": "鲂", -"魷": "鱿", -"魺": "鲄", -"鮁": "鲅", -"鮃": "鲆", -"鮊": "鲌", -"鮋": "鲉", -"鮍": "鲏", -"鮎": "鲇", -"鮐": "鲐", -"鮑": "鲍", -"鮒": "鲋", -"鮓": "鲊", -"鮚": "鲒", -"鮜": "鲘", -"鮝": "鲞", -"鮞": "鲕", -"鮦": "鲖", -"鮪": "鲔", -"鮫": "鲛", -"鮭": "鲑", -"鮮": "鲜", -"鮳": "鲓", -"鮶": "鲪", -"鮺": "鲝", -"鯀": "鲧", -"鯁": "鲠", -"鯇": "鲩", -"鯉": "鲤", -"鯊": "鲨", -"鯒": "鲬", -"鯔": "鲻", -"鯕": "鲯", -"鯖": "鲭", -"鯗": "鲞", -"鯛": "鲷", -"鯝": "鲴", -"鯡": "鲱", -"鯢": "鲵", -"鯤": "鲲", -"鯧": "鲳", -"鯨": "鲸", -"鯪": "鲮", -"鯫": "鲰", -"鯴": "鲺", -"鯷": "鳀", -"鯽": "鲫", -"鯿": "鳊", -"鰁": "鳈", -"鰂": "鲗", -"鰃": "鳂", -"鰈": "鲽", -"鰉": "鳇", -"鰍": "鳅", -"鰏": "鲾", -"鰐": "鳄", -"鰒": "鳆", -"鰓": "鳃", -"鰜": "鳒", -"鰟": "鳑", -"鰠": "鳋", -"鰣": "鲥", -"鰥": "鳏", -"鰨": "鳎", -"鰩": "鳐", -"鰭": "鳍", -"鰮": "鳁", -"鰱": "鲢", -"鰲": "鳌", -"鰳": "鳓", -"鰵": "鳘", -"鰷": "鲦", -"鰹": "鲣", -"鰺": "鲹", -"鰻": "鳗", -"鰼": "鳛", -"鰾": "鳔", -"鱂": "鳉", -"鱅": "鳙", -"鱈": "鳕", -"鱉": "鳖", -"鱒": "鳟", -"鱔": "鳝", -"鱖": "鳜", -"鱗": "鳞", -"鱘": "鲟", -"鱝": "鲼", -"鱟": "鲎", -"鱠": "鲙", -"鱣": "鳣", -"鱤": "鳡", -"鱧": "鳢", -"鱨": "鲿", -"鱭": "鲚", -"鱯": "鳠", -"鱷": "鳄", -"鱸": "鲈", -"鱺": "鲡", -"䰾": "鲃", -"䲁": "鳚", -"鳥": "鸟", -"鳧": "凫", -"鳩": "鸠", -"鳬": "凫", -"鳲": "鸤", -"鳳": "凤", -"鳴": "鸣", -"鳶": "鸢", -"鳾": "䴓", -"鴆": "鸩", -"鴇": "鸨", -"鴉": "鸦", -"鴒": "鸰", -"鴕": "鸵", -"鴛": "鸳", -"鴝": "鸲", -"鴞": "鸮", -"鴟": "鸱", -"鴣": "鸪", -"鴦": "鸯", -"鴨": "鸭", -"鴯": "鸸", -"鴰": "鸹", -"鴴": "鸻", -"鴷": "䴕", -"鴻": "鸿", -"鴿": "鸽", -"鵁": "䴔", -"鵂": "鸺", -"鵃": "鸼", -"鵐": "鹀", -"鵑": "鹃", -"鵒": "鹆", -"鵓": "鹁", -"鵜": "鹈", -"鵝": "鹅", -"鵠": "鹄", -"鵡": "鹉", -"鵪": "鹌", -"鵬": "鹏", -"鵮": "鹐", -"鵯": "鹎", -"鵲": "鹊", -"鵷": "鹓", -"鵾": "鹍", -"鶄": "䴖", -"鶇": "鸫", -"鶉": "鹑", -"鶊": "鹒", -"鶓": "鹋", -"鶖": "鹙", -"鶘": "鹕", -"鶚": "鹗", -"鶡": "鹖", -"鶥": "鹛", -"鶩": "鹜", -"鶪": "䴗", -"鶬": "鸧", -"鶯": "莺", -"鶲": "鹟", -"鶴": "鹤", -"鶹": "鹠", -"鶺": "鹡", -"鶻": "鹘", -"鶼": "鹣", -"鶿": "鹚", -"鷀": "鹚", -"鷁": "鹢", -"鷂": "鹞", -"鷄": "鸡", -"鷈": "䴘", -"鷊": "鹝", -"鷓": "鹧", -"鷖": "鹥", -"鷗": "鸥", -"鷙": "鸷", -"鷚": "鹨", -"鷥": "鸶", -"鷦": "鹪", -"鷫": "鹔", -"鷯": "鹩", -"鷲": "鹫", -"鷳": "鹇", -"鷸": "鹬", -"鷹": "鹰", -"鷺": "鹭", -"鷽": "鸴", -"鷿": "䴙", -"鸂": "㶉", -"鸇": "鹯", -"鸌": "鹱", -"鸏": "鹲", -"鸕": "鸬", -"鸘": "鹴", -"鸚": "鹦", -"鸛": "鹳", -"鸝": "鹂", -"鸞": "鸾", -"鹵": "卤", -"鹹": "咸", -"鹺": "鹾", -"鹽": "盐", -"麗": "丽", -"麥": "麦", -"麩": "麸", -"麯": "曲", -"麵": "面", -"麼": "么", -"麽": "么", -"黃": "黄", -"黌": "黉", -"點": "点", -"黨": "党", -"黲": "黪", -"黴": "霉", -"黶": "黡", -"黷": "黩", -"黽": "黾", -"黿": "鼋", -"鼉": "鼍", -"鼕": "冬", -"鼴": "鼹", -"齊": "齐", -"齋": "斋", -"齎": "赍", -"齏": "齑", -"齒": "齿", -"齔": "龀", -"齕": "龁", -"齗": "龂", -"齙": "龅", -"齜": "龇", -"齟": "龃", -"齠": "龆", -"齡": "龄", -"齣": "出", -"齦": "龈", -"齪": "龊", -"齬": "龉", -"齲": "龋", -"齶": "腭", -"齷": "龌", -"龍": "龙", -"龎": "厐", -"龐": "庞", -"龔": "龚", -"龕": "龛", -"龜": "龟", - -"幾畫": "几画", -"賣畫": "卖画", -"滷鹼": "卤碱", -"原畫": "原画", -"口鹼": "口碱", -"古畫": "古画", -"名畫": "名画", -"奇畫": "奇画", -"如畫": "如画", -"弱鹼": "弱碱", -"彩畫": "彩画", -"所畫": "所画", -"扉畫": "扉画", -"教畫": "教画", -"水鹼": "水碱", -"洋鹼": "洋碱", -"炭畫": "炭画", -"畫一": "画一", -"畫上": "画上", -"畫下": "画下", -"畫中": "画中", -"畫供": "画供", -"畫兒": "画儿", -"畫具": "画具", -"畫出": "画出", -"畫史": "画史", -"畫品": "画品", -"畫商": "画商", -"畫圈": "画圈", -"畫境": "画境", -"畫工": "画工", -"畫帖": "画帖", -"畫幅": "画幅", -"畫意": "画意", -"畫成": "画成", -"畫景": "画景", -"畫本": "画本", -"畫架": "画架", -"畫框": "画框", -"畫法": "画法", -"畫王": "画王", -"畫界": "画界", -"畫符": "画符", -"畫紙": "画纸", -"畫線": "画线", -"畫航": "画航", -"畫舫": "画舫", -"畫虎": "画虎", -"畫論": "画论", -"畫譜": "画谱", -"畫象": "画象", -"畫質": "画质", -"畫貼": "画贴", -"畫軸": "画轴", -"畫頁": "画页", -"鹽鹼": "盐碱", -"鹼": "碱", -"鹼基": "碱基", -"鹼度": "碱度", -"鹼水": "碱水", -"鹼熔": "碱熔", -"磁畫": "磁画", -"策畫": "策画", -"組畫": "组画", -"絹畫": "绢画", -"耐鹼": "耐碱", -"肉鹼": "肉碱", -"膠畫": "胶画", -"茶鹼": "茶碱", -"西畫": "西画", -"貼畫": "贴画", -"返鹼": "返碱", -"鍾鍛": "锺锻", -"鍛鍾": "锻锺", -"雕畫": "雕画", -"鯰": "鲶", -"三聯畫": "三联画", -"中國畫": "中国画", -"書畫": "书画", -"書畫社": "书画社", -"五筆畫": "五笔画", -"作畫": "作画", -"入畫": "入画", -"寫生畫": "写生画", -"刻畫": "刻画", -"動畫": "动画", -"勾畫": "勾画", -"單色畫": "单色画", -"卡通畫": "卡通画", -"國畫": "国画", -"圖畫": "图画", -"壁畫": "壁画", -"字畫": "字画", -"宣傳畫": "宣传画", -"工筆畫": "工笔画", -"年畫": "年画", -"幽默畫": "幽默画", -"指畫": "指画", -"描畫": "描画", -"插畫": "插画", -"擘畫": "擘画", -"春畫": "春画", -"木刻畫": "木刻画", -"機械畫": "机械画", -"比畫": "比画", -"毛筆畫": "毛笔画", -"水粉畫": "水粉画", -"油畫": "油画", -"海景畫": "海景画", -"漫畫": "漫画", -"點畫": "点画", -"版畫": "版画", -"畫": "画", -"畫像": "画像", -"畫冊": "画册", -"畫刊": "画刊", -"畫匠": "画匠", -"畫捲": "画卷", -"畫圖": "画图", -"畫壇": "画坛", -"畫室": "画室", -"畫家": "画家", -"畫屏": "画屏", -"畫展": "画展", -"畫布": "画布", -"畫師": "画师", -"畫廊": "画廊", -"畫報": "画报", -"畫押": "画押", -"畫板": "画板", -"畫片": "画片", -"畫畫": "画画", -"畫皮": "画皮", -"畫眉鳥": "画眉鸟", -"畫稿": "画稿", -"畫筆": "画笔", -"畫院": "画院", -"畫集": "画集", -"畫面": "画面", -"筆畫": "笔画", -"細密畫": "细密画", -"繪畫": "绘画", -"自畫像": "自画像", -"蠟筆畫": "蜡笔画", -"裸體畫": "裸体画", -"西洋畫": "西洋画", -"透視畫": "透视画", -"銅版畫": "铜版画", -"鍾": "锺", -"靜物畫": "静物画", -"餘": "馀", -} - -zh2TW = { -"缺省": "預設", -"串行": "串列", -"以太网": "乙太網", -"位图": "點陣圖", -"例程": "常式", -"信道": "通道", -"光标": "游標", -"光盘": "光碟", -"光驱": "光碟機", -"全角": "全形", -"加载": "載入", -"半角": "半形", -"变量": "變數", -"噪声": "雜訊", -"脱机": "離線", -"声卡": "音效卡", -"老字号": "老字號", -"字号": "字型大小", -"字库": "字型檔", -"字段": "欄位", -"字符": "字元", -"存盘": "存檔", -"寻址": "定址", -"尾注": "章節附註", -"异步": "非同步", -"总线": "匯流排", -"括号": "括弧", -"接口": "介面", -"控件": "控制項", -"权限": "許可權", -"盘片": "碟片", -"硅片": "矽片", -"硅谷": "矽谷", -"硬盘": "硬碟", -"磁盘": "磁碟", -"磁道": "磁軌", -"程控": "程式控制", -"端口": "埠", -"算子": "運算元", -"算法": "演算法", -"芯片": "晶片", -"芯片": "晶元", -"词组": "片語", -"译码": "解碼", -"软驱": "軟碟機", -"快闪存储器": "快閃記憶體", -"闪存": "快閃記憶體", -"鼠标": "滑鼠", -"进制": "進位", -"交互式": "互動式", -"仿真": "模擬", -"优先级": "優先順序", -"传感": "感測", -"便携式": "攜帶型", -"信息论": "資訊理論", -"写保护": "防寫", -"分布式": "分散式", -"分辨率": "解析度", -"服务器": "伺服器", -"等于": "等於", -"局域网": "區域網", -"计算机": "電腦", -"扫瞄仪": "掃瞄器", -"宽带": "寬頻", -"数据库": "資料庫", -"奶酪": "乳酪", -"巨商": "鉅賈", -"手电": "手電筒", -"万历": "萬曆", -"永历": "永曆", -"词汇": "辭彙", -"习用": "慣用", -"元音": "母音", -"任意球": "自由球", -"头球": "頭槌", -"入球": "進球", -"粒入球": "顆進球", -"打门": "射門", -"火锅盖帽": "蓋火鍋", -"打印机": "印表機", -"打印機": "印表機", -"字节": "位元組", -"字節": "位元組", -"打印": "列印", -"打印": "列印", -"硬件": "硬體", -"硬件": "硬體", -"二极管": "二極體", -"二極管": "二極體", -"三极管": "三極體", -"三極管": "三極體", -"软件": "軟體", -"軟件": "軟體", -"网络": "網路", -"網絡": "網路", -"人工智能": "人工智慧", -"航天飞机": "太空梭", -"穿梭機": "太空梭", -"因特网": "網際網路", -"互聯網": "網際網路", -"机器人": "機器人", -"機械人": "機器人", -"移动电话": "行動電話", -"流動電話": "行動電話", -"调制解调器": "數據機", -"調制解調器": "數據機", -"短信": "簡訊", -"短訊": "簡訊", -"乌兹别克斯坦": "烏茲別克", -"乍得": "查德", -"乍得": "查德", -"也门": "葉門", -"也門": "葉門", -"伯利兹": "貝里斯", -"伯利茲": "貝里斯", -"佛得角": "維德角", -"佛得角": "維德角", -"克罗地亚": "克羅埃西亞", -"克羅地亞": "克羅埃西亞", -"冈比亚": "甘比亞", -"岡比亞": "甘比亞", -"几内亚比绍": "幾內亞比索", -"幾內亞比紹": "幾內亞比索", -"列支敦士登": "列支敦斯登", -"列支敦士登": "列支敦斯登", -"利比里亚": "賴比瑞亞", -"利比里亞": "賴比瑞亞", -"加纳": "迦納", -"加納": "迦納", -"加蓬": "加彭", -"加蓬": "加彭", -"博茨瓦纳": "波札那", -"博茨瓦納": "波札那", -"卡塔尔": "卡達", -"卡塔爾": "卡達", -"卢旺达": "盧安達", -"盧旺達": "盧安達", -"危地马拉": "瓜地馬拉", -"危地馬拉": "瓜地馬拉", -"厄瓜多尔": "厄瓜多", -"厄瓜多爾": "厄瓜多", -"厄立特里亚": "厄利垂亞", -"厄立特里亞": "厄利垂亞", -"吉布提": "吉布地", -"吉布堤": "吉布地", -"哈萨克斯坦": "哈薩克", -"哥斯达黎加": "哥斯大黎加", -"哥斯達黎加": "哥斯大黎加", -"图瓦卢": "吐瓦魯", -"圖瓦盧": "吐瓦魯", -"土库曼斯坦": "土庫曼", -"圣卢西亚": "聖露西亞", -"聖盧西亞": "聖露西亞", -"圣基茨和尼维斯": "聖克里斯多福及尼維斯", -"聖吉斯納域斯": "聖克里斯多福及尼維斯", -"圣文森特和格林纳丁斯": "聖文森及格瑞那丁", -"聖文森特和格林納丁斯": "聖文森及格瑞那丁", -"圣马力诺": "聖馬利諾", -"聖馬力諾": "聖馬利諾", -"圭亚那": "蓋亞那", -"圭亞那": "蓋亞那", -"坦桑尼亚": "坦尚尼亞", -"坦桑尼亞": "坦尚尼亞", -"埃塞俄比亚": "衣索比亞", -"埃塞俄比亞": "衣索比亞", -"基里巴斯": "吉里巴斯", -"基里巴斯": "吉里巴斯", -"塔吉克斯坦": "塔吉克", -"塞拉利昂": "獅子山", -"塞拉利昂": "獅子山", -"塞浦路斯": "塞普勒斯", -"塞浦路斯": "塞普勒斯", -"塞舌尔": "塞席爾", -"塞舌爾": "塞席爾", -"多米尼加": "多明尼加", -"多明尼加共和國": "多明尼加", -"多米尼加联邦": "多米尼克", -"多明尼加聯邦": "多米尼克", -"安提瓜和巴布达": "安地卡及巴布達", -"安提瓜和巴布達": "安地卡及巴布達", -"尼日利亚": "奈及利亞", -"尼日利亞": "奈及利亞", -"尼日尔": "尼日", -"尼日爾": "尼日", -"巴巴多斯": "巴貝多", -"巴巴多斯": "巴貝多", -"巴布亚新几内亚": "巴布亞紐幾內亞", -"巴布亞新畿內亞": "巴布亞紐幾內亞", -"布基纳法索": "布吉納法索", -"布基納法索": "布吉納法索", -"布隆迪": "蒲隆地", -"布隆迪": "蒲隆地", -"希腊": "希臘", -"帕劳": "帛琉", -"意大利": "義大利", -"意大利": "義大利", -"所罗门群岛": "索羅門群島", -"所羅門群島": "索羅門群島", -"文莱": "汶萊", -"斯威士兰": "史瓦濟蘭", -"斯威士蘭": "史瓦濟蘭", -"斯洛文尼亚": "斯洛維尼亞", -"斯洛文尼亞": "斯洛維尼亞", -"新西兰": "紐西蘭", -"新西蘭": "紐西蘭", -"格林纳达": "格瑞那達", -"格林納達": "格瑞那達", -"格鲁吉亚": "喬治亞", -"格魯吉亞": "喬治亞", -"佐治亚": "喬治亞", -"佐治亞": "喬治亞", -"毛里塔尼亚": "茅利塔尼亞", -"毛里塔尼亞": "茅利塔尼亞", -"毛里求斯": "模里西斯", -"毛里裘斯": "模里西斯", -"沙特阿拉伯": "沙烏地阿拉伯", -"沙地阿拉伯": "沙烏地阿拉伯", -"波斯尼亚和黑塞哥维那": "波士尼亞赫塞哥維納", -"波斯尼亞黑塞哥維那": "波士尼亞赫塞哥維納", -"津巴布韦": "辛巴威", -"津巴布韋": "辛巴威", -"洪都拉斯": "宏都拉斯", -"洪都拉斯": "宏都拉斯", -"特立尼达和托巴哥": "千里達托貝哥", -"特立尼達和多巴哥": "千里達托貝哥", -"瑙鲁": "諾魯", -"瑙魯": "諾魯", -"瓦努阿图": "萬那杜", -"瓦努阿圖": "萬那杜", -"溫納圖萬": "那杜", -"科摩罗": "葛摩", -"科摩羅": "葛摩", -"科特迪瓦": "象牙海岸", -"突尼斯": "突尼西亞", -"索马里": "索馬利亞", -"索馬里": "索馬利亞", -"老挝": "寮國", -"老撾": "寮國", -"肯尼亚": "肯亞", -"肯雅": "肯亞", -"苏里南": "蘇利南", -"莫桑比克": "莫三比克", -"莱索托": "賴索托", -"萊索托": "賴索托", -"贝宁": "貝南", -"貝寧": "貝南", -"赞比亚": "尚比亞", -"贊比亞": "尚比亞", -"阿塞拜疆": "亞塞拜然", -"阿塞拜疆": "亞塞拜然", -"阿拉伯联合酋长国": "阿拉伯聯合大公國", -"阿拉伯聯合酋長國": "阿拉伯聯合大公國", -"马尔代夫": "馬爾地夫", -"馬爾代夫": "馬爾地夫", -"马耳他": "馬爾他", -"马里共和国": "馬利共和國", -"馬里共和國": "馬利共和國", -"方便面": "速食麵", -"快速面": "速食麵", -"即食麵": "速食麵", -"薯仔": "土豆", -"蹦极跳": "笨豬跳", -"绑紧跳": "笨豬跳", -"冷菜": "冷盤", -"凉菜": "冷盤", -"出租车": "計程車", -"台球": "撞球", -"桌球": "撞球", -"雪糕": "冰淇淋", -"卫生": "衛生", -"衞生": "衛生", -"平治": "賓士", -"奔驰": "賓士", -"積架": "捷豹", -"福士": "福斯", -"雪铁龙": "雪鐵龍", -"马自达": "馬自達", -"萬事得": "馬自達", -"拿破仑": "拿破崙", -"拿破侖": "拿破崙", -"布什": "布希", -"布殊": "布希", -"克林顿": "柯林頓", -"克林頓": "柯林頓", -"侯赛因": "海珊", -"侯賽因": "海珊", -"凡高": "梵谷", -"狄安娜": "黛安娜", -"戴安娜": "黛安娜", -"赫拉": "希拉", -} - -zh2HK = { -"打印机": "打印機", -"印表機": "打印機", -"字节": "位元組", -"字節": "位元組", -"打印": "打印", -"列印": "打印", -"硬件": "硬件", -"硬體": "硬件", -"二极管": "二極管", -"二極體": "二極管", -"三极管": "三極管", -"三極體": "三極管", -"数码": "數碼", -"數位": "數碼", -"软件": "軟件", -"軟體": "軟件", -"网络": "網絡", -"網路": "網絡", -"人工智能": "人工智能", -"人工智慧": "人工智能", -"航天飞机": "穿梭機", -"太空梭": "穿梭機", -"因特网": "互聯網", -"網際網路": "互聯網", -"机器人": "機械人", -"機器人": "機械人", -"移动电话": "流動電話", -"行動電話": "流動電話", -"调制解调器": "調制解調器", -"數據機": "調制解調器", -"短信": "短訊", -"簡訊": "短訊", -"乍得": "乍得", -"查德": "乍得", -"也门": "也門", -"葉門": "也門", -"伯利兹": "伯利茲", -"貝里斯": "伯利茲", -"佛得角": "佛得角", -"維德角": "佛得角", -"克罗地亚": "克羅地亞", -"克羅埃西亞": "克羅地亞", -"冈比亚": "岡比亞", -"甘比亞": "岡比亞", -"几内亚比绍": "幾內亞比紹", -"幾內亞比索": "幾內亞比紹", -"列支敦士登": "列支敦士登", -"列支敦斯登": "列支敦士登", -"利比里亚": "利比里亞", -"賴比瑞亞": "利比里亞", -"加纳": "加納", -"迦納": "加納", -"加蓬": "加蓬", -"加彭": "加蓬", -"博茨瓦纳": "博茨瓦納", -"波札那": "博茨瓦納", -"卡塔尔": "卡塔爾", -"卡達": "卡塔爾", -"卢旺达": "盧旺達", -"盧安達": "盧旺達", -"危地马拉": "危地馬拉", -"瓜地馬拉": "危地馬拉", -"厄瓜多尔": "厄瓜多爾", -"厄瓜多": "厄瓜多爾", -"厄立特里亚": "厄立特里亞", -"厄利垂亞": "厄立特里亞", -"吉布提": "吉布堤", -"吉布地": "吉布堤", -"哥斯达黎加": "哥斯達黎加", -"哥斯大黎加": "哥斯達黎加", -"图瓦卢": "圖瓦盧", -"吐瓦魯": "圖瓦盧", -"圣卢西亚": "聖盧西亞", -"聖露西亞": "聖盧西亞", -"圣基茨和尼维斯": "聖吉斯納域斯", -"聖克里斯多福及尼維斯": "聖吉斯納域斯", -"圣文森特和格林纳丁斯": "聖文森特和格林納丁斯", -"聖文森及格瑞那丁": "聖文森特和格林納丁斯", -"圣马力诺": "聖馬力諾", -"聖馬利諾": "聖馬力諾", -"圭亚那": "圭亞那", -"蓋亞那": "圭亞那", -"坦桑尼亚": "坦桑尼亞", -"坦尚尼亞": "坦桑尼亞", -"埃塞俄比亚": "埃塞俄比亞", -"衣索匹亞": "埃塞俄比亞", -"衣索比亞": "埃塞俄比亞", -"基里巴斯": "基里巴斯", -"吉里巴斯": "基里巴斯", -"狮子山": "獅子山", -"塞普勒斯": "塞浦路斯", -"塞舌尔": "塞舌爾", -"塞席爾": "塞舌爾", -"多米尼加": "多明尼加共和國", -"多明尼加": "多明尼加共和國", -"多米尼加联邦": "多明尼加聯邦", -"多米尼克": "多明尼加聯邦", -"安提瓜和巴布达": "安提瓜和巴布達", -"安地卡及巴布達": "安提瓜和巴布達", -"尼日利亚": "尼日利亞", -"奈及利亞": "尼日利亞", -"尼日尔": "尼日爾", -"尼日": "尼日爾", -"巴巴多斯": "巴巴多斯", -"巴貝多": "巴巴多斯", -"巴布亚新几内亚": "巴布亞新畿內亞", -"巴布亞紐幾內亞": "巴布亞新畿內亞", -"布基纳法索": "布基納法索", -"布吉納法索": "布基納法索", -"布隆迪": "布隆迪", -"蒲隆地": "布隆迪", -"義大利": "意大利", -"所罗门群岛": "所羅門群島", -"索羅門群島": "所羅門群島", -"斯威士兰": "斯威士蘭", -"史瓦濟蘭": "斯威士蘭", -"斯洛文尼亚": "斯洛文尼亞", -"斯洛維尼亞": "斯洛文尼亞", -"新西兰": "新西蘭", -"紐西蘭": "新西蘭", -"格林纳达": "格林納達", -"格瑞那達": "格林納達", -"格鲁吉亚": "喬治亞", -"格魯吉亞": "喬治亞", -"梵蒂冈": "梵蒂岡", -"毛里塔尼亚": "毛里塔尼亞", -"茅利塔尼亞": "毛里塔尼亞", -"毛里求斯": "毛里裘斯", -"模里西斯": "毛里裘斯", -"沙烏地阿拉伯": "沙特阿拉伯", -"波斯尼亚和黑塞哥维那": "波斯尼亞黑塞哥維那", -"波士尼亞赫塞哥維納": "波斯尼亞黑塞哥維那", -"津巴布韦": "津巴布韋", -"辛巴威": "津巴布韋", -"洪都拉斯": "洪都拉斯", -"宏都拉斯": "洪都拉斯", -"特立尼达和托巴哥": "特立尼達和多巴哥", -"千里達托貝哥": "特立尼達和多巴哥", -"瑙鲁": "瑙魯", -"諾魯": "瑙魯", -"瓦努阿图": "瓦努阿圖", -"萬那杜": "瓦努阿圖", -"科摩罗": "科摩羅", -"葛摩": "科摩羅", -"索马里": "索馬里", -"索馬利亞": "索馬里", -"老挝": "老撾", -"寮國": "老撾", -"肯尼亚": "肯雅", -"肯亞": "肯雅", -"莫桑比克": "莫桑比克", -"莫三比克": "莫桑比克", -"莱索托": "萊索托", -"賴索托": "萊索托", -"贝宁": "貝寧", -"貝南": "貝寧", -"赞比亚": "贊比亞", -"尚比亞": "贊比亞", -"阿塞拜疆": "阿塞拜疆", -"亞塞拜然": "阿塞拜疆", -"阿拉伯联合酋长国": "阿拉伯聯合酋長國", -"阿拉伯聯合大公國": "阿拉伯聯合酋長國", -"马尔代夫": "馬爾代夫", -"馬爾地夫": "馬爾代夫", -"馬利共和國": "馬里共和國", -"方便面": "即食麵", -"快速面": "即食麵", -"速食麵": "即食麵", -"泡麵": "即食麵", -"土豆": "馬鈴薯", -"华乐": "中樂", -"民乐": "中樂", -"計程車": "的士", -"出租车": "的士", -"公車": "巴士", -"自行车": "單車", -"犬只": "狗隻", -"台球": "桌球", -"撞球": "桌球", -"冰淇淋": "雪糕", -"賓士": "平治", -"捷豹": "積架", -"福斯": "福士", -"雪铁龙": "先進", -"雪鐵龍": "先進", -"沃尓沃": "富豪", -"马自达": "萬事得", -"馬自達": "萬事得", -"寶獅": "標致", -"拿破崙": "拿破侖", -"布什": "布殊", -"布希": "布殊", -"克林顿": "克林頓", -"柯林頓": "克林頓", -"萨达姆": "薩達姆", -"海珊": "侯賽因", -"侯赛因": "侯賽因", -"大卫·贝克汉姆": "大衛碧咸", -"迈克尔·欧文": "米高奧雲", -"珍妮弗·卡普里亚蒂": "卡佩雅蒂", -"马拉特·萨芬": "沙芬", -"迈克尔·舒马赫": "舒麥加", -"希特勒": "希特拉", -"狄安娜": "戴安娜", -"黛安娜": "戴安娜", -} - -zh2CN = { -"記憶體": "内存", -"預設": "默认", -"串列": "串行", -"乙太網": "以太网", -"點陣圖": "位图", -"常式": "例程", -"游標": "光标", -"光碟": "光盘", -"光碟機": "光驱", -"全形": "全角", -"共用": "共享", -"載入": "加载", -"半形": "半角", -"變數": "变量", -"雜訊": "噪声", -"因數": "因子", -"功能變數名稱": "域名", -"音效卡": "声卡", -"字型大小": "字号", -"字型檔": "字库", -"欄位": "字段", -"字元": "字符", -"存檔": "存盘", -"定址": "寻址", -"章節附註": "尾注", -"非同步": "异步", -"匯流排": "总线", -"括弧": "括号", -"介面": "接口", -"控制項": "控件", -"許可權": "权限", -"碟片": "盘片", -"矽片": "硅片", -"矽谷": "硅谷", -"硬碟": "硬盘", -"磁碟": "磁盘", -"磁軌": "磁道", -"程式控制": "程控", -"運算元": "算子", -"演算法": "算法", -"晶片": "芯片", -"晶元": "芯片", -"片語": "词组", -"軟碟機": "软驱", -"快閃記憶體": "快闪存储器", -"滑鼠": "鼠标", -"進位": "进制", -"互動式": "交互式", -"優先順序": "优先级", -"感測": "传感", -"攜帶型": "便携式", -"資訊理論": "信息论", -"迴圈": "循环", -"防寫": "写保护", -"分散式": "分布式", -"解析度": "分辨率", -"伺服器": "服务器", -"等於": "等于", -"區域網": "局域网", -"巨集": "宏", -"掃瞄器": "扫瞄仪", -"寬頻": "宽带", -"資料庫": "数据库", -"乳酪": "奶酪", -"鉅賈": "巨商", -"手電筒": "手电", -"萬曆": "万历", -"永曆": "永历", -"辭彙": "词汇", -"母音": "元音", -"自由球": "任意球", -"頭槌": "头球", -"進球": "入球", -"顆進球": "粒入球", -"射門": "打门", -"蓋火鍋": "火锅盖帽", -"印表機": "打印机", -"打印機": "打印机", -"位元組": "字节", -"字節": "字节", -"列印": "打印", -"打印": "打印", -"硬體": "硬件", -"二極體": "二极管", -"二極管": "二极管", -"三極體": "三极管", -"三極管": "三极管", -"數位": "数码", -"數碼": "数码", -"軟體": "软件", -"軟件": "软件", -"網路": "网络", -"網絡": "网络", -"人工智慧": "人工智能", -"太空梭": "航天飞机", -"穿梭機": "航天飞机", -"網際網路": "因特网", -"互聯網": "因特网", -"機械人": "机器人", -"機器人": "机器人", -"行動電話": "移动电话", -"流動電話": "移动电话", -"調制解調器": "调制解调器", -"數據機": "调制解调器", -"短訊": "短信", -"簡訊": "短信", -"烏茲別克": "乌兹别克斯坦", -"查德": "乍得", -"乍得": "乍得", -"也門": "", -"葉門": "也门", -"伯利茲": "伯利兹", -"貝里斯": "伯利兹", -"維德角": "佛得角", -"佛得角": "佛得角", -"克羅地亞": "克罗地亚", -"克羅埃西亞": "克罗地亚", -"岡比亞": "冈比亚", -"甘比亞": "冈比亚", -"幾內亞比紹": "几内亚比绍", -"幾內亞比索": "几内亚比绍", -"列支敦斯登": "列支敦士登", -"列支敦士登": "列支敦士登", -"利比里亞": "利比里亚", -"賴比瑞亞": "利比里亚", -"加納": "加纳", -"迦納": "加纳", -"加彭": "加蓬", -"加蓬": "加蓬", -"博茨瓦納": "博茨瓦纳", -"波札那": "博茨瓦纳", -"卡塔爾": "卡塔尔", -"卡達": "卡塔尔", -"盧旺達": "卢旺达", -"盧安達": "卢旺达", -"危地馬拉": "危地马拉", -"瓜地馬拉": "危地马拉", -"厄瓜多爾": "厄瓜多尔", -"厄瓜多": "厄瓜多尔", -"厄立特里亞": "厄立特里亚", -"厄利垂亞": "厄立特里亚", -"吉布堤": "吉布提", -"吉布地": "吉布提", -"哈薩克": "哈萨克斯坦", -"哥斯達黎加": "哥斯达黎加", -"哥斯大黎加": "哥斯达黎加", -"圖瓦盧": "图瓦卢", -"吐瓦魯": "图瓦卢", -"土庫曼": "土库曼斯坦", -"聖盧西亞": "圣卢西亚", -"聖露西亞": "圣卢西亚", -"聖吉斯納域斯": "圣基茨和尼维斯", -"聖克里斯多福及尼維斯": "圣基茨和尼维斯", -"聖文森特和格林納丁斯": "圣文森特和格林纳丁斯", -"聖文森及格瑞那丁": "圣文森特和格林纳丁斯", -"聖馬力諾": "圣马力诺", -"聖馬利諾": "圣马力诺", -"圭亞那": "圭亚那", -"蓋亞那": "圭亚那", -"坦桑尼亞": "坦桑尼亚", -"坦尚尼亞": "坦桑尼亚", -"埃塞俄比亞": "埃塞俄比亚", -"衣索匹亞": "埃塞俄比亚", -"衣索比亞": "埃塞俄比亚", -"吉里巴斯": "基里巴斯", -"基里巴斯": "基里巴斯", -"塔吉克": "塔吉克斯坦", -"塞拉利昂": "塞拉利昂", -"塞普勒斯": "塞浦路斯", -"塞浦路斯": "塞浦路斯", -"塞舌爾": "塞舌尔", -"塞席爾": "塞舌尔", -"多明尼加共和國": "多米尼加", -"多明尼加": "多米尼加", -"多明尼加聯邦": "多米尼加联邦", -"多米尼克": "多米尼加联邦", -"安提瓜和巴布達": "安提瓜和巴布达", -"安地卡及巴布達": "安提瓜和巴布达", -"尼日利亞": "尼日利亚", -"奈及利亞": "尼日利亚", -"尼日爾": "尼日尔", -"尼日": "尼日尔", -"巴貝多": "巴巴多斯", -"巴巴多斯": "巴巴多斯", -"巴布亞新畿內亞": "巴布亚新几内亚", -"巴布亞紐幾內亞": "巴布亚新几内亚", -"布基納法索": "布基纳法索", -"布吉納法索": "布基纳法索", -"蒲隆地": "布隆迪", -"布隆迪": "布隆迪", -"希臘": "希腊", -"帛琉": "帕劳", -"義大利": "意大利", -"意大利": "意大利", -"所羅門群島": "所罗门群岛", -"索羅門群島": "所罗门群岛", -"汶萊": "文莱", -"斯威士蘭": "斯威士兰", -"史瓦濟蘭": "斯威士兰", -"斯洛文尼亞": "斯洛文尼亚", -"斯洛維尼亞": "斯洛文尼亚", -"新西蘭": "新西兰", -"紐西蘭": "新西兰", -"格林納達": "格林纳达", -"格瑞那達": "格林纳达", -"格魯吉亞": "乔治亚", -"喬治亞": "乔治亚", -"梵蒂岡": "梵蒂冈", -"毛里塔尼亞": "毛里塔尼亚", -"茅利塔尼亞": "毛里塔尼亚", -"毛里裘斯": "毛里求斯", -"模里西斯": "毛里求斯", -"沙地阿拉伯": "沙特阿拉伯", -"沙烏地阿拉伯": "沙特阿拉伯", -"波斯尼亞黑塞哥維那": "波斯尼亚和黑塞哥维那", -"波士尼亞赫塞哥維納": "波斯尼亚和黑塞哥维那", -"津巴布韋": "津巴布韦", -"辛巴威": "津巴布韦", -"宏都拉斯": "洪都拉斯", -"洪都拉斯": "洪都拉斯", -"特立尼達和多巴哥": "特立尼达和托巴哥", -"千里達托貝哥": "特立尼达和托巴哥", -"瑙魯": "瑙鲁", -"諾魯": "瑙鲁", -"瓦努阿圖": "瓦努阿图", -"萬那杜": "瓦努阿图", -"溫納圖": "瓦努阿图", -"科摩羅": "科摩罗", -"葛摩": "科摩罗", -"象牙海岸": "科特迪瓦", -"突尼西亞": "突尼斯", -"索馬里": "索马里", -"索馬利亞": "索马里", -"老撾": "老挝", -"寮國": "老挝", -"肯雅": "肯尼亚", -"肯亞": "肯尼亚", -"蘇利南": "苏里南", -"莫三比克": "莫桑比克", -"莫桑比克": "莫桑比克", -"萊索托": "莱索托", -"賴索托": "莱索托", -"貝寧": "贝宁", -"貝南": "贝宁", -"贊比亞": "赞比亚", -"尚比亞": "赞比亚", -"亞塞拜然": "阿塞拜疆", -"阿塞拜疆": "阿塞拜疆", -"阿拉伯聯合酋長國": "阿拉伯联合酋长国", -"阿拉伯聯合大公國": "阿拉伯联合酋长国", -"南韓": "韩国", -"馬爾代夫": "马尔代夫", -"馬爾地夫": "马尔代夫", -"馬爾他": "马耳他", -"馬利共和國": "马里共和国", -"即食麵": "方便面", -"快速面": "方便面", -"速食麵": "方便面", -"泡麵": "方便面", -"笨豬跳": "蹦极跳", -"绑紧跳": "蹦极跳", -"冷盤": "凉菜", -"冷菜": "凉菜", -"散钱": "零钱", -"谐星": "笑星", -"夜学": "夜校", -"华乐": "民乐", -"中樂": "民乐", -"屋价": "房价", -"的士": "出租车", -"計程車": "出租车", -"公車": "公共汽车", -"單車": "自行车", -"節慶": "节日", -"芝士": "乾酪", -"狗隻": "犬只", -"士多啤梨": "草莓", -"忌廉": "奶油", -"桌球": "台球", -"撞球": "台球", -"雪糕": "冰淇淋", -"衞生": "卫生", -"衛生": "卫生", -"賓士": "奔驰", -"平治": "奔驰", -"積架": "捷豹", -"福斯": "大众", -"福士": "大众", -"雪鐵龍": "雪铁龙", -"萬事得": "马自达", -"馬自達": "马自达", -"寶獅": "标志", -"拿破崙": "拿破仑", -"布殊": "布什", -"布希": "布什", -"柯林頓": "克林顿", -"克林頓": "克林顿", -"薩達姆": "萨达姆", -"海珊": "萨达姆", -"梵谷": "凡高", -"大衛碧咸": "大卫·贝克汉姆", -"米高奧雲": "迈克尔·欧文", -"卡佩雅蒂": "珍妮弗·卡普里亚蒂", -"沙芬": "马拉特·萨芬", -"舒麥加": "迈克尔·舒马赫", -"希特拉": "希特勒", -"黛安娜": "戴安娜", -"希拉": "赫拉", -} - -zh2SG = { -"方便面": "快速面", -"速食麵": "快速面", -"即食麵": "快速面", -"蹦极跳": "绑紧跳", -"笨豬跳": "绑紧跳", -"凉菜": "冷菜", -"冷盤": "冷菜", -"零钱": "散钱", -"散紙": "散钱", -"笑星": "谐星", -"夜校": "夜学", -"民乐": "华乐", -"住房": "住屋", -"房价": "屋价", -"泡麵": "快速面", -} diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py new file mode 100644 index 00000000..52042412 --- /dev/null +++ b/zhenxun/builtin_plugins/__init__.py @@ -0,0 +1,131 @@ +import uuid + +from nonebot import require +from nonebot.drivers import Driver +from tortoise import Tortoise +from tortoise.exceptions import OperationalError + +from zhenxun.models.goods_info import GoodsInfo +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.decorator.shop import shop_register + +require("nonebot_plugin_apscheduler") +require("nonebot_plugin_alconna") +require("nonebot_plugin_session") +require("nonebot_plugin_userinfo") + + +import nonebot +import ujson as json + +driver: Driver = nonebot.get_driver() + + +SIGN_SQL = """ +select distinct on("user_id") t1.user_id, t1.checkin_count, t1.add_probability, t1.specify_probability, t1.impression +from public.sign_group_users t1 + join ( + select user_id, max(t2.impression) as max_impression + from public.sign_group_users t2 + group by user_id + ) t on t.user_id = t1.user_id and t.max_impression = t1.impression +""" + +BAG_SQL = """ +select t1.user_id, t1.gold, t1.property +from public.bag_users t1 + join ( + select user_id, max(t2.gold) as max_gold + from public.bag_users t2 + group by user_id + ) t on t.user_id = t1.user_id and t.max_gold = t1.gold +""" + + +@driver.on_startup +async def _(): + """签到与用户的数据迁移""" + if goods_list := await GoodsInfo.filter(uuid__isnull=True).all(): + for goods in goods_list: + goods.uuid = uuid.uuid1() # type: ignore + await GoodsInfo.bulk_update(goods_list, ["uuid"], 10) + await shop_register.load_register() + if ( + not await UserConsole.annotate().count() + and not await SignUser.annotate().count() + ): + try: + group_user = await GroupInfoUser.filter(uid__isnull=False).all() + user2uid = {u.user_id: u.uid for u in group_user} + flag = False + db = Tortoise.get_connection("default") + old_sign_list = await db.execute_query_dict(SIGN_SQL) + old_bag_list = await db.execute_query_dict(BAG_SQL) + goods = { + g["goods_name"]: g["uuid"] + for g in await GoodsInfo.annotate().values("goods_name", "uuid") + } + create_list = [] + sign_id_list = [] + max_uid = max(user2uid.values()) + 1 + for old_sign in old_sign_list: + sign_id_list.append(old_sign["user_id"]) + old_bag = [ + b for b in old_bag_list if b["user_id"] == old_sign["user_id"] + ] + if old_bag: + old_bag = old_bag[0] + property = json.loads(old_bag["property"]) + props = {} + if property: + for name, num in property.items(): + if name in goods: + props[goods[name]] = num + create_list.append( + UserConsole( + user_id=old_sign["user_id"], + platform="qq", + uid=user2uid.get(old_sign["user_id"]) or max_uid, + props=props, + gold=old_bag["gold"], + ) + ) + if not user2uid.get(old_sign["user_id"]): + max_uid += 1 + else: + create_list.append( + UserConsole( + user_id=old_sign["user_id"], platform="qq", uid=max_uid + ) + ) + max_uid += 1 + if create_list: + logger.info("开始迁移用户数据...") + await UserConsole.bulk_create(create_list, 10) + logger.info("迁移用户数据完成!") + create_list.clear() + uc_dict = {u.user_id: u for u in await UserConsole.all()} + for old_sign in old_sign_list: + user_console = uc_dict.get(old_sign["user_id"]) + if not user_console: + user_console = await UserConsole.get_user(old_sign["user_id"], "qq") + create_list.append( + SignUser( + user_id=old_sign["user_id"], + user_console=user_console, + platform="qq", + sign_count=old_sign["checkin_count"], + impression=old_sign["impression"], + add_probability=old_sign["add_probability"], + specify_probability=old_sign["specify_probability"], + ) + ) + if create_list: + logger.info("开始迁移签到数据...") + await SignUser.bulk_create(create_list, 10) + logger.info("迁移签到数据完成!") + except OperationalError as e: + logger.warning("数据迁移", e=e) diff --git a/plugins/genshin/__init__.py b/zhenxun/builtin_plugins/admin/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from plugins/genshin/__init__.py rename to zhenxun/builtin_plugins/admin/__init__.py diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py new file mode 100644 index 00000000..e98df047 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -0,0 +1,160 @@ +import nonebot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_alconna.matcher import AlconnaMatcher +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.exception import EmptyError +from zhenxun.utils.image_utils import ( + BuildImage, + build_sort_image, + group_image, + text2image, +) +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import admin_check, ensure_group + +__plugin_meta__ = PluginMetadata( + name="群组管理员帮助", + description="管理员帮助列表", + usage=""" + 管理员帮助 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=1, + ).dict(), +) + +_matcher = on_alconna( + Alconna("管理员帮助"), + rule=admin_check(1) & ensure_group, + priority=5, + block=True, +) + + +ADMIN_HELP_IMAGE = IMAGE_PATH / "ADMIN_HELP.png" +if ADMIN_HELP_IMAGE.exists(): + ADMIN_HELP_IMAGE.unlink() + + +async def build_help() -> BuildImage: + """构造管理员帮助图片 + + 异常: + EmptyError: 管理员帮助为空 + + 返回: + BuildImage: 管理员帮助图片 + """ + plugin_list = await PluginInfo.filter( + plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN] + ).all() + data_list = [] + for plugin in plugin_list: + if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path): + if _plugin.metadata: + data_list.append({"plugin": plugin, "metadata": _plugin.metadata}) + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + image_list = [] + for data in data_list: + plugin = data["plugin"] + metadata = data["metadata"] + try: + usage = None + description = None + if metadata.usage: + usage = await text2image( + metadata.usage, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + if metadata.description: + description = await text2image( + metadata.description, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + width = 0 + height = 100 + if usage: + width = usage.width + height += usage.height + if description and description.width > width: + width = description.width + height += description.height + font_width, font_height = BuildImage.get_text_size( + plugin.name + f"[{plugin.level}]", font + ) + if font_width > width: + width = font_width + A = BuildImage(width + 30, height + 120, "#EAEDF2") + await A.text((15, 10), plugin.name + f"[{plugin.level}]") + await A.text((15, 70), "简介:") + if not description: + description = BuildImage(A.width - 30, 30, (255, 255, 255)) + await description.circle_corner(10) + await A.paste(description, (15, 100)) + if not usage: + usage = BuildImage(A.width - 30, 30, (255, 255, 255)) + await usage.circle_corner(10) + await A.text((15, description.height + 115), "用法:") + await A.paste(usage, (15, description.height + 145)) + await A.circle_corner(10) + image_list.append(A) + except Exception as e: + logger.warning( + f"获取群管理员插件 {plugin.module}: {plugin.name} 设置失败...", + "管理员帮助", + e=e, + ) + if task_list := await TaskInfo.all(): + task_str = "\n".join([task.name for task in task_list]) + task_str = "通过 开启/关闭群被动 来控制群被动\n----------\n" + task_str + task_image = await text2image(task_str, padding=5, color=(255, 255, 255)) + await task_image.circle_corner(10) + A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2") + await A.text((25, 10), "被动技能") + await A.paste(task_image, (25, 50)) + await A.circle_corner(10) + image_list.append(A) + if not image_list: + raise EmptyError() + image_group, _ = group_image(image_list) + A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160) + text = await BuildImage.build_text_image( + "群管理员帮助", + size=40, + ) + tip = await BuildImage.build_text_image( + "注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red" + ) + await A.paste(text, (50, 30)) + await A.paste(tip, (50, 90)) + await A.save(ADMIN_HELP_IMAGE) + return BuildImage(1, 1) + + +@_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, +): + if not ADMIN_HELP_IMAGE.exists(): + try: + await build_help() + except EmptyError: + await MessageUtils.build_message("管理员帮助为空").finish(reply_to=True) + await MessageUtils.build_message(ADMIN_HELP_IMAGE).send() + logger.info("查看管理员帮助", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/admin/admin_watch.py b/zhenxun/builtin_plugins/admin/admin_watch.py new file mode 100644 index 00000000..7fe7fdb5 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/admin_watch.py @@ -0,0 +1,64 @@ +from nonebot import on_notice +from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent +from nonebot.plugin import PluginMetadata + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.level_user import LevelUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="群管理员变动监测", + description="检测群管理员变动, 添加与删除管理员默认权限, 当配置项 ADMIN_DEFAULT_AUTH 为空时, 不会添加管理员权限", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="admin_bot_manage", + key="ADMIN_DEFAULT_AUTH", + value=5, + help="设置群欢迎消息所需要的管理员权限等级", + default_value=5, + ) + ], + ).dict(), +) + + +admin_notice = on_notice(priority=5) + +base_config = Config.get("admin_bot_manage") + + +@admin_notice.handle() +async def _(event: GroupAdminNoticeEvent): + if event.sub_type == "set": + admin_default_auth = base_config.get("ADMIN_DEFAULT_AUTH") + if admin_default_auth is not None: + await LevelUser.set_level( + str(event.user_id), + str(event.group_id), + admin_default_auth, + ) + logger.info( + f"成为管理员,添加权限: {admin_default_auth}", + "群管理员变动监测", + session=event.user_id, + group_id=event.group_id, + ) + else: + logger.warning( + f"配置项 MODULE: [admin_bot_manage] | KEY: [ADMIN_DEFAULT_AUTH] 为空" + ) + elif event.sub_type == "unset": + await LevelUser.delete_level(str(event.user_id), str(event.group_id)) + logger.info( + "撤销群管理员, 取消权限等级", + "群管理员变动监测", + session=event.user_id, + group_id=event.group_id, + ) diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py new file mode 100644 index 00000000..0fab59b8 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -0,0 +1,261 @@ +from arclet.alconna import Args +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Arparma, + At, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import admin_check + +from ._data_source import BanManage + +base_config = Config.get("ban") + +__plugin_meta__ = PluginMetadata( + name="Ban", + description="你被逮捕了!丢进小黑屋!封禁用户以及群组,屏蔽消息", + usage=""" + 普通管理员 + 格式: + ban [At用户] -t [时长(分钟)] + + 示例: + ban @用户 : 永久拉黑用户 + ban @用户 -t 100 : 拉黑用户100分钟 + unban @用户 : 从小黑屋中拉出来 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPER_AND_ADMIN, + superuser_help=""" + 超级管理员额外命令 + 格式: + ban [At用户/用户Id] [时长] + ban列表: 获取所有Ban数据 + + 群组ban列表: 获取群组Ban数据 + 用户ban列表: 获取用户Ban数据 + + ban列表 -u [用户Id]: 查找指定用户ban数据 + ban列表 -g [群组Id]: 查找指定群组ban数据 + 示例: + ban列表 -u 123456789 : 查找用户123456789的ban数据 + ban列表 -g 123456789 : 查找群组123456789的ban数据 + + 私聊下: + 示例: + ban 123456789 : 永久拉黑用户123456789 + ban 123456789 -t 100 : 拉黑用户123456789 100分钟 + + ban -g 999999 : 拉黑群组为999999的群组 + ban -g 999999 -t 100 : 拉黑群组为999999的群组 100分钟 + + unban 123456789 : 从小黑屋中拉出来 + unban -g 999999 : 将群组9999999从小黑屋中拉出来 + """, + admin_level=base_config.get("BAN_LEVEL", 5), + configs=[ + RegisterConfig( + key="BAN_LEVEL", + value=5, + help="ban/unban所需要的管理员权限等级", + default_value=5, + type=int, + ) + ], + ).dict(), +) + + +_ban_matcher = on_alconna( + Alconna( + "ban", + Args["user?", [str, At]], + Option("-g|--group", Args["group_id", str]), + Option("-t|--time", Args["duration", int]), + ), + rule=admin_check("ban", "BAN_LEVEL"), + priority=5, + block=True, +) + +_unban_matcher = on_alconna( + Alconna( + "unban", + Args["user?", [str, At]], + Option("-g|--group", Args["group_id", str]), + ), + rule=admin_check("ban", "BAN_LEVEL"), + priority=5, + block=True, +) + +_status_matcher = on_alconna( + Alconna( + "ban列表", + Option("-u", Args["user_id", str], help_text="查找用户"), + Option("-g", Args["group_id", str], help_text="查找群组"), + Option("--user", action=store_true, help_text="过滤用户"), + Option("--group", action=store_true, help_text="过滤群组"), + ), + permission=SUPERUSER, + priority=1, + block=True, +) + +_status_matcher.shortcut( + "用户ban列表", + command="ban列表", + arguments=["--user"], + prefix=True, +) + +_status_matcher.shortcut( + "群组ban列表", + command="ban列表", + arguments=["--group"], + prefix=True, +) + + +@_status_matcher.handle() +async def _( + arparma: Arparma, + user_id: Match[str], + group_id: Match[str], +): + filter_type = None + if arparma.find("user"): + filter_type = "user" + if arparma.find("group"): + filter_type = "group" + _user_id = user_id.result if user_id.available else None + _group_id = group_id.result if group_id.available else None + if image := await BanManage.build_ban_image(filter_type, _user_id, _group_id): + await MessageUtils.build_message(image).finish(reply_to=True) + else: + await MessageUtils.build_message("数据为空捏...").finish(reply_to=True) + + +@_ban_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + user: Match[str | At], + duration: Match[int], + group_id: Match[str], +): + user_id = None + if user.available: + if isinstance(user.result, At): + user_id = user.result.target + else: + if session.id1 not in bot.config.superusers: + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) + user_id = user.result + _duration = duration.result * 60 if duration.available else -1 + if (gid := session.id3 or session.id2) and not group_id.available: + if group_id.available: + gid = group_id.result + await BanManage.ban( + user_id, gid, _duration, session, session.id1 in bot.config.superusers + ) + logger.info( + f"管理员Ban", + arparma.header_result, + session=session, + target=f"{gid}:{user_id}", + ) + await MessageUtils.build_message( + [ + "对 ", + At(flag="user", target=user_id) if isinstance(user.result, At) else user_id, # type: ignore + f" 狠狠惩戒了一番,一脚踢进了小黑屋!", + ] + ).finish(reply_to=True) + elif session.id1 in bot.config.superusers: + _group_id = group_id.result if group_id.available else None + await BanManage.ban(user_id, _group_id, _duration, session, True) + logger.info( + f"超级用户Ban", + arparma.header_result, + session=session, + target=f"{_group_id}:{user_id}", + ) + at_msg = user_id if user_id else f"群组:{_group_id}" + await MessageUtils.build_message( + f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!" + ).finish(reply_to=True) + + +@_unban_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + user: Match[str | At], + group_id: Match[str], +): + user_id = None + if user.available: + if isinstance(user.result, At): + user_id = user.result.target + else: + if session.id1 not in bot.config.superusers: + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) + user_id = user.result + if gid := session.id3 or session.id2: + if group_id.available: + gid = group_id.result + if await BanManage.unban( + user_id, gid, session, session.id1 in bot.config.superusers + ): + logger.info( + f"管理员UnBan", + arparma.header_result, + session=session, + target=f"{gid}:{user_id}", + ) + await MessageUtils.build_message( + [ + "将 ", + At(flag="user", target=user_id) if isinstance(user.result, At) else user_id, # type: ignore + f" 从黑屋中拉了出来并急救了一下!", + ] + ).finish(reply_to=True) + else: + await MessageUtils.build_message(f"该用户不在黑名单中捏...").finish( + reply_to=True + ) + elif session.id1 in bot.config.superusers: + _group_id = group_id.result if group_id.available else None + if await BanManage.unban(user_id, _group_id, session, True): + logger.info( + f"超级用户UnBan", + arparma.header_result, + session=session, + target=f"{_group_id}:{user_id}", + ) + at_msg = user_id if user_id else f"群组:{_group_id}" + await MessageUtils.build_message( + f"对 {at_msg} 从黑屋中拉了出来并急救了一下!" + ).finish(reply_to=True) + else: + await MessageUtils.build_message(f"该用户不在黑名单中捏...").finish( + reply_to=True + ) diff --git a/zhenxun/builtin_plugins/admin/ban/_data_source.py b/zhenxun/builtin_plugins/admin/ban/_data_source.py new file mode 100644 index 00000000..3d8f704d --- /dev/null +++ b/zhenxun/builtin_plugins/admin/ban/_data_source.py @@ -0,0 +1,131 @@ +import time +from typing import Literal + +from nonebot_plugin_session import EventSession + +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.level_user import LevelUser +from zhenxun.utils.image_utils import BuildImage, ImageTemplate + + +class BanManage: + + @classmethod + async def build_ban_image( + cls, + filter_type: Literal["group", "user"] | None, + user_id: str | None = None, + group_id: str | None = None, + ) -> BuildImage | None: + """构造Ban列表图片 + + 参数: + filter_type: 过滤类型 + user_id: 用户id + group_id: 群组id + + 返回: + BuildImage | None: Ban列表图片 + """ + data_list = None + query = BanConsole + if user_id: + query = query.filter(user_id=user_id) + elif group_id: + query = query.filter(group_id=group_id) + else: + if filter_type == "user": + query = query.filter(group_id__isnull=True) + elif filter_type == "group": + query = query.filter(user_id__isnull=True) + data_list = await query.all() + if not data_list: + return None + column_name = [ + "ID", + "用户ID", + "群组ID", + "BAN LEVEL", + "剩余时长(分钟)", + "操作员ID", + ] + row_data = [] + for data in data_list: + duration = int((data.ban_time + data.duration - time.time()) / 60) + if data.duration < 0: + duration = "∞" + row_data.append( + [ + data.id, + data.user_id, + data.group_id, + data.ban_level, + duration, + data.operator, + ] + ) + return await ImageTemplate.table_page( + "Ban / UnBan 列表", "在黑屋中狠狠调教!", column_name, row_data + ) + + @classmethod + async def is_ban(cls, user_id: str, group_id: str | None): + """判断用户是否被ban + + 参数: + user_id: 用户id + + 返回: + bool: 是否被ban + """ + return await BanConsole.is_ban(user_id, group_id) + + @classmethod + async def unban( + cls, + user_id: str | None, + group_id: str | None, + session: EventSession, + is_superuser: bool = False, + ) -> bool: + """unban目标用户 + + 参数: + user_id: 用户id + group_id: 群组id + session: Session + is_superuser: 是否为超级用户操作 + + 返回: + bool: 是否unban成功 + """ + user_level = 9999 + if not is_superuser and user_id and session.id1: + user_level = await LevelUser.get_user_level(session.id1, group_id) + if await BanConsole.check_ban_level(user_id, group_id, user_level): + await BanConsole.unban(user_id, group_id) + return True + return False + + @classmethod + async def ban( + cls, + user_id: str | None, + group_id: str | None, + duration: int, + session: EventSession, + is_superuser: bool, + ): + """ban掉目标用户 + + 参数: + user_id: 用户id + group_id: 群组id + duration: 时长,秒 + session: Session + is_superuser: 是否为超级用户操作 + """ + level = 9999 + if not is_superuser and user_id and session.id1: + level = await LevelUser.get_user_level(session.id1, group_id) + await BanConsole.ban(user_id, group_id, level, duration, session.id1) diff --git a/zhenxun/builtin_plugins/admin/group_member_update/__init__.py b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py new file mode 100644 index 00000000..c7036cc8 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py @@ -0,0 +1,66 @@ +from nonebot import on_notice +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import admin_check, ensure_group + +from ._data_source import MemberUpdateManage + +__plugin_meta__ = PluginMetadata( + name="更新群组成员列表", + description="更新群组成员列表", + usage=""" + 更新群组成员的基本信息 + 指令: + 更新群组成员信息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPER_AND_ADMIN, + admin_level=1, + ).dict(), +) + + +_matcher = on_alconna( + Alconna("更新群组成员信息"), + rule=admin_check(1) & ensure_group, + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma): + if gid := session.id3 or session.id2: + logger.info("更新群组成员信息", arparma.header_result, session=session) + await MemberUpdateManage.update(bot, gid) + await MessageUtils.build_message("已经成功更新了群组成员信息!").finish( + reply_to=True + ) + await MessageUtils.build_message("群组id为空...").send() + + +_notice = on_notice(priority=1, block=False) + + +@_notice.handle() +async def _(bot: Bot, event: GroupIncreaseNoticeEvent): + # TODO: 其他适配器的加群自动更新群组成员信息 + if str(event.user_id) == bot.self_id: + await MemberUpdateManage.update(bot, str(event.group_id)) + logger.info( + f"{NICKNAME}加入群聊更新群组信息", + "更新群组成员列表", + session=event.user_id, + group_id=event.group_id, + ) diff --git a/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py new file mode 100644 index 00000000..9a790625 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py @@ -0,0 +1,179 @@ +import time +from datetime import datetime, timedelta, timezone + +from nonebot.adapters import Bot + +# from nonebot.adapters.discord import Bot as DiscordBot +# from nonebot.adapters.dodo import Bot as DodoBot +from nonebot.adapters.dodo.models import MemberInfo + +# from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot + +from zhenxun.configs.config import Config +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.level_user import LevelUser +from zhenxun.services.log import logger + + +class MemberUpdateManage: + + @classmethod + async def update(cls, bot: Bot, group_id: str): + if isinstance(bot, v11Bot): + await cls.v11(bot, group_id) + elif isinstance(bot, v12Bot): + await cls.v12(bot, group_id) + # elif isinstance(bot, KaiheilaBot): + # await cls.kaiheila(bot, group_id) + # elif isinstance(bot, DodoBot): + # await cls.dodo(bot, group_id) + # elif isinstance(bot, DiscordBot): + # await cls.discord(bot, group_id) + + # @classmethod + # async def discord(cls, bot: DiscordBot, group_id: str): + # # TODO: discord更新群组成员信息 + # pass + + # @classmethod + # async def dodo(cls, bot: DodoBot, group_id: str): + # page_size = 100 + # result_size = 100 + # max_id = 0 + # exist_member_list = [] + # group_member_list: list[MemberInfo] = [] + # while result_size == page_size: + # group_member_data = await bot.get_member_list( + # island_source_id=group_id, page_size=page_size + # ) + # result_size = len(group_member_data.list) + # group_member_list += group_member_data.list + # max_id = group_member_data.max_id + # if group_member_list: + # for user in group_member_list: + # exist_member_list.append(user.dodo_source_id) + # await GroupInfoUser.update_or_create( + # user_id=user.dodo_source_id, + # group_id=group_id, + # defaults={ + # "user_name": user.nick_name or user.personal_nick_name, + # "user_join_time": user.join_time, + # "platform": "dodo", + # }, + # ) + # if delete_member_list := list( + # set(exist_member_list).difference( + # set(await GroupInfoUser.get_group_member_id_list(group_id)) + # ) + # ): + # await GroupInfoUser.filter( + # user_id__in=delete_member_list, group_id=group_id + # ).delete() + # logger.info( + # f"删除已退群用户", + # "更新群组成员信息", + # group_id=group_id, + # platform="dodo", + # ) + + # @classmethod + # async def kaiheila(cls, bot: KaiheilaBot, group_id: str): + # # TODO: kaiheila 更新群组成员信息 + # pass + + @classmethod + async def v11(cls, bot: v11Bot, group_id: str): + exist_member_list = [] + default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") + group_member_list = await bot.get_group_member_list(group_id=int(group_id)) + for user_info in group_member_list: + user_id = user_info["user_id"] + nickname = user_info["card"] or user_info["nickname"] + role = user_info["role"] + if default_auth: + if role in ["owner", "admin"] and not await LevelUser.is_group_flag( + str(user_id), group_id + ): + await LevelUser.set_level(user_id, group_id, default_auth) + if str(user_id) in bot.config.superusers: + await LevelUser.set_level(str(user_id), group_id, 9) + join_time = datetime.strptime( + time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"]) + ), + "%Y-%m-%d %H:%M:%S", + ) + await GroupInfoUser.update_or_create( + user_id=str(user_id), + group_id=group_id, + defaults={ + "user_name": nickname, + "user_join_time": join_time.replace( + tzinfo=timezone(timedelta(hours=8)) + ), + "platform": "qq", + }, + ) + exist_member_list.append(str(user_id)) + logger.debug( + "更新成功", "更新群组成员信息", session=user_id, group_id=group_id + ) + if delete_member_list := list( + set(exist_member_list).difference( + set(await GroupInfoUser.get_group_member_id_list(group_id)) + ) + ): + await GroupInfoUser.filter( + user_id__in=delete_member_list, group_id=group_id + ).delete() + logger.info( + f"删除已退群用户", "更新群组成员信息", group_id=group_id, platform="qq" + ) + + @classmethod + async def v12(cls, bot: v12Bot, group_id: str): + # TODO: v12更新群组成员信息 + pass + # exist_member_list = [] + # default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") + # group_member_list: list[GetGroupMemberInfoResp] = await bot.get_group_member_list( + # group_id=group_id + # ) + # for user_info in group_member_list: + # user_id = user_info.user_id + # nickname = user_info.user_displayname or user_info.user_name + # role = user_info["role"] + # if default_auth: + # if role in ["owner", "admin"] and not LevelUser.is_group_flag( + # str(user_id), group_id + # ): + # await LevelUser.set_level(user_id, group_id, default_auth) + # if str(user_id) in bot.config.superusers: + # await LevelUser.set_level(str(user_id), group_id, 9) + # join_time = datetime.strptime( + # time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"])), + # "%Y-%m-%d %H:%M:%S", + # ) + # await GroupInfoUser.update_or_create( + # user_id=str(user_id), + # group_id=group_id, + # defaults={ + # "user_name": nickname, + # "user_join_time": join_time.replace( + # tzinfo=timezone(timedelta(hours=8)) + # ), + # }, + # ) + # exist_member_list.append(str(user_id)) + # logger.debug("更新成功", "更新群组成员信息", session=user_id, group_id=group_id) + # if delete_member_list := list( + # set(exist_member_list).difference( + # set(await GroupInfoUser.get_group_member_id_list(group_id)) + # ) + # ): + # await GroupInfoUser.filter( + # user_id__in=delete_member_list, group_id=group_id + # ).delete() + # logger.info(f"删除已退群用户", "更新群组成员信息", group_id=group_id) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py new file mode 100644 index 00000000..76b10259 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -0,0 +1,372 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.message import MessageUtils + +from ._data_source import PluginManage, build_plugin, build_task +from .command import _group_status_matcher, _status_matcher + +base_config = Config.get("plugin_switch") + + +__plugin_meta__ = PluginMetadata( + name="功能开关", + description="对群组内的功能限制,超级用户可以对群组以及全局的功能被动开关限制", + usage=""" + 普通管理员 + 格式: + 开启/关闭[功能名称] : 开关功能 + 开启/关闭群被动[被动名称] : 群被动开关 + 开启/关闭所有插件 : 开启/关闭当前群组所有插件状态 + 开启/关闭所有群被动 : 开启/关闭当前群组所有群被动 + 群被动状态 : 查看被动技能开关状态 + 醒来 : 结束休眠 + 休息吧 : 群组休眠, 不会再响应命令 + + 示例: + 开启签到 : 开启签到 + 关闭签到 : 关闭签到 + 开启群被动早晚安 : 关闭被动任务早晚安 + + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPER_AND_ADMIN, + superuser_help=""" + 超级管理员额外命令 + 格式: + 插件列表 + 开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id] + + 开启/关闭插件df[功能名称]: 开启/关闭指定插件进群默认状态 + 开启/关闭所有插件df: 开启/关闭所有插件进群默认状态 + 开启/关闭所有插件: + 私聊中: 开启/关闭所有插件全局状态 + 群组中: 开启/关闭当前群组所有插件状态 + + 开启/关闭群被动[name] ?[-g [group_id]] + 私聊中: 开启/关闭全局指定的被动状态 + 群组中: 开启/关闭当前群组指定的被动状态 + 示例: + 关闭群被动早晚安 + 关闭群被动早晚安 -g 12355555 + + 开启/关闭所有群被动 -[g ?[group_id]] + 私聊中: 开启/关闭全局或指定群组被动状态 + 示例: + 开启所有群被动: 开启全局所有被动 + 开启所有群被动 -g 12345678: 开启群组12345678所有被动 + + 私聊下: + 示例: + 开启签到 : 全局开启签到 + 关闭签到 : 全局关闭签到 + 关闭签到 p : 全局私聊关闭签到 + 关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启) + """, + admin_level=base_config.get("CHANGE_GROUP_SWITCH_LEVEL", 2), + configs=[ + RegisterConfig( + key="CHANGE_GROUP_SWITCH_LEVEL", + value=2, + help="开关群功能权限", + default_value=2, + type=int, + ) + ], + ).dict(), +) + + +@_status_matcher.assign("$main") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, +): + if session.id1 in bot.config.superusers: + image = await build_plugin() + logger.info( + f"查看功能列表", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(image).finish(reply_to=True) + else: + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) + + +@_status_matcher.assign("open") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + plugin_name: Match[str], + group: Match[str], + task: Query[bool] = AlconnaQuery("task.value", False), + default_status: Query[bool] = AlconnaQuery("default.value", False), + all: Query[bool] = AlconnaQuery("all.value", False), +): + if not all.result and not plugin_name.available: + await MessageUtils.build_message("请输入功能名称").finish(reply_to=True) + name = plugin_name.result + gid = session.id3 or session.id2 + if gid: + """修改当前群组的数据""" + if task.result: + if all.result: + result = await PluginManage.unblock_group_all_task(gid) + logger.info(f"开启所有群组被动", arparma.header_result, session=session) + else: + result = await PluginManage.unblock_group_task(name, gid) + logger.info( + f"开启群组被动 {name}", arparma.header_result, session=session + ) + else: + if session.id1 in bot.config.superusers and default_status.result: + """单个插件的进群默认修改""" + result = await PluginManage.set_default_status(name, True) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + ) + else: + if all.result: + """所有插件""" + result = await PluginManage.set_all_plugin_status( + True, default_status.result, gid + ) + logger.info( + f"开启群组中全部功能", + arparma.header_result, + session=session, + ) + else: + result = await PluginManage.unblock_group_plugin(name, gid) + logger.info( + f"开启功能 {name}", arparma.header_result, session=session + ) + await MessageUtils.build_message(result).finish(reply_to=True) + elif session.id1 in bot.config.superusers: + """私聊""" + group_id = group.result if group.available else None + if all.result: + if task.result: + """关闭全局或指定群全部被动""" + if group_id: + result = await PluginManage.unblock_group_all_task(group_id) + else: + result = await PluginManage.unblock_global_all_task() + else: + result = await PluginManage.set_all_plugin_status( + True, default_status.result, group_id + ) + logger.info( + f"超级用户开启全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + if default_status.result: + result = await PluginManage.set_default_status(name, True) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + target=group_id, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + if task.result: + split_list = name.split() + if len(split_list) > 1: + name = split_list[0] + group_id = split_list[1] + if group_id: + result = await PluginManage.superuser_task_handle(name, group_id, True) + logger.info( + f"超级用户开启被动技能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) + else: + result = await PluginManage.unblock_global_task(name) + logger.info( + f"超级用户开启全局被动技能 {name}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + else: + result = await PluginManage.superuser_block(name, None, group_id) + logger.info( + f"超级用户开启功能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + + +@_status_matcher.assign("close") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + plugin_name: Match[str], + block_type: Match[str], + group: Match[str], + task: Query[bool] = AlconnaQuery("task.value", False), + default_status: Query[bool] = AlconnaQuery("default.value", False), + all: Query[bool] = AlconnaQuery("all.value", False), +): + if not all.result and not plugin_name.available: + await MessageUtils.build_message("请输入功能名称").finish(reply_to=True) + name = plugin_name.result + gid = session.id3 or session.id2 + if gid: + """修改当前群组的数据""" + if task.result: + if all.result: + result = await PluginManage.block_group_all_task(gid) + logger.info(f"开启所有群组被动", arparma.header_result, session=session) + else: + result = await PluginManage.block_group_task(name, gid) + logger.info( + f"关闭群组被动 {name}", arparma.header_result, session=session + ) + else: + if session.id1 in bot.config.superusers and default_status.result: + """单个插件的进群默认修改""" + result = await PluginManage.set_default_status(name, False) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + ) + else: + if all.result: + """所有插件""" + result = await PluginManage.set_all_plugin_status( + False, default_status.result, gid + ) + logger.info( + f"关闭群组中全部功能", + arparma.header_result, + session=session, + ) + else: + result = await PluginManage.block_group_plugin(name, gid) + logger.info( + f"关闭功能 {name}", arparma.header_result, session=session + ) + await MessageUtils.build_message(result).finish(reply_to=True) + elif session.id1 in bot.config.superusers: + group_id = group.result if group.available else None + if all.result: + if task.result: + """关闭全局或指定群全部被动""" + if group_id: + result = await PluginManage.block_group_all_task(group_id) + else: + result = await PluginManage.block_global_all_task() + else: + result = await PluginManage.set_all_plugin_status( + False, default_status.result, group_id + ) + logger.info( + f"超级用户关闭全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + if default_status.result: + result = await PluginManage.set_default_status(name, False) + logger.info( + f"超级用户关闭 {name} 功能进群默认开关", + arparma.header_result, + session=session, + target=group_id, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + if task.result: + split_list = name.split() + if len(split_list) > 1: + name = split_list[0] + group_id = split_list[1] + if group_id: + result = await PluginManage.superuser_task_handle(name, group_id, False) + logger.info( + f"超级用户关闭被动技能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) + else: + result = await PluginManage.block_global_task(name) + logger.info( + f"超级用户关闭全局被动技能 {name}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + else: + _type = BlockType.ALL + if block_type.available: + if block_type.result in ["p", "private"]: + _type = BlockType.PRIVATE + elif block_type.result in ["g", "group"]: + _type = BlockType.GROUP + result = await PluginManage.superuser_block(name, _type, group_id) + logger.info( + f"超级用户关闭功能 {name}, 禁用类型: {_type}", + arparma.header_result, + session=session, + target=group_id, + ) + await MessageUtils.build_message(result).finish(reply_to=True) + + +@_group_status_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + status: str, +): + if gid := session.id3 or session.id2: + if status == "sleep": + await PluginManage.sleep(gid) + logger.info("进行休眠", arparma.header_result, session=session) + await MessageUtils.build_message("那我先睡觉了...").finish() + else: + if await PluginManage.is_wake(gid): + await MessageUtils.build_message("我还醒着呢!").finish() + await PluginManage.wake(gid) + logger.info("醒来", arparma.header_result, session=session) + await MessageUtils.build_message("呜..醒来了...").finish() + return MessageUtils.build_message("群组id为空...").send() + + +@_status_matcher.assign("task") +async def _( + session: EventSession, + arparma: Arparma, +): + image = await build_task(session.id3 or session.id2) + if image: + logger.info( + f"查看群被动列表", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(image).finish(reply_to=True) + else: + await MessageUtils.build_message("获取群被动任务失败...").finish(reply_to=True) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py new file mode 100644 index 00000000..b10a929e --- /dev/null +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -0,0 +1,548 @@ +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo +from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.exception import GroupInfoNotFound +from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle + + +def plugin_row_style(column: str, text: str) -> RowStyle: + """被动技能文本风格 + + 参数: + column: 表头 + text: 文本内容 + + 返回: + RowStyle: RowStyle + """ + style = RowStyle() + if column == "全局状态": + if text == "开启": + style.font_color = "#67C23A" + else: + style.font_color = "#F56C6C" + if column == "加载状态": + if text == "SUCCESS": + style.font_color = "#67C23A" + else: + style.font_color = "#F56C6C" + return style + + +async def build_plugin() -> BuildImage: + column_name = [ + "ID", + "模块", + "名称", + "全局状态", + "禁用类型", + "加载状态", + "菜单分类", + "作者", + "版本", + "金币花费", + ] + plugin_list = await PluginInfo.filter(plugin_type__not=PluginType.HIDDEN).all() + column_data = [] + for plugin in plugin_list: + column_data.append( + [ + plugin.id, + plugin.module, + plugin.name, + "开启" if plugin.status else "关闭", + plugin.block_type, + "SUCCESS" if plugin.load_status else "ERROR", + plugin.menu_type, + plugin.author, + plugin.version, + plugin.cost_gold, + ] + ) + return await ImageTemplate.table_page( + "Plugin", + "插件状态", + column_name, + column_data, + text_style=plugin_row_style, + ) + + +def task_row_style(column: str, text: str) -> RowStyle: + """被动技能文本风格 + + 参数: + column: 表头 + text: 文本内容 + + 返回: + RowStyle: RowStyle + """ + style = RowStyle() + if column in ["群组状态", "全局状态"]: + if text == "开启": + style.font_color = "#67C23A" + else: + style.font_color = "#F56C6C" + return style + + +async def build_task(group_id: str | None) -> BuildImage: + """构造被动技能状态图片 + + 参数: + group_id: 群组id + + 异常: + GroupInfoNotFound: 未找到群组 + + 返回: + BuildImage: 被动技能状态图片 + """ + task_list = await TaskInfo.all() + column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"] + group = None + if group_id: + group = await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ) + if not group: + raise GroupInfoNotFound() + else: + column_name.remove("群组状态") + column_data = [] + for task in task_list: + if group: + column_data.append( + [ + task.id, + task.module, + task.name, + "开启" if task.module not in group.block_task else "关闭", + "开启" if task.status else "关闭", + task.run_time or "-", + ] + ) + else: + column_data.append( + [ + task.id, + task.module, + task.name, + "开启" if task.status else "关闭", + task.run_time or "-", + ] + ) + return await ImageTemplate.table_page( + "Task", + "被动技能状态", + column_name, + column_data, + text_style=task_row_style, + ) + + +class PluginManage: + + @classmethod + async def set_default_status(cls, plugin_name: str, status: bool) -> str: + """设置插件进群默认状态 + + 参数: + plugin_name: 插件名称 + status: 状态 + + 返回: + str: 返回信息 + """ + if plugin_name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(plugin_name)) + else: + plugin = await PluginInfo.get_or_none(name=plugin_name) + if plugin: + plugin.default_status = status + await plugin.save(update_fields=["default_status"]) + return f'成功将 {plugin.name} 进群默认状态修改为: {"开启" if status else "关闭"}' + return f"没有找到这个功能喔..." + + @classmethod + async def set_all_plugin_status( + cls, status: bool, is_default: bool = False, group_id: str | None = None + ) -> str: + """修改所有插件状态 + + 参数: + status: 状态 + is_default: 是否进群默认. + group_id: 指定群组id. + + 返回: + str: 返回信息 + """ + if is_default: + await PluginInfo.filter(plugin_type=PluginType.NORMAL).update( + default_status=status + ) + return f'成功将所有功能进群默认状态修改为: {"开启" if status else "关闭"}' + if group_id: + if group := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): + module_list = await PluginInfo.filter( + plugin_type=PluginType.NORMAL + ).values_list("module", flat=True) + if status: + for module in module_list: + group.block_plugin = group.block_plugin.replace( + f"{module},", "" + ) + else: + module_list = await PluginInfo.filter( + plugin_type=PluginType.NORMAL + ).values_list("module", flat=True) + group.block_plugin = ",".join(module_list) + "," # type: ignore + await group.save(update_fields=["block_plugin"]) + return f'成功将此群组所有功能状态修改为: {"开启" if status else "关闭"}' + return "获取群组失败..." + await PluginInfo.filter(plugin_type=PluginType.NORMAL).update( + status=status, block_type=BlockType.ALL if not status else None + ) + return f'成功将所有功能全局状态修改为: {"开启" if status else "关闭"}' + + @classmethod + async def is_wake(cls, group_id: str) -> bool: + """是否醒来 + + 参数: + group_id: 群组id + + 返回: + bool: 是否醒来 + """ + if c := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): + return c.status + return False + + @classmethod + async def sleep(cls, group_id: str): + """休眠 + + 参数: + group_id: 群组id + """ + await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( + status=False + ) + + @classmethod + async def wake(cls, group_id: str): + """醒来 + + 参数: + group_id: 群组id + """ + await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( + status=True + ) + + @classmethod + async def block(cls, module: str): + """禁用 + + 参数: + module: 模块名 + """ + await PluginInfo.filter(module=module).update(status=False) + + @classmethod + async def unblock(cls, module: str): + """启用 + + 参数: + module: 模块名 + """ + await PluginInfo.filter(module=module).update(status=True) + + @classmethod + async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str: + """禁用群组插件 + + 参数: + plugin_name: 插件名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_plugin(plugin_name, group_id, False) + + @classmethod + async def unblock_group_task(cls, task_name: str, group_id: str) -> str: + """启用被动技能 + + 参数: + task_name: 被动技能名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task(task_name, group_id, False) + + @classmethod + async def unblock_group_all_task(cls, group_id: str) -> str: + """启用被动技能 + + 参数: + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task("", group_id, False, True) + + @classmethod + async def block_group_task(cls, task_name: str, group_id: str) -> str: + """禁用被动技能 + + 参数: + task_name: 被动技能名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task(task_name, group_id, True) + + @classmethod + async def block_group_all_task(cls, group_id: str) -> str: + """禁用被动技能 + + 参数: + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task("", group_id, True, True) + + @classmethod + async def block_global_all_task(cls) -> str: + """禁用全局被动技能 + + 返回: + str: 返回信息 + """ + await TaskInfo.all().update(status=False) + return "已全局禁用所有被动状态" + + @classmethod + async def block_global_task(cls, name: str) -> str: + """禁用全局被动技能 + + 参数: + name: 被动技能名称 + + 返回: + str: 返回信息 + """ + await TaskInfo.filter(name=name).update(status=False) + return f"已全局禁用被动状态 {name}" + + @classmethod + async def unblock_global_all_task(cls) -> str: + """开启全局被动技能 + + 返回: + str: 返回信息 + """ + await TaskInfo.all().update(status=True) + return "已全局开启所有被动状态" + + @classmethod + async def unblock_global_task(cls, name: str) -> str: + """开启全局被动技能 + + 参数: + name: 被动技能名称 + + 返回: + str: 返回信息 + """ + await TaskInfo.filter(name=name).update(status=True) + return f"已全局开启被动状态 {name}" + + @classmethod + async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str: + """启用群组插件 + + 参数: + plugin_name: 插件名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_plugin(plugin_name, group_id, True) + + @classmethod + async def _change_group_task( + cls, task_name: str, group_id: str, status: bool, is_all: bool = False + ) -> str: + """改变群组被动技能状态 + + 参数: + task_name: 被动技能名称 + group_id: 群组Id + status: 状态 + is_all: 所有群被动 + + 返回: + str: 返回信息 + """ + status_str = "关闭" if status else "开启" + if is_all: + modules = await TaskInfo.annotate().values_list("module", flat=True) + if modules: + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + group.block_task = ",".join(modules) + "," # type: ignore + else: + for module in modules: + group.block_task = group.block_task.replace(f"{module},", "") + await group.save(update_fields=["block_task"]) + return f"已成功{status_str}全部被动技能!" + else: + if task := await TaskInfo.get_or_none(name=task_name): + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + group.block_task += f"{task.module}," + else: + if f"super:{task.module}," in group.block_task: + return f"{status_str} {task_name} 被动技能失败,当前群组该被动已被管理员禁用" + group.block_task = group.block_task.replace(f"{task.module},", "") + await group.save(update_fields=["block_task"]) + return f"已成功{status_str} {task_name} 被动技能!" + return "没有找到这个被动技能喔..." + + @classmethod + async def _change_group_plugin( + cls, plugin_name: str, group_id: str, status: bool + ) -> str: + """修改群组插件状态 + + 参数: + plugin_name: 插件名称 + group_id: 群组id + status: 插件状态 + + 返回: + str: 返回信息 + """ + + if plugin_name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(plugin_name)) + else: + plugin = await PluginInfo.get_or_none(name=plugin_name) + status_str = "开启" if status else "关闭" + if plugin: + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + if plugin.module in group.block_plugin: + group.block_plugin = group.block_plugin.replace( + f"{plugin.module},", "" + ) + await group.save(update_fields=["block_plugin"]) + return f"已成功{status_str} {plugin.name} 功能!" + else: + if plugin.module not in group.block_plugin: + group.block_plugin += f"{plugin.module}," + await group.save(update_fields=["block_plugin"]) + return f"已成功{status_str} {plugin.name} 功能!" + return f"该功能已经{status_str}了喔,不要重复{status_str}..." + return "没有找到这个功能喔..." + + @classmethod + async def superuser_task_handle( + cls, task_name: str, group_id: str | None, status: bool + ) -> str: + """超级用户禁用被动技能 + + 参数: + task_name: 被动技能名称 + group_id: 群组id + status: 状态 + + 返回: + str: 返回信息 + """ + if task := await TaskInfo.get_or_none(name=task_name): + status_str = "开启" if status else "关闭" + if group_id: + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + group.block_task = group.block_task.replace( + f"super:{task.module},", "" + ) + else: + group.block_task += f"super:{task.module}," + await group.save(update_fields=["block_task"]) + return f"已成功将群组 {group_id} 被动技能 {task_name} {status_str}!" + return "没有找到这个群组喔..." + return "没有找到这个功能喔..." + + @classmethod + async def superuser_block( + cls, plugin_name: str, block_type: BlockType | None, group_id: str | None + ) -> str: + """超级用户禁用插件 + + 参数: + plugin_name: 插件名称 + block_type: 禁用类型 + group_id: 群组id + + 返回: + str: 返回信息 + """ + if plugin_name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(plugin_name)) + else: + plugin = await PluginInfo.get_or_none(name=plugin_name) + if plugin: + if group_id: + if group := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): + if f"super:{plugin.module}," not in group.block_plugin: + group.block_plugin += f"super:{plugin.module}," + await group.save(update_fields=["block_plugin"]) + return ( + f"已成功关闭群组 {group.group_name} 的 {plugin_name} 功能!" + ) + return "此群组该功能已被超级用户关闭,不要重复关闭..." + return "群组信息未更新,请先更新群组信息..." + plugin.block_type = block_type + plugin.status = not bool(block_type) + await plugin.save(update_fields=["status", "block_type"]) + if not block_type: + return f"已成功将 {plugin.name} 全局启用!" + else: + if block_type == BlockType.ALL: + return f"已成功将 {plugin.name} 全局关闭!" + if block_type == BlockType.GROUP: + return f"已成功将 {plugin.name} 全局群组关闭!" + if block_type == BlockType.PRIVATE: + return f"已成功将 {plugin.name} 全局私聊关闭!" + return "没有找到这个功能喔..." diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py new file mode 100644 index 00000000..6d33b4df --- /dev/null +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -0,0 +1,162 @@ +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + Args, + Option, + Subcommand, + on_alconna, + store_true, +) + +from zhenxun.utils.rules import admin_check, ensure_group + +_status_matcher = on_alconna( + Alconna( + "switch", + Option("-t|--task", action=store_true, help_text="被动技能"), + Option("-df|--default", action=store_true, help_text="进群默认开关"), + Option("--all", action=store_true, help_text="全部插件/被动"), + Option("-g|--group", Args["group?", str], help_text="指定群组"), + Subcommand( + "open", + Args["plugin_name?", [str, int]], + ), + Subcommand( + "close", + Args["plugin_name?", [str, int]], + Option( + "-t|--type", + Args["block_type?", ["all", "a", "private", "p", "group", "g"]], + ), + ), + ), + rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL"), + priority=5, + block=True, +) + +_group_status_matcher = on_alconna( + Alconna("group-status", Args["status", ["sleep", "wake"]]), + rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL") + & ensure_group + & to_me(), + priority=5, + block=True, +) + +_status_matcher.shortcut( + r"插件列表", + command="switch", + arguments=[], + prefix=True, +) + +_status_matcher.shortcut( + r"群被动状态", + command="switch", + arguments=["--task"], + prefix=True, +) + + +_status_matcher.shortcut( + r"开启群被动\s*(?P.+)", + command="switch", + arguments=["open", "{name}", "--task"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭群被动\s*(?P.+)", + command="switch", + arguments=["close", "{name}", "--task"], + prefix=True, +) + + +_status_matcher.shortcut( + r"开启(所有|全部)群被动", + command="switch", + arguments=["open", "--task", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭(所有|全部)群被动", + command="switch", + arguments=["close", "--task", "--all"], + prefix=True, +) + + +_status_matcher.shortcut( + r"开启所有(插件|功能)", + command="switch", + arguments=["open", "s", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"开启所有(插件|功能)df", + command="switch", + arguments=["open", "s", "-df", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"开启(插件|功能)df(?P.+)", + command="switch", + arguments=["open", "{name}", "-df"], + prefix=True, +) + +_status_matcher.shortcut( + r"开启(?P.+)", + command="switch", + arguments=["open", "{name}"], + prefix=True, +) + + +_status_matcher.shortcut( + r"关闭所有(插件|功能)", + command="switch", + arguments=["close", "s", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭所有(插件|功能)df", + command="switch", + arguments=["close", "s", "-df", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭(?P.+)", + command="switch", + arguments=["close", "{name}"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭(插件|功能)df(?P.+)", + command="switch", + arguments=["close", "{name}", "-df"], + prefix=True, +) + + +_group_status_matcher.shortcut( + r"醒来", + command="group-status", + arguments=["wake"], + prefix=True, +) + +_group_status_matcher.shortcut( + r"休息吧", + command="group-status", + arguments=["sleep"], + prefix=True, +) diff --git a/zhenxun/builtin_plugins/admin/welcome_message.py b/zhenxun/builtin_plugins/admin/welcome_message.py new file mode 100644 index 00000000..6cb6c8dc --- /dev/null +++ b/zhenxun/builtin_plugins/admin/welcome_message.py @@ -0,0 +1,122 @@ +import os +import shutil +from typing import Annotated, Dict + +import ujson as json +from nonebot import on_command +from nonebot.params import Command +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Image +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.rules import admin_check, ensure_group + +base_config = Config.get("admin_bot_manage") + +__plugin_meta__ = PluginMetadata( + name="自定义群欢迎消息", + description="自定义群欢迎消息", + usage=""" + 设置欢迎消息 欢迎新人! + 设置欢迎消息 欢迎你 -at + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("SET_GROUP_WELCOME_MESSAGE_LEVEL", 2), + configs=[ + RegisterConfig( + module="admin_bot_manage", + key="SET_GROUP_WELCOME_MESSAGE_LEVEL", + value=2, + help="设置群欢迎消息所需要的管理员权限等级", + default_value=2, + ) + ], + ).dict(), +) + +_matcher = on_command( + "设置欢迎消息", + rule=admin_check("admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL") + & ensure_group, + priority=5, + block=True, +) + + +BASE_PATH = DATA_PATH / "welcome_message" +BASE_PATH.mkdir(parents=True, exist_ok=True) + +# 旧数据迁移 +old_file = DATA_PATH / "custom_welcome_msg" / "custom_welcome_msg.json" +if old_file.exists(): + try: + old_data: Dict[str, str] = json.load(old_file.open(encoding="utf8")) + for group_id, message in old_data.items(): + file = BASE_PATH / "qq" / f"{group_id}" / "text.json" + file.parent.mkdir(parents=True, exist_ok=True) + json.dump( + {"at": "[at]" in message, "message": message.replace("[at]", "")}, + file.open("w", encoding="utf8"), + ensure_ascii=False, + indent=4, + ) + logger.debug("群欢迎消息数据迁移", group_id=group_id) + shutil.rmtree(old_file.parent.absolute()) + except Exception as e: + pass + + +@_matcher.handle() +async def _( + session: EventSession, + message: UniMsg, + command: Annotated[tuple[str, ...], Command()], +): + path = BASE_PATH / f"{session.platform or session.bot_type}" / f"{session.id2}" + if session.id3: + path = ( + BASE_PATH + / f"{session.platform or session.bot_type}" + / f"{session.id3}" + / f"{session.id2}" + ) + file = path / "text.json" + idx = 0 + text = "" + for f in os.listdir(path): + (path / f).unlink() + message[0].text = message[0].text.replace(command[0], "").strip() + for msg in message: + if isinstance(msg, alcText): + text += msg.text + elif isinstance(msg, Image): + if msg.url: + text += f"[image:{idx}]" + await AsyncHttpx.download_file(msg.url, path / f"{idx}.png") + idx += 1 + else: + logger.debug("图片 URL 为空...", command[0]) + if not file.exists(): + file.parent.mkdir(exist_ok=True, parents=True) + is_at = "-at" in message + text = text.replace("-at", "") + json.dump( + {"at": is_at, "message": text}, + file.open("w"), + ensure_ascii=False, + indent=4, + ) + uni_msg = alcText("设置欢迎消息成功: \n") + message + await uni_msg.send() + logger.info(f"设置群欢迎消息成功: {text}", command[0], session=session) diff --git a/basic_plugins/chat_history/__init__.py b/zhenxun/builtin_plugins/chat_history/__init__.py similarity index 100% rename from basic_plugins/chat_history/__init__.py rename to zhenxun/builtin_plugins/chat_history/__init__.py diff --git a/zhenxun/builtin_plugins/chat_history/chat_message.py b/zhenxun/builtin_plugins/chat_history/chat_message.py new file mode 100644 index 00000000..2cfdd607 --- /dev/null +++ b/zhenxun/builtin_plugins/chat_history/chat_message.py @@ -0,0 +1,83 @@ +from nonebot import on_message +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.chat_history import ChatHistory +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="消息存储", + description="消息存储,被动存储群消息", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="chat_history", + key="FLAG", + value=True, + help="是否开启消息自从存储", + default_value=True, + type=bool, + ) + ], + ).dict(), +) + + +def rule(message: UniMsg) -> bool: + return bool(Config.get_config("chat_history", "FLAG") and message) + + +chat_history = on_message(rule=rule, priority=1, block=False) + + +TEMP_LIST = [] + + +@chat_history.handle() +async def _(message: UniMsg, session: EventSession): + # group_id = session.id3 or session.id2 + group_id = session.id2 + TEMP_LIST.append( + ChatHistory( + user_id=session.id1, + group_id=group_id, + text=str(message), + plain_text=message.extract_plain_text(), + bot_id=session.bot_id, + platform=session.platform, + ) + ) + + +@scheduler.scheduled_job( + "interval", + minutes=1, +) +async def _(): + try: + message_list = TEMP_LIST.copy() + TEMP_LIST.clear() + if message_list: + await ChatHistory.bulk_create(message_list) + logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务") + except Exception as e: + logger.error(f"定时批量添加聊天记录", "定时任务", e=e) + + +# @test.handle() +# async def _(event: MessageEvent): +# print(await ChatHistory.get_user_msg(event.user_id, "private")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) +# print(await ChatHistory.get_user_msg(event.user_id, "group")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) +# print(await ChatHistory.get_group_msg(event.group_id)) +# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py new file mode 100644 index 00000000..1287e1b0 --- /dev/null +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -0,0 +1,134 @@ +from datetime import datetime, timedelta + +import pytz +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + Query, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="消息统计", + description="消息统计查询", + usage=""" + 格式: + 消息排行 ?[type [日,周,月,年]] ?[--des] + + 快捷: + [日,周,月,年]消息排行 ?[数量] + + 示例: + 消息排行 : 所有记录排行 + 日消息排行 : 今日记录排行 + 周消息排行 : 今日记录排行 + 月消息排行 : 今日记录排行 + 年消息排行 : 今日记录排行 + 消息排行 周 --des : 逆序周记录排行 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.NORMAL, + menu_type="数据统计", + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "消息排行", + Option("--des", action=store_true, help_text="逆序"), + Args["type?", ["日", "周", "月", "年"]]["count?", int, 10], + ), + aliases={"消息统计"}, + priority=5, + block=True, +) + +_matcher.shortcut( + r"(?P['日', '周', '月', '年'])?消息(排行|统计)\s?(?P\d+)?", + command="消息排行", + arguments=["{type}", "{cnt}"], + prefix=True, +) + + +@_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + type: Match[str], + count: Query[int] = Query("count", 10), +): + group_id = session.id3 or session.id2 + time_now = datetime.now() + date_scope = None + zero_today = time_now - timedelta( + hours=time_now.hour, minutes=time_now.minute, seconds=time_now.second + ) + date = type.result if type.available else None + if date: + if date in ["日"]: + date_scope = (zero_today, time_now) + elif date in ["周"]: + date_scope = (time_now - timedelta(days=7), time_now) + elif date in ["月"]: + date_scope = (time_now - timedelta(days=30), time_now) + column_name = ["名次", "昵称", "发言次数"] + if rank_data := await ChatHistory.get_group_msg_rank( + group_id, count.result, "DES" if arparma.find("des") else "DESC", date_scope + ): + idx = 1 + data_list = [] + for uid, num in rank_data: + if user := await GroupInfoUser.filter( + user_id=uid, group_id=group_id + ).first(): + user_name = user.user_name + else: + user_name = uid + data_list.append([idx, user_name, num]) + idx += 1 + if not date_scope: + if date_scope := await ChatHistory.get_group_first_msg_datetime(group_id): + date_scope = date_scope.astimezone( + pytz.timezone("Asia/Shanghai") + ).replace(microsecond=0) + else: + date_scope = time_now.replace(microsecond=0) + date_str = f"{str(date_scope).split('+')[0]} - 至今" + else: + date_str = f"{date_scope[0].replace(microsecond=0)} - {date_scope[1].replace(microsecond=0)}" + A = await ImageTemplate.table_page( + f"消息排行({count.result})", date_str, column_name, data_list + ) + logger.info( + f"查看消息排行 数量={count.result}", arparma.header_result, session=session + ) + await MessageUtils.build_message(A).finish(reply_to=True) + await MessageUtils.build_message("群组消息记录为空...").finish() + + +# # @test.handle() +# # async def _(event: MessageEvent): +# # print(await ChatHistory.get_user_msg(event.user_id, "private")) +# # print(await ChatHistory.get_user_msg_count(event.user_id, "private")) +# # print(await ChatHistory.get_user_msg(event.user_id, "group")) +# # print(await ChatHistory.get_user_msg_count(event.user_id, "group")) +# # print(await ChatHistory.get_group_msg(event.group_id)) +# # print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py new file mode 100644 index 00000000..f274c484 --- /dev/null +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -0,0 +1,101 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + AlconnaQuery, + Args, + Match, + Option, + Query, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +from ._data_source import create_help_img, get_plugin_help +from ._utils import GROUP_HELP_PATH + +__plugin_meta__ = PluginMetadata( + name="帮助", + description="帮助", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + key="type", + value="normal", + help="帮助图片样式 ['normal', 'HTML']", + default_value="normal", + ) + ], + ).dict(), +) + + +SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png" +if SIMPLE_HELP_IMAGE.exists(): + SIMPLE_HELP_IMAGE.unlink() + +_matcher = on_alconna( + Alconna( + "功能", + Args["name?", str], + Option("-s|--superuser", action=store_true, help_text="超级用户帮助"), + ), + aliases={"help", "帮助", "菜单"}, + rule=to_me(), + priority=1, + block=True, +) + + +@_matcher.handle() +async def _( + bot: Bot, + name: Match[str], + session: EventSession, + is_superuser: Query[bool] = AlconnaQuery("superuser.value", False), +): + _is_superuser = False + if is_superuser.available: + _is_superuser = is_superuser.result + if name.available: + if _is_superuser and session.id1 not in bot.config.superusers: + _is_superuser = False + if result := await get_plugin_help(name.result, _is_superuser): + if isinstance(result, BuildImage): + await MessageUtils.build_message(result).send(reply_to=True) + else: + await MessageUtils.build_message(result).send(reply_to=True) + else: + await MessageUtils.build_message("没有此功能的帮助信息...").send( + reply_to=True + ) + logger.info( + f"查看帮助详情: {name.result}", + "帮助", + session=session, + ) + else: + if gid := session.id3 or session.id2: + _image_path = GROUP_HELP_PATH / f"{gid}.png" + if not _image_path.exists(): + await create_help_img(gid) + await MessageUtils.build_message(_image_path).finish() + else: + if not SIMPLE_HELP_IMAGE.exists(): + if SIMPLE_HELP_IMAGE.exists(): + SIMPLE_HELP_IMAGE.unlink() + await create_help_img(None) + await MessageUtils.build_message(SIMPLE_HELP_IMAGE).finish() diff --git a/basic_plugins/help/_config.py b/zhenxun/builtin_plugins/help/_config.py similarity index 69% rename from basic_plugins/help/_config.py rename to zhenxun/builtin_plugins/help/_config.py index 90411d42..b38bf066 100644 --- a/basic_plugins/help/_config.py +++ b/zhenxun/builtin_plugins/help/_config.py @@ -1,4 +1,3 @@ -from typing import Optional, List, Any, Union, Dict from pydantic import BaseModel @@ -11,4 +10,4 @@ class PluginList(BaseModel): plugin_type: str icon: str logo: str - items: List[Item] + items: list[Item] diff --git a/zhenxun/builtin_plugins/help/_data_source.py b/zhenxun/builtin_plugins/help/_data_source.py new file mode 100644 index 00000000..72467b2e --- /dev/null +++ b/zhenxun/builtin_plugins/help/_data_source.py @@ -0,0 +1,54 @@ +import nonebot + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.utils.image_utils import BuildImage, ImageTemplate + +from ._utils import HelpImageBuild + +random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help" + +background = IMAGE_PATH / "background" / "0.png" + + +async def create_help_img(group_id: str | None): + """ + 说明: + 生成帮助图片 + 参数: + :param group_id: 群号 + """ + await HelpImageBuild().build_image(group_id) + + +async def get_plugin_help(name: str, is_superuser: bool) -> str | BuildImage: + """获取功能的帮助信息 + + 参数: + name: 插件名称或id + is_superuser: 是否为超级用户 + """ + if name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(name), load_status=True) + else: + plugin = await PluginInfo.get_or_none(name__iexact=name, load_status=True) + if plugin: + _plugin = nonebot.get_plugin_by_module_name(plugin.module_path) + if _plugin and _plugin.metadata: + items = None + if is_superuser: + extra = _plugin.metadata.extra + if usage := extra.get("superuser_help"): + items = { + "简介": _plugin.metadata.description, + "用法": usage, + } + else: + items = { + "简介": _plugin.metadata.description, + "用法": _plugin.metadata.usage, + } + if items: + return await ImageTemplate.hl_page(plugin.name, items) + return "糟糕! 该功能没有帮助喔..." + return "没有查找到这个功能噢..." diff --git a/basic_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py similarity index 55% rename from basic_plugins/help/_utils.py rename to zhenxun/builtin_plugins/help/_utils.py index 4a9ddad4..89588314 100644 --- a/basic_plugins/help/_utils.py +++ b/zhenxun/builtin_plugins/help/_utils.py @@ -1,20 +1,20 @@ import os import random -from typing import Dict, List, Optional +from typing import Dict -from configs.config import Config -from configs.path_config import DATA_PATH, IMAGE_PATH, TEMPLATE_PATH -from utils.decorator import Singleton -from utils.image_utils import BuildImage, build_sort_image, group_image -from utils.manager import group_manager, plugin_data_manager -from utils.manager.models import PluginData, PluginType +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH, TEMPLATE_PATH +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.image_utils import BuildImage, build_sort_image, group_image from ._config import Item GROUP_HELP_PATH = DATA_PATH / "group_help" GROUP_HELP_PATH.mkdir(exist_ok=True, parents=True) -for x in os.listdir(GROUP_HELP_PATH): - group_help_image = GROUP_HELP_PATH / x +for f in os.listdir(GROUP_HELP_PATH): + group_help_image = GROUP_HELP_PATH / f group_help_image.unlink() BACKGROUND_PATH = IMAGE_PATH / "background" / "help" / "simple_help" @@ -22,11 +22,10 @@ BACKGROUND_PATH = IMAGE_PATH / "background" / "help" / "simple_help" LOGO_PATH = TEMPLATE_PATH / "menu" / "res" / "logo" -@Singleton class HelpImageBuild: def __init__(self): - self._data: Dict[str, PluginData] = plugin_data_manager.get_data() - self._sort_data: Dict[str, List[PluginData]] = {} + self._data: list[PluginInfo] = [] + self._sort_data: Dict[str, list[PluginInfo]] = {} self._image_list = [] self.icon2str = { "normal": "fa fa-cog", @@ -42,24 +41,28 @@ class HelpImageBuild: "群内小游戏": "fa fa-gamepad", } - def sort_type(self): + async def sort_type(self): """ - 说明: - 对插件按照菜单类型分类 + 对插件按照菜单类型分类 """ - if not self._sort_data.keys(): - for key in self._data.keys(): - plugin_data = self._data[key] - if plugin_data.plugin_type == PluginType.NORMAL: - if not self._sort_data.get(plugin_data.menu_type[0]): # type: ignore - self._sort_data[plugin_data.menu_type[0]] = [] # type: ignore - self._sort_data[plugin_data.menu_type[0]].append(self._data[key]) # type: ignore + if not self._data: + self._data = await PluginInfo.filter( + plugin_type=PluginType.NORMAL, load_status=True + ) + if not self._sort_data: + for plugin in self._data: + menu_type = plugin.menu_type or "normal" + if menu_type == "normal": + menu_type = "功能" + if not self._sort_data.get(menu_type): + self._sort_data[menu_type] = [] + self._sort_data[menu_type].append(plugin) - async def build_image(self, group_id: Optional[int]): + async def build_image(self, group_id: str | None): if group_id: help_image = GROUP_HELP_PATH / f"{group_id}.png" else: - help_image = IMAGE_PATH / f"simple_help.png" + help_image = IMAGE_PATH / f"SIMPLE_HELP.png" build_type = Config.get_config("help", "TYPE") if build_type == "HTML": byt = await self.build_html_image(group_id) @@ -67,32 +70,34 @@ class HelpImageBuild: f.write(byt) else: img = await self.build_pil_image(group_id) - img.save(help_image) + await img.save(help_image) - async def build_html_image(self, group_id: Optional[int]) -> bytes: + async def build_html_image(self, group_id: str | None) -> bytes: from nonebot_plugin_htmlrender import template_to_pic - self.sort_type() + await self.sort_type() classify = {} for menu in self._sort_data: for plugin in self._sort_data[menu]: sta = 0 - if not plugin.plugin_status.status: - if group_id and plugin.plugin_status.block_type in ["all", "group"]: - sta = 2 - if not group_id and plugin.plugin_status.block_type in [ - "all", - "private", + if not plugin.status: + if group_id and plugin.block_type in [ + BlockType.ALL, + BlockType.GROUP, ]: sta = 2 - if group_id and not group_manager.get_plugin_super_status( - plugin.model, group_id + if not group_id and plugin.block_type in [ + BlockType.ALL, + BlockType.PRIVATE, + ]: + sta = 2 + if group_id and ( + group := await GroupConsole.get_or_none(group_id=group_id) ): - sta = 2 - if group_id and not group_manager.get_plugin_status( - plugin.model, group_id - ): - sta = 1 + if f"{plugin.module}:super," in group.block_plugin: + sta = 2 + if f"{plugin.module}," in group.block_plugin: + sta = 1 if classify.get(menu): classify[menu].append(Item(plugin_name=plugin.name, sta=sta)) else: @@ -132,28 +137,29 @@ class HelpImageBuild: ) return pic - async def build_pil_image(self, group_id: Optional[int]) -> BuildImage: - """ - 说明: - 构造帮助图片 + async def build_pil_image(self, group_id: str | None) -> BuildImage: + """构造帮助图片 + 参数: - :param group_id: 群号 + group_id: 群号 """ self._image_list = [] - self.sort_type() + await self.sort_type() font_size = 24 build_type = Config.get_config("help", "TYPE") - _image = BuildImage(0, 0, plain_text="1", font_size=font_size) + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) for idx, menu_type in enumerate(self._sort_data.keys()): plugin_list = self._sort_data[menu_type] - wh_list = [_image.getsize(x.name) for x in plugin_list] - wh_list.append(_image.getsize(menu_type)) + wh_list = [ + BuildImage.get_text_size(f"{x.id}.{x.name}", font) for x in plugin_list + ] + wh_list.append(BuildImage.get_text_size(menu_type, font)) # sum_height = sum([x[1] for x in wh_list]) if build_type == "VV": sum_height = 50 * len(plugin_list) + 10 else: sum_height = (font_size + 6) * len(plugin_list) + 10 - max_width = max([x[0] for x in wh_list]) + 20 + max_width = max([x[0] for x in wh_list]) + 30 bk = BuildImage( max_width + 40, sum_height + 50, @@ -170,90 +176,73 @@ class HelpImageBuild: color="white" if not idx % 2 else "black", ) curr_h = 10 - for i, plugin_data in enumerate(plugin_list): + group = await GroupConsole.get_or_none(group_id=group_id) + for i, plugin in enumerate(plugin_list): text_color = (255, 255, 255) if idx % 2 else (0, 0, 0) - if group_id and not group_manager.get_plugin_status( - plugin_data.model, group_id - ): + if group and f"{plugin.module}," in group.block_plugin: text_color = (252, 75, 13) pos = None # 禁用状态划线 - if ( - not plugin_data.plugin_status.status - and plugin_data.plugin_status.block_type in ["group", "all"] - ) or ( - group_id - and not group_manager.get_plugin_super_status( - plugin_data.model, group_id - ) + if plugin.block_type in [BlockType.ALL, BlockType.GROUP] or ( + group and f"super:{plugin.module}," in group.block_plugin ): - w = curr_h + int(B.getsize(plugin_data.name)[1] / 2) + 2 + w = curr_h + int(B.getsize(plugin.name)[1] / 2) + 2 pos = ( 7, w, - B.getsize(plugin_data.name)[0] + 35, + B.getsize(plugin.name)[0] + 35, w, ) if build_type == "VV": name_image = await self.build_name_image( # type: ignore max_width, - plugin_data.name, + plugin.name, "black" if not idx % 2 else "white", text_color, pos, ) - await B.apaste( - name_image, (0, curr_h), True, center_type="by_width" - ) + await B.paste(name_image, (0, curr_h), center_type="width") curr_h += name_image.h + 5 else: - await B.atext( - (10, curr_h), f"{i + 1}.{plugin_data.name}", text_color - ) + await B.text((10, curr_h), f"{plugin.id}.{plugin.name}", text_color) if pos: - await B.aline(pos, (236, 66, 7), 3) + await B.line(pos, (236, 66, 7), 3) curr_h += font_size + 5 - if menu_type == "normal": - menu_type = "功能" - await bk.atext((0, 14), menu_type, center_type="by_width") - await bk.apaste(B, (0, 50)) - await bk.atransparent(2) + await bk.text((0, 14), menu_type, center_type="width") + await bk.paste(B, (0, 50)) + await bk.transparent(2) # await bk.acircle_corner(point_list=['lt', 'rt']) self._image_list.append(bk) image_group, h = group_image(self._image_list) + + async def _a(image: BuildImage): + await image.filter("GaussianBlur", 5) + B = await build_sort_image( image_group, h, background_path=BACKGROUND_PATH, - background_handle=lambda image: image.filter("GaussianBlur", 5), + background_handle=_a, ) w = 10 h = 10 for msg in [ "目前支持的功能列表:", - "可以通过 ‘帮助[功能名称]’ 来获取对应功能的使用方法", + "可以通过 ‘帮助 [功能名称或功能Id]’ 来获取对应功能的使用方法", ]: - text = BuildImage( - 0, - 0, - plain_text=msg, - font_size=24, - font="HYWenHei-85W.ttf", - ) - B.paste(text, (w, h), True) + text = await BuildImage.build_text_image(msg, "HYWenHei-85W.ttf", 24) + await B.paste(text, (w, h)) h += 50 if msg == "目前支持的功能列表:": w += 50 - await B.apaste( - BuildImage( - 0, - 0, - plain_text="注: 红字代表功能被群管理员禁用,红线代表功能正在维护", - font_size=24, - font="HYWenHei-85W.ttf", - font_color=(231, 74, 57), - ), + text = await BuildImage.build_text_image( + "注: 红字代表功能被群管理员禁用,红线代表功能正在维护", + "HYWenHei-85W.ttf", + 24, + (231, 74, 57), + ) + await B.paste( + text, (300, 10), - True, ) return B diff --git a/zhenxun/builtin_plugins/help_help.py b/zhenxun/builtin_plugins/help_help.py new file mode 100644 index 00000000..b2682aa4 --- /dev/null +++ b/zhenxun/builtin_plugins/help_help.py @@ -0,0 +1,67 @@ +import os +import random + +from nonebot import on_message +from nonebot.matcher import Matcher +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.services.log import logger +from zhenxun.utils._build_image import BuildImage +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="功能名称当命令检测", + description="功能名称当命令检测", + usage=f"""被动""".strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + ).dict(), +) + +_matcher = on_message(rule=to_me(), priority=996, block=False) + + +_path = IMAGE_PATH / "_base" / "laugh" + + +@_matcher.handle() +async def _(matcher: Matcher, message: UniMsg, session: EventSession): + gid = session.id3 or session.id2 + if await BanConsole.is_ban(session.id1, gid): + return + if gid: + if await BanConsole.is_ban(None, gid): + return + if g := await GroupConsole.get_group(gid): + if g.level < 0: + return + if text := message.extract_plain_text().strip(): + if plugin := await PluginInfo.get_or_none( + name=text, load_status=True, plugin_type=PluginType.NORMAL + ): + image = None + if _path.exists(): + if files := os.listdir(_path): + image = _path / random.choice(files) + message_list = [] + if image: + message_list.append(image) + message_list.append( + f"桀桀桀,预判到会有 '笨蛋' 把功能名称当命令用,特地前来嘲笑!但还是好心来帮帮你啦!\n请at我发送 '帮助{plugin.name}' 或者 '帮助{plugin.id}' 来获取该功能帮助!" + ) + logger.info( + f"检测到功能名称当命令使用,已发送帮助信息", "功能帮助", session=session + ) + await MessageUtils.build_message(message_list).send(reply_to=True) + matcher.stop_propagation() diff --git a/zhenxun/builtin_plugins/hooks/__init__.py b/zhenxun/builtin_plugins/hooks/__init__.py new file mode 100644 index 00000000..80aa7181 --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/__init__.py @@ -0,0 +1,43 @@ +from pathlib import Path + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "hook", + "CHECK_NOTICE_INFO_CD", + 300, + help="群检测,个人权限检测等各种检测提示信息cd", + default_value=300, + type=int, +) + +Config.add_plugin_config( + "hook", + "MALICIOUS_BAN_TIME", + 30, + help="恶意命令触发检测触发后ban的时长(分钟)", + default_value=30, + type=int, +) + +Config.add_plugin_config( + "hook", + "MALICIOUS_CHECK_TIME", + 5, + help="恶意命令触发检测规定时间内(秒)", + default_value=5, + type=int, +) + +Config.add_plugin_config( + "hook", + "MALICIOUS_BAN_COUNT", + 6, + help="恶意命令触发检测最大触发次数", + default_value=6, + type=int, +) + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py new file mode 100644 index 00000000..8a277165 --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -0,0 +1,489 @@ +from nonebot.adapters import Bot +from nonebot.exception import IgnoredException +from nonebot.matcher import Matcher +from nonebot_plugin_alconna import At, UniMsg +from nonebot_plugin_session import EventSession +from pydantic import BaseModel +from tortoise.exceptions import IntegrityError + +from zhenxun.configs.config import Config +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.level_user import LevelUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.plugin_limit import PluginLimit +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import ( + BlockType, + GoldHandle, + LimitWatchType, + PluginLimitType, + PluginType, +) +from zhenxun.utils.exception import InsufficientGold +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import CountLimiter, FreqLimiter, UserBlockLimiter + + +class Limit(BaseModel): + + limit: PluginLimit + limiter: FreqLimiter | UserBlockLimiter | CountLimiter + + class Config: + arbitrary_types_allowed = True + + +class LimitManage: + + add_module = [] + + cd_limit: dict[str, Limit] = {} + block_limit: dict[str, Limit] = {} + count_limit: dict[str, Limit] = {} + + @classmethod + def add_limit(cls, limit: PluginLimit): + """添加限制 + + 参数: + limit: PluginLimit + """ + if limit.module not in cls.add_module: + cls.add_module.append(limit.module) + if limit.limit_type == PluginLimitType.BLOCK: + cls.block_limit[limit.module] = Limit( + limit=limit, limiter=UserBlockLimiter() + ) + elif limit.limit_type == PluginLimitType.CD: + cls.cd_limit[limit.module] = Limit( + limit=limit, limiter=FreqLimiter(limit.cd) + ) + elif limit.limit_type == PluginLimitType.COUNT: + cls.count_limit[limit.module] = Limit( + limit=limit, limiter=CountLimiter(limit.max_count) + ) + + @classmethod + def unblock( + cls, module: str, user_id: str, group_id: str | None, channel_id: str | None + ): + """解除插件block + + 参数: + module: 模块名 + user_id: 用户id + group_id: 群组id + channel_id: 频道id + """ + if limit_model := cls.block_limit.get(module): + limit = limit_model.limit + limiter: UserBlockLimiter = limit_model.limiter # type: ignore + key_type = user_id + if group_id and limit.watch_type == LimitWatchType.GROUP: + key_type = channel_id or group_id + limiter.set_false(key_type) + + @classmethod + async def check( + cls, + module: str, + user_id: str, + group_id: str | None, + channel_id: str | None, + session: EventSession, + ): + """检测限制 + + 参数: + module: 模块名 + user_id: 用户id + group_id: 群组id + channel_id: 频道id + session: Session + + 异常: + IgnoredException: IgnoredException + """ + if limit_model := cls.cd_limit.get(module): + await cls.__check(limit_model, user_id, group_id, channel_id, session) + if limit_model := cls.block_limit.get(module): + await cls.__check(limit_model, user_id, group_id, channel_id, session) + if limit_model := cls.count_limit.get(module): + await cls.__check(limit_model, user_id, group_id, channel_id, session) + + @classmethod + async def __check( + cls, + limit_model: Limit, + user_id: str, + group_id: str | None, + channel_id: str | None, + session: EventSession, + ): + """检测限制 + + 参数: + limit_model: Limit + user_id: 用户id + group_id: 群组id + channel_id: 频道id + session: Session + + 异常: + IgnoredException: IgnoredException + """ + if limit_model: + limit = limit_model.limit + limiter = limit_model.limiter + is_limit = ( + LimitWatchType.ALL + or (group_id and limit.watch_type == LimitWatchType.GROUP) + or (not group_id and limit.watch_type == LimitWatchType.USER) + ) + key_type = user_id + if group_id and limit.watch_type == LimitWatchType.GROUP: + key_type = channel_id or group_id + if is_limit and not limiter.check(key_type): + if limit.result: + await MessageUtils.build_message(limit.result).send() + logger.debug( + f"{limit.module}({limit.limit_type}) 正在限制中...", + "HOOK", + session=session, + ) + raise IgnoredException(f"{limit.module} 正在限制中...") + else: + if isinstance(limiter, FreqLimiter): + limiter.start_cd(key_type) + if isinstance(limiter, UserBlockLimiter): + limiter.set_true(key_type) + if isinstance(limiter, CountLimiter): + limiter.increase(key_type) + + +class IsSuperuserException(Exception): + pass + + +class AuthChecker: + """ + 权限检查 + """ + + def __init__(self): + check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD") + if check_notice_info_cd is None or check_notice_info_cd < 0: + raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0") + self._flmt = FreqLimiter(check_notice_info_cd) + self._flmt_g = FreqLimiter(check_notice_info_cd) + self._flmt_s = FreqLimiter(check_notice_info_cd) + self._flmt_c = FreqLimiter(check_notice_info_cd) + + async def auth( + self, + matcher: Matcher, + bot: Bot, + session: EventSession, + message: UniMsg, + ): + """权限检查 + + 参数: + matcher: matcher + bot: bot + session: EventSession + message: UniMsg + """ + is_ignore = False + cost_gold = 0 + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + if user_id and matcher.plugin and (module_path := matcher.plugin.module_name): + try: + user = await UserConsole.get_user(user_id, session.platform) + except IntegrityError as e: + logger.debug( + "重复创建用户,已跳过全选该次权限...", "HOOK", session=session, e=e + ) + return + if plugin := await PluginInfo.get_or_none(module_path=module_path): + if plugin.plugin_type == PluginType.HIDDEN and plugin.name != "帮助": + logger.debug("插件为HIDDEN且不是帮助功能,已跳过...") + return + try: + cost_gold = await self.auth_cost(user, plugin, session) + if session.id1 in bot.config.superusers: + if plugin.plugin_type == PluginType.SUPERUSER: + raise IsSuperuserException() + if not plugin.limit_superuser: + cost_gold = 0 + raise IsSuperuserException() + await self.auth_group(plugin, session, message) + await self.auth_admin(plugin, session) + await self.auth_plugin(plugin, session) + await self.auth_limit(plugin, session) + except IsSuperuserException: + logger.debug( + f"超级用户或被ban跳过权限检测...", "HOOK", session=session + ) + except IgnoredException: + is_ignore = True + LimitManage.unblock( + matcher.plugin.name, user_id, group_id, channel_id + ) + if cost_gold and user_id: + """花费金币""" + try: + await UserConsole.reduce_gold( + user_id, + cost_gold, + GoldHandle.PLUGIN, + matcher.plugin.name if matcher.plugin else "", + session.platform, + ) + except InsufficientGold: + if u := await UserConsole.get_user(user_id): + u.gold = 0 + await u.save(update_fields=["gold"]) + logger.debug(f"调用功能花费金币: {cost_gold}", "HOOK", session=session) + if is_ignore: + raise IgnoredException("权限检测 ignore") + + async def auth_limit(self, plugin: PluginInfo, session: EventSession): + """插件限制 + + 参数: + plugin: PluginInfo + session: EventSession + """ + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + limit_list: list[PluginLimit] = await plugin.plugin_limit.all() # type: ignore + for limit in limit_list: + LimitManage.add_limit(limit) + if user_id: + await LimitManage.check( + plugin.module, user_id, group_id, channel_id, session + ) + + async def auth_plugin(self, plugin: PluginInfo, session: EventSession): + """插件状态 + + 参数: + plugin: PluginInfo + session: EventSession + """ + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + if user_id: + if group_id: + if await GroupConsole.is_super_block_plugin( + group_id, plugin.module, channel_id + ): + """超级用户群组插件状态""" + if self._flmt_s.check(group_id or user_id): + self._flmt_s.start_cd(group_id or user_id) + await MessageUtils.build_message( + "超级管理员禁用了该群此功能..." + ).send(reply_to=True) + logger.debug( + f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...", + "HOOK", + session=session, + ) + raise IgnoredException("超级管理员禁用了该群此功能...") + if await GroupConsole.is_block_plugin( + group_id, plugin.module, channel_id + ): + """群组插件状态""" + if self._flmt_s.check(group_id or user_id): + self._flmt_s.start_cd(group_id or user_id) + await MessageUtils.build_message("该群未开启此功能...").send( + reply_to=True + ) + logger.debug( + f"{plugin.name}({plugin.module}) 未开启此功能...", + "HOOK", + session=session, + ) + raise IgnoredException("该群未开启此功能...") + if not plugin.status and plugin.block_type == BlockType.GROUP: + """全局群组禁用""" + try: + if self._flmt_c.check(group_id): + self._flmt_c.start_cd(group_id) + await MessageUtils.build_message( + "该功能在群组中已被禁用..." + ).send(reply_to=True) + except Exception as e: + logger.error( + "auth_plugin 发送消息失败", "HOOK", session=session, e=e + ) + logger.debug( + f"{plugin.name}({plugin.module}) 该插件在群组中已被禁用...", + "HOOK", + session=session, + ) + raise IgnoredException("该插件在群组中已被禁用...") + else: + if not plugin.status and plugin.block_type == BlockType.PRIVATE: + """全局私聊禁用""" + try: + if self._flmt_c.check(user_id): + self._flmt_c.start_cd(user_id) + await MessageUtils.build_message( + "该功能在私聊中已被禁用..." + ).send() + except Exception as e: + logger.error( + "auth_admin 发送消息失败", "HOOK", session=session, e=e + ) + logger.debug( + f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用...", + "HOOK", + session=session, + ) + raise IgnoredException("该插件在私聊中已被禁用...") + if not plugin.status and plugin.block_type == BlockType.ALL: + """全局状态""" + if group_id: + if await GroupConsole.is_super_group(group_id, channel_id): + raise IsSuperuserException() + if self._flmt_s.check(group_id or user_id): + self._flmt_s.start_cd(group_id or user_id) + await MessageUtils.build_message("全局未开启此功能...").send() + logger.debug( + f"{plugin.name}({plugin.module}) 全局未开启此功能...", + "HOOK", + session=session, + ) + raise IgnoredException("全局未开启此功能...") + + async def auth_admin(self, plugin: PluginInfo, session: EventSession): + """管理员命令 个人权限 + + 参数: + plugin: PluginInfo + session: EventSession + """ + user_id = session.id1 + group_id = session.id3 or session.id2 + if user_id and plugin.admin_level: + if group_id: + if not await LevelUser.check_level( + user_id, group_id, plugin.admin_level + ): + try: + if self._flmt.check(user_id): + self._flmt.start_cd(user_id) + await MessageUtils.build_message( + [ + At(flag="user", target=user_id), + f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}", + ] + ).send(reply_to=True) + except Exception as e: + logger.error( + "auth_admin 发送消息失败", "HOOK", session=session, e=e + ) + logger.debug( + f"{plugin.name}({plugin.module}) 管理员权限不足...", + "HOOK", + session=session, + ) + raise IgnoredException("管理员权限不足...") + else: + if not await LevelUser.check_level(user_id, None, plugin.admin_level): + try: + await MessageUtils.build_message( + f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" + ).send() + except Exception as e: + logger.error( + "auth_admin 发送消息失败", "HOOK", session=session, e=e + ) + logger.debug( + f"{plugin.name}({plugin.module}) 管理员权限不足...", + "HOOK", + session=session, + ) + raise IgnoredException("权限不足") + + async def auth_group( + self, plugin: PluginInfo, session: EventSession, message: UniMsg + ): + """群黑名单检测 群总开关检测 + + 参数: + plugin: PluginInfo + session: EventSession + message: UniMsg + """ + if group_id := session.id3 or session.id2: + text = message.extract_plain_text() + group = await GroupConsole.get_group(group_id) + if not group: + """群不存在""" + raise IgnoredException("群不存在") + if group.level < 0: + """群权限小于0""" + logger.debug( + f"群黑名单, 群权限-1...", + "HOOK", + session=session, + ) + raise IgnoredException("群黑名单") + if not group.status: + """群休眠""" + if text.strip() != "醒来": + logger.debug( + f"功能总开关关闭状态...", + "HOOK", + session=session, + ) + raise IgnoredException("功能总开关关闭状态") + + async def auth_cost( + self, user: UserConsole, plugin: PluginInfo, session: EventSession + ) -> int: + """检测是否满足金币条件 + + 参数: + user: UserConsole + plugin: PluginInfo + session: EventSession + + 返回: + int: 需要消耗的金币 + """ + if user.gold < plugin.cost_gold: + """插件消耗金币不足""" + try: + await MessageUtils.build_message( + f"金币不足..该功能需要{plugin.cost_gold}金币.." + ).send() + except Exception as e: + logger.error("auth_cost 发送消息失败", "HOOK", session=session, e=e) + logger.debug( + f"{plugin.name}({plugin.module}) 金币限制..该功能需要{plugin.cost_gold}金币..", + "HOOK", + session=session, + ) + raise IgnoredException(f"{plugin.name}({plugin.module}) 金币限制...") + return plugin.cost_gold + + +checker = AuthChecker() diff --git a/zhenxun/builtin_plugins/hooks/auth_hook.py b/zhenxun/builtin_plugins/hooks/auth_hook.py new file mode 100644 index 00000000..938f8222 --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/auth_hook.py @@ -0,0 +1,35 @@ +from typing import Optional + +from nonebot.adapters.onebot.v11 import Bot, Event, MessageEvent +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor, run_preprocessor +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from ._auth_checker import LimitManage, checker + + +# # 权限检测 +@run_preprocessor +async def _(matcher: Matcher, bot: Bot, session: EventSession, message: UniMsg): + await checker.auth(matcher, bot, session, message) + + +# 解除命令block阻塞 +@run_postprocessor +async def _( + matcher: Matcher, + exception: Optional[Exception], + bot: Bot, + event: Event, + session: EventSession, +): + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + if user_id and matcher.plugin: + module = matcher.plugin.name + LimitManage.unblock(module, user_id, group_id, channel_id) diff --git a/zhenxun/builtin_plugins/hooks/ban_hook.py b/zhenxun/builtin_plugins/hooks/ban_hook.py new file mode 100644 index 00000000..b4923eff --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/ban_hook.py @@ -0,0 +1,70 @@ +from nonebot.adapters import Bot, Event +from nonebot.exception import IgnoredException +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.typing import T_State +from nonebot_plugin_alconna import At +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.models.ban_console import BanConsole +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import FreqLimiter + +Config.add_plugin_config( + "hook", + "BAN_RESULT", + "才不会给你发消息.", + help="对被ban用户发送的消息", +) + +_flmt = FreqLimiter(300) + + +# 检查是否被ban +@run_preprocessor +async def _( + matcher: Matcher, bot: Bot, event: Event, state: T_State, session: EventSession +): + if plugin := matcher.plugin: + if metadata := plugin.metadata: + extra = metadata.extra + if extra.get("plugin_type") == PluginType.HIDDEN: + return + user_id = session.id1 + group_id = session.id3 or session.id2 + if group_id: + if user_id in bot.config.superusers: + return + if await BanConsole.is_ban(None, group_id): + raise IgnoredException("群组处于黑名单中...") + if user_id: + ban_result = Config.get_config("hook", "BAN_RESULT") + if user_id in bot.config.superusers: + return + if await BanConsole.is_ban(user_id, group_id): + time = await BanConsole.check_ban_time(user_id, group_id) + if time == -1: + time_str = "∞" + else: + time = abs(int(time)) + if time < 60: + time_str = str(time) + " 秒" + else: + minute = int(time / 60) + if minute > 60: + hours = int(minute / 60) + minute = minute % 60 + time_str = f"{hours} 小时 {minute}分钟" + else: + time_str = f"{minute} 分钟" + if ban_result and _flmt.check(user_id): + _flmt.start_cd(user_id) + await MessageUtils.build_message( + [ + At(flag="user", target=user_id), + f"{ban_result}\n在..在 {time_str} 后才会理你喔", + ] + ).send() + raise IgnoredException("用户处于黑名单中...") diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py new file mode 100644 index 00000000..dd42e3ea --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -0,0 +1,102 @@ +import time +from collections import defaultdict + +from nonebot.adapters.onebot.v11 import Bot +from nonebot.exception import IgnoredException +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.typing import T_State +from nonebot_plugin_alconna import At +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.models.ban_console import BanConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +malicious_check_time = Config.get_config("hook", "MALICIOUS_CHECK_TIME") +malicious_ban_count = Config.get_config("hook", "MALICIOUS_BAN_COUNT") + +if not malicious_check_time: + raise ValueError("模块: [hook], 配置项: [MALICIOUS_CHECK_TIME] 为空或小于0") +if not malicious_ban_count: + raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_COUNT] 为空或小于0") + + +class BanCheckLimiter: + """ + 恶意命令触发检测 + """ + + def __init__(self, default_check_time: float = 5, default_count: int = 4): + self.mint = defaultdict(int) + self.mtime = defaultdict(float) + self.default_check_time = default_check_time + self.default_count = default_count + + def add(self, key: str | int | float): + if self.mint[key] == 1: + self.mtime[key] = time.time() + self.mint[key] += 1 + + def check(self, key: str | int | float) -> bool: + if time.time() - self.mtime[key] > self.default_check_time: + self.mtime[key] = time.time() + self.mint[key] = 0 + return False + if ( + self.mint[key] >= self.default_count + and time.time() - self.mtime[key] < self.default_check_time + ): + self.mtime[key] = time.time() + self.mint[key] = 0 + return True + return False + + +_blmt = BanCheckLimiter( + malicious_check_time, + malicious_ban_count, +) + + +# 恶意触发命令检测 +@run_preprocessor +async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): + module = None + if plugin := matcher.plugin: + module = plugin.module_name + if metadata := plugin.metadata: + extra = metadata.extra + if extra.get("plugin_type") == PluginType.HIDDEN: + return + user_id = session.id1 + group_id = session.id3 or session.id2 + malicious_ban_time = Config.get_config("hook", "MALICIOUS_BAN_TIME") + if not malicious_ban_time: + raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0") + if user_id: + if module: + if _blmt.check(f"{user_id}__{module}"): + await BanConsole.ban( + user_id, group_id, 9, malicious_ban_time * 60, bot.self_id + ) + logger.info( + f"触发了恶意触发检测: {matcher.plugin_name}", + "HOOK", + session=session, + ) + await MessageUtils.build_message( + [ + At(flag="user", target=user_id), + f"检测到恶意触发命令,您将被封禁 30 分钟", + ] + ).send() + logger.debug( + f"触发了恶意触发检测: {matcher.plugin_name}", + "HOOK", + session=session, + ) + raise IgnoredException("检测到恶意触发命令") + _blmt.add(f"{user_id}__{module}") diff --git a/zhenxun/builtin_plugins/hooks/withdraw_hook.py b/zhenxun/builtin_plugins/hooks/withdraw_hook.py new file mode 100644 index 00000000..3cb4aadb --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/withdraw_hook.py @@ -0,0 +1,30 @@ +import asyncio + +from nonebot.adapters import Bot +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor + +from zhenxun.utils.withdraw_manage import WithdrawManager + + +@run_postprocessor +async def _( + matcher: Matcher, + exception: Exception | None, + bot: Bot, +): + tasks = [] + index_list = list(WithdrawManager._data.keys()) + for index in index_list: + ( + bot, + message_id, + time, + ) = WithdrawManager._data[index] + tasks.append( + asyncio.ensure_future( + WithdrawManager.withdraw_message(bot, message_id, time) + ) + ) + WithdrawManager.remove(index) + await asyncio.gather(*tasks) diff --git a/zhenxun/builtin_plugins/init/__init__.py b/zhenxun/builtin_plugins/init/__init__.py new file mode 100644 index 00000000..fa10c4d7 --- /dev/null +++ b/zhenxun/builtin_plugins/init/__init__.py @@ -0,0 +1,41 @@ +from pathlib import Path + +import nonebot +from nonebot.adapters import Bot + +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) + + +driver = nonebot.get_driver() + + +@driver.on_bot_connect +async def _(bot: Bot): + """将bot已存在的群组添加群认证 + + 参数: + bot: Bot + """ + if PlatformUtils.get_platform(bot) == "qq": + logger.debug(f"更新Bot: {bot.self_id} 的群认证...") + group_list, _ = await PlatformUtils.get_group_list(bot) + gid_list = [g.group_id for g in group_list] + db_group_list = await GroupConsole.all().values_list("group_id", flat=True) + create_list = [] + update_id = [] + for gid in gid_list: + if gid not in db_group_list: + create_list.append(GroupConsole(group_id=gid, group_flag=1)) + else: + update_id.append(gid) + if create_list: + await GroupConsole.bulk_create(create_list, 10) + else: + await GroupConsole.filter(group_id__in=update_id).update(group_flag=1) + logger.debug( + f"更新Bot: {bot.self_id} 的群认证完成,共创建 {len(create_list)} 条数据,共修改 {len(update_id)} 条数据..." + ) diff --git a/zhenxun/builtin_plugins/init/init_config.py b/zhenxun/builtin_plugins/init/init_config.py new file mode 100644 index 00000000..26534d4d --- /dev/null +++ b/zhenxun/builtin_plugins/init/init_config.py @@ -0,0 +1,122 @@ +from pathlib import Path + +import nonebot +from nonebot import get_loaded_plugins +from nonebot.drivers import Driver +from nonebot.plugin import Plugin +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import RegisterConfig +from zhenxun.services.log import logger + +_yaml = YAML(pure=True) +_yaml.allow_unicode = True +_yaml.indent = 2 + +driver: Driver = nonebot.get_driver() + +SIMPLE_CONFIG_FILE = DATA_PATH / "config.yaml" + +old_config_file = Path() / "zhenxun" / "configs" / "config.yaml" +if old_config_file.exists(): + old_config_file.rename(SIMPLE_CONFIG_FILE) + + +def _handle_config(plugin: Plugin): + """处理配置项 + + 参数: + plugin: Plugin + """ + if plugin.metadata and plugin.metadata.extra: + extra = plugin.metadata.extra + if configs := extra.get("configs"): + for config in configs: + reg_config = RegisterConfig(**config) + module = reg_config.module or plugin.name + g_config = Config.get(module) + g_config.name = plugin.metadata.name + Config.add_plugin_config( + module, + reg_config.key, + reg_config.value, + help=reg_config.help, + default_value=reg_config.default_value, + type=reg_config.type, + arg_parser=reg_config.arg_parser, + _override=False, + ) + + +def _generate_simple_config(): + """ + 生成简易配置 + + 异常: + AttributeError: _description_ + """ + # 读取用户配置 + _data = {} + _tmp_data = {} + if SIMPLE_CONFIG_FILE.exists(): + _data = _yaml.load(SIMPLE_CONFIG_FILE.open(encoding="utf8")) + # 将简易配置文件的数据填充到配置文件 + for module in Config.keys(): + _tmp_data[module] = {} + for k in Config[module].configs.keys(): + try: + if _data.get(module) and k in _data[module].keys(): + Config.set_config(module, k, _data[module][k]) + _tmp_data[module][k] = Config.get_config(module, k) + except AttributeError as e: + raise AttributeError(f"{e}\n" + "可能为config.yaml配置文件填写不规范") + Config.save() + temp_file = DATA_PATH / "temp_config.yaml" + # 重新生成简易配置文件 + try: + with open(temp_file, "w", encoding="utf8") as wf: + # yaml.dump(_tmp_data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True) + _yaml.dump(_tmp_data, wf) + with open(temp_file, "r", encoding="utf8") as rf: + _data = _yaml.load(rf) + # 添加注释 + for module in _data.keys(): + help_text = "" + plugin_name = Config.get(module).name or module + help_text += plugin_name + "\n" + for k in _data[module].keys(): + help_text += f"{k}: {Config[module].configs[k].help}" + "\n" + _data.yaml_set_comment_before_after_key(after=help_text[:-1], key=module) + with SIMPLE_CONFIG_FILE.open("w", encoding="utf8") as wf: + _yaml.dump(_data, wf) + except Exception as e: + logger.error(f"生成简易配置注释错误...", e=e) + if temp_file.exists(): + temp_file.unlink() + + +@driver.on_startup +def _(): + """ + 初始化插件数据配置 + """ + plugins2config_file = DATA_PATH / "configs" / "plugins2config.yaml" + for plugin in get_loaded_plugins(): + if plugin.metadata: + _handle_config(plugin) + if not Config.is_empty(): + Config.save() + _data: CommentedMap = _yaml.load(plugins2config_file.open(encoding="utf8")) + for module in _data.keys(): + plugin_name = Config.get(module).name + _data.yaml_set_comment_before_after_key( + after=f"{plugin_name}", + key=module, + ) + # 存完插件基本设置 + with plugins2config_file.open("w", encoding="utf8") as wf: + _yaml.dump(_data, wf) + _generate_simple_config() diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py new file mode 100644 index 00000000..49ec8dfb --- /dev/null +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -0,0 +1,415 @@ +import nonebot +import ujson as json +from nonebot import get_loaded_plugins +from nonebot.drivers import Driver +from nonebot.plugin import Plugin +from ruamel.yaml import YAML + +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import PluginExtraData, PluginSetting +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.plugin_limit import PluginLimit +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import ( + BlockType, + LimitCheckType, + LimitWatchType, + PluginLimitType, + PluginType, +) + +_yaml = YAML(pure=True) +_yaml.allow_unicode = True +_yaml.indent = 2 + +driver: Driver = nonebot.get_driver() + + +async def _handle_setting( + plugin: Plugin, + plugin_list: list[PluginInfo], + limit_list: list[PluginLimit], + task_list: list[TaskInfo], +): + """处理插件设置 + + 参数: + plugin: Plugin + plugin_list: 插件列表 + limit_list: 插件限制列表 + """ + metadata = plugin.metadata + if metadata: + extra = metadata.extra + extra_data = PluginExtraData(**extra) + logger.debug(f"{metadata.name}:{plugin.name} -> {extra}", "初始化插件数据") + setting = extra_data.setting or PluginSetting() + if metadata.type == "library": + extra_data.plugin_type = PluginType.HIDDEN + extra_data.menu_type = "" + plugin_list.append( + PluginInfo( + module=plugin.name, + module_path=plugin.module_name, + name=metadata.name, + author=extra_data.author, + version=extra_data.version, + level=setting.level, + default_status=setting.default_status, + limit_superuser=setting.limit_superuser, + menu_type=extra_data.menu_type, + cost_gold=setting.cost_gold, + plugin_type=extra_data.plugin_type, + admin_level=extra_data.admin_level, + ) + ) + if extra_data.limits: + for limit in extra_data.limits: + limit_list.append( + PluginLimit( + module=plugin.name, + module_path=plugin.module_name, + limit_type=limit._type, + watch_type=limit.watch_type, + status=limit.status, + check_type=limit.check_type, + result=limit.result, + cd=getattr(limit, "cd", None), + max_count=getattr(limit, "max_count", None), + ) + ) + if extra_data.tasks: + for task in extra_data.tasks: + task_list.append( + TaskInfo( + module=task.module, + name=task.name, + status=task.status, + run_time=task.run_time, + ) + ) + + +@driver.on_startup +async def _(): + """ + 初始化插件数据配置 + """ + plugin_list: list[PluginInfo] = [] + limit_list: list[PluginLimit] = [] + task_list: list[TaskInfo] = [] + module2id = {} + load_plugin = [] + if module_list := await PluginInfo.all().values("id", "module_path"): + module2id = {m["module_path"]: m["id"] for m in module_list} + for plugin in get_loaded_plugins(): + load_plugin.append(plugin.module_name) + if plugin.metadata: + await _handle_setting(plugin, plugin_list, limit_list, task_list) + create_list = [] + update_list = [] + for plugin in plugin_list: + if plugin.module_path not in module2id: + create_list.append(plugin) + else: + plugin.id = module2id[plugin.module_path] + update_list.append(plugin) + if create_list: + await PluginInfo.bulk_create(create_list, 10) + if update_list: + await PluginInfo.bulk_update( + update_list, + ["name", "author", "version", "admin_level"], + 10, + ) + if limit_list: + limit_create = [] + plugins = [] + if module_path_list := [limit.module_path for limit in limit_list]: + plugins = await PluginInfo.filter(module_path__in=module_path_list).all() + if plugins: + for limit in limit_list: + if l := [p for p in plugins if p.module_path == limit.module_path]: + plugin = l[0] + limit_type_list = [ + _limit.limit_type for _limit in await plugin.plugin_limit.all() # type: ignore + ] + if limit.limit_type not in limit_type_list: + limit.plugin = plugin + limit_create.append(limit) + if limit_create: + await PluginLimit.bulk_create(limit_create, 10) + if task_list: + module_dict = { + t[1]: t[0] for t in await TaskInfo.all().values_list("id", "module") + } + create_list = [] + update_list = [] + for task in task_list: + if task.module not in module_dict: + create_list.append(task) + else: + task.id = module_dict[task.module] + update_list.append(task) + if create_list: + await TaskInfo.bulk_create(create_list, 10) + if update_list: + await TaskInfo.bulk_update( + update_list, + ["run_time", "status", "name"], + 10, + ) + await data_migration() + await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True) + await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False) + + +async def data_migration(): + await limit_migration() + await plugin_migration() + await group_migration() + + +async def limit_migration(): + """插件限制迁移""" + cd_file = DATA_PATH / "configs" / "plugins2cd.yaml" + block_file = DATA_PATH / "configs" / "plugins2block.yaml" + count_file = DATA_PATH / "configs" / "plugins2count.yaml" + limit_data: dict[str, list[tuple[str, dict]]] = {} + if cd_file.exists(): + with open(cd_file, encoding="utf8") as f: + if data := _yaml.load(f): + for k in data["PluginCdLimit"]: + limit_data[k] = [("CD", data["PluginCdLimit"][k])] + cd_file.unlink() + if block_file.exists(): + with open(block_file, encoding="utf8") as f: + if data := _yaml.load(f): + for k in data["PluginBlockLimit"]: + if k in limit_data: + limit_data[k].append(("BLOCK", data["PluginBlockLimit"][k])) + else: + limit_data[k] = [("BLOCK", data["PluginBlockLimit"][k])] + block_file.unlink() + if count_file.exists(): + with open(count_file, encoding="utf8") as f: + if data := _yaml.load(f): + for k in data["PluginCountLimit"]: + if k in limit_data: + limit_data[k].append(("COUNT", data["PluginCountLimit"][k])) + else: + limit_data[k] = [("COUNT", data["PluginCountLimit"][k])] + count_file.unlink() + if limit_data: + logger.info("开始迁移插件限制数据...") + update_list = [] + create_list = [] + plugins = await PluginInfo.filter(module__in=limit_data.keys()) + for plugin in plugins: + limits: list[PluginLimit] = await plugin.plugin_limit.all() # type: ignore + exits_limit = [x[0] for x in limit_data[plugin.module]] + _not_create_type = [] + for limit in limits: + if _limit_list := [ + x[1] + for x in limit_data[plugin.module] + if x[0] == str(limit.limit_type) + ]: + """修改""" + _not_create_type.append(str(limit.limit_type)) + _limit = _limit_list[0] + watch_type = LimitWatchType.USER + if _limit.get("watch_type") == "group": + watch_type = LimitWatchType.GROUP + check_type = LimitCheckType.ALL + if _limit.get("check_type") == "private": + check_type = LimitCheckType.PRIVATE + elif _limit.get("check_type") == "group": + check_type = LimitCheckType.GROUP + limit.watch_type = watch_type + limit.result = _limit.get("rst", "") + limit.status = _limit.get("status", True) + if limit.watch_type != PluginLimitType.COUNT: + limit.check_type = check_type + if limit.watch_type == PluginLimitType.CD: + limit.cd = _limit["cd"] + if limit.watch_type == PluginLimitType.COUNT: + limit.max_count = _limit["count"] + await limit.save() + update_list.append(limit) + for s in [e for e in exits_limit if e not in _not_create_type]: + if _limit_list := [ + x[1] for x in limit_data[plugin.module] if s == x[0] + ]: + _limit = _limit_list[0] + limit_type = PluginLimitType.CD + if s == "BLOCK": + limit_type = PluginLimitType.BLOCK + elif s == "COUNT": + limit_type = PluginLimitType.COUNT + watch_type = LimitWatchType.USER + if _limit.get("watch_type") == "group": + watch_type = LimitWatchType.GROUP + check_type = LimitCheckType.ALL + if _limit.get("check_type") == "private": + check_type = LimitCheckType.PRIVATE + elif _limit.get("check_type") == "group": + check_type = LimitCheckType.GROUP + create_list.append( + PluginLimit( + module=plugin.module, + module_path=plugin.module_path, + plugin=plugin, + limit_type=limit_type, + watch_type=watch_type, + status=_limit.get("status", True), + check_type=check_type, + result=_limit.get("rst", ""), + cd=_limit.get("cd"), + max_count=_limit.get("max_count"), + ) + ) + # TODO: 批量错误 tortoise.exceptions.OperationalError: syntax error at or near "ALL" + # if update_list: + # await PluginLimit.bulk_update( + # update_list, + # [ + # "watch_type", + # "status", + # "check_type", + # "result", + # "cd", + # "max_count", + # ], + # 10, + # ) + if create_list: + await PluginLimit.bulk_create(create_list, 10) + logger.info("迁移插件限制数据完成!") + + +async def plugin_migration(): + """迁移插件数据""" + setting_file = DATA_PATH / "configs" / "plugins2settings.yaml" + plugin_file = DATA_PATH / "manager" / "plugins_manager.json" + if setting_file.exists(): + with open(setting_file, encoding="utf8") as f: + if data := _yaml.load(f): + logger.info("开始迁移插件setting数据...") + data = data["PluginSettings"] + plugins = await PluginInfo.filter(module__in=data.keys()) + for plugin in plugins: + if plugin_data_list := [ + data[p] for p in data if p == plugin.module + ]: + plugin_data = plugin_data_list[0] + plugin.default_status = plugin_data.get("default_status", True) + plugin.level = plugin_data.get("level", 5) + plugin.limit_superuser = plugin_data.get( + "limit_superuser", False + ) + plugin.menu_type = plugin_data.get("plugin_type", ["功能"])[0] + plugin.cost_gold = plugin_data.get("cost_gold", 0) + await PluginInfo.bulk_update( + plugins, + [ + "default_status", + "level", + "limit_superuser", + "menu_type", + "cost_gold", + ], + 10, + ) + setting_file.unlink() + logger.info("迁移插件setting数据完成!") + if plugin_file.exists(): + with open(plugin_file, encoding="utf8") as f: + if data := json.load(f): + logger.info("开始迁移插件数据...") + plugins = await PluginInfo.filter(module__in=data.keys()) + for plugin in plugins: + if plugin_data := data.get(plugin.module): + plugin.status = plugin_data.get("status", True) + block_type = None + get_block = plugin_data.get("block_type") + if get_block == "all": + block_type = BlockType.ALL + elif get_block == "private": + block_type = BlockType.PRIVATE + elif get_block == "group": + block_type = BlockType.GROUP + plugin.block_type = block_type + await plugin.save(update_fields=["status", "block_type"]) + # TODO: tortoise.exceptions.OperationalError: syntax error at or near "ALL" + # await PluginInfo.bulk_update(plugins, ["status", "block_type"], 10) + plugin_file.unlink() + logger.info("迁移插件数据完成!") + + +async def group_migration(): + """ + 群组数据迁移 + """ + group_file = DATA_PATH / "manager" / "group_manager.json" + if group_file.exists(): + with open(group_file, encoding="utf8") as f: + if data := json.load(f): + logger.info("开始迁移群组数据...") + update_list = [] + create_list = [] + white_group = data["white_group"] + close_task = data["close_task"] + old_group_list: dict = data["group_manager"] + if close_task: + """全局被动关闭""" + await TaskInfo.filter(module__in=close_task).update(status=False) + group_list = await GroupConsole.filter( + group_id__in=old_group_list.keys() + ) + for old_group_id in old_group_list: + old_group = old_group_list[old_group_id] + block_plugin = "" + block_task = "" + status = old_group.get("status", True) + level = old_group.get("level", 5) + if close_plugins := old_group.get("close_plugins"): + block_plugin = ",".join(close_plugins) + "," + if group_task_status := old_group.get("group_task_status"): + close_task = [ + t for t in group_task_status if not group_task_status[t] + ] + block_task = ",".join(close_task) + "," + if group_ := [g for g in group_list if g.group_id == old_group_id]: + group = group_[0] + if group.group_id in white_group: + group.is_super = True + group.status = status + group.block_plugin = block_plugin + group.block_task = block_task + group.level = level + update_list.append(group) + else: + """添加""" + create_list.append( + GroupConsole( + group_id=old_group_id, + status=status, + level=level, + block_plugin=block_plugin, + block_task=block_task, + is_super=old_group_id in white_group, + ) + ) + if update_list: + await GroupConsole.bulk_update( + update_list, + ["is_super", "status", "block_plugin", "block_task"], + 10, + ) + if create_list: + await GroupConsole.bulk_create(create_list, 10) + group_file.unlink() + logger.info("迁移群组数据完成!") diff --git a/zhenxun/builtin_plugins/nickname.py b/zhenxun/builtin_plugins/nickname.py new file mode 100644 index 00000000..f5cce1fb --- /dev/null +++ b/zhenxun/builtin_plugins/nickname.py @@ -0,0 +1,274 @@ +import random +from typing import Any, List + +from nonebot import on_regex +from nonebot.adapters import Bot +from nonebot.params import Depends, RegexGroup +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Option, on_alconna, store_true +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="昵称系统", + description="区区昵称,才不想叫呢!", + usage=f""" + 个人昵称,将替换{NICKNAME}称呼你的名称,群聊 与 私聊 昵称相互独立,全局昵称设置将更改您目前所有群聊中及私聊的昵称 + 指令: + 以后叫我 [昵称]: 设置当前群聊/私聊的昵称 + 全局昵称设置 [昵称]: 设置当前所有群聊和私聊的昵称 + {NICKNAME}我是谁 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.NORMAL, + menu_type="其他", + configs=[ + RegisterConfig( + key="BLACK_WORD", + value=["爸", "爹", "爷", "父"], + help="昵称所屏蔽的关键词,已设置的昵称会被替换为 *,未设置的昵称会在设置时提示", + default_value=None, + type=List[str], + ) + ], + ).dict(), +) + +_nickname_matcher = on_regex( + "(?:以后)?(?:叫我|请叫我|称呼我)(.*)", + rule=to_me(), + priority=5, + block=True, +) + +_global_nickname_matcher = on_regex( + "设置全局昵称(.*)", rule=to_me(), priority=5, block=True +) + +_matcher = on_alconna( + Alconna( + "nickname", + Option("--name", action=store_true, help_text="用户昵称"), + Option("--cancel", action=store_true, help_text="取消昵称"), + ), + rule=to_me(), + priority=5, + block=True, +) + +_matcher.shortcut( + "我(是谁|叫什么)", + command="nickname", + arguments=["--name"], + prefix=True, +) + +_matcher.shortcut( + "取消昵称", + command="nickname", + arguments=["--cancel"], + prefix=True, +) + + +CALL_NAME = [ + "好啦好啦,我知道啦,{},以后就这么叫你吧", + f"嗯嗯,{NICKNAME}" + "记住你的昵称了哦,{}", + "好突然,突然要叫你昵称什么的...{}..", + f"{NICKNAME}" + "会好好记住{}的,放心吧", + "好..好.,那窝以后就叫你{}了.", +] + +REMIND = [ + "我肯定记得你啊,你是{}啊", + "我不会忘记你的,你也不要忘记我!{}", + f"哼哼,{NICKNAME}" + "记忆力可是很好的,{}", + "嗯?你是失忆了嘛...{}..", + f"不要小看{NICKNAME}" + "的记忆力啊!笨蛋{}!QAQ", + "哎?{}..怎么了吗..突然这样问..", +] + +CANCEL = [ + f"呜..{NICKNAME}" + "睡一觉就会忘记的..和梦一样..{}", + "窝知道了..{}..", + f"是{NICKNAME}" + "哪里做的不好嘛..好吧..晚安{}", + "呃,{},下次我绝对绝对绝对不会再忘记你!", + "可..可恶!{}!太可恶了!呜", +] + + +def CheckNickname(): + """ + 检查名称是否合法 + """ + + async def dependency( + bot: Bot, + session: EventSession, + reg_group: tuple[Any, ...] = RegexGroup(), + ): + black_word = Config.get_config("nickname", "BLACK_WORD") + (name,) = reg_group + logger.debug(f"昵称检查: {name}", "昵称设置", session=session) + if not name: + await MessageUtils.build_message("叫你空白?叫你虚空?叫你无名??").finish( + at_sender=True + ) + if session.id1 in bot.config.superusers: + logger.debug( + f"超级用户设置昵称, 跳过合法检测: {name}", "昵称设置", session=session + ) + return + if len(name) > 20: + await MessageUtils.build_message("昵称可不能超过20个字!").finish( + at_sender=True + ) + if name in bot.config.nickname: + await MessageUtils.build_message("笨蛋!休想占用我的名字! #").finish( + at_sender=True + ) + if black_word: + for x in name: + if x in black_word: + logger.debug("昵称设置禁止字符: [{x}]", "昵称设置", session=session) + await MessageUtils.build_message(f"字符 [{x}] 为禁止字符!").finish( + at_sender=True + ) + for word in black_word: + if word in name: + logger.debug( + "昵称设置禁止字符: [{word}]", "昵称设置", session=session + ) + await MessageUtils.build_message(f"字符 [{x}] 为禁止字符!").finish( + at_sender=True + ) + + return Depends(dependency) + + +@_nickname_matcher.handle(parameterless=[CheckNickname()]) +async def _( + session: EventSession, + user_info: UserInfo = EventUserInfo(), + reg_group: tuple[Any, ...] = RegexGroup(), +): + if session.id1: + (name,) = reg_group + if len(name) < 5: + if random.random() < 0.3: + name = "~".join(name) + if gid := session.id3 or session.id2: + await GroupInfoUser.set_user_nickname( + session.id1, + gid, + name, + user_info.user_displayname + or user_info.user_remark + or user_info.user_name, + session.platform, + ) + logger.info(f"设置群昵称成功: {name}", "昵称设置", session=session) + await MessageUtils.build_message( + random.choice(CALL_NAME).format(name) + ).finish(reply_to=True) + else: + await FriendUser.set_user_nickname( + session.id1, + name, + user_info.user_displayname + or user_info.user_remark + or user_info.user_name, + session.platform, + ) + logger.info(f"设置私聊昵称成功: {name}", "昵称设置", session=session) + await MessageUtils.build_message( + random.choice(CALL_NAME).format(name) + ).finish(reply_to=True) + await MessageUtils.build_message("用户id为空...").send() + + +@_global_nickname_matcher.handle(parameterless=[CheckNickname()]) +async def _( + session: EventSession, + nickname: str = UserName(), + reg_group: tuple[Any, ...] = RegexGroup(), +): + if session.id1: + (name,) = reg_group + await FriendUser.set_user_nickname( + session.id1, + name, + nickname, + session.platform, + ) + await GroupInfoUser.filter(user_id=session.id1).update(nickname=name) + logger.info(f"设置全局昵称成功: {name}", "设置全局昵称", session=session) + await MessageUtils.build_message(random.choice(CALL_NAME).format(name)).finish( + reply_to=True + ) + await MessageUtils.build_message("用户id为空...").send() + + +@_matcher.assign("name") +async def _(session: EventSession, user_info: UserInfo = EventUserInfo()): + if session.id1: + if gid := session.id3 or session.id2: + nickname = await GroupInfoUser.get_user_nickname(session.id1, gid) + card = user_info.user_displayname or user_info.user_name + else: + nickname = await FriendUser.get_user_nickname(session.id1) + card = user_info.user_name + if nickname: + await MessageUtils.build_message( + random.choice(REMIND).format(nickname) + ).finish(reply_to=True) + else: + await MessageUtils.build_message( + random.choice( + [ + "没..没有昵称嘛,{}", + "啊,你是{}啊,我想叫你的昵称!", + "是{}啊,有什么事吗?", + "你是{}?", + ] + ).format(card) + ).finish(reply_to=True) + await MessageUtils.build_message("用户id为空...").send() + + +@_matcher.assign("cancel") +async def _(bot: Bot, session: EventSession, user_info: UserInfo = EventUserInfo()): + if session.id1: + gid = session.id3 or session.id2 + if gid: + nickname = await GroupInfoUser.get_user_nickname(session.id1, gid) + else: + nickname = await FriendUser.get_user_nickname(session.id1) + if nickname: + await MessageUtils.build_message( + random.choice(CANCEL).format(nickname) + ).send(reply_to=True) + if gid: + await GroupInfoUser.set_user_nickname(session.id1, gid, "") + else: + await FriendUser.set_user_nickname(session.id1, "") + await BanConsole.ban(session.id1, gid, 9, 60, bot.self_id) + return + else: + await MessageUtils.build_message("你在做梦吗?你没有昵称啊").finish( + reply_to=True + ) + await MessageUtils.build_message("用户id为空...").send() diff --git a/zhenxun/builtin_plugins/platform/__init__.py b/zhenxun/builtin_plugins/platform/__init__.py new file mode 100644 index 00000000..448afcf7 --- /dev/null +++ b/zhenxun/builtin_plugins/platform/__init__.py @@ -0,0 +1,11 @@ +import os +from pathlib import Path + +import nonebot + +path = Path(__file__).parent + +for f in os.listdir(path): + _p = path / f + if _p.is_dir(): + nonebot.load_plugins(str(_p.resolve())) diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py new file mode 100644 index 00000000..e04f8e5a --- /dev/null +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -0,0 +1,317 @@ +import os +import random +import re +from datetime import datetime + +import nonebot +import ujson as json +from nonebot import on_notice, on_request +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import ( + GroupDecreaseNoticeEvent, + GroupIncreaseNoticeEvent, +) +from nonebot.adapters.onebot.v12 import ( + GroupMemberDecreaseEvent, + GroupMemberIncreaseEvent, +) +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import At + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.level_user import LevelUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType, RequestHandleType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import FreqLimiter + +__plugin_meta__ = PluginMetadata( + name="QQ群事件处理", + description="群事件处理", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="invite_manager", + key="message", + value=f"请不要未经同意就拉{NICKNAME}入群!告辞!", + help="强制拉群后进群回复的内容", + ), + RegisterConfig( + module="invite_manager", + key="flag", + value=True, + help="强制拉群后进群退出并回复内容", + default_value=True, + type=bool, + ), + RegisterConfig( + module="invite_manager", + key="welcome_msg_cd", + value=5, + help="群欢迎消息cd", + default_value=5, + type=int, + ), + RegisterConfig( + module="_task", + key="DEFAULT_GROUP_WELCOME", + value=True, + help="被动 进群欢迎 进群默认开关状态", + default_value=True, + type=bool, + ), + RegisterConfig( + module="_task", + key="DEFAULT_REFUND_GROUP_REMIND", + value=True, + help="被动 退群提醒 进群默认开关状态", + default_value=True, + type=bool, + ), + ], + tasks=[ + Task(module="group_welcome", name="进群欢迎"), + Task(module="refund_group_remind", name="退群提醒"), + ], + ).dict(), +) + + +superuser = nonebot.get_driver().config.platform_superusers["qq"][0] + +base_config = Config.get("invite_manager") + + +limit_cd = base_config.get("welcome_msg_cd") + +_flmt = FreqLimiter(limit_cd) + + +group_increase_handle = on_notice(priority=1, block=False) +"""群员增加处理""" +group_decrease_handle = on_notice(priority=1, block=False) +"""群员减少处理""" +add_group = on_request(priority=1, block=False) +"""加群同意请求""" + + +@group_increase_handle.handle() +async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent): + user_id = str(event.user_id) + group_id = str(event.group_id) + if user_id == bot.self_id: + """新成员为bot本身""" + group = await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ) + if not group or group.group_flag == 0: + """群聊不存在或被强制拉群""" + if base_config.get("flag"): + """退出群组""" + try: + if result_msg := base_config.get("message"): + await bot.send_group_msg( + group_id=event.group_id, message=result_msg + ) + await bot.set_group_leave(group_id=event.group_id) + await bot.send_private_msg( + user_id=int(superuser), + message=f"触发强制入群保护,已成功退出群聊 {group_id}...", + ) + logger.info( + f"强制拉群或未有群信息,退出群聊成功", + "入群检测", + group_id=event.group_id, + ) + if req := await FgRequest.get_or_none( + group_id=group_id, handle_type__isnull=True + ): + req.handle_type = RequestHandleType.IGNORE + await req.save(update_fields=["handle_type"]) + except Exception as e: + logger.error( + f"强制拉群或未有群信息,退出群聊失败", + "入群检测", + group_id=event.group_id, + e=e, + ) + await bot.send_private_msg( + user_id=int(superuser), + message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...", + ) + await GroupConsole.filter(group_id=group_id).delete() + else: + """允许群组并设置群认证,默认群功能开关""" + if group: + await GroupConsole.filter( + group_id=group_id, channel_id__isnull=True + ).update(group_flag=1) + else: + block_plugin = "" + if plugin_list := await PluginInfo.filter( + default_status=False + ).all(): + for plugin in plugin_list: + block_plugin += f"{plugin.module}," + group_info = await bot.get_group_info(group_id=event.group_id) + await GroupConsole.create( + group_id=group_info["group_id"], + group_name=group_info["group_name"], + max_member_count=group_info["max_member_count"], + member_count=group_info["member_count"], + group_flag=1, + block_plugin=block_plugin, + platform="qq", + ) + """刷新群管理员权限""" + admin_default_auth = Config.get_config( + "admin_bot_manage", "ADMIN_DEFAULT_AUTH" + ) + # 即刻刷新权限 + for user_info in await bot.get_group_member_list( + group_id=event.group_id + ): + """即刻刷新权限""" + if ( + user_info["role"] + in [ + "owner", + "admin", + ] + and not await LevelUser.is_group_flag( + user_info["user_id"], group_id + ) + and admin_default_auth is not None + ): + await LevelUser.set_level( + user_info["user_id"], + user_info["group_id"], + admin_default_auth, + ) + logger.debug( + f"添加默认群管理员权限: {admin_default_auth}", + "入群检测", + session=user_info["user_id"], + group_id=user_info["group_id"], + ) + if str(user_info["user_id"]) in bot.config.superusers: + await LevelUser.set_level( + user_info["user_id"], user_info["group_id"], 9 + ) + logger.debug( + f"添加超级用户权限: 9", + "入群检测", + session=user_info["user_id"], + group_id=user_info["group_id"], + ) + else: + join_time = datetime.now() + user_info = await bot.get_group_member_info( + group_id=event.group_id, user_id=event.user_id + ) + await GroupInfoUser.update_or_create( + user_id=str(user_info["user_id"]), + group_id=str(user_info["group_id"]), + defaults={"user_name": user_info["nickname"], "user_join_time": join_time}, + ) + logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新成功") + + if _flmt.check(group_id): + """群欢迎消息""" + _flmt.start_cd(group_id) + path = DATA_PATH / "welcome_message" / "qq" / f"{group_id}" + data = json.load((path / "text.json").open()) + message = data["message"] + msg_split = re.split(r"\[image:\d+\]", message) + msg_list = [] + if data["at"]: + msg_list.append(At(flag="user", target=user_id)) + for i, text in enumerate(msg_split): + msg_list.append(text) + img_file = path / f"{i}.png" + if img_file.exists(): + msg_list.append(img_file) + if not TaskInfo.is_block("group_welcome", group_id): + logger.info(f"发送群欢迎消息...", "入群检测", group_id=group_id) + if msg_list: + await MessageUtils.build_message(msg_list).send() + else: + image = ( + IMAGE_PATH + / "qxz" + / random.choice(os.listdir(IMAGE_PATH / "qxz")) + ) + await MessageUtils.build_message( + [ + "新人快跑啊!!本群现状↓(快使用自定义!)", + image, + ] + ).send() + + +@group_decrease_handle.handle() +async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent): + if event.sub_type == "kick_me": + """踢出Bot""" + group_id = event.group_id + operator_id = event.operator_id + if user := await GroupInfoUser.get_or_none( + user_id=str(event.operator_id), group_id=str(event.group_id) + ): + operator_name = user.user_name + else: + operator_name = "None" + group = await GroupConsole.filter(group_id=str(group_id)).first() + group_name = group.group_name if group else "" + coffee = int(superuser) + await bot.send_private_msg( + user_id=coffee, + message=f"****呜..一份踢出报告****\n" + f"我被 {operator_name}({operator_id})\n" + f"踢出了 {group_name}({group_id})\n" + f"日期:{str(datetime.now()).split('.')[0]}", + ) + if group: + await group.delete() + return + if str(event.user_id) == bot.self_id: + """踢出Bot""" + await GroupConsole.filter(group_id=str(event.group_id)).delete() + return + if user := await GroupInfoUser.get_or_none( + user_id=str(event.user_id), group_id=str(event.group_id) + ): + user_name = user.user_name + else: + user_name = f"{event.user_id}" + await GroupInfoUser.filter( + user_id=str(event.user_id), group_id=str(event.group_id) + ).delete() + logger.info( + f"名称: {user_name} 退出群聊", + "group_decrease_handle", + session=event.user_id, + group_id=event.group_id, + ) + result = "" + if event.sub_type == "leave": + result = f"{user_name}离开了我们..." + if event.sub_type == "kick": + operator = await bot.get_group_member_info( + user_id=event.operator_id, group_id=event.group_id + ) + operator_name = operator["card"] if operator["card"] else operator["nickname"] + result = f"{user_name} 被 {operator_name} 送走了." + if not TaskInfo.is_block("refund_group_remind", str(event.group_id)): + await group_decrease_handle.send(f"{result}") diff --git a/zhenxun/builtin_plugins/record_request.py b/zhenxun/builtin_plugins/record_request.py new file mode 100644 index 00000000..e894d61e --- /dev/null +++ b/zhenxun/builtin_plugins/record_request.py @@ -0,0 +1,209 @@ +import time +from datetime import datetime + +import nonebot +from nonebot import on_message, on_request +from nonebot.adapters.onebot.v11 import ActionFailed +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v11 import FriendRequestEvent, GroupRequestEvent +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +base_config = Config.get("invite_manager") + +__plugin_meta__ = PluginMetadata( + name="记录请求", + description="记录 好友/群组 请求", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="invite_manager", + key="AUTO_ADD_FRIEND", + value=False, + help="是否自动同意好友添加", + type=bool, + default_value=False, + ) + ], + ).dict(), +) + + +class Timer: + data: dict[str, float] = {} + + @classmethod + def check(cls, uid: int | str): + if uid not in cls.data: + return True + return time.time() - cls.data[uid] > 5 * 60 + + @classmethod + def clear(cls): + now = time.time() + cls.data = {k: v for k, v in cls.data.items() if v - now < 5 * 60} + + +# TODO: 其他平台请求 + +friend_req = on_request(priority=5, block=True) +group_req = on_request(priority=5, block=True) +_t = on_message(priority=999, block=False, rule=lambda: False) + + +@friend_req.handle() +async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSession): + superuser = nonebot.get_driver().config.platform_superusers["qq"][0] + if event.user_id and Timer.check(event.user_id): + logger.debug(f"收录好友请求...", "好友请求", target=event.user_id) + user = await bot.get_stranger_info(user_id=event.user_id) + nickname = user["nickname"] + # sex = user["sex"] + # age = str(user["age"]) + comment = event.comment + if superuser: + superuser = int(superuser) + await MessageUtils.build_message( + f"*****一份好友申请*****\n" + f"昵称:{nickname}({event.user_id})\n" + f"自动同意:{'√' if base_config.get('AUTO_ADD_FRIEND') else '×'}\n" + f"日期:{str(datetime.now()).split('.')[0]}\n" + f"备注:{event.comment}" + ).send(target=PlatformUtils.get_target(bot, superuser)) + if base_config.get("AUTO_ADD_FRIEND"): + logger.debug( + f"已开启好友请求自动同意,成功通过该请求", + "好友请求", + target=event.user_id, + ) + await bot.set_friend_add_request(flag=event.flag, approve=True) + await FriendUser.create( + user_id=str(user["user_id"]), user_name=user["nickname"] + ) + else: + # 旧请求全部设置为过期 + await FgRequest.filter( + request_type=RequestType.FRIEND, + user_id=str(event.user_id), + handle_type__isnull=True, + ).update(handle_type=RequestHandleType.EXPIRE) + await FgRequest.create( + request_type=RequestType.FRIEND, + platform=session.platform, + bot_id=bot.self_id, + flag=event.flag, + user_id=event.user_id, + nickname=nickname, + comment=comment, + ) + else: + logger.debug(f"好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id) + + +@group_req.handle() +async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession): + superuser = nonebot.get_driver().config.platform_superusers["qq"][0] + # 邀请 + if event.sub_type == "invite": + if str(event.user_id) in bot.config.superusers: + try: + logger.debug( + f"超级用户自动同意加入群聊", + "群聊请求", + session=event.user_id, + target=event.group_id, + ) + if isinstance(bot, v11Bot): + group_info = await bot.get_group_info(group_id=event.group_id) + max_member_count = group_info["max_member_count"] + member_count = group_info["member_count"] + else: + group_info = await bot.get_group_info(group_id=str(event.group_id)) + max_member_count = 0 + member_count = 0 + await GroupConsole.update_or_create( + group_id=str(event.group_id), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": max_member_count, + "member_count": member_count, + "group_flag": 1, + }, + ) + await bot.set_group_add_request( + flag=event.flag, sub_type="invite", approve=True + ) + except ActionFailed as e: + logger.error( + "超级用户自动同意加入群聊发生错误", + "群聊请求", + session=event.user_id, + target=event.group_id, + e=e, + ) + else: + if Timer.check(f"{event.user_id}:{event.group_id}"): + logger.debug( + f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求", + "群聊请求", + target=event.group_id, + ) + nickname = await FriendUser.get_user_name(str(event.user_id)) + await Text( + f"*****一份入群申请*****\n" + f"申请人:{nickname}({event.user_id})\n" + f"群聊:{event.group_id}\n" + f"邀请日期:{datetime.now().replace(microsecond=0)}" + ).send_to(target=TargetQQPrivate(user_id=superuser), bot=bot) + await bot.send_private_msg( + user_id=event.user_id, + message=f"想要邀请我偷偷入群嘛~已经提醒{NICKNAME}的管理员大人了\n" + "请确保已经群主或群管理沟通过!\n" + "等待管理员处理吧!", + ) + # 旧请求全部设置为过期 + await FgRequest.filter( + request_type=RequestType.GROUP, + user_id=str(event.user_id), + group_id=str(event.group_id), + handle_type__isnull=True, + ).update(handle_type=RequestHandleType.EXPIRE) + await FgRequest.create( + request_type=RequestType.GROUP, + platform=session.platform, + bot_id=bot.self_id, + flag=event.flag, + user_id=str(event.user_id), + nickname=nickname, + group_id=str(event.group_id), + ) + else: + logger.debug( + f"群聊请求五分钟内重复, 已忽略", + "群聊请求", + target=f"{event.user_id}:{event.group_id}", + ) + + +@scheduler.scheduled_job( + "interval", + minutes=5, +) +async def _(): + Timer.clear() diff --git a/basic_plugins/super_cmd/__init__.py b/zhenxun/builtin_plugins/scheduler/__init__.py old mode 100755 new mode 100644 similarity index 95% rename from basic_plugins/super_cmd/__init__.py rename to zhenxun/builtin_plugins/scheduler/__init__.py index 87ae4077..eb35e275 --- a/basic_plugins/super_cmd/__init__.py +++ b/zhenxun/builtin_plugins/scheduler/__init__.py @@ -1,5 +1,5 @@ -from pathlib import Path - -import nonebot - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/scheduler/auto_backup.py b/zhenxun/builtin_plugins/scheduler/auto_backup.py new file mode 100644 index 00000000..92f1119b --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/auto_backup.py @@ -0,0 +1,62 @@ +import shutil +from pathlib import Path + +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.configs.config import Config +from zhenxun.services.log import logger + +Config.add_plugin_config( + "_backup", + "BACKUP_FLAG", + True, + help="是否开启文件备份", + default_value=True, + type=bool, +) + +Config.add_plugin_config( + "_backup", + "BACKUP_DIR_OR_FILE", + [ + "data/black_word", + "data/configs", + "data/statistics", + "data/word_bank", + "data/manager", + "configs", + ], + help="备份的文件夹或文件", + default_value=[], + type=list[str], +) + + +# 自动备份 +@scheduler.scheduled_job( + "cron", + hour=3, + minute=25, +) +async def _(): + if Config.get_config("_backup", "BACKUP_FLAG"): + _backup_path = Path() / "backup" + _backup_path.mkdir(exist_ok=True, parents=True) + if backup_dir_or_file := Config.get_config("_backup", "BACKUP_DIR_OR_FILE"): + for path_file in backup_dir_or_file: + try: + path = Path(path_file) + _p = _backup_path / path_file + if path.exists(): + if path.is_dir(): + if _p.exists(): + shutil.rmtree(_p, ignore_errors=True) + shutil.copytree(path_file, _p) + else: + if _p.exists(): + _p.unlink() + shutil.copy(path_file, _p) + logger.debug(f"已完成自动备份:{path_file}", "自动备份") + except Exception as e: + logger.error(f"自动备份文件 {path_file} 发生错误", "自动备份", e=e) + logger.info("自动备份成功...", "自动备份") diff --git a/zhenxun/builtin_plugins/scheduler/auto_update_group.py b/zhenxun/builtin_plugins/scheduler/auto_update_group.py new file mode 100644 index 00000000..8e62bc69 --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/auto_update_group.py @@ -0,0 +1,37 @@ +import nonebot +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils + + +# 自动更新群组信息 +@scheduler.scheduled_job( + "cron", + hour=3, + minute=1, +) +async def _(): + bots = nonebot.get_bots() + for bot in bots.values(): + try: + await PlatformUtils.update_group(bot) + except Exception as e: + logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e) + logger.info("自动更新群组成员信息成功...") + + +# 自动更新好友信息 +@scheduler.scheduled_job( + "cron", + hour=3, + minute=1, +) +async def _(): + bots = nonebot.get_bots() + for bot in bots.values(): + try: + await PlatformUtils.update_friend(bot) + except Exception as e: + logger.error(f"自动更新好友信息错误", "自动更新好友", e=e) + logger.info("自动更新好友信息成功...") diff --git a/zhenxun/builtin_plugins/scheduler/chat_check.py b/zhenxun/builtin_plugins/scheduler/chat_check.py new file mode 100644 index 00000000..599dff35 --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/chat_check.py @@ -0,0 +1,54 @@ +from datetime import datetime, timedelta + +import nonebot +import pytz +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils + + +@scheduler.scheduled_job( + "cron", + hour=4, + minute=40, +) +async def _(): + """检测群组发言时间并禁用全部被动""" + update_list = [] + for bot in nonebot.get_bots().values(): + group_list, _ = await PlatformUtils.get_group_list(bot) + group_list = [g for g in group_list if g.channel_id == None] + for group in group_list: + try: + last_message = ( + await ChatHistory.filter(group_id=group.group_id) + .annotate() + .order_by("-create_time") + .first() + ) + if last_message: + now = datetime.now(pytz.timezone("Asia/Shanghai")) + if modules := await TaskInfo.annotate().values_list( + "module", flat=True + ): + if now - timedelta(days=2) > last_message.create_time: + _group, _ = await GroupConsole.get_or_create( + group_id=group.group_id, channel_id__isnull=True + ) + _group.block_task = ",".join(modules) + "," # type: ignore + update_list.append(_group) + logger.info( + "群组两日内未发送任何消息,关闭该群全部被动", + "Chat检测", + target=_group.group_id, + ) + except Exception as e: + logger.error( + "检测群组发言时间失败...", "Chat检测", target=group.group_id + ) + if update_list: + await GroupConsole.bulk_update(update_list, ["block_task"], 10) diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py new file mode 100644 index 00000000..d12e0765 --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -0,0 +1,73 @@ +import nonebot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, Task +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import broadcast_group + +__plugin_meta__ = PluginMetadata( + name="早晚安被动技能", + description="早晚安被动技能", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + tasks=[ + Task(module="group_welcome", name="进群欢迎"), + Task(module="refund_group_remind", name="退群提醒"), + ], + ).dict(), +) + +driver = nonebot.get_driver() + + +@driver.on_startup +async def _(): + if not await TaskInfo.exists(module="morning_goodnight"): + await TaskInfo.create( + module="morning_goodnight", + name="早晚安", + status=True, + ) + + +async def check(group_id: str) -> bool: + return not await TaskInfo.is_block("morning_goodnight", group_id) + + +# 早上好 +@scheduler.scheduled_job( + "cron", + hour=6, + minute=1, +) +async def _(): + message = MessageUtils.build_message(["早上好", IMAGE_PATH / "zhenxun" / "zao.jpg"]) + await broadcast_group(message, log_cmd="被动早晚安", check_func=check) + logger.info("每日早安发送...") + + +# # 睡觉了 +@scheduler.scheduled_job( + "cron", + hour=23, + minute=59, +) +async def _(): + message = MessageUtils.build_message( + [f"{NICKNAME}要睡觉了,你们也要早点睡呀", IMAGE_PATH / "zhenxun" / "sleep.jpg"] + ) + await broadcast_group( + message, + log_cmd="被动早晚安", + check_func=check, + ) + logger.info("每日晚安发送...") diff --git a/zhenxun/builtin_plugins/scripts.py b/zhenxun/builtin_plugins/scripts.py new file mode 100644 index 00000000..d8abefe2 --- /dev/null +++ b/zhenxun/builtin_plugins/scripts.py @@ -0,0 +1,64 @@ +from asyncio.exceptions import TimeoutError + +import nonebot +from nonebot.drivers import Driver +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.configs.path_config import TEXT_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +try: + import ujson as json +except ModuleNotFoundError: + import json + + +driver: Driver = nonebot.get_driver() + + +@driver.on_startup +async def update_city(): + """ + 部分插件需要中国省份城市 + 这里直接更新,避免插件内代码重复 + """ + china_city = TEXT_PATH / "china_city.json" + data = {} + if not china_city.exists(): + try: + logger.debug("开始更新城市列表...") + res = await AsyncHttpx.get( + "http://www.weather.com.cn/data/city3jdata/china.html", timeout=5 + ) + res.encoding = "utf8" + provinces_data = json.loads(res.text) + for province in provinces_data.keys(): + data[provinces_data[province]] = [] + res = await AsyncHttpx.get( + f"http://www.weather.com.cn/data/city3jdata/provshi/{province}.html", + timeout=5, + ) + res.encoding = "utf8" + city_data = json.loads(res.text) + for city in city_data.keys(): + data[provinces_data[province]].append(city_data[city]) + with open(china_city, "w", encoding="utf8") as f: + json.dump(data, f, indent=4, ensure_ascii=False) + logger.info("自动更新城市列表完成.....") + except TimeoutError as e: + logger.warning("自动更新城市列表超时...", e=e) + except ValueError as e: + logger.warning("自动城市列表失败.....", e=e) + except Exception as e: + logger.error(f"自动城市列表未知错误", e=e) + + +# 自动更新城市列表 +@scheduler.scheduled_job( + "cron", + hour=6, + minute=1, +) +async def _(): + await update_city() diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py new file mode 100644 index 00000000..9ce4c51c --- /dev/null +++ b/zhenxun/builtin_plugins/shop/__init__.py @@ -0,0 +1,149 @@ +from nonebot.adapters import Bot, Event +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Subcommand, + UniMessage, + UniMsg, + on_alconna, +) +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.utils import BaseBlock, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.message import MessageUtils + +from ._data_source import ShopManage + +__plugin_meta__ = PluginMetadata( + name="商店", + description="商店系统[金币回收计划]", + usage=""" + 商品操作 + 指令: + 我的金币 + 我的道具 + 使用道具 [名称/Id] + 购买道具 [名称/Id] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.NORMAL, + menu_type="商店", + limits=[BaseBlock(check_type=BlockType.GROUP)], + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "商店", + Subcommand("my-cost", help_text="我的金币"), + Subcommand("my-props", help_text="我的道具"), + Subcommand("buy", Args["name", str]["num", int, 1], help_text="购买道具"), + Subcommand("use", Args["name", str]["num?", int, 1], help_text="使用道具"), + ), + priority=5, + block=True, +) + +_matcher.shortcut( + "我的金币", + command="商店", + arguments=["my-cost"], + prefix=True, +) + +_matcher.shortcut( + "我的道具", + command="商店", + arguments=["my-props"], + prefix=True, +) + +_matcher.shortcut( + "购买道具", + command="商店", + arguments=["buy", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + "使用道具", + command="商店", + arguments=["use", "{%0}"], + prefix=True, +) + + +@_matcher.assign("$main") +async def _(session: EventSession, arparma: Arparma): + image = await ShopManage.build_shop_image() + logger.info("查看商店", arparma.header_result, session=session) + await MessageUtils.build_message(image).send() + + +@_matcher.assign("my-cost") +async def _(session: EventSession, arparma: Arparma): + if session.id1: + logger.info("查看金币", arparma.header_result, session=session) + gold = await ShopManage.my_cost(session.id1, session.platform) + await MessageUtils.build_message(f"你的当前余额: {gold}").send(reply_to=True) + else: + await MessageUtils.build_message(f"用户id为空...").send(reply_to=True) + + +@_matcher.assign("my-props") +async def _( + session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo() +): + if session.id1: + logger.info("查看道具", arparma.header_result, session=session) + if image := await ShopManage.my_props( + session.id1, + user_info.user_displayname or user_info.user_name, + session.platform, + ): + await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True) + return await MessageUtils.build_message(f"你的道具为空捏...").send( + reply_to=True + ) + else: + await MessageUtils.build_message(f"用户id为空...").send(reply_to=True) + + +@_matcher.assign("buy") +async def _(session: EventSession, arparma: Arparma, name: str, num: int): + if session.id1: + logger.info( + f"购买道具 {name}, 数量: {num}", + arparma.header_result, + session=session, + ) + result = await ShopManage.buy_prop(session.id1, name, num, session.platform) + await MessageUtils.build_message(result).send(reply_to=True) + else: + await MessageUtils.build_message(f"用户id为空...").send(reply_to=True) + + +@_matcher.assign("use") +async def _( + bot: Bot, + event: Event, + message: UniMsg, + session: EventSession, + arparma: Arparma, + name: str, + num: int, +): + result = await ShopManage.use(bot, event, session, message, name, num, "") + logger.info(f"使用道具 {name}, 数量: {num}", arparma.header_result, session=session) + if isinstance(result, str): + await MessageUtils.build_message(result).send(reply_to=True) + elif isinstance(result, UniMessage): + await result.finish(reply_to=True) diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py new file mode 100644 index 00000000..c7b5c68b --- /dev/null +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -0,0 +1,616 @@ +import asyncio +import inspect +import time +from types import MappingProxyType +from typing import Any, Callable, Literal + +from nonebot.adapters import Bot, Event +from nonebot_plugin_alconna import UniMessage, UniMsg +from nonebot_plugin_session import EventSession +from pydantic import BaseModel, create_model + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.goods_info import GoodsInfo +from zhenxun.models.user_console import UserConsole +from zhenxun.models.user_gold_log import UserGoldLog +from zhenxun.models.user_props_log import UserPropsLog +from zhenxun.services.log import logger +from zhenxun.utils.enum import GoldHandle, PropHandle +from zhenxun.utils.image_utils import BuildImage, ImageTemplate, text2image + +ICON_PATH = IMAGE_PATH / "shop_icon" + + +class Goods(BaseModel): + + name: str + """商品名称""" + before_handle: list[Callable] = [] + """使用前函数""" + after_handle: list[Callable] = [] + """使用后函数""" + func: Callable | None = None + """使用函数""" + params: Any = None + """参数""" + send_success_msg: bool = True + """使用成功是否发送消息""" + max_num_limit: int = 1 + """单次使用最大次数""" + model: Any = None + """model""" + session: EventSession | None = None + """EventSession""" + + +class ShopParam(BaseModel): + + goods_name: str + """商品名称""" + user_id: int + """用户id""" + group_id: int + """群聊id""" + bot: Any + """bot""" + event: Event + """event""" + num: int + """道具单次使用数量""" + text: str + """text""" + send_success_msg: bool = True + """是否发送使用成功信息""" + max_num_limit: int = 1 + """单次使用最大次数""" + session: EventSession | None = None + """EventSession""" + + +class ShopManage: + + uuid2goods: dict[str, Goods] = {} + + @classmethod + def __build_params( + cls, + bot: Bot, + event: Event, + session: EventSession, + message: UniMsg, + goods: Goods, + num: int, + text: str, + ) -> tuple[ShopParam, dict[str, Any]]: + """构造参数 + + 参数: + bot: bot + event: event + goods_name: 商品名称 + num: 数量 + text: 其他信息 + """ + _kwargs = goods.params + model = goods.model( + **{ + "goods_name": goods.name, + "bot": bot, + "event": event, + "user_id": session.id1, + "group_id": session.id3 or session.id2, + "num": num, + "text": text, + "session": session, + } + ) + return model, { + **_kwargs, + "_bot": bot, + "event": event, + "user_id": session.id1, + "group_id": session.id3 or session.id2, + "num": num, + "text": text, + "goods_name": goods.name, + } + + @classmethod + def __parse_args( + cls, + args: MappingProxyType, + param: ShopParam, + session: EventSession, + message: UniMsg, + **kwargs, + ) -> list[Any]: + """解析参数 + + 参数: + args: MappingProxyType + param: ShopParam + + 返回: + list[Any]: 参数 + """ + param_list = [] + _bot = param.bot + param.bot = None + param_json = param.dict() + param_json["bot"] = _bot + for par in args.keys(): + if par in ["shop_param"]: + param_list.append(param) + elif par in ["session"]: + param_list.append(session) + elif par in ["message"]: + param_list.append(message) + elif par not in ["args", "kwargs"]: + param_list.append(param_json.get(par)) + if kwargs.get(par) is not None: + del kwargs[par] + return param_list + + @classmethod + async def run_before_after( + cls, + goods: Goods, + param: ShopParam, + run_type: Literal["after", "before"], + **kwargs, + ): + """运行使用前使用后函数 + + 参数: + goods: Goods + param: 参数 + run_type: 运行类型 + """ + fun_list = goods.before_handle if run_type == "before" else goods.after_handle + if fun_list: + for func in fun_list: + args = inspect.signature(func).parameters + if args and list(args.keys())[0] != "kwargs": + if asyncio.iscoroutinefunction(func): + await func(*cls.__parse_args(args, param, **kwargs)) + else: + func(*cls.__parse_args(args, param, **kwargs)) + else: + if asyncio.iscoroutinefunction(func): + await func(**kwargs) + else: + func(**kwargs) + + @classmethod + async def __run( + cls, + goods: Goods, + param: ShopParam, + session: EventSession, + message: UniMsg, + **kwargs, + ) -> str | UniMessage | None: + """运行道具函数 + + 参数: + goods: Goods + param: ShopParam + + 返回: + str | MessageFactory | None: 使用完成后返回信息 + """ + args = inspect.signature(goods.func).parameters # type: ignore + if goods.func: + if args and list(args.keys())[0] != "kwargs": + if asyncio.iscoroutinefunction(goods.func): + return await goods.func( + *cls.__parse_args(args, param, session, message, **kwargs) + ) + else: + return goods.func( + *cls.__parse_args(args, param, session, message, **kwargs) + ) + else: + if asyncio.iscoroutinefunction(goods.func): + return await goods.func( + **kwargs, + ) + else: + return goods.func(**kwargs) + + @classmethod + async def use( + cls, + bot: Bot, + event: Event, + session: EventSession, + message: UniMsg, + goods_name: str, + num: int, + text: str, + ) -> str | UniMessage | None: + """使用道具 + + 参数: + bot: Bot + event: Event + session: Session + message: 消息 + goods_name: 商品名称 + num: 使用数量 + text: 其他信息 + + 返回: + str | MessageFactory | None: 使用完成后返回信息 + """ + if goods_name.isdigit(): + user = await UserConsole.get_user(user_id=session.id1) # type: ignore + uuid = list(user.props.keys())[int(goods_name)] + goods_info = await GoodsInfo.get_or_none(uuid=uuid) + else: + goods_info = await GoodsInfo.get_or_none(goods_name=goods_name) + if not goods_info: + return f"{goods_name} 不存在..." + if goods_info.is_passive: + return f"{goods_name} 是被动道具, 无法使用..." + goods = cls.uuid2goods.get(goods_info.uuid) + if not goods or not goods.func: + return f"{goods_name} 未注册使用函数, 无法使用..." + param, kwargs = cls.__build_params( + bot, event, session, message, goods, num, text + ) + if num > param.max_num_limit: + return f"{goods_name} 单次使用最大数量为{param.max_num_limit}..." + await cls.run_before_after(goods, param, "before", **kwargs) + result = await cls.__run(goods, param, session, message, **kwargs) + await cls.run_before_after(goods, param, "after", **kwargs) + if not result and param.send_success_msg: + result = f"使用道具 {goods.name} {num} 次成功!" + return result + + @classmethod + async def register_use( + cls, + name: str, + uuid: str, + func: Callable, + send_success_msg: bool = True, + max_num_limit: int = 1, + before_handle: list[Callable] = [], + after_handle: list[Callable] = [], + **kwargs, + ): + """注册使用方法 + + 参数: + uuid: uuid + func: 使用函数 + send_success_msg: 使用成功时发送消息. + max_num_limit: 单次最大使用限制. + before_handle: 使用前函数. + after_handle: 使用后函数. + + 异常: + ValueError: 该商品使用函数已被注册! + """ + if uuid in cls.uuid2goods: + raise ValueError("该商品使用函数已被注册!") + kwargs["send_success_msg"] = send_success_msg + kwargs["max_num_limit"] = max_num_limit + # TODO: create_model(f"{uuid}_model", __base__=ShopParam, **kwargs) + cls.uuid2goods[uuid] = Goods( + model=None,# create_model(f"{uuid}_model", __base__=ShopParam, **kwargs), + params=kwargs, + before_handle=before_handle, + after_handle=after_handle, + name=name, + func=func, + ) + + @classmethod + async def buy_prop( + cls, user_id: str, name: str, num: int = 1, platform: str | None = None + ) -> str: + """购买道具 + + 参数: + user_id: 用户id + name: 道具名称 + num: 购买数量. + platform: 平台. + + 返回: + str: 返回小 + """ + if name == "神秘药水": + return "你们看看就好啦,这是不可能卖给你们的~" + if num < 0: + return "购买的数量要大于0!" + goods_list = await GoodsInfo.annotate().order_by("id").all() + goods_list = [ + goods + for goods in goods_list + if goods.goods_limit_time > time.time() or goods.goods_limit_time == 0 + ] + if name.isdigit(): + goods = goods_list[int(name) - 1] + else: + if filter_goods := [g for g in goods_list if g.goods_name == name]: + goods = filter_goods[0] + else: + return "道具名称不存在..." + user = await UserConsole.get_user(user_id, platform) + price = goods.goods_price * num * goods.goods_discount + if user.gold < price: + return "糟糕! 您的金币好像不太够哦..." + count = await UserPropsLog.filter( + user_id=user_id, handle=PropHandle.BUY + ).count() + if goods.daily_limit and count >= goods.daily_limit: + return "今天的购买已达限制了喔!" + await UserGoldLog.create(user_id=user_id, gold=price, handle=GoldHandle.BUY) + await UserPropsLog.create( + user_id=user_id, uuid=goods.uuid, gold=price, num=num, handle=PropHandle.BUY + ) + logger.info( + f"花费 {price} 金币购买 {goods.goods_name} ×{num} 成功!", + "购买道具", + session=user_id, + ) + user.gold -= int(price) + if goods.uuid not in user.props: + user.props[goods.uuid] = 0 + user.props[goods.uuid] += num + await user.save(update_fields=["gold", "props"]) + return f"花费 {price} 金币购买 {goods.goods_name} ×{num} 成功!" + + @classmethod + async def my_props( + cls, user_id: str, name: str, platform: str | None = None + ) -> BuildImage | None: + """获取道具背包 + + 参数: + user_id: 用户id + name: 用户昵称 + platform: 平台. + + 返回: + BuildImage | None: 道具背包图片 + """ + user = await UserConsole.get_user(user_id, platform) + if not user.props: + return None + result = await GoodsInfo.filter(uuid__in=user.props.keys()).all() + data_list = [] + uuid2goods = {item.uuid: item for item in result} + column_name = ["-", "使用ID", "名称", "数量", "简介"] + for i, p in enumerate(user.props): + if prop := uuid2goods.get(p): + data_list.append( + [ + (ICON_PATH / prop.icon, 33, 33) if prop.icon else "", + i, + prop.goods_name, + user.props[p], + prop.goods_description, + ] + ) + + return await ImageTemplate.table_page( + f"{name}的道具仓库", "", column_name, data_list + ) + + @classmethod + async def my_cost(cls, user_id: str, platform: str | None = None) -> int: + """用户金币 + + 参数: + user_id: 用户id + platform: 平台. + + 返回: + int: 金币数量 + """ + user = await UserConsole.get_user(user_id, platform) + return user.gold + + @classmethod + async def build_shop_image(cls) -> BuildImage: + """制作商店图片 + + 返回: + BuildImage: 商店图片 + """ + goods_lst = await GoodsInfo.get_all_goods() + _dc = {} + font_h = BuildImage.get_text_size("正")[1] + h = 10 + _list: list[GoodsInfo] = [] + for goods in goods_lst: + if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time: + _list.append(goods) + # A = BuildImage(1100, h, color="#f9f6f2") + total_n = 0 + image_list = [] + for idx, goods in enumerate(_list): + name_image = BuildImage( + 580, 40, font_size=25, color="#e67b6b", font="CJGaoDeGuo.otf" + ) + await name_image.text( + (15, 0), f"{idx + 1}.{goods.goods_name}", center_type="height" + ) + await name_image.line((380, -5, 280, 45), "#a29ad6", 5) + await name_image.text((390, 0), "售价:", center_type="height") + if goods.goods_discount != 1: + discount_price = int(goods.goods_discount * goods.goods_price) + old_price_image = await BuildImage.build_text_image( + str(goods.goods_price), font_color=(194, 194, 194), size=15 + ) + await old_price_image.line( + ( + 0, + int(old_price_image.height / 2), + old_price_image.width + 1, + int(old_price_image.height / 2), + ), + (0, 0, 0), + ) + await name_image.paste(old_price_image, (440, 0)) + await name_image.text((440, 15), str(discount_price), (255, 255, 255)) + else: + await name_image.text( + (440, 0), + str(goods.goods_price), + (255, 255, 255), + center_type="height", + ) + _tmp = await BuildImage.build_text_image(str(goods.goods_price), size=25) + await name_image.text( + ( + 440 + _tmp.width, + 0, + ), + f" 金币", + center_type="height", + ) + des_image = None + font_img = BuildImage(600, 80, font_size=20, color="#a29ad6") + p = font_img.getsize("简介:")[0] + 20 + if goods.goods_description: + des_list = goods.goods_description.split("\n") + desc = "" + for des in des_list: + if font_img.getsize(des)[0] > font_img.width - p - 20: + msg = "" + tmp = "" + for i in range(len(des)): + if font_img.getsize(tmp)[0] < font_img.width - p - 20: + tmp += des[i] + else: + msg += tmp + "\n" + tmp = des[i] + desc += msg + if tmp: + desc += tmp + else: + desc += des + "\n" + if desc[-1] == "\n": + desc = desc[:-1] + des_image = await text2image(desc, color="#a29ad6") + goods_image = BuildImage( + 600, + (50 + des_image.height) if des_image else 50, + font_size=20, + color="#a29ad6", + font="CJGaoDeGuo.otf", + ) + if des_image: + await goods_image.text((15, 50), "简介:") + await goods_image.paste(des_image, (p, 50)) + await name_image.circle_corner(5) + await goods_image.paste(name_image, (0, 5), center_type="width") + await goods_image.circle_corner(20) + bk = BuildImage( + 1180, + (50 + des_image.height) if des_image else 50, + font_size=15, + color="#f9f6f2", + font="CJGaoDeGuo.otf", + ) + if goods.icon and (ICON_PATH / goods.icon).exists(): + icon = BuildImage(70, 70, background=ICON_PATH / goods.icon) + await bk.paste(icon) + await bk.paste(goods_image, (70, 0)) + n = 0 + _w = 650 + # 添加限时图标和时间 + if goods.goods_limit_time > 0: + n += 140 + _limit_time_logo = BuildImage( + 40, 40, background=f"{IMAGE_PATH}/other/time.png" + ) + await bk.paste(_limit_time_logo, (_w + 50, 0)) + _time_img = await BuildImage.build_text_image("限时!", size=23) + await bk.paste( + _time_img, + (_w + 90, 10), + ) + limit_time = time.strftime( + "%Y-%m-%d %H:%M", time.localtime(goods.goods_limit_time) + ).split() + y_m_d = limit_time[0] + _h_m = limit_time[1].split(":") + h_m = _h_m[0] + "时 " + _h_m[1] + "分" + await bk.text((_w + 55, 38), str(y_m_d)) + await bk.text((_w + 65, 57), str(h_m)) + _w += 140 + if goods.goods_discount != 1: + n += 140 + _discount_logo = BuildImage( + 30, 30, background=f"{IMAGE_PATH}/other/discount.png" + ) + await bk.paste(_discount_logo, (_w + 50, 10)) + _tmp = await BuildImage.build_text_image("折扣!", size=23) + await bk.paste(_tmp, (_w + 90, 15)) + _tmp = await BuildImage.build_text_image( + f"{10 * goods.goods_discount:.1f} 折", + size=30, + font_color=(85, 156, 75), + ) + await bk.paste(_tmp, (_w + 50, 44)) + _w += 140 + if goods.daily_limit != 0: + n += 140 + _daily_limit_logo = BuildImage( + 35, 35, background=f"{IMAGE_PATH}/other/daily_limit.png" + ) + await bk.paste(_daily_limit_logo, (_w + 50, 10)) + _tmp = await BuildImage.build_text_image( + "限购!", + size=23, + ) + await bk.paste(_tmp, (_w + 90, 20)) + _tmp = await BuildImage.build_text_image( + f"{goods.daily_limit}", size=30 + ) + await bk.paste(_tmp, (_w + 72, 45)) + if total_n < n: + total_n = n + if n: + await bk.line((650, -1, 650 + n, -1), "#a29ad6", 5) + # await bk.aline((650, 80, 650 + n, 80), "#a29ad6", 5) + + # 添加限时图标和时间 + image_list.append(bk) + # await A.apaste(bk, (0, current_h), True) + # current_h += 90 + h = 0 + current_h = 0 + for img in image_list: + h += img.height + 10 + A = BuildImage(1100, h, color="#f9f6f2") + for img in image_list: + await A.paste(img, (0, current_h)) + current_h += img.height + 10 + w = 950 + if total_n: + w += total_n + h = A.height + 230 + 100 + h = 1000 if h < 1000 else h + shop_logo = BuildImage(100, 100, background=f"{IMAGE_PATH}/other/shop_text.png") + shop = BuildImage(w, h, font_size=20, color="#f9f6f2") + await shop.paste(A, (20, 230)) + await shop.paste(shop_logo, (450, 30)) + await shop.text( + ( + int((1000 - shop.getsize("注【通过 序号 或者 商品名称 购买】")[0]) / 2), + 170, + ), + "注【通过 序号 或者 商品名称 购买】", + ) + await shop.text( + (20, h - 100), + "神秘药水\t\t售价:9999999金币\n\t\t鬼知道会有什么效果~", + ) + return shop diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py new file mode 100644 index 00000000..ac09256a --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -0,0 +1,162 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils + +from ._data_source import SignManage +from .goods_register import driver +from .utils import clear_sign_data_pic + +__plugin_meta__ = PluginMetadata( + name="签到", + description="每日签到,证明你在这里", + usage=""" + 每日签到 + 会影响色图概率和开箱次数,以及签到的随机道具获取 + 指令: + 签到 + 我的签到 + 好感度排行 + 好感度总排行 + * 签到时有 3% 概率 * 2 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + module="send_setu", + key="INITIAL_SETU_PROBABILITY", + value=0.7, + help="初始色图概率,总概率 = 初始色图概率 + 好感度", + default_value=0.7, + type=float, + ), + RegisterConfig( + key="MAX_SIGN_GOLD", + value=200, + help="签到好感度加成额外获得的最大金币数", + default_value=200, + type=int, + ), + RegisterConfig( + key="SIGN_CARD1_PROB", + value=0.2, + help="签到好感度双倍加持卡Ⅰ掉落概率", + default_value=0.2, + type=float, + ), + RegisterConfig( + key="SIGN_CARD2_PROB", + value=0.09, + help="签到好感度双倍加持卡Ⅲ掉落概率", + default_value=0.09, + type=float, + ), + RegisterConfig( + key="SIGN_CARD3_PROB", + value=0.05, + help="签到好感度双倍加持卡Ⅲ掉落概率", + default_value=0.05, + type=float, + ), + ], + limits=[PluginCdBlock()], + ).dict(), +) + + +_sign_matcher = on_alconna( + Alconna( + "签到", + Option("--my", action=store_true, help_text="我的签到"), + Option( + "-l|--list", + Args["num", int, 10], + action=store_true, + help_text="好感度排行", + ), + Option("-g|--global", action=store_true, help_text="全局排行"), + ), + priority=5, + block=True, +) + +_sign_matcher.shortcut( + "我的签到", + command="签到", + arguments=["--my"], + prefix=True, +) + +_sign_matcher.shortcut( + "好感度排行", + command="签到", + arguments=["--list"], + prefix=True, +) + +_sign_matcher.shortcut( + "好感度总排行", + command="签到", + arguments=["--list", "--global"], + prefix=True, +) + + +@_sign_matcher.assign("$main") +async def _(session: EventSession, arparma: Arparma, nickname: str = UserName()): + if session.id1: + if path := await SignManage.sign(session, nickname): + logger.info("签到成功", arparma.header_result, session=session) + await MessageUtils.build_message(path).finish() + return MessageUtils.build_message("用户id为空...").send() + + +@_sign_matcher.assign("my") +async def _(session: EventSession, arparma: Arparma, nickname: str = UserName()): + if session.id1: + if image := await SignManage.sign(session, nickname, True): + logger.info("查看我的签到", arparma.header_result, session=session) + await MessageUtils.build_message(image).finish() + return MessageUtils.build_message("用户id为空...").send() + + +@_sign_matcher.assign("list") +async def _(session: EventSession, arparma: Arparma, num: int): + gid = session.id3 or session.id2 + if not arparma.find("global") and not gid: + await MessageUtils.build_message( + "私聊中无法查看 '好感度排行',请发送 '好感度总排行'" + ).finish() + if session.id1: + if arparma.find("global"): + gid = None + if image := await SignManage.rank(session.id1, num, gid): + logger.info("查看签到排行", arparma.header_result, session=session) + await MessageUtils.build_message(image).finish() + return MessageUtils.build_message("用户id为空...").send() + + +@scheduler.scheduled_job( + "interval", + hours=1, +) +async def _(): + try: + clear_sign_data_pic() + logger.info("清理日常签到图片数据数据完成...", "签到") + except Exception as e: + logger.error(f"清理日常签到图片数据数据失败...", e=e) diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py new file mode 100644 index 00000000..539972c8 --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -0,0 +1,181 @@ +import random +import secrets +from datetime import datetime +from pathlib import Path + +import pytz +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.sign_log import SignLog +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage, ImageTemplate +from zhenxun.utils.utils import get_user_avatar + +from ._random_event import random_event +from .utils import get_card + +ICON_PATH = IMAGE_PATH / "_icon" + +PLATFORM_PATH = { + "dodo": ICON_PATH / "dodo.png", + "discord": ICON_PATH / "discord.png", + "kaiheila": ICON_PATH / "kook.png", + "qq": ICON_PATH / "qq.png", +} + + +class SignManage: + + @classmethod + async def rank( + cls, user_id: str, num: int, group_id: str | None = None + ) -> BuildImage: + """好感度排行 + + 参数: + user_id: 用户id + num: 排行榜数量 + group_id: 群组id + + 返回: + BuildImage: 构造图片 + """ + query = SignUser + if group_id: + user_list = await GroupInfoUser.filter(group_id=group_id).values_list( + "user_id", flat=True + ) + query = query.filter(user_id__in=user_list) + all_list = ( + await query.annotate() + .order_by("-impression") + .values_list("user_id", flat=True) + ) + index = all_list.index(user_id) + 1 # type: ignore + user_list = await query.annotate().order_by("-impression").limit(num).all() + user_id_list = [u.user_id for u in user_list] + column_name = ["排名", "-", "名称", "好感度", "签到次数", "平台"] + friend_list = await FriendUser.filter(user_id__in=user_id_list).values_list( + "user_id", "user_name" + ) + uid2name = {f[0]: f[1] for f in friend_list} + group_member_list = await GroupInfoUser.filter( + user_id__in=user_id_list + ).values_list("user_id", "user_name") + for gm in group_member_list: + uid2name[gm[0]] = gm[1] + data_list = [] + for i, user in enumerate(user_list): + bytes = await get_user_avatar(user.user_id) + data_list.append( + [ + f"{i+1}", + (bytes, 30, 30) if user.platform == "qq" else "", + uid2name.get(user.user_id), + user.impression, + user.sign_count, + (PLATFORM_PATH.get(user.platform), 30, 30), + ] + ) + if group_id: + title = "好感度群组内排行" + tip = f"你的排名在本群第 {index} 位哦!" + else: + title = "好感度全局排行" + tip = f"你的排名在全局第 {index} 位哦!" + return await ImageTemplate.table_page(title, tip, column_name, data_list) + + @classmethod + async def sign( + cls, session: EventSession, nickname: str, is_card_view: bool = False + ) -> Path | None: + """签到 + + 参数: + session: Session + nickname: 用户昵称 + is_card_view: 是否展示卡片 + + 返回: + Path: 卡片路径 + """ + if not session.id1: + return None + now = datetime.now(pytz.timezone("Asia/Shanghai")) + user_console = await UserConsole.get_user(session.id1, session.platform) + user, _ = await SignUser.get_or_create( + user_id=session.id1, + defaults={"user_console": user_console, "platform": session.platform}, + ) + new_log = ( + await SignLog.filter(user_id=session.id1).order_by("-create_time").first() + ) + log_time = None + if new_log: + log_time = new_log.create_time.astimezone( + pytz.timezone("Asia/Shanghai") + ).date() + if not is_card_view: + if not new_log or (log_time and log_time != now.date()): + return await cls._handle_sign_in(user, nickname, session) + return await get_card( + user, nickname, -1, user_console.gold, "", is_card_view=is_card_view + ) + + @classmethod + async def _handle_sign_in( + cls, + user: SignUser, + nickname: str, + session: EventSession, + ) -> Path: + """签到处理 + + 参数: + user: SignUser + nickname: 用户昵称 + session: Session + + 返回: + Path: 卡片路径 + """ + impression_added = (secrets.randbelow(99) + 1) / 100 + rand = random.random() + add_probability = float(user.add_probability) + specify_probability = user.specify_probability + if rand + add_probability > 0.97: + impression_added *= 2 + elif rand < specify_probability: + impression_added *= 2 + await SignUser.sign(user, impression_added, session.bot_id, session.platform) + gold = random.randint(1, 100) + gift = random_event(float(user.impression)) + if isinstance(gift, int): + gold += gift + await UserConsole.add_gold( + user.user_id, gold + gift, "sign_in", session.platform + ) + gift = f"额外金币 +{gift}" + else: + await UserConsole.add_gold(user.user_id, gold, "sign_in", session.platform) + await UserConsole.add_props(user.user_id, gift, 1, session.platform) + gift += " + 1" + logger.info( + f"签到成功. score: {user.impression:.2f} " + f"(+{impression_added:.2f}).获取金币/道具: {gold}", + "签到", + session=session, + ) + return await get_card( + user, + nickname, + impression_added, + gold, + gift, + rand + add_probability > 0.97 or rand < specify_probability, + ) diff --git a/plugins/sign_in/random_event.py b/zhenxun/builtin_plugins/sign_in/_random_event.py old mode 100755 new mode 100644 similarity index 68% rename from plugins/sign_in/random_event.py rename to zhenxun/builtin_plugins/sign_in/_random_event.py index b451d848..32d133c1 --- a/plugins/sign_in/random_event.py +++ b/zhenxun/builtin_plugins/sign_in/_random_event.py @@ -1,31 +1,33 @@ -import random -from typing import Tuple, Union - -from configs.config import Config - -PROB_DATA = None - - -def random_event(impression: float) -> Tuple[Union[str, int], str]: - """ - 签到随机事件 - :param impression: 好感度 - :return: 额外奖励 和 类型 - """ - global PROB_DATA - if not PROB_DATA: - PROB_DATA = { - Config.get_config("sign_in", "SIGN_CARD3_PROB"): "好感度双倍加持卡Ⅲ", - Config.get_config("sign_in", "SIGN_CARD2_PROB"): "好感度双倍加持卡Ⅱ", - Config.get_config("sign_in", "SIGN_CARD1_PROB"): "好感度双倍加持卡Ⅰ", - } - rand = random.random() - impression / 1000 - for prob in PROB_DATA.keys(): - if rand <= prob: - return PROB_DATA[prob], "props" - gold = random.randint( - 1, random.randint(1, int(1 if impression < 1 else impression)) - ) - max_sign_gold = Config.get_config("sign_in", "MAX_SIGN_GOLD") - gold = max_sign_gold if gold > max_sign_gold else gold - return gold, "gold" +import random + +from zhenxun.configs.config import Config + +PROB_DATA = None + + +def random_event(impression: float) -> str | int: + """签到随机事件 + + 参数: + impression: 好感度 + + 返回: + 额外奖励 和 类型 + """ + global PROB_DATA + if not PROB_DATA: + PROB_DATA = { + Config.get_config("sign_in", "SIGN_CARD3_PROB"): "好感度双倍加持卡Ⅲ", + Config.get_config("sign_in", "SIGN_CARD2_PROB"): "好感度双倍加持卡Ⅱ", + Config.get_config("sign_in", "SIGN_CARD1_PROB"): "好感度双倍加持卡Ⅰ", + } + rand = random.random() - impression / 1000 + for prob in PROB_DATA.keys(): + if rand <= prob: + return PROB_DATA[prob] + gold = random.randint( + 1, random.randint(1, int(1 if impression < 1 else impression)) + ) + max_sign_gold = Config.get_config("sign_in", "MAX_SIGN_GOLD") + gold = max_sign_gold if gold > max_sign_gold else gold + return gold diff --git a/zhenxun/builtin_plugins/sign_in/config.py b/zhenxun/builtin_plugins/sign_in/config.py new file mode 100644 index 00000000..e2bfdbc6 --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/config.py @@ -0,0 +1,49 @@ +from zhenxun.configs.path_config import IMAGE_PATH + +SIGN_RESOURCE_PATH = IMAGE_PATH / "sign" / "sign_res" +SIGN_TODAY_CARD_PATH = IMAGE_PATH / "sign" / "today_card" +SIGN_BORDER_PATH = SIGN_RESOURCE_PATH / "border" +SIGN_BACKGROUND_PATH = SIGN_RESOURCE_PATH / "background" + +SIGN_BORDER_PATH.mkdir(exist_ok=True, parents=True) +SIGN_BACKGROUND_PATH.mkdir(exist_ok=True, parents=True) + + +lik2relation = { + "0": "路人", + "1": "陌生", + "2": "初识", + "3": "普通", + "4": "熟悉", + "5": "信赖", + "6": "相知", + "7": "厚谊", + "8": "亲密", +} + +level2attitude = { + "0": "排斥", + "1": "警惕", + "2": "可以交流", + "3": "一般", + "4": "是个好人", + "5": "好朋友", + "6": "可以分享小秘密", + "7": "喜欢", + "8": "恋人", +} + +weekdays = {1: "Mon", 2: "Tue", 3: "Wed", 4: "Thu", 5: "Fri", 6: "Sat", 7: "Sun"} + +lik2level = { + 9999: "9", + 400: "8", + 270: "7", + 200: "6", + 140: "5", + 90: "4", + 50: "3", + 25: "2", + 10: "1", + 0: "0", +} diff --git a/zhenxun/builtin_plugins/sign_in/goods_register.py b/zhenxun/builtin_plugins/sign_in/goods_register.py new file mode 100644 index 00000000..b73cffc0 --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/goods_register.py @@ -0,0 +1,72 @@ +from decimal import Decimal + +import nonebot +from nonebot.drivers import Driver +from nonebot_plugin_session import EventSession + +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.decorator.shop import NotMeetUseConditionsException, shop_register + +driver: Driver = nonebot.get_driver() + + +# @driver.on_startup +# async def _(): +# """ +# 导入内置的三个商品 +# """ + + +@shop_register( + name=("好感度双倍加持卡Ⅰ", "好感度双倍加持卡Ⅱ", "好感度双倍加持卡Ⅲ"), + price=(30, 150, 250), + des=( + "下次签到双倍好感度概率 + 10%(谁才是真命天子?)(同类商品将覆盖)", + "下次签到双倍好感度概率 + 20%(平平庸庸)(同类商品将覆盖)", + "下次签到双倍好感度概率 + 30%(金币才是真命天子!)(同类商品将覆盖)", + ), + load_status=True, + icon=( + "favorability_card_1.png", + "favorability_card_2.png", + "favorability_card_3.png", + ), + **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, # type: ignore +) +async def _(session: EventSession, user_id: int, group_id: int, prob: float): + if session.id1: + user_console = await UserConsole.get_user(session.id1, session.platform) + user, _ = await SignUser.get_or_create( + user_id=user_id, + defaults={"platform": session.platform, "user_console": user_console}, + ) + user.add_probability = Decimal(prob) + await user.save(update_fields=["add_probability"]) + + +@shop_register( + name="测试道具A", + price=99, + des="随便侧而出", + load_status=False, + icon="sword.png", +) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "使用测试道具") + + +@shop_register.before_handle(name="测试道具A", load_status=False) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "第一个使用前函数(before handle)") + + +@shop_register.before_handle(name="测试道具A", load_status=False) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "第二个使用前函数(before handle)222") + raise NotMeetUseConditionsException("太笨了!") # 抛出异常,阻断使用,并返回信息 + + +@shop_register.after_handle(name="测试道具A", load_status=False) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "第一个使用后函数(after handle)") diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py new file mode 100644 index 00000000..b8fe2853 --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -0,0 +1,335 @@ +import os +import random +from datetime import datetime +from io import BytesIO +from pathlib import Path + +import nonebot +import pytz +from nonebot.drivers import Driver + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.sign_log import SignLog +from zhenxun.models.sign_user import SignUser +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import get_user_avatar + +from .config import ( + SIGN_BACKGROUND_PATH, + SIGN_BORDER_PATH, + SIGN_RESOURCE_PATH, + SIGN_TODAY_CARD_PATH, + level2attitude, + lik2level, + lik2relation, +) + +driver: Driver = nonebot.get_driver() + + +@driver.on_startup +async def init_image(): + SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) + SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) + await generate_progress_bar_pic() + clear_sign_data_pic() + + +async def get_card( + user: SignUser, + nickname: str, + add_impression: float, + gold: int | None, + gift: str, + is_double: bool = False, + is_card_view: bool = False, +) -> Path: + """获取好感度卡片 + + 参数: + user: SignUser + nickname: 用户昵称 + impression: 新增的好感度 + gold: 金币 + gift: 礼物 + is_double: 是否触发双倍. + is_card_view: 是否展示好感度卡片. + + 返回: + Path: 卡片路径 + """ + user_id = user.user_id + date = datetime.now().date() + _type = "view" if is_card_view else "sign" + file_name = f"{user_id}_{_type}_{date}.png" + view_name = f"{user_id}_view_{date}.png" + card_file = Path(SIGN_TODAY_CARD_PATH) / file_name + if card_file.exists(): + return IMAGE_PATH / "sign" / "today_card" / file_name + else: + if add_impression == -1: + card_file = Path(SIGN_TODAY_CARD_PATH) / view_name + if card_file.exists(): + return card_file + is_card_view = True + return await _generate_card( + user, nickname, add_impression, gold, gift, is_double, is_card_view + ) + + +async def _generate_card( + user: SignUser, + nickname: str, + impression: float, + gold: int | None, + gift: str, + is_double: bool = False, + is_card_view: bool = False, +) -> Path: + """生成签到卡片 + + 参数: + user: SignUser + nickname: 用户昵称 + impression: 新增的好感度 + gold: 金币 + gift: 礼物 + is_double: 是否触发双倍. + is_card_view: 是否展示好感度卡片. + + 返回: + Path: 卡片路径 + """ + ava_bk = BuildImage(140, 140, (255, 255, 255, 0)) + ava_border = BuildImage( + 140, + 140, + background=SIGN_BORDER_PATH / "ava_border_01.png", + ) + if user.platform == "qq" and (byt := await get_user_avatar(user.user_id)): + ava = BuildImage(107, 107, background=BytesIO(byt)) + else: + ava = BuildImage(107, 107, (0, 0, 0)) + await ava.circle() + await ava_bk.paste(ava, (19, 18)) + await ava_bk.paste(ava_border, center_type="center") + add_impression = impression + impression = float(user.impression) + info_img = BuildImage(250, 150, color=(255, 255, 255, 0), font_size=15) + level, next_impression, previous_impression = get_level_and_next_impression( + impression + ) + interpolation = next_impression - impression + if level == "9": + level = "8" + interpolation = 0 + await info_img.text((0, 0), f"· 好感度等级:{level} [{lik2relation[level]}]") + await info_img.text((0, 20), f"· {NICKNAME}对你的态度:{level2attitude[level]}") + await info_img.text((0, 40), f"· 距离升级还差 {interpolation:.2f} 好感度") + + bar_bk = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar_white.png") + bar = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar.png") + ratio = 1 - (next_impression - user.impression) / ( + next_impression - previous_impression + ) + if next_impression == 0: + ratio = 0 + await bar.resize(width=int(bar.width * ratio) or 1, height=bar.height) + await bar_bk.paste(bar) + font_size = 30 + if "好感度双倍加持卡" in gift: + font_size = 20 + gift_border = BuildImage( + 270, + 100, + background=SIGN_BORDER_PATH / "gift_border_02.png", + font_size=font_size, + ) + await gift_border.text((0, 0), gift, center_type="center") + + bk = BuildImage( + 876, + 424, + background=SIGN_BACKGROUND_PATH + / random.choice(os.listdir(SIGN_BACKGROUND_PATH)), + font_size=25, + ) + A = BuildImage(876, 274, background=SIGN_RESOURCE_PATH / "white.png") + line = BuildImage(2, 180, color="black") + await A.transparent(2) + await A.paste(ava_bk, (25, 80)) + await A.paste(line, (200, 70)) + nickname_img = await BuildImage.build_text_image( + nickname, size=50, font_color=(255, 255, 255) + ) + user_console = await user.user_console + if user_console and user_console.uid: + uid = f"{user_console.uid}".rjust(12, "0") + uid = uid[:4] + " " + uid[4:8] + " " + uid[8:] + else: + uid = "XXXX XXXX XXXX" + uid_img = await BuildImage.build_text_image( + f"UID: {uid}", size=30, font_color=(255, 255, 255) + ) + image1 = await bk.build_text_image("Accumulative check-in for", bk.font, size=30) + image2 = await bk.build_text_image("days", bk.font, size=30) + sign_day_img = await BuildImage.build_text_image( + f"{user.sign_count}", size=40, font_color=(211, 64, 33) + ) + tip_width = image1.width + image2.width + sign_day_img.width + 60 + tip_height = max([image1.height, image2.height, sign_day_img.height]) + tip_image = BuildImage(tip_width, tip_height, (255, 255, 255, 0)) + await tip_image.paste(image1, (0, 7)) + await tip_image.paste(sign_day_img, (image1.width + 7, 0)) + await tip_image.paste(image2, (image1.width + sign_day_img.width + 15, 7)) + + lik_text1_img = await BuildImage.build_text_image("当前", size=20) + lik_text2_img = await BuildImage.build_text_image( + f"好感度:{user.impression:.2f}", size=30 + ) + watermark = await BuildImage.build_text_image( + f"{NICKNAME}@{datetime.now().year}", size=15, font_color=(155, 155, 155) + ) + today_data = BuildImage(300, 300, color=(255, 255, 255, 0), font_size=20) + if is_card_view: + today_sign_text_img = await BuildImage.build_text_image("", size=30) + value_list = ( + await SignUser.annotate() + .order_by("-impression") + .values_list("user_id", flat=True) + ) + index = value_list.index(user.user_id) + 1 # type: ignore + rank_img = await BuildImage.build_text_image( + f"* 好感度排名第 {index} 位", size=30 + ) + await A.paste(rank_img, ((A.width - rank_img.width - 32), 20)) + last_log = ( + await SignLog.filter(user_id=user.user_id).order_by("create_time").first() + ) + last_date = "从未" + if last_log: + last_date = last_log.create_time.astimezone( + pytz.timezone("Asia/Shanghai") + ).date() + await today_data.text( + (0, 0), + f"上次签到日期:{last_date}", + ) + await today_data.text((0, 25), f"总金币:{gold}") + default_setu_prob = ( + Config.get_config("send_setu", "INITIAL_SETU_PROBABILITY") * 100 # type: ignore + ) + await today_data.text( + (0, 50), + f"色图概率:{(default_setu_prob + float(user.impression) if user.impression < 100 else 100):.2f}%", + ) + await today_data.text((0, 75), f"开箱次数:{(20 + int(user.impression / 3))}") + _type = "view" + else: + await A.paste(gift_border, (570, 140)) + today_sign_text_img = await BuildImage.build_text_image("今日签到", size=30) + if is_double: + await today_data.text((0, 0), f"好感度 + {add_impression / 2:.2f} × 2") + else: + await today_data.text((0, 0), f"好感度 + {add_impression:.2f}") + await today_data.text((0, 25), f"金币 + {gold}") + _type = "sign" + current_date = datetime.now() + current_datetime_str = current_date.strftime("%Y-%m-%d %a %H:%M:%S") + data = current_date.date() + data_img = await BuildImage.build_text_image( + f"时间:{current_datetime_str}", size=20 + ) + await bk.paste(nickname_img, (30, 15)) + await bk.paste(uid_img, (30, 85)) + await bk.paste(A, (0, 150)) + # await bk.text((30, 167), "Accumulative check-in for") + # _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.width + 45 + # await bk.paste(sign_day_img, (398, 158)) + # await bk.text((_x, 167), "days") + await bk.paste(tip_image, (10, 167)) + await bk.paste(data_img, (220, 370)) + await bk.paste(lik_text1_img, (220, 240)) + await bk.paste(lik_text2_img, (262, 234)) + await bk.paste(bar_bk, (225, 275)) + await bk.paste(info_img, (220, 305)) + await bk.paste(today_sign_text_img, (550, 180)) + await bk.paste(today_data, (580, 220)) + await bk.paste(watermark, (15, 400)) + await bk.save(SIGN_TODAY_CARD_PATH / f"{user.user_id}_{_type}_{data}.png") + return IMAGE_PATH / "sign" / "today_card" / f"{user.user_id}_{_type}_{data}.png" + + +async def generate_progress_bar_pic(): + """ + 初始化进度条图片 + """ + bg_2 = (254, 1, 254) + bg_1 = (0, 245, 246) + + bk = BuildImage(1000, 50) + img_x = BuildImage(50, 50, color=bg_2) + await img_x.circle() + await img_x.crop((25, 0, 50, 50)) + img_y = BuildImage(50, 50, color=bg_1) + await img_y.circle() + await img_y.crop((0, 0, 25, 50)) + A = BuildImage(950, 50) + width, height = A.size + + step_r = (bg_2[0] - bg_1[0]) / width + step_g = (bg_2[1] - bg_1[1]) / width + step_b = (bg_2[2] - bg_1[2]) / width + + for y in range(0, width): + bg_r = round(bg_1[0] + step_r * y) + bg_g = round(bg_1[1] + step_g * y) + bg_b = round(bg_1[2] + step_b * y) + for x in range(0, height): + await A.point((y, x), fill=(bg_r, bg_g, bg_b)) + await bk.paste(img_y, (0, 0)) + await bk.paste(A, (25, 0)) + await bk.paste(img_x, (975, 0)) + await bk.save(SIGN_RESOURCE_PATH / "bar.png") + + A = BuildImage(950, 50) + bk = BuildImage(1000, 50) + img_x = BuildImage(50, 50) + await img_x.circle() + await img_x.crop((25, 0, 50, 50)) + img_y = BuildImage(50, 50) + await img_y.circle() + await img_y.crop((0, 0, 25, 50)) + await bk.paste(img_y, (0, 0)) + await bk.paste(A, (25, 0)) + await bk.paste(img_x, (975, 0)) + await bk.save(SIGN_RESOURCE_PATH / "bar_white.png") + + +def get_level_and_next_impression(impression: float) -> tuple[str, int, int]: + """获取当前好感等级与下一等级的差距 + + 参数: + impression: 好感度 + + 返回: + tuple[str, int, int]: 好感度等级中文,好感度等级,下一等级好感差距 + """ + if impression == 0: + return lik2level[10], 10, 0 + keys = list(lik2level.keys()) + for i in range(len(keys)): + if impression > keys[i]: + return lik2level[keys[i]], keys[i - 1], keys[i] + return lik2level[10], 10, 0 + + +def clear_sign_data_pic(): + """ + 清空当前签到图片数据 + """ + date = datetime.now().date() + for file in os.listdir(SIGN_TODAY_CARD_PATH): + if str(date) not in file: + os.remove(SIGN_TODAY_CARD_PATH / file) diff --git a/zhenxun/builtin_plugins/superuser/__init__.py b/zhenxun/builtin_plugins/superuser/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py new file mode 100644 index 00000000..7395ff90 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py @@ -0,0 +1,64 @@ +from typing import Annotated + +from nonebot import on_command +from nonebot.adapters import Bot +from nonebot.params import Command +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from ._data_source import BroadcastManage + +__plugin_meta__ = PluginMetadata( + name="广播", + description="昭告天下!", + usage=""" + 广播 [消息] [图片] + 示例:广播 你们好! + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + configs=[ + RegisterConfig( + module="_task", + key="DEFAULT_BROADCAST", + value=True, + help="被动 广播 进群默认开关状态", + default_value=True, + type=bool, + ) + ], + tasks=[Task(module="broadcast", name="广播")], + ).dict(), +) + +_matcher = on_command("广播", priority=1, permission=SUPERUSER, block=True) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + message: UniMsg, + command: Annotated[tuple[str, ...], Command()], +): + for msg in message: + if isinstance(msg, alcText) and msg.text.strip().startswith(command[0]): + msg.text = msg.text.replace(command[0], "", 1).strip() + break + await MessageUtils.build_message("正在发送..请等一下哦!").send() + count, error_count = await BroadcastManage.send(bot, message, session) + result = f"成功广播 {count} 个群组" + if error_count: + result += f"\n广播失败 {error_count} 个群组" + await MessageUtils.build_message(f"发送广播完成!\n{result}").send(reply_to=True) + logger.info(f"发送广播信息: {message}", "广播", session=session) diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py new file mode 100644 index 00000000..617f0d44 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -0,0 +1,74 @@ +import nonebot_plugin_alconna as alc +from nonebot.adapters import Bot + +# from nonebot.adapters.discord import Bot as DiscordBot +# from nonebot.adapters.dodo import Bot as DodoBot +# from nonebot.adapters.kaiheila import Bot as KaiheilaBot +# from nonebot.adapters.onebot.v11 import Bot as v11Bot +# from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot_plugin_alconna import Image, UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + + +class BroadcastManage: + + @classmethod + async def send( + cls, bot: Bot, message: UniMsg, session: EventSession + ) -> tuple[int, int]: + """发送广播消息 + + 参数: + bot: Bot + message: 消息内容 + session: Session + + 返回: + tuple[int, int]: 发送成功的群组数量, 发送失败的群组数量 + """ + message_list = [] + for msg in message: + if isinstance(msg, alc.Image) and msg.url: + message_list.append(Image(url=msg.url)) + elif isinstance(msg, alc.Text): + message_list.append(msg.text) + group_list, _ = await PlatformUtils.get_group_list(bot) + if group_list: + error_count = 0 + for group in group_list: + try: + if not await TaskInfo.is_block( + group.group_id, + "broadcast", # group.channel_id + ): + target = PlatformUtils.get_target( + bot, None, group.channel_id or group.group_id + ) + if target: + await MessageUtils.build_message(message_list).send( + target, bot + ) + logger.debug( + "发送成功", + "广播", + session=session, + target=f"{group.group_id}:{group.channel_id}", + ) + else: + logger.warning("target为空", "广播", session=session) + except Exception as e: + error_count += 1 + logger.error( + "发送失败", + "广播", + session=session, + target=f"{group.group_id}:{group.channel_id}", + e=e, + ) + return len(group_list) - error_count, error_count + return 0, 0 diff --git a/zhenxun/builtin_plugins/superuser/clear_data.py b/zhenxun/builtin_plugins/superuser/clear_data.py new file mode 100644 index 00000000..10770bf9 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/clear_data.py @@ -0,0 +1,98 @@ +import os +import time + +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.utils import run_sync +from nonebot_plugin_alconna import Alconna, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import ResourceDirManager + +__plugin_meta__ = PluginMetadata( + name="清理数据", + description="清理已添加的临时文件夹中的数据", + usage=""" + 清理临时数据 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_matcher = on_alconna( + Alconna("清理临时数据"), + rule=to_me(), + permission=SUPERUSER, + priority=5, + block=True, +) + + +ResourceDirManager.add_temp_dir(TEMP_PATH, True) + + +@_matcher.handle() +async def _(session: EventSession): + await MessageUtils.build_message("开始清理临时数据...").send() + size = await _clear_data() + await MessageUtils.build_message( + "共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024) + ).send() + logger.info( + "清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), + session=session, + ) + + +@run_sync +def _clear_data() -> float: + logger.debug("开始清理临时文件...") + size = 0 + dir_list = [dir_ for dir_ in ResourceDirManager.temp_path if dir_.exists()] + for dir_ in dir_list: + logger.debug(f"尝试清理文件夹: {dir_.absolute()}", "清理临时数据") + dir_size = 0 + for file in os.listdir(dir_): + file = dir_ / file + if file.is_file(): + try: + if time.time() - os.path.getatime(file) > 10: + file_size = os.path.getsize(file) + file.unlink() + size += file_size + dir_size += file_size + logger.debug(f"移除临时文件: {file.absolute()}", "清理临时数据") + except Exception as e: + logger.error( + f"清理临时数据错误,临时文件夹: {dir_.absolute()}...", + "清理临时数据", + e=e, + ) + logger.debug( + "清理临时文件夹大小: {:.2f}MB".format(size / 1024 / 1024), "清理临时数据" + ) + return float(size) + + +@scheduler.scheduled_job( + "cron", + hour=1, + minute=1, +) +async def _(): + size = await _clear_data() + logger.info( + "自动清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), + "定时任务", + ) diff --git a/zhenxun/builtin_plugins/superuser/exec_sql.py b/zhenxun/builtin_plugins/superuser/exec_sql.py new file mode 100644 index 00000000..e99cd8f6 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/exec_sql.py @@ -0,0 +1,103 @@ +from nonebot import on_command +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession +from tortoise import Tortoise + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.db_context import TestSQL +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="数据库操作", + description="执行sql语句与查看表", + usage=""" + 查看所有表 + exec [sql语句] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + +_matcher = on_command( + "exec", + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + +_table_matcher = on_command( + "查看所有表", + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + +SELECT_TABLE_SQL = """ +select a.tablename as name,d.description as desc from pg_tables a + left join pg_class c on relname=tablename + left join pg_description d on oid=objoid and objsubid=0 where a.schemaname = 'public' +""" + + +@_matcher.handle() +async def _(session: EventSession, message: UniMsg): + sql_text = message.extract_plain_text().strip() + if sql_text.startswith("exec"): + sql_text = sql_text[4:].strip() + if not sql_text: + await MessageUtils.build_message("需要执行的的SQL语句!").finish() + logger.info(f"执行SQL语句: {sql_text}", "exec", session=session) + try: + if not sql_text.lower().startswith("select"): + await TestSQL.raw(sql_text) + else: + db = Tortoise.get_connection("default") + res = await db.execute_query_dict(sql_text) + _column = [] + for r in res: + if len(r) > len(_column): + _column = r.keys() + data_list = [] + for r in res: + data = [] + for c in _column: + data.append(r.get(c)) + data_list.append(data) + table = await ImageTemplate.table_page( + "EXEC", f"总共有 {len(data_list)} 条数据捏", list(_column), data_list + ) + await MessageUtils.build_message(table).send() + except Exception as e: + logger.error("执行 SQL 语句失败...", session=session, e=e) + await MessageUtils.build_message(f"执行 SQL 语句失败... {type(e)}").finish() + await MessageUtils.build_message("执行 SQL 语句成功!").finish() + + +@_table_matcher.handle() +async def _(session: EventSession): + try: + db = Tortoise.get_connection("default") + query = await db.execute_query_dict(SELECT_TABLE_SQL) + column_name = ["表名", "简介"] + data_list = [] + for table in query: + data_list.append([table["name"], table["desc"]]) + logger.info("查看数据库所有表", "查看所有表", session=session) + table = await ImageTemplate.table_page( + "数据库表", f"总共有 {len(data_list)} 张表捏", column_name, data_list + ) + await MessageUtils.build_message(table).send() + except Exception as e: + logger.error("获取表数据失败...", session=session, e=e) + await MessageUtils.build_message(f"获取表数据失败... {type(e)}").send() diff --git a/zhenxun/builtin_plugins/superuser/fg_manage.py b/zhenxun/builtin_plugins/superuser/fg_manage.py new file mode 100644 index 00000000..e2ff51d7 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/fg_manage.py @@ -0,0 +1,99 @@ +from nonebot.adapters import Bot +from nonebot.adapters.kaiheila.exception import ApiNotAvailable +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import admin_check, ensure_group + +__plugin_meta__ = PluginMetadata( + name="好友群组列表", + description="查看好友群组列表以", + usage=""" + 查看所有好友 + 查看所有群组 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + +_friend_matcher = on_alconna( + Alconna("好友列表"), + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + +_group_matcher = on_alconna( + Alconna("群组列表"), + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + +# _friend_handle_matcher = on_alconna( +# Alconna( +# "好友操作", +# Subcommand("delete", Args["uid", str], help_text="删除好友"), +# Subcommand("send", Args["uid", str]["message", str], help_text="发送消息"), +# ) +# ) + +# _group_handle_matcher = on_alconna( +# Alconna( +# "群组操作", +# Subcommand("delete", Args["gid", str], help_text="删除好友"), +# Subcommand("send", Args["gid", str]["message", str], help_text="发送消息"), +# ) +# ) + + +@_friend_matcher.handle() +async def _( + bot: Bot, + session: EventSession, +): + try: + # TODO: 其他adapter的好友api + fl = await bot.get_friend_list() + msg = ["{user_id} {nickname}".format_map(g) for g in fl] + msg = "\n".join(msg) + msg = f"| UID | 昵称 | 共{len(fl)}个好友\n" + msg + await MessageUtils.build_message(msg).send() + logger.info("查看好友列表", "好友列表", session=session) + except (ApiNotAvailable, AttributeError) as e: + await MessageUtils.build_message("Api未实现...").send() + except Exception as e: + logger.error("好友列表发生错误", "好友列表", session=session, e=e) + await MessageUtils.build_message("其他未知错误...").send() + + +@_group_matcher.handle() +async def _( + bot: Bot, + session: EventSession, +): + try: + # TODO: 其他adapter的群组api + gl = await bot.get_group_list() + msg = ["{group_id} {group_name}".format_map(g) for g in gl] + msg = "\n".join(msg) + msg = f"| GID | 名称 | 共{len(gl)}个群组\n" + msg + await MessageUtils.build_message(msg).send() + logger.info("查看群组列表", "群组列表", session=session) + except (ApiNotAvailable, AttributeError) as e: + await MessageUtils.build_message("Api未实现...").send() + except Exception as e: + logger.error("查看群组列表发生错误", "群组列表", session=session, e=e) + await MessageUtils.build_message("其他未知错误...").send() diff --git a/zhenxun/builtin_plugins/superuser/group_manage.py b/zhenxun/builtin_plugins/superuser/group_manage.py new file mode 100644 index 00000000..41044f94 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/group_manage.py @@ -0,0 +1,207 @@ +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.params import Depends +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_State +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + Subcommand, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="管理群操作", + description="管理群操作", + usage=""" + 群权限 | 群白名单 | 退出群 操作 + 退群,添加/删除群白名单,添加/删除群认证,当在群组中这五个命令且没有指定群号时,默认指定当前群组 + 指令: + 格式: + group-manage modify-level [权限等级] ?[群组Id] : 修改群权限 + group-manage super-handle [群组Id] [--del 删除操作] : 添加/删除群白名单 + group-manage auth-handle [群组Id] [--del 删除操作] : 添加/删除群认证 + group-manage del-group [群组Id] : 退出指定群 + + 快捷: + group-manage modify-level : 修改群权限 + group-manage super-handle : 添加/删除群白名单 + group-manage auth-handle : 添加/删除群认证 + group-manage del-group : 退群 + + 示例: + 修改群权限 7 : 在群组中修改当前群组权限为7 + 修改群权限 7 1234556 : 修改 123456 群组的权限等级为7 + 添加/删除群白名单 1234567 : 添加/删除 1234567 为群白名单 + 添加/删除群认证 1234567 : 添加/删除 1234567 为群认证 + 退群 12344566 : 退出指定群组 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "group-manage", + Subcommand( + "modify-level", Args["level", int]["group_id?", int], help_text="修改群权限" + ), + Subcommand( + "super-handle", + Option("--del", action=store_true, help_text="删除"), + Args["group_id", int], + help_text="添加/删除群白名单", + ), + Subcommand( + "auth-handle", + Option("--del", action=store_true, help_text="删除"), + Args["group_id", int], + help_text="添加/删除群认证", + ), + Subcommand("del-group", Args["group_id", int], help_text="退出群组"), + ), + permission=SUPERUSER, + priority=1, + block=True, +) + +_matcher.shortcut( + r"修改群权限\s?(?P-?\d+)\s?(?P\d+)?", + command="group-manage", + arguments=["modify-level", "{level}", "{group_id}"], + prefix=True, +) + +_matcher.shortcut( + "添加群白名单", + command="group-manage", + arguments=["super-handle", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + "删除群白名单", + command="group-manage", + arguments=["super-handle", "{%0}", "--del"], + prefix=True, +) + +_matcher.shortcut( + "添加群认证", + command="group-manage", + arguments=["auth-handle", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + "删除群认证", + command="group-manage", + arguments=["auth-handle", "{%0}", "--del"], + prefix=True, +) + +_matcher.shortcut( + "退群", + command="group-manage", + arguments=["del-group", "{%0}"], + prefix=True, +) + + +def CheckGroupId(): + """ + 检测群组id + """ + + async def dependency( + session: EventSession, + group_id: Match[int], + state: T_State, + ): + gid = session.id3 or session.id2 + if group_id.available: + gid = group_id.result + if not gid: + await MessageUtils.build_message("群组id不能为空...").finish() + state["group_id"] = gid + + return Depends(dependency) + + +@_matcher.assign("modify-level", parameterless=[CheckGroupId()]) +async def _(session: EventSession, arparma: Arparma, state: T_State, level: int): + gid = state["group_id"] + group, _ = await GroupConsole.get_or_create(group_id=gid) + old_level = group.level + group.level = level + await group.save(update_fields=["level"]) + await MessageUtils.build_message("群权限修改成功!").send(reply_to=True) + logger.info( + f"修改群权限: {old_level} -> {level}", + arparma.header_result, + session=session, + target=gid, + ) + + +@_matcher.assign("super-handle", parameterless=[CheckGroupId()]) +async def _(session: EventSession, arparma: Arparma, state: T_State): + gid = state["group_id"] + group = await GroupConsole.get_or_none(group_id=gid) + if not group: + await MessageUtils.build_message("群组信息不存在, 请更新群组信息...").finish() + s = "删除" if arparma.find("del") else "添加" + group.is_super = not arparma.find("del") + await group.save(update_fields=["is_super"]) + await MessageUtils.build_message(f"{s}群白名单成功!").send(reply_to=True) + logger.info(f"{s}群白名单", arparma.header_result, session=session, target=gid) + + +@_matcher.assign("auth-handle", parameterless=[CheckGroupId()]) +async def _(session: EventSession, arparma: Arparma, state: T_State): + gid = state["group_id"] + await GroupConsole.update_or_create( + group_id=gid, defaults={"group_flag": 0 if arparma.find("del") else 1} + ) + s = "删除" if arparma.find("del") else "添加" + await MessageUtils.build_message(f"{s}群认证成功!").send(reply_to=True) + logger.info(f"{s}群白名单", arparma.header_result, session=session, target=gid) + + +@_matcher.assign("del-group") +async def _(bot: Bot, session: EventSession, arparma: Arparma, group_id: int): + if isinstance(bot, v11Bot): + group_list = [g["group_id"] for g in await bot.get_group_list()] + if group_id not in group_list: + logger.debug("群组不存在", "退群", session=session, target=group_id) + await MessageUtils.build_message(f"{NICKNAME}未在该群组中...").finish() + try: + await bot.set_group_leave(group_id=group_id) + logger.info( + f"{NICKNAME}退出群组成功", "退群", session=session, target=group_id + ) + await MessageUtils.build_message(f"退出群组 {group_id} 成功!").send() + await GroupConsole.filter(group_id=group_id).delete() + except Exception as e: + logger.error(f"退出群组失败", "退群", session=session, target=group_id, e=e) + await MessageUtils.build_message(f"退出群组 {group_id} 失败...").send() + else: + # TODO: 其他平台的退群操作 + await MessageUtils.build_message(f"暂未支持退群操作...").send() diff --git a/basic_plugins/__init__.py b/zhenxun/builtin_plugins/superuser/power/__ini__.py old mode 100755 new mode 100644 similarity index 100% rename from basic_plugins/__init__.py rename to zhenxun/builtin_plugins/superuser/power/__ini__.py diff --git a/zhenxun/builtin_plugins/superuser/reload_setting.py b/zhenxun/builtin_plugins/superuser/reload_setting.py new file mode 100644 index 00000000..d4c6d3a1 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/reload_setting.py @@ -0,0 +1,68 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="重载配置", + description="重新加载config.yaml", + usage=""" + 重载配置 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + configs=[ + RegisterConfig( + key="AUTO_RELOAD", + value=False, + help="自动重载配置文件", + default_value=False, + type=bool, + ), + RegisterConfig( + key="AUTO_RELOAD_TIME", + value=180, + help="自动重载配置文件时长", + default_value=180, + type=int, + ), + ], + ).dict(), +) + +_matcher = on_alconna( + Alconna( + "重载配置", + ), + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + Config.reload() + logger.debug("自动重载配置文件", arparma.header_result, session=session) + await MessageUtils.build_message("重载完成!").send(reply_to=True) + + +@scheduler.scheduled_job( + "interval", + seconds=Config.get_config("reload_setting", "AUTO_RELOAD_TIME", 180), +) +async def _(): + if Config.get_config("reload_setting", "AUTO_RELOAD"): + Config.reload() + logger.debug("已自动重载配置文件...") diff --git a/zhenxun/builtin_plugins/superuser/request_manage.py b/zhenxun/builtin_plugins/superuser/request_manage.py new file mode 100644 index 00000000..7f7c4497 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/request_manage.py @@ -0,0 +1,275 @@ +from io import BytesIO + +from arclet.alconna import Args, Option +from arclet.alconna.typing import CommandMeta +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + AlconnaQuery, + Arparma, + Query, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.fg_request import FgRequest +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType +from zhenxun.utils.exception import NotFoundError +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import get_user_avatar + +usage = """ +查看请求 +清空请求 +请求处理 -fa [id] / 同意好友请求 [id] # 同意好友请求 +请求处理 -fr [id] / 拒绝好友请求 [id] # 拒绝好友请求 +请求处理 -fi [id] / 忽略好友请求 [id] # 忽略好友请求 +请求处理 -ga [id] / 同意群组请求 [id] # 同意群聊请求 +请求处理 -gr [id] / 拒绝群组请求 [id] # 拒绝群聊请求 +请求处理 -gi [id] / 忽略群组请求 [id] # 忽略群聊请求 +""".strip() + + +__plugin_meta__ = PluginMetadata( + name="请求处理", + description="好友与邀请群组请求处理", + usage=usage, + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_req_matcher = on_alconna( + Alconna( + "请求处理", + Args["handle", ["-fa", "-fr", "-fi", "-ga", "-gr", "-gi"]]["id", int], + meta=CommandMeta( + description="好友/群组请求处理", + usage=usage, + example="同意好友请求 20", + compact=True, + ), + ), + permission=SUPERUSER, + priority=1, + rule=to_me(), + block=True, +) + +_read_matcher = on_alconna( + Alconna( + "查看请求", + Option("-f|--friend", action=store_true, help_text="查看好友请求"), + Option("-g|--group", action=store_true, help_text="查看群组请求"), + meta=CommandMeta( + description="查看所有请求或好友群组请求", + usage="查看请求\n查看请求 -f\n查看请求-g", + example="查看请求 -f", + compact=True, + ), + ), + permission=SUPERUSER, + priority=1, + rule=to_me(), + block=True, +) + +_clear_matcher = on_alconna( + Alconna( + "清空请求", + Option("-f|--friend", action=store_true, help_text="清空好友请求"), + Option("-g|--group", action=store_true, help_text="清空群组请求"), + meta=CommandMeta( + description="清空请求", + usage="清空请求\n清空请求 -f\n清空请求-g", + example="清空请求 -f", + compact=True, + ), + ), + permission=SUPERUSER, + priority=1, + rule=to_me(), + block=True, +) + +reg_arg_list = [ + (r"同意好友请求", ["-fa", "{%0}"]), + (r"拒绝好友请求", ["-fr", "{%0}"]), + (r"忽略好友请求", ["-fi", "{%0}"]), + (r"同意群组请求", ["-ga", "{%0}"]), + (r"拒绝群组请求", ["-gr", "{%0}"]), + (r"忽略群组请求", ["-gi", "{%0}"]), +] + +for r in reg_arg_list: + _req_matcher.shortcut( + r[0], + command="请求处理", + arguments=r[1], + prefix=True, + ) + + +@_req_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + handle: str, + id: int, + arparma: Arparma, +): + type_dict = { + "a": RequestHandleType.APPROVE, + "r": RequestHandleType.REFUSED, + "i": RequestHandleType.IGNORE, + } + handle_type = type_dict[handle[-1]] + try: + if handle_type == RequestHandleType.APPROVE: + await FgRequest.approve(bot, id) + if handle_type == RequestHandleType.REFUSED: + await FgRequest.refused(bot, id) + if handle_type == RequestHandleType.IGNORE: + await FgRequest.ignore(id) + except NotFoundError: + await MessageUtils.build_message("未发现此id的请求...").finish(reply_to=True) + except Exception: + await MessageUtils.build_message("其他错误, 可能flag已失效...").finish( + reply_to=True + ) + logger.info("处理请求", arparma.header_result, session=session) + await MessageUtils.build_message("成功处理请求!").finish(reply_to=True) + + +@_read_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + is_friend: Query[bool] = AlconnaQuery("friend.value", False), + is_group: Query[bool] = AlconnaQuery("group.value", False), +): + if all_request := await FgRequest.filter(handle_type__isnull=True).all(): + req_list = list(all_request) + req_list.reverse() + friend_req: list[FgRequest] = [] + group_req: list[FgRequest] = [] + for req in req_list: + if req.request_type == RequestType.FRIEND: + friend_req.append(req) + else: + group_req.append(req) + if is_friend.result: + group_req = [] + elif is_group.result: + friend_req = [] + req_image_list: list[BuildImage] = [] + for i, req_list in enumerate([friend_req, group_req]): + img_list = [] + for req in req_list: + content = await get_user_avatar(req.user_id) + ava_img = BuildImage( + 80, 80, background=BytesIO(content) if content else None + ) + await ava_img.circle() + handle_img = BuildImage( + 130, 32, font_size=15, color="#EEEFF4", font="HYWenHei-85W.ttf" + ) + await handle_img.text((0, 0), "同意/拒绝/忽略", center_type="center") + await handle_img.circle_corner(10) + background = BuildImage(500, 100, font_size=22, color=(255, 255, 255)) + await background.paste(ava_img, (55, 0), center_type="height") + if session.platform and session.platform != "unknown": + platform_icon = BuildImage( + 30, + 30, + background=IMAGE_PATH / "_icon" / f"{session.platform}.png", + ) + await background.paste(platform_icon, (46, 10)) + await background.text((150, 12), req.nickname) + if i == 0: + comment_img = await BuildImage.build_text_image( + f"对方留言:{req.comment}", size=15, font_color=(140, 140, 143) + ) + else: + comment_img = await BuildImage.build_text_image( + f"群组:{req.group_id}", size=15, font_color=(140, 140, 143) + ) + await background.paste(comment_img, (150, 65)) + tag = await BuildImage.build_text_image( + f"{req.platform}", + size=13, + color=(0, 167, 250), + font="HYWenHei-85W.ttf", + font_color=(255, 255, 255), + padding=(1, 6, 1, 6), + ) + await tag.circle_corner(5) + await background.paste(tag, (150, 42)) + await background.paste(handle_img, (360, 35)) + _id_img = BuildImage( + 32, 32, font_size=15, color="#EEEFF4", font="HYWenHei-85W.ttf" + ) + await _id_img.text((0, 0), f"{req.id}", center_type="center") + await _id_img.circle_corner(10) + await background.paste(_id_img, (10, 0), center_type="height") + img_list.append(background) + A = await BuildImage.auto_paste(img_list, 1) + if A: + result_image = BuildImage( + A.width, A.height + 30, color=(255, 255, 255), font_size=20 + ) + await result_image.paste(A, (0, 30)) + _type_text = "好友请求" if i == 0 else "群组请求" + await result_image.text((15, 13), _type_text, fill=(140, 140, 143)) + req_image_list.append(result_image) + if not req_image_list: + await MessageUtils.build_message("没有任何请求喔...").finish(reply_to=True) + if len(req_image_list) == 1: + await MessageUtils.build_message(req_image_list[0]).finish() + width = sum([img.width for img in req_image_list]) + height = max([img.height for img in req_image_list]) + background = BuildImage(width, height) + await background.paste(req_image_list[0]) + await req_image_list[1].line((0, 10, 1, req_image_list[1].height - 10), width=1) + await background.paste(req_image_list[1], (req_image_list[1].width, 0)) + logger.info("查看请求", arparma.header_result, session=session) + await MessageUtils.build_message(background).finish() + await MessageUtils.build_message("没有任何请求喔...").finish(reply_to=True) + + +@_clear_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + is_friend: Query[bool] = AlconnaQuery("friend.value", False), + is_group: Query[bool] = AlconnaQuery("group.value", False), +): + _type = "" + if is_friend.result: + _type = "好友" + await FgRequest.filter( + handle_type__isnull=True, request_type=RequestType.FRIEND + ).update(handle_type=RequestHandleType.IGNORE) + elif is_group.result: + _type = "群组" + await FgRequest.filter( + handle_type__isnull=True, request_type=RequestType.GROUP + ).update(handle_type=RequestHandleType.IGNORE) + else: + _type = "所有" + await FgRequest.filter(handle_type__isnull=True).update( + handle_type=RequestHandleType.IGNORE + ) + logger.info(f"清空{_type}请求", arparma.header_result, session=session) + await MessageUtils.build_message(f"已清空{_type}请求!").finish() diff --git a/zhenxun/builtin_plugins/superuser/set_admin.py b/zhenxun/builtin_plugins/superuser/set_admin.py new file mode 100644 index 00000000..033d7a3d --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/set_admin.py @@ -0,0 +1,121 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + At, + Match, + Subcommand, + on_alconna, +) +from nonebot_plugin_session import EventSession, SessionLevel + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.level_user import LevelUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="用户权限管理", + description="设置用户权限", + usage=""" + 权限设置 add [level: 权限等级] [at: at对象或用户id] [gid: 群组] + 权限设置 delete [at: at对象或用户id] + + 权限设置 add 5 @user + 权限设置 add 5 422 352352 + + 权限设置 delete @user + 权限设置 delete 123456 + + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "权限设置", + Subcommand( + "add", + Args["level", int]["uid", [str, At]]["gid?", str], + help_text="添加权限", + ), + Subcommand("delete", Args["uid", [str, At]]["gid?", str], help_text="删除权限"), + ), + permission=SUPERUSER, + priority=5, + block=True, +) + + +@_matcher.assign("add") +async def _( + session: EventSession, + arparma: Arparma, + level: int, + gid: Match[str], + uid: str | At, +): + group_id = gid.result if gid.available else session.id3 or session.id2 + if group_id: + if isinstance(uid, At): + uid = uid.target + user = await LevelUser.get_or_none(user_id=uid, group_id=group_id) + old_level = user.user_level if user else 0 + await LevelUser.set_level(uid, group_id, level, 1) + logger.info( + f"修改权限: {old_level} -> {level}", arparma.header_result, session=session + ) + if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: + await MessageUtils.build_message( + [ + "成功为 ", + At(flag="user", target=uid), + f" 设置权限:{old_level} -> {level}", + ] + ).finish(reply_to=True) + await MessageUtils.build_message( + f"成功为 \n群组:{group_id}\n用户:{uid} \n设置权限!\n权限:{old_level} -> {level}" + ).finish() + await MessageUtils.build_message(f"设置权限时群组不能为空...").finish() + + +@_matcher.assign("delete") +async def _( + session: EventSession, + arparma: Arparma, + gid: Match[str], + uid: str | At, +): + group_id = gid.result if gid.available else session.id3 or session.id2 + if group_id: + if isinstance(uid, At): + uid = uid.target + if user := await LevelUser.get_or_none(user_id=uid, group_id=group_id): + await user.delete() + if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: + logger.info( + f"删除权限: {user.user_level} -> 0", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message( + ["成功删除 ", At(flag="user", target=uid), f" 的权限等级!"] + ).finish(reply_to=True) + logger.info( + f"删除群组用户权限: {user.user_level} -> 0", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message( + f"成功删除 \n群组:{group_id}\n用户:{uid} \n的权限等级!\n权限:{user.user_level} -> 0" + ).finish() + await MessageUtils.build_message(f"对方目前暂无权限喔...").finish() + await MessageUtils.build_message(f"设置权限时群组不能为空...").finish() diff --git a/zhenxun/builtin_plugins/superuser/super_help.py b/zhenxun/builtin_plugins/superuser/super_help.py new file mode 100644 index 00000000..5fa1e09e --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/super_help.py @@ -0,0 +1,159 @@ +import nonebot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_alconna.matcher import AlconnaMatcher +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.exception import EmptyError +from zhenxun.utils.image_utils import ( + BuildImage, + build_sort_image, + group_image, + text2image, +) +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import admin_check, ensure_group + +__plugin_meta__ = PluginMetadata( + name="超级用户帮助", + description="超级用户帮助", + usage=""" + 超级用户帮助 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + +_matcher = on_alconna( + Alconna("超级用户帮助"), + permission=SUPERUSER, + priority=5, + block=True, +) + + +SUPERUSER_HELP_IMAGE = IMAGE_PATH / "SUPERUSER_HELP.png" +if SUPERUSER_HELP_IMAGE.exists(): + SUPERUSER_HELP_IMAGE.unlink() + + +async def build_help() -> BuildImage: + """构造超级用户帮助图片 + + 异常: + EmptyError: 超级用户帮助为空 + + 返回: + BuildImage: 超级用户帮助图片 + """ + plugin_list = await PluginInfo.filter(plugin_type=PluginType.SUPERUSER).all() + data_list = [] + for plugin in plugin_list: + if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path): + if _plugin.metadata: + data_list.append({"plugin": plugin, "metadata": _plugin.metadata}) + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + image_list = [] + for data in data_list: + plugin = data["plugin"] + metadata = data["metadata"] + try: + usage = None + description = None + if metadata.usage: + usage = await text2image( + metadata.usage, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + if metadata.description: + description = await text2image( + metadata.description, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + width = 0 + height = 100 + if usage: + width = usage.width + height += usage.height + if description and description.width > width: + width = description.width + height += description.height + font_width, font_height = BuildImage.get_text_size( + plugin.name + f"[{plugin.level}]", font + ) + if font_width > width: + width = font_width + A = BuildImage(width + 30, height + 120, "#EAEDF2") + await A.text((15, 10), plugin.name + f"[{plugin.level}]") + await A.text((15, 70), "简介:") + if not description: + description = BuildImage(A.width - 30, 30, (255, 255, 255)) + await description.circle_corner(10) + await A.paste(description, (15, 100)) + if not usage: + usage = BuildImage(A.width - 30, 30, (255, 255, 255)) + await usage.circle_corner(10) + await A.text((15, description.height + 115), "用法:") + await A.paste(usage, (15, description.height + 145)) + await A.circle_corner(10) + image_list.append(A) + except Exception as e: + logger.warning( + f"获取超级用户管理员插件 {plugin.module}: {plugin.name} 设置失败...", + "超级用户帮助", + e=e, + ) + if task_list := await TaskInfo.all(): + task_str = "\n".join([task.name for task in task_list]) + task_str = "通过 开启/关闭群被动 来控制群被动\n----------\n" + task_str + task_image = await text2image(task_str, padding=5, color=(255, 255, 255)) + await task_image.circle_corner(10) + A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2") + await A.text((25, 10), "被动技能") + await A.paste(task_image, (25, 50)) + await A.circle_corner(10) + image_list.append(A) + if not image_list: + raise EmptyError() + image_group, _ = group_image(image_list) + A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160) + text = await BuildImage.build_text_image( + "超级用户帮助", + size=40, + ) + tip = await BuildImage.build_text_image( + "注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red" + ) + await A.paste(text, (50, 30)) + await A.paste(tip, (50, 90)) + await A.save(SUPERUSER_HELP_IMAGE) + return BuildImage(1, 1) + + +@_matcher.handle() +async def _( + session: EventSession, + matcher: AlconnaMatcher, + arparma: Arparma, +): + if not SUPERUSER_HELP_IMAGE.exists(): + try: + await build_help() + except EmptyError: + await MessageUtils.build_message("超级用户帮助为空").finish(reply_to=True) + await MessageUtils.build_message(SUPERUSER_HELP_IMAGE).send() + logger.info("查看超级用户帮助", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info.py b/zhenxun/builtin_plugins/superuser/update_fg_info.py new file mode 100644 index 00000000..6afac3d4 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/update_fg_info.py @@ -0,0 +1,89 @@ +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +__plugin_meta__ = PluginMetadata( + name="更新群组/好友信息", + description="更新群组/好友信息", + usage=""" + 更新群组信息 + 更新好友信息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_group_matcher = on_alconna( + Alconna( + "更新群组信息", + ), + permission=SUPERUSER, + rule=to_me(), + priority=1, + block=True, +) + +_friend_matcher = on_alconna( + Alconna( + "更新好友信息", + ), + permission=SUPERUSER, + rule=to_me(), + priority=1, + block=True, +) + + +@_group_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, +): + try: + num = await PlatformUtils.update_group(bot) + logger.info( + f"更新群聊信息完成,共更新了 {num} 个群组的信息!", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(f"成功更新了 {num} 个群组的信息").send() + except Exception as e: + logger.error( + "更新群组信息发生错误", arparma.header_result, session=session, e=e + ) + await MessageUtils.build_message("其他未知错误...").send() + + +@_friend_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, +): + try: + num = await PlatformUtils.update_friend(bot) + logger.info( + f"更新好友信息完成,共更新了 {num} 个好友的信息!", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(f"成功更新了 {num} 个好友的信息").send() + except Exception as e: + logger.error( + "更新好友信息发生错误", arparma.header_result, session=session, e=e + ) + await MessageUtils.build_message("其他未知错误...").send() diff --git a/configs/config.py b/zhenxun/configs/config.py similarity index 92% rename from configs/config.py rename to zhenxun/configs/config.py index 8a8ac935..e1140a88 100644 --- a/configs/config.py +++ b/zhenxun/configs/config.py @@ -1,6 +1,5 @@ import platform from pathlib import Path -from typing import Optional from .utils import ConfigsManager @@ -30,7 +29,7 @@ database: str = "" # 数据库名称 # 代理,例如 "http://127.0.0.1:7890" # 如果是WLS 可以 f"http://{hostip}:7890" 使用寄主机的代理 -SYSTEM_PROXY: Optional[str] = None # 全局代理 +SYSTEM_PROXY: str | None = None # 全局代理 Config = ConfigsManager(Path() / "data" / "configs" / "plugins2config.yaml") diff --git a/zhenxun/configs/path_config.py b/zhenxun/configs/path_config.py new file mode 100644 index 00000000..1a22cb02 --- /dev/null +++ b/zhenxun/configs/path_config.py @@ -0,0 +1,33 @@ +from pathlib import Path + +# 图片路径 +IMAGE_PATH = Path() / "resources" / "image" +# 语音路径 +RECORD_PATH = Path() / "resources" / "record" +# 文本路径 +TEXT_PATH = Path() / "resources" / "text" +# 日志路径 +LOG_PATH = Path() / "log" +# 字体路径 +FONT_PATH = Path() / "resources" / "font" +# 数据路径 +DATA_PATH = Path() / "data" +# 临时数据路径 +TEMP_PATH = Path() / "resources" / "temp" +# 网页模板路径 +TEMPLATE_PATH = Path() / "resources" / "template" + + + +IMAGE_PATH.mkdir(parents=True, exist_ok=True) +RECORD_PATH.mkdir(parents=True, exist_ok=True) +TEXT_PATH.mkdir(parents=True, exist_ok=True) +LOG_PATH.mkdir(parents=True, exist_ok=True) +FONT_PATH.mkdir(parents=True, exist_ok=True) +DATA_PATH.mkdir(parents=True, exist_ok=True) +TEMP_PATH.mkdir(parents=True, exist_ok=True) + + + + + diff --git a/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py similarity index 50% rename from configs/utils/__init__.py rename to zhenxun/configs/utils/__init__.py index a4fd27af..4ba9c47c 100644 --- a/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -1,49 +1,176 @@ import copy from pathlib import Path -from typing import Any, Callable, Dict, Optional, Type, Union +from typing import Any, Callable, Dict, Set, Type import cattrs from pydantic import BaseModel -from ruamel import yaml from ruamel.yaml import YAML from ruamel.yaml.scanner import ScannerError -from services.log import logger +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, LimitWatchType, PluginLimitType, PluginType + +_yaml = YAML(pure=True) +_yaml.indent = 2 +_yaml.allow_unicode = True -class Config(BaseModel): +class RegisterConfig(BaseModel): + """ + 注册配置项 + """ + key: str + """配置项键""" + value: Any + """配置项值""" + module: str | None = None + """模块名""" + help: str | None + """配置注解""" + default_value: Any | None = None + """默认值""" + type: Any = None + """参数类型""" + arg_parser: Callable | None = None + """参数解析""" + + +class ConfigModel(BaseModel): """ 配置项 """ value: Any """配置项值""" - name: Optional[str] - """插件名称""" - help: Optional[str] + help: str | None """配置注解""" - default_value: Optional[Any] = None + default_value: Any | None = None """默认值""" - level_module: Optional[str] - """受权限模块""" type: Any = None """参数类型""" - arg_parser: Optional[Callable] = None + arg_parser: Callable | None = None """参数解析""" class ConfigGroup(BaseModel): - """ 配置组 """ module: str """模块名""" - configs: Dict[str, Config] = {} + name: str | None = None + """插件名""" + configs: Dict[str, ConfigModel] = {} """配置项列表""" + def get(self, c: str, default: Any = None) -> Any: + cfg = self.configs.get(c.upper()) + if cfg is not None: + if cfg.value is not None: + return cfg.value + if cfg.default_value is not None: + return cfg.default_value + return default + + +class BaseBlock(BaseModel): + """ + 插件阻断基本类(插件阻断限制) + """ + + status: bool = True + """限制状态""" + check_type: BlockType = BlockType.ALL + """检查类型""" + watch_type: LimitWatchType = LimitWatchType.USER + """监听对象""" + result: str | None = None + """阻断时回复内容""" + _type: PluginLimitType = PluginLimitType.BLOCK + """类型""" + + +class PluginCdBlock(BaseBlock): + """ + 插件cd限制 + """ + + cd: int = 5 + """cd""" + _type: PluginLimitType = PluginLimitType.CD + """类型""" + + +class PluginCountBlock(BaseBlock): + """ + 插件次数限制 + """ + + max_count: int + """最大调用次数""" + _type: PluginLimitType = PluginLimitType.COUNT + """类型""" + + +class PluginSetting(BaseModel): + """ + 插件基本配置 + """ + + level: int = 5 + """群权限等级""" + default_status: bool = True + """进群默认开关状态""" + limit_superuser: bool = False + """是否限制超级用户""" + cost_gold: int = 0 + """调用插件花费金币""" + + +class Task(BaseBlock): + module: str + """被动技能模块名""" + name: str + """被动技能名称""" + status: bool = True + """全局开关状态""" + run_time: str | None = None + """运行时间""" + + +class PluginExtraData(BaseModel): + """ + 插件扩展信息 + """ + + author: str | None = None + """作者""" + version: str | None = None + """版本""" + plugin_type: PluginType = PluginType.NORMAL + """插件类型""" + menu_type: str = "功能" + """菜单类型""" + admin_level: int | None = None + """管理员插件所需权限等级""" + configs: list[RegisterConfig] | None = None + """插件配置""" + setting: PluginSetting | None = None + """插件基本配置""" + limits: list[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None + """插件限制""" + tasks: list[Task] | None = None + """技能被动""" + superuser_help: str | None = None + """超级用户帮助""" + aliases: Set[str] = set() + """额外名称""" + sql_list: list[str] | None = None + """常用sql""" + class NoSuchConfig(Exception): pass @@ -57,8 +184,7 @@ class ConfigsManager: def __init__(self, file: Path): self._data: Dict[str, ConfigGroup] = {} self._simple_data: dict = {} - self._admin_level_data = [] - self._simple_file = Path() / "configs" / "config.yaml" + self._simple_file = DATA_PATH / "config.yaml" _yaml = YAML() if file: file.parent.mkdir(exist_ok=True, parents=True) @@ -75,38 +201,58 @@ class ConfigsManager: f"**********************************************" ) + def set_name(self, module: str, name: str): + """设置插件配置中文名出 + + 参数: + module: 模块名 + name: 中文名称 + + 异常: + ValueError: module不能为为空 + """ + if not module: + raise ValueError("set_name: module不能为为空") + if data := self._data.get(module): + data.name = name + def add_plugin_config( self, module: str, key: str, - value: Optional[Any], + value: Any, *, - name: Optional[str] = None, - help_: Optional[str] = None, - default_value: Optional[Any] = None, - type: Optional[Type] = None, - arg_parser: Optional[Callable] = None, + help: str | None = None, + default_value: Any = None, + type: Type | None = None, + arg_parser: Callable | None = None, _override: bool = False, ): + """为插件添加一个配置,不会被覆盖,只有第一个生效 + + 参数: + module: 模块 + key: 键 + value: 值 + help: 配置注解. + default_value: 默认值. + type: 值类型. + arg_parser: 值解析器,一般与webui配合使用. + _override: 强制覆盖值. + + 异常: + ValueError: module和key不能为为空 + ValueError: 填写错误 """ - 为插件添加一个配置,不会被覆盖,只有第一个生效 - :param module: 模块 - :param key: 键 - :param value: 值 - :param name: 插件名称 - :param help_: 配置注解 - :param default_value: 默认值 - :param _override: 强制覆盖值 - """ + if not module or not key: raise ValueError("add_plugin_config: module和key不能为为空") if module in self._data and (config := self._data[module].configs.get(key)): - config.help = help_ + config.help = help config.arg_parser = arg_parser config.type = type if _override: config.value = value - config.name = name config.default_value = default_value else: _module = None @@ -116,18 +262,13 @@ class ConfigsManager: raise ValueError(f"module: {module} 填写错误") _module = module_split[-1] module = module_split[0] - if "[LEVEL]" in key and _module: - key = key.replace("[LEVEL]", "").strip() - self._admin_level_data.append((_module, value)) key = key.upper() if not self._data.get(module): self._data[module] = ConfigGroup(module=module) - self._data[module].configs[key] = Config( + self._data[module].configs[key] = ConfigModel( value=value, - name=name, - help=help_, + help=help, default_value=default_value, - level_module=_module, type=type, ) @@ -139,32 +280,36 @@ class ConfigsManager: auto_save: bool = False, save_simple_data: bool = True, ): - """ - 设置配置值 - :param module: 模块名 - :param key: 配置名称 - :param value: 值 - :param auto_save: 自动保存 - :param save_simple_data: 保存至config.yaml + """设置配置值 + + 参数: + module: 模块名 + key: 配置名称 + value: 值 + auto_save: 自动保存. + save_simple_data: 保存至config.yaml. """ if module in self._data: - if ( - self._data[module].configs.get(key) - and self._data[module].configs[key] != value - ): + data = self._data[module].configs.get(key) + if data and data != value: self._data[module].configs[key].value = value self._simple_data[module][key] = value if auto_save: self.save(save_simple_data=save_simple_data) - def get_config( - self, module: str, key: str, default: Optional[Any] = None - ) -> Optional[Any]: - """ - 获取指定配置值 - :param module: 模块名 - :param key: 配置名称 - :param default: 没有key值内容的默认返回值 + def get_config(self, module: str, key: str, default: Any = None) -> Any: + """获取指定配置值 + + 参数: + module: 模块名 + key: 配置键 + default: 没有key值内容的默认返回值. + + 异常: + NoSuchConfig: 未查询到配置 + + 返回: + Any: 配置值 """ logger.debug( f"尝试获取配置 MODULE: [{module}] | KEY: [{key}]" @@ -174,9 +319,11 @@ class ConfigsManager: if module in self._data.keys(): config = self._data[module].configs.get(key) if not config: - config = self._data[module].configs.get(f"{key} [LEVEL]") + config = self._data[module].configs.get(key) if not config: - raise NoSuchConfig(f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]") + raise NoSuchConfig( + f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]" + ) if config.arg_parser: value = config.arg_parser(value or config.default_value) else: @@ -207,41 +354,34 @@ class ConfigsManager: ) return value - def get_level2module(self, module: str, key: str) -> Optional[str]: - """ - 获取指定key所绑定的module,一般为权限等级 - :param module: 模块名 - :param key: 配置名称 - :return: - """ - if self._data.get(module) is not None: - if config := self._data[module].configs.get(key): - return config.level_module + def get(self, key: str) -> ConfigGroup: + """获取插件配置数据 - def get(self, key: str) -> Optional[ConfigGroup]: - """ - 获取插件配置数据 - :param key: 名称 - """ - return self._data.get(key) + 参数: + key: 键,一般为模块名 - def save( - self, path: Optional[Union[str, Path]] = None, save_simple_data: bool = False - ): + 返回: + ConfigGroup: ConfigGroup """ - 保存数据 - :param path: 路径 - :param save_simple_data: 同时保存至config.yaml + return self._data.get(key) or ConfigGroup(module="") + + def save(self, path: str | Path | None = None, save_simple_data: bool = False): + """保存数据 + + 参数: + path: 路径. + save_simple_data: 同时保存至config.yaml. """ if save_simple_data: with open(self._simple_file, "w", encoding="utf8") as f: - yaml.dump( - self._simple_data, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) + # yaml.dump( + # self._simple_data, + # f, + # indent=2, + # Dumper=yaml.RoundTripDumper, + # allow_unicode=True, + # ) + _yaml.dump(self._simple_data, f) path = path or self.file data = {} for module in self._data: @@ -249,16 +389,16 @@ class ConfigsManager: for config in self._data[module].configs: value = self._data[module].configs[config].dict() del value["type"] + del value["arg_parser"] data[module][config] = value with open(path, "w", encoding="utf8") as f: - yaml.dump( - data, f, indent=2, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) + # yaml.dump( + # data, f, indent=2, Dumper=yaml.RoundTripDumper, allow_unicode=True + # ) + _yaml.dump(data, f) def reload(self): - """ - 重新加载配置文件 - """ + """重新加载配置文件""" _yaml = YAML() if self._simple_file.exists(): with open(self._simple_file, "r", encoding="utf8") as f: @@ -269,14 +409,12 @@ class ConfigsManager: self.save() def load_data(self): - """ - 加载数据 + """加载数据 - Raises: - ValueError: _description_ + 异常: + ValueError: 配置文件为空! """ if self.file.exists(): - _yaml = YAML() with open(self.file, "r", encoding="utf8") as f: temp_data = _yaml.load(f) if not temp_data: @@ -291,19 +429,15 @@ class ConfigsManager: for module in temp_data: config_group = ConfigGroup(module=module) for config in temp_data[module]: - config_group.configs[config] = Config(**temp_data[module][config]) + config_group.configs[config] = ConfigModel( + **temp_data[module][config] + ) count += 1 self._data[module] = config_group logger.info( f"加载配置完成,共加载 {len(temp_data)} 个配置组及对应 {count} 个配置项" ) - def get_admin_level_data(self): - """ - 获取管理插件等级 - """ - return self._admin_level_data - def get_data(self) -> Dict[str, ConfigGroup]: return copy.deepcopy(self._data) diff --git a/zhenxun/models/bag_user.py b/zhenxun/models/bag_user.py new file mode 100644 index 00000000..711de8f7 --- /dev/null +++ b/zhenxun/models/bag_user.py @@ -0,0 +1,161 @@ +from typing import Dict + +from tortoise import fields + +from zhenxun.services.db_context import Model + +from .goods_info import GoodsInfo + + +class BagUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + gold = fields.IntField(default=100) + """金币数量""" + spend_total_gold = fields.IntField(default=0) + """花费金币总数""" + get_total_gold = fields.IntField(default=0) + """获取金币总数""" + get_today_gold = fields.IntField(default=0) + """今日获取金币""" + spend_today_gold = fields.IntField(default=0) + """今日获取金币""" + property: Dict[str, int] = fields.JSONField(default={}) # type: ignore + """道具""" + + class Meta: + table = "bag_users" + table_description = "用户道具数据表" + unique_together = ("user_id", "group_id") + + @classmethod + async def get_gold(cls, user_id: str, group_id: str) -> int: + """获取当前金币 + + 参数: + user_id: 用户id + group_id: 所在群组id + + 返回: + int: 金币数量 + """ + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + return user.gold + + @classmethod + async def get_property( + cls, user_id: str, group_id: str, only_active: bool = False + ) -> Dict[str, int]: + """获取当前道具 + + 参数: + user_id: 用户id + group_id: 所在群组id + only_active: 仅仅获取主动使用的道具 + + 返回: + Dict[str, int]: 道具名称与数量 + """ + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + if only_active and user.property: + data = {} + name_list = [ + x.goods_name + for x in await GoodsInfo.get_all_goods() + if not x.is_passive + ] + for key in [x for x in user.property if x in name_list]: + data[key] = user.property[key] + return data + return user.property + + @classmethod + async def add_gold(cls, user_id: str, group_id: str, num: int): + """增加金币 + + 参数: + user_id: 用户id + group_id: 所在群组id + num: 金币数量 + """ + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + user.gold = user.gold + num + user.get_total_gold = user.get_total_gold + num + user.get_today_gold = user.get_today_gold + num + await user.save(update_fields=["gold", "get_today_gold", "get_total_gold"]) + + @classmethod + async def spend_gold(cls, user_id: str, group_id: str, num: int): + """花费金币 + + 参数: + user_id: 用户id + group_id: 所在群组id + num: 金币数量 + """ + user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) + user.gold = user.gold - num + user.spend_total_gold = user.spend_total_gold + num + user.spend_today_gold = user.spend_today_gold + num + await user.save(update_fields=["gold", "spend_total_gold", "spend_today_gold"]) + + @classmethod + async def add_property(cls, user_id: str, group_id: str, name: str, num: int = 1): + """增加道具 + + 参数: + user_id: 用户id + group_id: 所在群组id + name: 道具名称 + num: 道具数量 + """ + user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) + property_ = user.property + if property_.get(name) is None: + property_[name] = 0 + property_[name] += num + user.property = property_ + await user.save(update_fields=["property"]) + + @classmethod + async def delete_property( + cls, user_id: str, group_id: str, name: str, num: int = 1 + ) -> bool: + """使用/删除 道具 + + 参数: + user_id: 用户id + group_id: 所在群组id + name: 道具名称 + num: 使用个数 + + 返回: + bool: 是否使用/删除成功 + """ + user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) + property_ = user.property + if name in property_: + if (n := property_.get(name, 0)) < num: + return False + if n == num: + del property_[name] + else: + property_[name] -= num + await user.save(update_fields=["property"]) + return True + return False + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE bag_users DROP props;", # 删除 props 字段 + "ALTER TABLE bag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE bag_users ALTER COLUMN user_id TYPE character varying(255);", + # 将user_id字段类型改为character varying(255) + "ALTER TABLE bag_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py new file mode 100644 index 00000000..0ab0fc69 --- /dev/null +++ b/zhenxun/models/ban_console.py @@ -0,0 +1,174 @@ +import time + +from tortoise import fields +from typing_extensions import Self + +from zhenxun.services.db_context import Model +from zhenxun.services.log import logger +from zhenxun.utils.exception import UserAndGroupIsNone + + +class BanConsole(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, null=True) + """用户id""" + group_id = fields.CharField(255, null=True) + """群组id""" + ban_level = fields.IntField() + """使用ban命令的用户等级""" + ban_time = fields.BigIntField() + """ban开始的时间""" + duration = fields.BigIntField() + """ban时长""" + operator = fields.CharField(255) + """使用Ban命令的用户""" + + class Meta: + table = "ban_console" + table_description = ".ban/b了 封禁人员/群组数据表" + + @classmethod + async def _get_data(cls, user_id: str | None, group_id: str | None) -> Self | None: + """获取数据 + + 参数: + user_id: 用户id + group_id: 群组id + + 异常: + UserAndGroupIsNone: 用户id和群组id都为空 + + 返回: + Self | None: Self + """ + if not user_id and not group_id: + raise UserAndGroupIsNone() + user = None + if user_id: + if group_id: + user = await cls.get_or_none(user_id=user_id, group_id=group_id) + else: + user = await cls.get_or_none(user_id=user_id, group_id__isnull=True) + else: + if group_id: + user = await cls.get_or_none(user_id__isnull=True, group_id=group_id) + return user + + @classmethod + async def check_ban_level( + cls, user_id: str | None, group_id: str | None, level: int + ) -> bool: + """检测ban掉目标的用户与unban用户的权限等级大小 + + 参数: + user_id: 用户id + group_id: 群组id + level: 权限等级 + + 返回: + bool: 权限判断,能否unban + """ + user = await cls._get_data(user_id, group_id) + if user: + logger.debug( + f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", + target=f"{group_id}:{user_id}", + ) + return user.ban_level <= level + return False + + @classmethod + async def check_ban_time( + cls, user_id: str | None, group_id: str | None = None + ) -> int: + """检测用户被ban时长 + + 参数: + user_id: 用户id + + 返回: + int: ban剩余时长,-1时为永久ban,0表示未被ban + """ + logger.debug(f"获取用户ban时长", target=f"{group_id}:{user_id}") + user = await cls._get_data(user_id, group_id) + if not user and user_id: + user = await cls._get_data(user_id, None) + if user: + if user.duration == -1: + return -1 + _time = time.time() - (user.ban_time + user.duration) + if _time > 0: + return 0 + return int(time.time() - user.ban_time - user.duration) + return 0 + + @classmethod + async def is_ban(cls, user_id: str | None, group_id: str | None = None) -> bool: + """判断用户是否被ban + + 参数: + user_id: 用户id + + 返回: + bool: 是否被ban + """ + logger.debug(f"检测是否被ban", target=f"{group_id}:{user_id}") + if await cls.check_ban_time(user_id, group_id): + return True + else: + await cls.unban(user_id, group_id) + return False + + @classmethod + async def ban( + cls, + user_id: str | None, + group_id: str | None, + ban_level: int, + duration: int, + operator: str | None = None, + ): + """ban掉目标用户 + + 参数: + user_id: 用户id + group_id: 群组id + ban_level: 使用命令者的权限等级 + duration: 时长,分钟,-1时为永久 + operator: 操作者id + """ + logger.debug( + f"封禁用户/群组,等级:{ban_level},时长: {duration}", + target=f"{group_id}:{user_id}", + ) + user = await cls._get_data(user_id, group_id) + if user: + await cls.unban(user_id, group_id) + await cls.create( + user_id=user_id, + group_id=group_id, + ban_level=ban_level, + ban_time=int(time.time()), + duration=duration, + operator=operator or 0, + ) + + @classmethod + async def unban(cls, user_id: str | None, group_id: str | None = None) -> bool: + """unban用户 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + bool: 是否被ban + """ + user = await cls._get_data(user_id, group_id) + if user: + logger.debug("解除封禁", target=f"{group_id}:{user_id}") + await user.delete() + return True + return False diff --git a/models/chat_history.py b/zhenxun/models/chat_history.py similarity index 65% rename from models/chat_history.py rename to zhenxun/models/chat_history.py index 60afd130..02425987 100644 --- a/models/chat_history.py +++ b/zhenxun/models/chat_history.py @@ -1,10 +1,11 @@ from datetime import datetime, timedelta -from typing import Any, List, Literal, Optional, Tuple, Union +from typing import Literal, Tuple from tortoise import fields from tortoise.functions import Count +from typing_extensions import Self -from services.db_context import Model +from zhenxun.services.db_context import Model class ChatHistory(Model): @@ -23,6 +24,8 @@ class ChatHistory(Model): """创建时间""" bot_id = fields.CharField(255, null=True) """bot记录id""" + platform = fields.CharField(255, null=True) + """平台""" class Meta: table = "chat_history" @@ -31,22 +34,21 @@ class ChatHistory(Model): @classmethod async def get_group_msg_rank( cls, - gid: Union[int, str], + gid: str | None, limit: int = 10, order: str = "DESC", - date_scope: Optional[Tuple[datetime, datetime]] = None, - ) -> List["ChatHistory"]: - """ - 说明: - 获取排行数据 + date_scope: tuple[datetime, datetime] | None = None, + ) -> list[Self]: + """获取排行数据 + 参数: - :param gid: 群号 - :param limit: 获取数量 - :param order: 排序类型,desc,des - :param date_scope: 日期范围 + gid: 群号 + limit: 获取数量 + order: 排序类型,desc,des + date_scope: 日期范围 """ o = "-" if order == "DESC" else "" - query = cls.filter(group_id=str(gid)) + query = cls.filter(group_id=gid) if gid else cls if date_scope: query = query.filter(create_time__range=date_scope) return list( @@ -59,50 +61,51 @@ class ChatHistory(Model): @classmethod async def get_group_first_msg_datetime( - cls, group_id: Union[int, str] - ) -> Optional[datetime]: - """ - 说明: - 获取群第一条记录消息时间 + cls, group_id: str | None + ) -> datetime | None: + """获取群第一条记录消息时间 + 参数: - :param group_id: 群组id + group_id: 群组id """ - if ( - message := await cls.filter(group_id=str(group_id)) - .order_by("create_time") - .first() - ): + if group_id: + message = ( + await cls.filter(group_id=group_id).order_by("create_time").first() + ) + else: + message = await cls.all().order_by("create_time").first() + if message: return message.create_time + return None @classmethod async def get_message( cls, - uid: Union[int, str], - gid: Union[int, str], + uid: str, + gid: str, type_: Literal["user", "group"], - msg_type: Optional[Literal["private", "group"]] = None, - days: Optional[Union[int, Tuple[datetime, datetime]]] = None, - ) -> List["ChatHistory"]: - """ - 说明: - 获取消息查询query + msg_type: Literal["private", "group"] | None = None, + days: int | Tuple[datetime, datetime] | None = None, + ) -> list[Self]: + """获取消息查询query + 参数: - :param uid: 用户id - :param gid: 群聊id - :param type_: 类型,私聊或群聊 - :param msg_type: 消息类型,用户或群聊 - :param days: 限制日期 + uid: 用户id + gid: 群聊id + type_: 类型,私聊或群聊 + msg_type: 消息类型,用户或群聊 + days: 限制日期 """ if type_ == "user": - query = cls.filter(user_id=str(uid)) + query = cls.filter(user_id=uid) if msg_type == "private": query = query.filter(group_id__isnull=True) elif msg_type == "group": query = query.filter(group_id__not_isnull=True) else: - query = cls.filter(group_id=str(gid)) + query = cls.filter(group_id=gid) if uid: - query = query.filter(user_id=str(uid)) + query = query.filter(user_id=uid) if days: if isinstance(days, int): query = query.filter( @@ -123,4 +126,5 @@ class ChatHistory(Model): "ALTER TABLE chat_history ALTER COLUMN group_id TYPE character varying(255);", "ALTER TABLE chat_history ADD bot_id VARCHAR(255);", # 添加bot_id字段 "ALTER TABLE chat_history ALTER COLUMN bot_id TYPE character varying(255);", + "ALTER TABLE chat_history ADD COLUMN platform character varying(255);", ] diff --git a/zhenxun/models/fg_request.py b/zhenxun/models/fg_request.py new file mode 100644 index 00000000..84f2e4c8 --- /dev/null +++ b/zhenxun/models/fg_request.py @@ -0,0 +1,125 @@ +from nonebot.adapters import Bot +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import RequestHandleType, RequestType +from zhenxun.utils.exception import NotFoundError + + +class FgRequest(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + request_type = fields.CharEnumField( + RequestType, default=None, description="请求类型" + ) + """请求类型""" + platform = fields.CharField(255, description="平台") + """平台""" + bot_id = fields.CharField(255, description="Bot Id") + """botId""" + flag = fields.CharField(max_length=255, default="", description="flag") + """flag""" + user_id = fields.CharField(max_length=255, description="请求用户id") + """请求用户id""" + group_id = fields.CharField(max_length=255, null=True, description="邀请入群id") + """邀请入群id""" + nickname = fields.CharField(max_length=255, description="请求人名称") + """对象名称""" + comment = fields.CharField(max_length=255, null=True, description="验证信息") + """验证信息""" + handle_type = fields.CharEnumField( + RequestHandleType, null=True, description="处理类型" + ) + """处理类型""" + + class Meta: + table = "fg_request" + table_description = "好友群组请求" + + @classmethod + async def approve(cls, bot: Bot, id: int): + """同意请求 + + 参数: + bot: Bot + id: 请求id + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(bot, id, RequestHandleType.APPROVE) + + @classmethod + async def refused(cls, bot: Bot, id: int): + """拒绝请求 + + 参数: + bot: Bot + id: 请求id + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(bot, id, RequestHandleType.REFUSED) + + @classmethod + async def ignore(cls, id: int): + """忽略请求 + + 参数: + id: 请求id + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(None, id, RequestHandleType.IGNORE) + + @classmethod + async def expire(cls, id: int): + """忽略请求 + + 参数: + bot: Bot + id: 请求id + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(None, id, RequestHandleType.EXPIRE) + + @classmethod + async def _handle_request( + cls, + bot: Bot | None, + id: int, + handle_type: RequestHandleType, + ): + """处理请求 + + 参数: + bot: Bot + id: 请求id + handle_type: 处理类型 + + 异常: + NotFoundError: 未发现请求 + """ + req = await cls.get_or_none(id=id) + if not req: + raise NotFoundError + req.handle_type = handle_type + await req.save(update_fields=["handle_type"]) + if bot and handle_type not in [ + RequestHandleType.IGNORE, + RequestHandleType.EXPIRE, + ]: + if req.request_type == RequestType.FRIEND: + await bot.set_friend_add_request( + flag=req.flag, approve=handle_type == RequestHandleType.APPROVE + ) + else: + await bot.set_group_add_request( + flag=req.flag, + sub_type="invite", + approve=handle_type == RequestHandleType.APPROVE, + ) diff --git a/zhenxun/models/friend_user.py b/zhenxun/models/friend_user.py new file mode 100644 index 00000000..597ce4f1 --- /dev/null +++ b/zhenxun/models/friend_user.py @@ -0,0 +1,83 @@ +from typing import Union + +from tortoise import fields + +from zhenxun.configs.config import Config +from zhenxun.services.db_context import Model + + +class FriendUser(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + user_name = fields.CharField(max_length=255, default="", description="用户名称") + """用户名称""" + nickname = fields.CharField(max_length=255, null=True, description="用户自定义昵称") + """私聊下自定义昵称""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "friend_users" + table_description = "好友信息数据表" + + @classmethod + async def get_user_name(cls, user_id: str) -> str: + """获取好友用户名称 + + 参数: + user_id: 用户id + """ + if user := await cls.get_or_none(user_id=user_id): + return user.user_name + return "" + + @classmethod + async def get_user_nickname(cls, user_id: str) -> str: + """获取用户昵称 + + 参数: + user_id: 用户id + """ + if user := await cls.get_or_none(user_id=user_id): + if user.nickname: + _tmp = "" + if black_word := Config.get_config("nickname", "BLACK_WORD"): + for x in user.nickname: + _tmp += "*" if x in black_word else x + return _tmp + return "" + + @classmethod + async def set_user_nickname( + cls, + user_id: str, + nickname: str, + uname: str | None = None, + platform: str | None = None, + ): + """设置用户昵称 + + 参数: + user_id: 用户id + nickname: 昵称 + uname: 用户昵称 + platform: 平台 + """ + defaults = {"nickname": nickname} + if uname is not None: + defaults["user_name"] = uname + if platform is not None: + defaults["platform"] = platform + await cls.update_or_create( + user_id=user_id, + defaults=defaults, + ) + + @classmethod + def _run_script(cls): + return [ + "ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE friend_users ADD COLUMN platform character varying(255) default 'qq';", + ] diff --git a/zhenxun/models/goods_info.py b/zhenxun/models/goods_info.py new file mode 100644 index 00000000..f776500b --- /dev/null +++ b/zhenxun/models/goods_info.py @@ -0,0 +1,162 @@ +import uuid +from typing import Dict + +from tortoise import fields +from typing_extensions import Self + +from zhenxun.services.db_context import Model + + +class GoodsInfo(Model): + __tablename__ = "goods_info" + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + uuid = fields.CharField(255, null=True) + """uuid""" + goods_name = fields.CharField(255, unique=True) + """商品名称""" + goods_price = fields.IntField() + """价格""" + goods_description = fields.TextField() + """描述""" + goods_discount = fields.FloatField(default=1) + """折扣""" + goods_limit_time = fields.BigIntField(default=0) + """限时""" + daily_limit = fields.IntField(default=0) + """每日限购""" + is_passive = fields.BooleanField(default=False) + """是否为被动道具""" + icon = fields.TextField(null=True) + """图标路径""" + + class Meta: + table = "goods_info" + table_description = "商品数据表" + + @classmethod + async def add_goods( + cls, + goods_name: str, + goods_price: int, + goods_description: str, + goods_discount: float = 1, + goods_limit_time: int = 0, + daily_limit: int = 0, + is_passive: bool = False, + icon: str | None = None, + ) -> str: + """添加商品 + + 参数: + goods_name: 商品名称 + goods_price: 商品价格 + goods_description: 商品简介 + goods_discount: 商品折扣 + goods_limit_time: 商品限时 + daily_limit: 每日购买限制 + is_passive: 是否为被动道具 + icon: 图标 + """ + if not await cls.exists(goods_name=goods_name): + uuid_ = uuid.uuid1() + await cls.create( + uuid=uuid_, + goods_name=goods_name, + goods_price=goods_price, + goods_description=goods_description, + goods_discount=goods_discount, + goods_limit_time=goods_limit_time, + daily_limit=daily_limit, + is_passive=is_passive, + icon=icon, + ) + return str(uuid_) + else: + return (await cls.get(goods_name=goods_name)).uuid + + @classmethod + async def delete_goods(cls, goods_name: str) -> bool: + """删除商品 + + 参数: + goods_name: 商品名称 + + 返回: + bool: 是否删除成功 + """ + if goods := await cls.get_or_none(goods_name=goods_name): + await goods.delete() + return True + return False + + @classmethod + async def update_goods( + cls, + goods_name: str, + goods_price: int | None = None, + goods_description: str | None = None, + goods_discount: float | None = None, + goods_limit_time: int | None = None, + daily_limit: int | None = None, + is_passive: bool | None = None, + icon: str | None = None, + ): + """更新商品信息 + + 参数: + goods_name: 商品名称 + goods_price: 商品价格 + goods_description: 商品简介 + goods_discount: 商品折扣 + goods_limit_time: 商品限时时间 + daily_limit: 每日次数限制 + is_passive: 是否为被动 + icon: 图标 + """ + if goods := await cls.get_or_none(goods_name=goods_name): + await cls.update_or_create( + goods_name=goods_name, + defaults={ + "goods_price": goods_price or goods.goods_price, + "goods_description": goods_description or goods.goods_description, + "goods_discount": goods_discount or goods.goods_discount, + "goods_limit_time": ( + goods_limit_time + if goods_limit_time is not None + else goods.goods_limit_time + ), + "daily_limit": ( + daily_limit if daily_limit is not None else goods.daily_limit + ), + "is_passive": ( + is_passive if is_passive is not None else goods.is_passive + ), + "icon": icon or goods.icon, + }, + ) + + @classmethod + async def get_all_goods(cls) -> list[Self]: + """ + 获得全部有序商品对象 + """ + query = await cls.all() + id_lst = [x.id for x in query] + goods_lst = [] + for _ in range(len(query)): + min_id = min(id_lst) + goods_lst.append([x for x in query if x.id == min_id][0]) + id_lst.remove(min_id) + return goods_lst + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE goods_info ADD uuid VARCHAR(255);", + "ALTER TABLE goods_info ADD daily_limit Integer DEFAULT 0;", + "ALTER TABLE goods_info ADD is_passive boolean DEFAULT False;", + "ALTER TABLE goods_info ADD icon VARCHAR(255);", + "ALTER TABLE goods_info DROP daily_purchase_limit;", # 删除 daily_purchase_limit 字段 + ] diff --git a/zhenxun/models/group_console.py b/zhenxun/models/group_console.py new file mode 100644 index 00000000..88959e8b --- /dev/null +++ b/zhenxun/models/group_console.py @@ -0,0 +1,139 @@ +from tortoise import fields +from typing_extensions import Self + +from zhenxun.services.db_context import Model + + +class GroupConsole(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + group_id = fields.CharField(255, description="群组id") + """群聊id""" + channel_id = fields.CharField(255, null=True, description="频道id") + """频道id""" + group_name = fields.TextField(default="", description="群组名称") + """群聊名称""" + max_member_count = fields.IntField(default=0, description="最大人数") + """最大人数""" + member_count = fields.IntField(default=0, description="当前人数") + """当前人数""" + status = fields.BooleanField(default=True, description="群状态") + """群状态""" + level = fields.IntField(default=5, description="群权限") + """群权限""" + is_super = fields.BooleanField( + default=False, description="超级用户指定,可以使用全局关闭的功能" + ) + """超级用户指定群,可以使用全局关闭的功能""" + group_flag = fields.IntField(default=0, description="群认证标记") + """群认证标记""" + block_plugin = fields.TextField(default="", description="禁用插件") + """禁用插件""" + block_task = fields.TextField(default="", description="禁用插件") + """禁用插件""" + platform = fields.CharField(255, default="qq", description="所属平台") + """所属平台""" + + class Meta: + table = "group_console" + table_description = "群组信息表" + unique_together = ("group_id", "channel_id") + + @classmethod + async def get_group(cls, group_id: str, channel_id: str | None = None) -> Self: + """获取群组 + + 参数: + group_id: 群组id + channel_id: 频道id. + + 返回: + Self: GroupConsole + """ + if channel_id: + return await cls.get(group_id=group_id, channel_id=channel_id) + return await cls.get(group_id=group_id, channel_id__isnull=True) + + @classmethod + async def is_super_group(cls, group_id: str, channel_id: str | None = None) -> bool: + """是否超级用户指定群 + + 参数: + group_id: 群组id + channel_id: 频道id. + + 返回: + bool: 是否超级用户指定群 + """ + if group := await cls.get_or_none(group_id=group_id): + return group.is_super + return False + + @classmethod + async def is_super_block_plugin( + cls, group_id: str, module: str, channel_id: str | None = None + ) -> bool: + """查看群组是否超级用户禁用功能 + + 参数: + group_id: 群组id + module: 模块名称 + channel_id: 频道id + + 返回: + bool: 是否禁用被动 + """ + return await cls.exists( + group_id=group_id, + channel_id=channel_id, + block_plugin__contains=f"super:{module},", + ) + + @classmethod + async def is_block_plugin( + cls, group_id: str, module: str, channel_id: str | None = None + ) -> bool: + """查看群组是否禁用功能 + + 参数: + group_id: 群组id + module: 模块名称 + channel_id: 频道id + + 返回: + bool: 是否禁用被动 + """ + return await cls.exists( + group_id=group_id, + channel_id=channel_id, + block_plugin__contains=f"{module},", + ) + + @classmethod + async def is_block_task( + cls, group_id: str, task: str, channel_id: str | None = None + ) -> bool: + """查看群组是否禁用被动 + + 参数: + group_id: 群组id + task: 任务模块 + channel_id: 频道id + + 返回: + bool: 是否禁用被动 + """ + if not channel_id: + return await cls.exists( + group_id=group_id, + channel_id__isnull=True, + block_task__contains=f"{task},", + ) + return await cls.exists( + group_id=group_id, channel_id=channel_id, block_task__contains=f"{task}," + ) + + @classmethod + def _run_script(cls): + return [] diff --git a/zhenxun/models/group_info.py b/zhenxun/models/group_info.py new file mode 100644 index 00000000..8183bf16 --- /dev/null +++ b/zhenxun/models/group_info.py @@ -0,0 +1,51 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class GroupInfo(Model): + group_id = fields.CharField(255, pk=True, description="群组id") + """群聊id""" + # channel_id = fields.CharField(255, description="群组id") + # """频道id""" + group_name = fields.TextField(default="", description="群组名称") + """群聊名称""" + max_member_count = fields.IntField(default=0, description="最大人数") + """最大人数""" + member_count = fields.IntField(default=0, description="当前人数") + """当前人数""" + group_flag = fields.IntField(default=0, description="群认证标记") + """群认证标记""" + block_plugin = fields.TextField(default="", description="禁用插件") + """禁用插件""" + block_task = fields.TextField(default="", description="禁用插件") + """禁用插件""" + platform = fields.CharField(255, default="qq", description="所属平台") + """所属平台""" + + class Meta: + table = "group_info" + table_description = "群聊信息表" + + @classmethod + async def is_block_task(cls, group_id: str, task: str) -> bool: + """查看群组是否禁用被动 + + 参数: + group_id: 群组id + task: 任务模块 + + 返回: + bool: 是否禁用被动 + """ + return await cls.exists(group_id=group_id, block_task__contains=f"{task},") + + @classmethod + def _run_script(cls): + return [ + "ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag + "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE group_info ADD block_plugin Text NOT NULL DEFAULT '';", + "ALTER TABLE group_info ADD block_task Text NOT NULL DEFAULT '';", + "ALTER TABLE group_info ADD platform character varying(255) NOT NULL DEFAULT 'qq';", + ] diff --git a/zhenxun/models/group_member_info.py b/zhenxun/models/group_member_info.py new file mode 100644 index 00000000..cf73f14d --- /dev/null +++ b/zhenxun/models/group_member_info.py @@ -0,0 +1,113 @@ +from typing import Set + +from tortoise import fields +from zhenxun.configs.config import Config + +from zhenxun.services.db_context import Model +from zhenxun.services.log import logger + + +class GroupInfoUser(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + user_name = fields.CharField(255, default="") + """用户昵称""" + group_id = fields.CharField(255) + """群聊id""" + user_join_time = fields.DatetimeField(null=True) + """用户入群时间""" + nickname = fields.CharField(255, null=True) + """群聊昵称""" + uid = fields.BigIntField(null=True) + """用户uid""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "group_info_users" + table_description = "群员信息数据表" + unique_together = ("user_id", "group_id") + + @classmethod + async def get_group_member_id_list(cls, group_id: str) -> Set[int]: + """获取该群所有用户id + + 参数: + group_id: 群号 + """ + return set( + await cls.filter(group_id=group_id).values_list("user_id", flat=True) + ) # type: ignore + + @classmethod + async def set_user_nickname( + cls, + user_id: str, + group_id: str, + nickname: str, + uname: str | None = None, + platform: str | None = None, + ): + """设置群员在该群内的昵称 + + 参数: + user_id: 用户id + group_id: 群号 + nickname: 昵称 + uname: 用户昵称 + platform: 平台 + """ + defaults = {"nickname": nickname} + if uname is not None: + defaults["user_name"] = uname + if platform is not None: + defaults["platform"] = platform + await cls.update_or_create( + user_id=user_id, + group_id=group_id, + defaults=defaults, + ) + + @classmethod + async def get_user_all_group(cls, user_id: str) -> list[int]: + """获取该用户所在的所有群聊 + + 参数: + user_id: 用户id + """ + return list( + await cls.filter(user_id=str(user_id)).values_list("group_id", flat=True) + ) # type: ignore + + @classmethod + async def get_user_nickname(cls, user_id: str, group_id: str) -> str: + """获取用户在该群的昵称 + + 参数: + user_id: 用户id + group_id: 群号 + """ + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): + if user.nickname: + nickname = "" + if black_word := Config.get_config("nickname", "BLACK_WORD"): + for x in user.nickname: + nickname += "*" if x in black_word else x + return nickname + return user.nickname + return "" + + + @classmethod + async def _run_script(cls): + return [ + "alter table group_info_users alter user_join_time drop not null;", # 允许 user_join_time 为空 + "ALTER TABLE group_info_users ALTER COLUMN user_join_time TYPE timestamp with time zone USING user_join_time::timestamp with time zone;", + "ALTER TABLE group_info_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id + "ALTER TABLE group_info_users ALTER COLUMN user_id TYPE character varying(255);", + # 将user_id字段类型改为character varying(255) + "ALTER TABLE group_info_users ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE group_info_users ADD COLUMN platform character varying(255) default 'qq';", + ] diff --git a/zhenxun/models/level_user.py b/zhenxun/models/level_user.py new file mode 100644 index 00000000..c2c4fc31 --- /dev/null +++ b/zhenxun/models/level_user.py @@ -0,0 +1,127 @@ +from datetime import datetime +from typing import Union + +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class LevelUser(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + user_level = fields.BigIntField() + """用户权限等级""" + group_flag = fields.IntField(default=0) + """特殊标记,是否随群管理员变更而设置权限""" + + class Meta: + table = "level_users" + table_description = "用户权限数据库" + unique_together = ("user_id", "group_id") + + @classmethod + async def get_user_level(cls, user_id: str, group_id: str | None) -> int: + """获取用户在群内的等级 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + int: 权限等级 + """ + if not group_id: + return 0 + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): + return user.user_level + return 0 + + @classmethod + async def set_level( + cls, + user_id: str, + group_id: str, + level: int, + group_flag: int = 0, + ): + """设置用户在群内的权限 + + 参数: + user_id: 用户id + group_id: 群组id + level: 权限等级 + group_flag: 是否被自动更新刷新权限 0:是, 1:否. + """ + await cls.update_or_create( + user_id=user_id, + group_id=group_id, + defaults={ + "user_level": level, + "group_flag": group_flag, + }, + ) + + @classmethod + async def delete_level(cls, user_id: str, group_id: str) -> bool: + """删除用户权限 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + bool: 是否含有用户权限 + """ + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): + await user.delete() + return True + return False + + @classmethod + async def check_level(cls, user_id: str, group_id: str | None, level: int) -> bool: + """检查用户权限等级是否大于 level + + 参数: + user_id: 用户id + group_id: 群组id + level: 权限等级 + + 返回: + bool: 是否大于level + """ + if group_id: + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): + return user.user_level >= level + else: + if user_list := await cls.filter(user_id=user_id).all(): + user = max(user_list, key=lambda x: x.user_level) + return user.user_level >= level + return False + + @classmethod + async def is_group_flag(cls, user_id: str, group_id: str) -> bool: + """检测是否会被自动更新刷新权限 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + bool: 是否会被自动更新权限刷新 + """ + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): + return user.group_flag == 1 + return False + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE level_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id + "ALTER TABLE level_users ALTER COLUMN user_id TYPE character varying(255);", + # 将user_id字段类型改为character varying(255) + "ALTER TABLE level_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/models/plugin_info.py b/zhenxun/models/plugin_info.py new file mode 100644 index 00000000..0eaea950 --- /dev/null +++ b/zhenxun/models/plugin_info.py @@ -0,0 +1,51 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import BlockType, PluginType + +from .plugin_limit import PluginLimit + + +class PluginInfo(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + module = fields.CharField(255, description="模块名") + """模块名""" + module_path = fields.CharField(255, description="模块路径", unique=True) + """模块路径""" + name = fields.CharField(255, description="插件名称") + """插件名称""" + status = fields.BooleanField(default=True, description="全局开关状态") + """全局开关状态""" + block_type: BlockType | None = fields.CharEnumField( + BlockType, default=None, null=True, description="禁用类型" + ) + """禁用类型""" + load_status = fields.BooleanField(default=True, description="加载状态") + """加载状态""" + author = fields.CharField(255, null=True, description="作者") + """作者""" + version = fields.CharField(max_length=255, null=True, description="版本") + """版本""" + level = fields.IntField(default=5, description="所需群权限") + """所需群权限""" + default_status = fields.BooleanField(default=True, description="进群默认开关状态") + """进群默认开关状态""" + limit_superuser = fields.BooleanField(default=False, description="是否限制超级用户") + """是否限制超级用户""" + menu_type = fields.CharField(max_length=255, default="功能", description="菜单类型") + """菜单类型""" + plugin_type = fields.CharEnumField(PluginType, null=True, description="插件类型") + """插件类型""" + cost_gold = fields.IntField(default=0, description="调用插件所需金币") + """调用插件所需金币""" + plugin_limit = fields.ReverseRelation["PluginLimit"] + """插件限制""" + admin_level = fields.IntField(default=0, null=True, description="调用所需权限等级") + """调用所需权限等级""" + is_delete = fields.BooleanField(default=False, description="是否删除") + """是否删除""" + + class Meta: + table = "plugin_info" + table_description = "插件基本信息" diff --git a/zhenxun/models/plugin_limit.py b/zhenxun/models/plugin_limit.py new file mode 100644 index 00000000..e6b185e7 --- /dev/null +++ b/zhenxun/models/plugin_limit.py @@ -0,0 +1,40 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import LimitCheckType, LimitWatchType, PluginLimitType + + +class PluginLimit(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + module = fields.CharField(255, description="模块名") + """模块名""" + module_path = fields.CharField(255, description="模块路径") + """模块路径""" + plugin = fields.ForeignKeyField( + "models.PluginInfo", + related_name="plugin_limit", + on_delete=fields.CASCADE, + description="所属插件", + ) + """所属插件""" + limit_type = fields.CharEnumField(PluginLimitType, description="限制类型") + """限制类型""" + watch_type = fields.CharEnumField(LimitWatchType, description="监听类型") + """监听类型""" + status = fields.BooleanField(default=True, description="限制的开关状态") + """限制的开关状态""" + check_type = fields.CharEnumField( + LimitCheckType, default=LimitCheckType.ALL, description="检查类型" + ) + """检查类型""" + result = fields.CharField(max_length=255, null=True, description="返回信息") + """返回信息""" + cd = fields.IntField(null=True, description="cd") + """cd""" + max_count = fields.IntField(null=True, description="最大调用次数") + """最大调用次数""" + + class Meta: + table = "plugin_limit" + table_description = "插件限制" diff --git a/models/sign_group_user.py b/zhenxun/models/sign_group_user.py old mode 100755 new mode 100644 similarity index 82% rename from models/sign_group_user.py rename to zhenxun/models/sign_group_user.py index 702ff4ae..ca397270 --- a/models/sign_group_user.py +++ b/zhenxun/models/sign_group_user.py @@ -3,7 +3,7 @@ from typing import List, Literal, Optional, Tuple, Union from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class SignGroupUser(Model): @@ -50,7 +50,7 @@ class SignGroupUser(Model): @classmethod async def get_all_impression( cls, group_id: Union[int, str] - ) -> Tuple[List[str], List[float], List[str]]: + ) -> Tuple[List[str], List[float], List[str]]: """ 说明: 获取该群所有用户 id 及对应 好感度 @@ -73,8 +73,9 @@ class SignGroupUser(Model): @classmethod async def _run_script(cls): - return ["ALTER TABLE sign_group_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE sign_group_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE sign_group_users ALTER COLUMN group_id TYPE character varying(255);" - ] + return [ + "ALTER TABLE sign_group_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id + "ALTER TABLE sign_group_users ALTER COLUMN user_id TYPE character varying(255);", + # 将user_id字段类型改为character varying(255) + "ALTER TABLE sign_group_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/models/sign_log.py b/zhenxun/models/sign_log.py new file mode 100644 index 00000000..cbfca947 --- /dev/null +++ b/zhenxun/models/sign_log.py @@ -0,0 +1,26 @@ +from datetime import datetime +from typing import List, Literal, Optional, Tuple, Union + +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class SignLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, description="用户id") + """用户id""" + impression = fields.DecimalField(10, 3, default=0, description="好感度") + """好感度""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + bot_id = fields.CharField(255, null=True, description="botId") + """bot记录id""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "sign_log" + table_description = "用户签到记录表" diff --git a/zhenxun/models/sign_user.py b/zhenxun/models/sign_user.py new file mode 100644 index 00000000..eb25b6cb --- /dev/null +++ b/zhenxun/models/sign_user.py @@ -0,0 +1,72 @@ +from tortoise import fields +from typing_extensions import Self + +from zhenxun.services.db_context import Model + +from .sign_log import SignLog +from .user_console import UserConsole + + +class SignUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + sign_count = fields.IntField(default=0, description="签到次数") + """签到次数""" + impression = fields.DecimalField(10, 3, default=0, description="好感度") + """好感度""" + user_console: fields.OneToOneRelation[UserConsole] = fields.OneToOneField( + "models.UserConsole", related_name="user_console", description="用户数据" + ) + """用户数据""" + add_probability = fields.DecimalField( + 10, 3, default=0, description="双倍签到增加概率" + ) + """双倍签到增加概率""" + specify_probability = fields.DecimalField( + 10, 3, default=0, description="指定双倍概率" + ) + """使用指定双倍概率""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "sign_users" + table_description = "用户签到数据表" + + @classmethod + async def sign( + cls, + user_id: str | Self, + impression: float, + bot_id: str | None = None, + platform: str | None = None, + ) -> Self: + """签到 + + 参数: + user_id: 用户id + impression: 好感度 + bot_id: bot Id + platform: 平台 + """ + if isinstance(user_id, SignUser): + user = user_id + else: + user, _ = await cls.get_or_create( + user_id=user_id, defaults={"platform": platform} + ) + user.impression = float(user.impression) + impression + user.add_probability = 0 + user.specify_probability = 0 + user.sign_count += 1 + await user.save() + await SignLog.create( + user_id=user.user_id, + impression=impression, + bot_id=bot_id, + platform=platform, + ) + return user diff --git a/models/statistics.py b/zhenxun/models/statistics.py similarity index 87% rename from models/statistics.py rename to zhenxun/models/statistics.py index 28ce3814..43576fcb 100644 --- a/models/statistics.py +++ b/zhenxun/models/statistics.py @@ -1,8 +1,6 @@ - - from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class Statistics(Model): @@ -20,7 +18,7 @@ class Statistics(Model): class Meta: table = "statistics" - table_description = "用户权限数据库" + table_description = "插件调用统计数据库" @classmethod async def _run_script(cls): @@ -28,4 +26,4 @@ class Statistics(Model): "ALTER TABLE statistics RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id "ALTER TABLE statistics ALTER COLUMN user_id TYPE character varying(255);", "ALTER TABLE statistics ALTER COLUMN group_id TYPE character varying(255);", - ] \ No newline at end of file + ] diff --git a/zhenxun/models/task_info.py b/zhenxun/models/task_info.py new file mode 100644 index 00000000..3ca1fb49 --- /dev/null +++ b/zhenxun/models/task_info.py @@ -0,0 +1,55 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + +from .ban_console import BanConsole +from .group_console import GroupConsole + + +class TaskInfo(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + module = fields.CharField(255, description="被动技能模块名") + """被动技能模块名""" + name = fields.CharField(255, description="被动技能名称") + """被动技能名称""" + status = fields.BooleanField(default=True, description="全局开关状态") + """全局开关状态""" + run_time = fields.CharField(255, null=True, description="运行时间") + """运行时间""" + run_count = fields.IntField(default=0, description="运行次数") + """运行次数""" + + class Meta: + table = "task_info" + table_description = "被动技能基本信息" + + @classmethod + async def is_block(cls, module: str, group_id: str | None) -> bool: + """判断被动技能是否可以发送 + + 参数: + module: 被动技能模块名 + group_id: 群组id + + 返回: + bool: 是否可以发送 + """ + if task := await cls.get_or_none(module=module): + """被动全局状态""" + if not task.status: + return True + if group_id: + if await GroupConsole.is_block_task(group_id, module): + """群组是否禁用被动""" + return True + if g := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): + """群组权限是否小于0""" + if g.level < 0: + return True + if await BanConsole.is_ban(None, group_id): + """群组是否被ban""" + return True + return False diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py new file mode 100644 index 00000000..8ac2a204 --- /dev/null +++ b/zhenxun/models/user_console.py @@ -0,0 +1,140 @@ +from typing import Dict + +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import GoldHandle +from zhenxun.utils.exception import InsufficientGold + +from .user_gold_log import UserGoldLog + + +class UserConsole(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + uid = fields.IntField(description="UID", unique=True) + """UID""" + gold = fields.IntField(default=100, description="金币数量") + """金币数量""" + sign = fields.ReverseRelation["SignUser"] # type: ignore + """好感度""" + props: Dict[str, int] = fields.JSONField(default={}) # type: ignore + """道具""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + + class Meta: + table = "user_console" + table_description = "用户数据表" + + @classmethod + async def get_user(cls, user_id: str, platform: str | None = None) -> "UserConsole": + """获取用户 + + 参数: + user_id: 用户id + platform: 平台. + + 返回: + UserConsole: UserConsole + """ + if not await cls.exists(user_id=user_id): + await cls.create( + user_id=user_id, platform=platform, uid=await cls.get_new_uid() + ) + # user, _ = await UserConsole.get_or_create( + # user_id=user_id, + # defaults={"platform": platform, "uid": await cls.get_new_uid()}, + # ) + return await cls.get(user_id=user_id) + + @classmethod + async def get_new_uid(cls) -> int: + """获取最新uid + + 返回: + int: 最新uid + """ + if user := await cls.annotate().order_by("-uid").first(): + return user.uid + 1 + return 1 + + @classmethod + async def add_gold( + cls, user_id: str, gold: int, source: str, platform: str | None = None + ): + """添加金币 + + 参数: + user_id: 用户id + gold: 金币 + source: 来源 + platform: 平台. + """ + user, _ = await cls.get_or_create( + user_id=user_id, + defaults={"platform": platform, "uid": await cls.get_new_uid()}, + ) + user.gold += gold + await user.save(update_fields=["gold"]) + await UserGoldLog.create( + user_id=user_id, gold=gold, handle=GoldHandle.GET, source=source + ) + + @classmethod + async def reduce_gold( + cls, + user_id: str, + gold: int, + handle: GoldHandle, + plugin_module: str, + platform: str | None = None, + ): + """消耗金币 + + 参数: + user_id: 用户id + gold: 金币 + handle: 金币处理 + plugin_name: 插件模块 + platform: 平台. + + 异常: + InsufficientGold: 金币不足 + """ + user, _ = await cls.get_or_create( + user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()} + ) + if user.gold < gold: + raise InsufficientGold() + user.gold -= gold + await user.save(update_fields=["gold"]) + await UserGoldLog.create( + user_id=user_id, gold=gold, handle=handle, source=plugin_module + ) + + @classmethod + async def add_props( + cls, user_id: str, goods_uuid: str, num: int = 1, platform: str | None = None + ): + """添加道具 + + 参数: + user_id: 用户id + goods_uuid: 道具uuid + num: 道具数量. + platform: 平台. + """ + user, _ = await cls.get_or_create( + user_id=user_id, + defaults={"platform": platform, "uid": await cls.get_new_uid()}, + ) + if goods_uuid not in user.props: + user.props[goods_uuid] = 0 + user.props[goods_uuid] += num + await user.save(update_fields=["props"]) diff --git a/zhenxun/models/user_gold_log.py b/zhenxun/models/user_gold_log.py new file mode 100644 index 00000000..30e83242 --- /dev/null +++ b/zhenxun/models/user_gold_log.py @@ -0,0 +1,24 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import GoldHandle + + +class UserGoldLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, description="用户id") + """用户id""" + gold = fields.IntField(description="金币") + """金币""" + handle = fields.CharEnumField(GoldHandle, default=None, description="道具处理类型") + """金币处理类型""" + source = fields.CharField(255, null=True, description="来源插件") + """来源插件""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + + class Meta: + table = "user_gold_log" + table_description = "用户金币记录表" diff --git a/zhenxun/models/user_props.py b/zhenxun/models/user_props.py new file mode 100644 index 00000000..3e3a5e2b --- /dev/null +++ b/zhenxun/models/user_props.py @@ -0,0 +1,25 @@ +from typing import Dict + +from tortoise import fields + +from zhenxun.services.db_context import Model + +from .sign_user import SignUser + + +class UserProps(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + name = fields.CharField(255, description="道具名称") + """道具名称""" + property: Dict[str, int] = fields.JSONField(default={}) # type: ignore + """道具""" + platform = fields.CharField(255, null=True) + """平台""" + + class Meta: + table = "user_props" + table_description = "用户道具表" diff --git a/zhenxun/models/user_props_log.py b/zhenxun/models/user_props_log.py new file mode 100644 index 00000000..16ae405c --- /dev/null +++ b/zhenxun/models/user_props_log.py @@ -0,0 +1,30 @@ +from typing import Dict + +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import PropHandle + +from .sign_user import SignUser + + +class UserPropsLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, description="用户id") + """用户id""" + uuid = fields.CharField(255, description="道具uuid") + """道具uuid""" + num = fields.IntField(null=True, description="道具金币") + """数量""" + gold = fields.IntField(null=True, description="道具金币") + """道具金币""" + handle = fields.CharEnumField(PropHandle, default=None, description="道具处理类型") + """道具处理类型""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + + class Meta: + table = "user_props_log" + table_description = "用户道具记录表" diff --git a/zhenxun/plugins/about.py b/zhenxun/plugins/about.py new file mode 100644 index 00000000..7c3b923e --- /dev/null +++ b/zhenxun/plugins/about.py @@ -0,0 +1,41 @@ +from pathlib import Path + +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="识番", + description="想要更加了解真寻吗", + usage=""" + 指令: + 关于 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").dict(), +) + + +_matcher = on_alconna(Alconna("关于"), priority=5, block=True, rule=to_me()) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + ver_file = Path() / "__version__" + version = None + if ver_file.exists(): + with open(ver_file, "r", encoding="utf8") as f: + version = f.read().split(":")[-1].strip() + info = f""" +『绪山真寻Bot』 +版本:{version} +简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处 +项目地址:https://github.com/HibiKier/zhenxun_bot +文档地址:https://hibikier.github.io/zhenxun_bot/ + """.strip() + await MessageUtils.build_message(info).send() + logger.info("查看关于", arparma.header_result, session=session) diff --git a/zhenxun/plugins/ai/__init__.py b/zhenxun/plugins/ai/__init__.py new file mode 100644 index 00000000..28e15354 --- /dev/null +++ b/zhenxun/plugins/ai/__init__.py @@ -0,0 +1,87 @@ +from typing import List + +from nonebot import on_message +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils + +from .data_source import get_chat_result, hello, no_result + +__plugin_meta__ = PluginMetadata( + name="AI", + description="屑Ai", + usage=f""" + 与{NICKNAME}普普通通的对话吧! + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + module="alapi", + key="ALAPI_TOKEN", + value=None, + help="在 https://admin.alapi.cn/user/login 登录后获取token", + ), + RegisterConfig(key="TL_KEY", value=[], help="图灵Key", type=List[str]), + RegisterConfig( + key="ALAPI_AI_CHECK", + value=False, + help="是否检测青云客骂娘回复", + default_value=False, + type=bool, + ), + RegisterConfig( + key="TEXT_FILTER", + value=["鸡", "口交"], + help="文本过滤器,将敏感词更改为*", + type=List[str], + ), + ], + ).dict(), +) + + +ai = on_message(rule=to_me(), priority=998) + + +@ai.handle() +async def _(message: UniMsg, session: EventSession, uname: str = UserName()): + if not message or message.extract_plain_text() in [ + "你好啊", + "你好", + "在吗", + "在不在", + "您好", + "您好啊", + "你好", + "在", + ]: + await hello().finish() + if not session.id1: + await Text("用户id不存在...").finish() + gid = session.id3 or session.id2 + if gid: + nickname = await GroupInfoUser.get_user_nickname(session.id1, gid) + else: + nickname = await FriendUser.get_user_nickname(session.id1) + if not nickname: + nickname = uname + result = await get_chat_result(message, session.id1, nickname) + logger.info(f"问题:{message} ---- 回答:{result}", "ai", session=session) + if result: + result = str(result) + for t in Config.get_config("ai", "TEXT_FILTER"): + result = result.replace(t, "*") + await MessageUtils.build_message(result).finish() + else: + await no_result().finish() diff --git a/plugins/ai/data_source.py b/zhenxun/plugins/ai/data_source.py old mode 100755 new mode 100644 similarity index 61% rename from plugins/ai/data_source.py rename to zhenxun/plugins/ai/data_source.py index 295b5b40..555f3885 --- a/plugins/ai/data_source.py +++ b/zhenxun/plugins/ai/data_source.py @@ -1,222 +1,241 @@ -import os -import random -import re - -from configs.config import NICKNAME, Config -from configs.path_config import DATA_PATH, IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.message_builder import face, image - -from .utils import ai_message_manager - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -url = "http://openapi.tuling123.com/openapi/api/v2" - -check_url = "https://v2.alapi.cn/api/censor/text" - -index = 0 - -anime_data = json.load(open(DATA_PATH / "anime.json", "r", encoding="utf8")) - - -async def get_chat_result(text: str, img_url: str, user_id: int, nickname: str) -> str: - """ - 获取 AI 返回值,顺序: 特殊回复 -> 图灵 -> 青云客 - :param text: 问题 - :param img_url: 图片链接 - :param user_id: 用户id - :param nickname: 用户昵称 - :return: 回答 - """ - global index - ai_message_manager.add_message(user_id, text) - special_rst = await ai_message_manager.get_result(user_id, nickname) - if special_rst: - ai_message_manager.add_result(user_id, special_rst) - return special_rst - if index == 5: - index = 0 - if len(text) < 6 and random.random() < 0.6: - keys = anime_data.keys() - for key in keys: - if text.find(key) != -1: - return random.choice(anime_data[key]).replace("你", nickname) - rst = await tu_ling(text, img_url, user_id) - if not rst: - rst = await xie_ai(text) - if not rst: - return no_result() - if nickname: - if len(nickname) < 5: - if random.random() < 0.5: - nickname = "~".join(nickname) + "~" - if random.random() < 0.2: - if nickname.find("大人") == -1: - nickname += "大~人~" - rst = str(rst).replace("小主人", nickname).replace("小朋友", nickname) - ai_message_manager.add_result(user_id, rst) - return rst - - -# 图灵接口 -async def tu_ling(text: str, img_url: str, user_id: int) -> str: - """ - 获取图灵接口的回复 - :param text: 问题 - :param img_url: 图片链接 - :param user_id: 用户id - :return: 图灵回复 - """ - global index - TL_KEY = Config.get_config("ai", "TL_KEY") - req = None - if not TL_KEY: - return "" - try: - if text: - req = { - "perception": { - "inputText": {"text": text}, - "selfInfo": { - "location": {"city": "陨石坑", "province": "火星", "street": "第5坑位"} - }, - }, - "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, - } - elif img_url: - req = { - "reqType": 1, - "perception": { - "inputImage": {"url": img_url}, - "selfInfo": { - "location": {"city": "陨石坑", "province": "火星", "street": "第5坑位"} - }, - }, - "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, - } - except IndexError: - index = 0 - return "" - text = "" - response = await AsyncHttpx.post(url, json=req) - if response.status_code != 200: - return no_result() - resp_payload = json.loads(response.text) - if int(resp_payload["intent"]["code"]) in [4003]: - return "" - if resp_payload["results"]: - for result in resp_payload["results"]: - if result["resultType"] == "text": - text = result["values"]["text"] - if "请求次数超过" in text: - text = "" - return text - - -# 屑 AI -async def xie_ai(text: str) -> str: - """ - 获取青云客回复 - :param text: 问题 - :return: 青云可回复 - """ - res = await AsyncHttpx.get( - f"http://api.qingyunke.com/api.php?key=free&appid=0&msg={text}" - ) - content = "" - try: - data = json.loads(res.text) - if data["result"] == 0: - content = data["content"] - if "菲菲" in content: - content = content.replace("菲菲", NICKNAME) - if "艳儿" in content: - content = content.replace("艳儿", NICKNAME) - if "公众号" in content: - content = "" - if "{br}" in content: - content = content.replace("{br}", "\n") - if "提示" in content: - content = content[: content.find("提示")] - if "淘宝" in content or "taobao.com" in content: - return "" - while True: - r = re.search("{face:(.*)}", content) - if r: - id_ = r.group(1) - content = content.replace( - "{" + f"face:{id_}" + "}", str(face(int(id_))) - ) - else: - break - return ( - content - if not content and not Config.get_config("ai", "ALAPI_AI_CHECK") - else await check_text(content) - ) - except Exception as e: - logger.error(f"Ai xie_ai 发生错误 {type(e)}:{e}") - return "" - - -def hello() -> str: - """ - 一些打招呼的内容 - """ - result = random.choice( - ( - "哦豁?!", - "你好!Ov<", - f"库库库,呼唤{NICKNAME}做什么呢", - "我在呢!", - "呼呼,叫俺干嘛", - ) - ) - img = random.choice(os.listdir(IMAGE_PATH / "zai")) - if img[-4:] == ".gif": - result += image(IMAGE_PATH / "zai" / img) - else: - result += image(IMAGE_PATH / "zai" / img) - return result - - -# 没有回答时回复内容 -def no_result() -> str: - """ - 没有回答时的回复 - """ - return random.choice( - [ - "你在说啥子?", - f"纯洁的{NICKNAME}没听懂", - "下次再告诉你(下次一定)", - "你觉得我听懂了吗?嗯?", - "我!不!知!道!", - ] - ) + image( - IMAGE_PATH / "noresult" / random.choice(os.listdir(IMAGE_PATH / "noresult")) - ) - - -async def check_text(text: str) -> str: - """ - ALAPI文本检测,主要针对青云客API,检测为恶俗文本改为无回复的回答 - :param text: 回复 - """ - if not Config.get_config("alapi", "ALAPI_TOKEN"): - return text - params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text} - try: - data = (await AsyncHttpx.get(check_url, timeout=2, params=params)).json() - if data["code"] == 200: - if data["data"]["conclusion_type"] == 2: - return "" - except Exception as e: - logger.error(f"检测违规文本错误...{type(e)}:{e}") - return text +import os +import random +import re + +import ujson as json +from nonebot_plugin_alconna import UniMessage, UniMsg + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils + +from .utils import ai_message_manager + +url = "http://openapi.tuling123.com/openapi/api/v2" + +check_url = "https://v2.alapi.cn/api/censor/text" + +index = 0 + +anime_data = json.load(open(DATA_PATH / "anime.json", "r", encoding="utf8")) + + +async def get_chat_result( + message: UniMsg, user_id: str, nickname: str +) -> UniMessage | None: + """获取 AI 返回值,顺序: 特殊回复 -> 图灵 -> 青云客 + + 参数: + text: 问题 + img_url: 图片链接 + user_id: 用户id + nickname: 用户昵称 + + 返回 + str: 回答 + """ + global index + text = message.extract_plain_text() + ai_message_manager.add_message(user_id, text) + special_rst = await ai_message_manager.get_result(user_id, nickname) + if special_rst: + ai_message_manager.add_result(user_id, special_rst) + return MessageUtils.build_message(special_rst) + if index == 5: + index = 0 + if len(text) < 6 and random.random() < 0.6: + keys = anime_data.keys() + for key in keys: + if text.find(key) != -1: + return random.choice(anime_data[key]).replace("你", nickname) + rst = await tu_ling(text, "", user_id) + if not rst: + rst = await xie_ai(text) + if not rst: + return None + if nickname: + if len(nickname) < 5: + if random.random() < 0.5: + nickname = "~".join(nickname) + "~" + if random.random() < 0.2: + if nickname.find("大人") == -1: + nickname += "大~人~" + rst = str(rst).replace("小主人", nickname).replace("小朋友", nickname) + ai_message_manager.add_result(user_id, rst) + for t in Config.get_config("ai", "TEXT_FILTER"): + rst = rst.replace(t, "*") + return MessageUtils.build_message(rst) + + +# 图灵接口 +async def tu_ling(text: str, img_url: str, user_id: str) -> str | None: + """获取图灵接口的回复 + + 参数: + text: 问题 + img_url: 图片链接 + user_id: 用户id + + 返回 + str: 图灵回复 + """ + global index + TL_KEY = Config.get_config("ai", "TL_KEY") + req = None + if not TL_KEY: + return None + try: + if text: + req = { + "perception": { + "inputText": {"text": text}, + "selfInfo": { + "location": { + "city": "陨石坑", + "province": "火星", + "street": "第5坑位", + } + }, + }, + "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, + } + elif img_url: + req = { + "reqType": 1, + "perception": { + "inputImage": {"url": img_url}, + "selfInfo": { + "location": { + "city": "陨石坑", + "province": "火星", + "street": "第5坑位", + } + }, + }, + "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, + } + except IndexError: + index = 0 + return None + text = "" + response = await AsyncHttpx.post(url, json=req) + if response.status_code != 200: + return None + resp_payload = json.loads(response.text) + if int(resp_payload["intent"]["code"]) in [4003]: + return None + if resp_payload["results"]: + for result in resp_payload["results"]: + if result["resultType"] == "text": + text = result["values"]["text"] + if "请求次数超过" in text: + text = "" + return text + + +# 屑 AI +async def xie_ai(text: str) -> str: + """获取青云客回复 + + 参数: + text: 问题 + + 返回: + str: 青云可回复 + """ + res = await AsyncHttpx.get( + f"http://api.qingyunke.com/api.php?key=free&appid=0&msg={text}" + ) + content = "" + try: + data = json.loads(res.text) + if data["result"] == 0: + content = data["content"] + if "菲菲" in content: + content = content.replace("菲菲", NICKNAME) + if "艳儿" in content: + content = content.replace("艳儿", NICKNAME) + if "公众号" in content: + content = "" + if "{br}" in content: + content = content.replace("{br}", "\n") + if "提示" in content: + content = content[: content.find("提示")] + if "淘宝" in content or "taobao.com" in content: + return "" + while True: + r = re.search("{face:(.*)}", content) + if r: + id_ = r.group(1) + content = content.replace("{" + f"face:{id_}" + "}", "") + else: + break + return ( + content + if not content and not Config.get_config("ai", "ALAPI_AI_CHECK") + else await check_text(content) + ) + except Exception as e: + logger.error(f"Ai xie_ai 发生错误", e=e) + return "" + + +def hello() -> UniMessage: + """一些打招呼的内容""" + result = random.choice( + ( + "哦豁?!", + "你好!Ov<", + f"库库库,呼唤{NICKNAME}做什么呢", + "我在呢!", + "呼呼,叫俺干嘛", + ) + ) + img = random.choice(os.listdir(IMAGE_PATH / "zai")) + return MessageUtils.build_message([IMAGE_PATH / "zai" / img, result]) + + +def no_result() -> UniMessage: + """ + 没有回答时的回复 + """ + return MessageUtils.build_message( + [ + random.choice( + [ + "你在说啥子?", + f"纯洁的{NICKNAME}没听懂", + "下次再告诉你(下次一定)", + "你觉得我听懂了吗?嗯?", + "我!不!知!道!", + ] + ), + IMAGE_PATH + / "noresult" + / random.choice(os.listdir(IMAGE_PATH / "noresult")), + ] + ) + + +async def check_text(text: str) -> str: + """ALAPI文本检测,主要针对青云客API,检测为恶俗文本改为无回复的回答 + + 参数: + text: 回复 + + 返回: + str: 检测文本 + """ + if not Config.get_config("alapi", "ALAPI_TOKEN"): + return text + params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text} + try: + data = (await AsyncHttpx.get(check_url, timeout=2, params=params)).json() + if data["code"] == 200: + if data["data"]["conclusion_type"] == 2: + return "" + except Exception as e: + logger.error(f"检测违规文本错误...", e=e) + return text diff --git a/plugins/ai/utils.py b/zhenxun/plugins/ai/utils.py old mode 100755 new mode 100644 similarity index 76% rename from plugins/ai/utils.py rename to zhenxun/plugins/ai/utils.py index 3b00941b..946bc234 --- a/plugins/ai/utils.py +++ b/zhenxun/plugins/ai/utils.py @@ -1,140 +1,153 @@ -from utils.manager import StaticData -from configs.config import NICKNAME -from models.ban_user import BanUser -from typing import Optional -import random -import time - - -class AiMessageManager(StaticData): - def __init__(self): - super().__init__(None) - self._same_message = [ - "为什么要发一样的话?", - "请不要再重复对我说一句话了,不然我就要生气了!", - "别再发这句话了,我已经知道了...", - "你是只会说这一句话吗?", - "[*],你发我也发!", - "[uname],[*]", - f"救命!有笨蛋一直给{NICKNAME}发一样的话!", - "这句话你已经给我发了{}次了,再发就生气!", - ] - self._repeat_message = [ - f"请不要学{NICKNAME}说话", - f"为什么要一直学{NICKNAME}说话?", - "你再学!你再学我就生气了!", - f"呜呜,你是想欺负{NICKNAME}嘛..", - "[uname]不要再学我说话了!", - "再学我说话,我就把你拉进黑名单(生气", - "你再学![uname]是个笨蛋!", - "你已经学我说话{}次了!别再学了!", - ] - - def add_message(self, user_id: int, message: str): - """ - 添加用户消息 - :param user_id: 用户id - :param message: 消息内容 - """ - if message: - if self._data.get(user_id) is None: - self._data[user_id] = { - "time": time.time(), - "message": [], - "result": [], - "repeat_count": 0, - } - if time.time() - self._data[user_id]["time"] > 60 * 10: - self._data[user_id]["message"].clear() - self._data[user_id]["time"] = time.time() - self._data[user_id]["message"].append(message.strip()) - - def add_result(self, user_id: int, message: str): - """ - 添加回复用户的消息 - :param user_id: 用户id - :param message: 回复消息内容 - """ - if message: - if self._data.get(user_id) is None: - self._data[user_id] = { - "time": time.time(), - "message": [], - "result": [], - "repeat_count": 0, - } - if time.time() - self._data[user_id]["time"] > 60 * 10: - self._data[user_id]["result"].clear() - self._data[user_id]["repeat_count"] = 0 - self._data[user_id]["time"] = time.time() - self._data[user_id]["result"].append(message.strip()) - - async def get_result(self, user_id: int, nickname: str) -> Optional[str]: - """ - 特殊消息特殊回复 - :param user_id: 用户id - :param nickname: 用户昵称 - """ - try: - if len(self._data[user_id]["message"]) < 2: - return None - except KeyError: - return None - msg = await self._get_user_repeat_message_result(user_id) - if not msg: - msg = await self._get_user_same_message_result(user_id) - if msg: - if "[uname]" in msg: - msg = msg.replace("[uname]", nickname) - if not msg.startswith("生气了!你好烦,闭嘴!") and "[*]" in msg: - msg = msg.replace("[*]", self._data[user_id]["message"][-1]) - return msg - - async def _get_user_same_message_result(self, user_id: int) -> Optional[str]: - """ - 重复消息回复 - :param user_id: 用户id - """ - msg = self._data[user_id]["message"][-1] - cnt = 0 - _tmp = self._data[user_id]["message"][:-1] - _tmp.reverse() - for s in _tmp: - if s == msg: - cnt += 1 - else: - break - if cnt > 1: - if random.random() < 0.5 and cnt > 3: - rand = random.randint(60, 300) - await BanUser.ban(user_id, 9, rand) - self._data[user_id]["message"].clear() - return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" - return random.choice(self._same_message).format(cnt) - return None - - async def _get_user_repeat_message_result(self, user_id: int) -> Optional[str]: - """ - 复读真寻的消息回复 - :param user_id: 用户id - """ - msg = self._data[user_id]["message"][-1] - if self._data[user_id]["result"]: - rst = self._data[user_id]["result"][-1] - else: - return None - if msg == rst: - self._data[user_id]["repeat_count"] += 1 - cnt = self._data[user_id]["repeat_count"] - if cnt > 1: - if random.random() < 0.5 and cnt > 3: - rand = random.randint(60, 300) - await BanUser.ban(user_id, 9, rand) - self._data[user_id]["result"].clear() - self._data[user_id]["repeat_count"] = 0 - return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" - return random.choice(self._repeat_message).format(cnt) - return None - - -ai_message_manager = AiMessageManager() +import random +import time + +from zhenxun.configs.config import NICKNAME +from zhenxun.models.ban_console import BanConsole + + +class AiMessageManager: + def __init__(self): + self._data = {} + self._same_message = [ + "为什么要发一样的话?", + "请不要再重复对我说一句话了,不然我就要生气了!", + "别再发这句话了,我已经知道了...", + "你是只会说这一句话吗?", + "[*],你发我也发!", + "[uname],[*]", + f"救命!有笨蛋一直给{NICKNAME}发一样的话!", + "这句话你已经给我发了{}次了,再发就生气!", + ] + self._repeat_message = [ + f"请不要学{NICKNAME}说话", + f"为什么要一直学{NICKNAME}说话?", + "你再学!你再学我就生气了!", + f"呜呜,你是想欺负{NICKNAME}嘛..", + "[uname]不要再学我说话了!", + "再学我说话,我就把你拉进黑名单(生气", + "你再学![uname]是个笨蛋!", + "你已经学我说话{}次了!别再学了!", + ] + + def add_message(self, user_id: str, message: str): + """添加用户消息 + + 参数: + user_id: 用户id + message: 消息内容 + """ + if message: + if self._data.get(user_id) is None: + self._data[user_id] = { + "time": time.time(), + "message": [], + "result": [], + "repeat_count": 0, + } + if time.time() - self._data[user_id]["time"] > 60 * 10: + self._data[user_id]["message"].clear() + self._data[user_id]["time"] = time.time() + self._data[user_id]["message"].append(message.strip()) + + def add_result(self, user_id: str, message: str): + """添加回复用户的消息 + + 参数: + user_id: 用户id + message: 回复消息内容 + """ + if message: + if self._data.get(user_id) is None: + self._data[user_id] = { + "time": time.time(), + "message": [], + "result": [], + "repeat_count": 0, + } + if time.time() - self._data[user_id]["time"] > 60 * 10: + self._data[user_id]["result"].clear() + self._data[user_id]["repeat_count"] = 0 + self._data[user_id]["time"] = time.time() + self._data[user_id]["result"].append(message.strip()) + + async def get_result(self, user_id: str, nickname: str) -> str | None: + """特殊消息特殊回复 + + 参数: + user_id: 用户id + nickname: 用户昵称 + + 返回: + str | None: 回答 + """ + try: + if len(self._data[user_id]["message"]) < 2: + return None + except KeyError: + return None + msg = await self._get_user_repeat_message_result(user_id) + if not msg: + msg = await self._get_user_same_message_result(user_id) + if msg: + if "[uname]" in msg: + msg = msg.replace("[uname]", nickname) + if not msg.startswith("生气了!你好烦,闭嘴!") and "[*]" in msg: + msg = msg.replace("[*]", self._data[user_id]["message"][-1]) + return msg + + async def _get_user_same_message_result(self, user_id: str) -> str | None: + """重复消息回复 + + 参数: + user_id: 用户id + + 返回: + str | None: 回答 + """ + msg = self._data[user_id]["message"][-1] + cnt = 0 + _tmp = self._data[user_id]["message"][:-1] + _tmp.reverse() + for s in _tmp: + if s == msg: + cnt += 1 + else: + break + if cnt > 1: + if random.random() < 0.5 and cnt > 3: + rand = random.randint(60, 300) + await BanConsole.ban(user_id, None, 9, rand, None) + self._data[user_id]["message"].clear() + return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" + return random.choice(self._same_message).format(cnt) + return None + + async def _get_user_repeat_message_result(self, user_id: str) -> str | None: + """复读真寻的消息回复 + + 参数: + user_id: 用户id + + 返回: + str | None: 回答 + """ + msg = self._data[user_id]["message"][-1] + if self._data[user_id]["result"]: + rst = self._data[user_id]["result"][-1] + else: + return None + if msg == rst: + self._data[user_id]["repeat_count"] += 1 + cnt = self._data[user_id]["repeat_count"] + if cnt > 1: + if random.random() < 0.5 and cnt > 3: + rand = random.randint(60, 300) + await BanConsole.ban(user_id, None, 9, rand, None) + self._data[user_id]["result"].clear() + self._data[user_id]["repeat_count"] = 0 + return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" + return random.choice(self._repeat_message).format(cnt) + return None + + +ai_message_manager = AiMessageManager() diff --git a/zhenxun/plugins/alapi/__init__.py b/zhenxun/plugins/alapi/__init__.py new file mode 100644 index 00000000..3efe4113 --- /dev/null +++ b/zhenxun/plugins/alapi/__init__.py @@ -0,0 +1,14 @@ +from pathlib import Path + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "alapi", + "ALAPI_TOKEN", + None, + help="在https://admin.alapi.cn/user/login登录后获取token", +) + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/alapi/_data_source.py b/zhenxun/plugins/alapi/_data_source.py similarity index 65% rename from plugins/alapi/_data_source.py rename to zhenxun/plugins/alapi/_data_source.py index 916143e2..61037ab9 100644 --- a/plugins/alapi/_data_source.py +++ b/zhenxun/plugins/alapi/_data_source.py @@ -1,13 +1,16 @@ -from typing import Optional, Tuple, Union -from configs.config import Config -from utils.http_utils import AsyncHttpx +from zhenxun.configs.config import Config +from zhenxun.utils.http_utils import AsyncHttpx -async def get_data(url: str, params: Optional[dict] = None) -> Tuple[Union[dict, str], int]: - """ - 获取ALAPI数据 - :param url: 请求链接 - :param params: 参数 +async def get_data(url: str, params: dict | None = None) -> tuple[dict | str, int]: + """获取ALAPI数据 + + 参数: + url: 请求链接 + params: 参数 + + 返回: + tuple[dict | str, int]: 返回信息 """ if not params: params = {} diff --git a/zhenxun/plugins/alapi/comments_163.py b/zhenxun/plugins/alapi/comments_163.py new file mode 100644 index 00000000..d05a5aa9 --- /dev/null +++ b/zhenxun/plugins/alapi/comments_163.py @@ -0,0 +1,57 @@ +from nonebot import on_regex +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from ._data_source import get_data + +comments_163_url = "https://v2.alapi.cn/api/comment" + +__plugin_meta__ = PluginMetadata( + name="网易云热评", + description="生了个人,我很抱歉", + usage=""" + 到点了,还是防不了下塔 + 指令: + 网易云热评/到点了/12点了 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("网易云热评"), + priority=5, + block=True, +) + +_matcher.shortcut( + "(到点了|12点了)", + command="网易云热评", + arguments=[], + prefix=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + data, code = await get_data(comments_163_url) + if code != 200 and isinstance(data, str): + await MessageUtils.build_message(data).finish(reply_to=True) + data = data["data"] # type: ignore + comment = data["comment_content"] # type: ignore + song_name = data["title"] # type: ignore + await MessageUtils.build_message(f"{comment}\n\t——《{song_name}》").send( + reply_to=True + ) + logger.info( + f" 发送网易云热评: {comment} \n\t\t————{song_name}", + arparma.header_result, + session=session, + ) diff --git a/zhenxun/plugins/alapi/cover.py b/zhenxun/plugins/alapi/cover.py new file mode 100644 index 00000000..e26bf79c --- /dev/null +++ b/zhenxun/plugins/alapi/cover.py @@ -0,0 +1,46 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Image, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from ._data_source import get_data + +cover_url = "https://v2.alapi.cn/api/bilibili/cover" + +__plugin_meta__ = PluginMetadata( + name="b封面", + description="快捷的b站视频封面获取方式", + usage=""" + b封面 [链接/av/bv/cv/直播id] + 示例:b封面 av86863038 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", menu_type="一些工具" + ).dict(), +) + +_matcher = on_alconna( + Alconna("b封面", Args["url", str]), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma, url: str): + params = {"c": url} + data, code = await get_data(cover_url, params) + if code != 200 and isinstance(data, str): + await MessageUtils.build_message(data).finish(reply_to=True) + data = data["data"] # type: ignore + title = data["title"] # type: ignore + img = data["cover"] # type: ignore + await MessageUtils.build_message([f"title:{title}\n", Image(url=img)]).send( + reply_to=True + ) + logger.info( + f" 获取b站封面: {title} url:{img}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/alapi/jitang.py b/zhenxun/plugins/alapi/jitang.py new file mode 100644 index 00000000..63dc93e2 --- /dev/null +++ b/zhenxun/plugins/alapi/jitang.py @@ -0,0 +1,48 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from ._data_source import get_data + +url = "https://v2.alapi.cn/api/soul" + +__plugin_meta__ = PluginMetadata( + name="鸡汤", + description="喏,亲手为你煮的鸡汤", + usage=""" + 不喝点什么感觉有点不舒服 + 指令: + 鸡汤 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("鸡汤"), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + try: + data, code = await get_data(url) + if code != 200 and isinstance(data, str): + await MessageUtils.build_message(data).finish(reply_to=True) + await MessageUtils.build_message(data["data"]["content"]).send(reply_to=True) # type: ignore + logger.info( + f" 发送鸡汤:" + data["data"]["content"], # type:ignore + arparma.header_result, + session=session, + ) + except Exception as e: + await MessageUtils.build_message("鸡汤煮坏掉了...").send() + logger.error(f"鸡汤煮坏掉了", e=e) diff --git a/zhenxun/plugins/alapi/poetry.py b/zhenxun/plugins/alapi/poetry.py new file mode 100644 index 00000000..4d359498 --- /dev/null +++ b/zhenxun/plugins/alapi/poetry.py @@ -0,0 +1,57 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from ._data_source import get_data + +__plugin_meta__ = PluginMetadata( + name="古诗", + description="为什么突然文艺起来了!", + usage=""" + 平白无故念首诗 + 示例:念诗/来首诗/念首诗 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("念诗"), + priority=5, + block=True, +) + +_matcher.shortcut( + "(来首诗|念首诗)", + command="念诗", + arguments=[], + prefix=True, +) + + +poetry_url = "https://v2.alapi.cn/api/shici" + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + data, code = await get_data(poetry_url) + if code != 200 and isinstance(data, str): + await MessageUtils.build_message(data).finish(reply_to=True) + data = data["data"] # type: ignore + content = data["content"] # type: ignore + title = data["origin"] # type: ignore + author = data["author"] # type: ignore + await MessageUtils.build_message(f"{content}\n\t——{author}《{title}》").send( + reply_to=True + ) + logger.info( + f" 发送古诗: f'{content}\n\t--{author}《{title}》'", + arparma.header_result, + session=session, + ) diff --git a/zhenxun/plugins/black_word/__init__.py b/zhenxun/plugins/black_word/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/plugins/black_word/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/black_word/black_watch.py b/zhenxun/plugins/black_word/black_watch.py new file mode 100644 index 00000000..be081a12 --- /dev/null +++ b/zhenxun/plugins/black_word/black_watch.py @@ -0,0 +1,61 @@ +from nonebot.adapters import Bot, Event +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from .utils import black_word_manager + +__plugin_meta__ = PluginMetadata( + name="敏感词文本监听", + description="敏感词文本监听", + usage="".strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + ).dict(), +) + +base_config = Config.get("black_word") + + +# 黑名单词汇检测 +@run_preprocessor +async def _( + bot: Bot, message: UniMsg, matcher: Matcher, event: Event, session: EventSession +): + gid = session.id3 or session.id2 + if session.id1: + if ( + event.is_tome() + and matcher.plugin_name == "black_word" + and not await BanConsole.is_ban(session.id1, gid) + ): + msg = message.extract_plain_text() + if session.id1 in bot.config.superusers: + return logger.debug( + f"超级用户跳过黑名单词汇检查 Message: {msg}", target=session.id1 + ) + if gid: + """屏蔽群权限-1的群""" + group, _ = await GroupConsole.get_or_create( + group_id=gid, channel_id__isnull=True + ) + if group.level < 0: + return + if await BanConsole.is_ban(None, gid): + """屏蔽群被ban的群""" + return + if await black_word_manager.check(bot, session, msg) and base_config.get( + "CONTAIN_BLACK_STOP_PROPAGATION" + ): + matcher.stop_propagation() diff --git a/zhenxun/plugins/black_word/black_word.py b/zhenxun/plugins/black_word/black_word.py new file mode 100644 index 00000000..8b819c6a --- /dev/null +++ b/zhenxun/plugins/black_word/black_word.py @@ -0,0 +1,235 @@ +from datetime import datetime +from typing import List + +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +from .data_source import set_user_punish, show_black_text_image + +__plugin_meta__ = PluginMetadata( + name="敏感词检测", + description="请注意你的发言!", + usage=""" + 惩罚机制: 检测内容提示 + 设置惩罚 [uid] [id] [level]: 设置惩罚内容, 此id需要通过`记录名单 -u:uid`来获取 + 记录名单: 查看检测记录名单 + 记录名单: + -u [uid] 指定用户记录名单 + -g [gid] 指定群组记录名单 + -d [date] 指定日期 + -dt ['=', '>', '<'] 大于小于等于指定日期 + + 示例: + 设置惩罚 123123123 0 1 + 记录名单 -u 123123123 + 记录名单 -g 333333 + 记录名单 -d 2022-11-11 + 记录名单 -d 2022-11-11 -dt > + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + menu_type="其他", + configs=[ + RegisterConfig( + key="CYCLE_DAYS", + value=30, + help="黑名单词汇记录周期", + default_value=30, + type=int, + ), + RegisterConfig( + key="TOLERATE_COUNT", + value=[5, 1, 1, 1, 1], + help="各个级别惩罚的容忍次数, 依次为: 1, 2, 3, 4, 5", + default_value=[5, 1, 1, 1, 1], + type=List[int], + ), + RegisterConfig( + key="AUTO_PUNISH", + value=True, + help="是否启动自动惩罚机制", + default_value=True, + type=bool, + ), + RegisterConfig( + key="BAN_4_DURATION", + value=360, + help="Ban时长(分钟),四级惩罚,可以为指定数字或指定列表区间(随机),例如 [30, 360]", + default_value=360, + type=int, + ), + RegisterConfig( + key="BAN_3_DURATION", + value=7, + help="Ban时长(天),三级惩罚,可以为指定数字或指定列表区间(随机),例如 [7, 30]", + default_value=7, + type=int, + ), + RegisterConfig( + key="WARNING_RESULT", + value=f"请注意对{NICKNAME}的发言内容", + help="口头警告内容", + default_value=None, + ), + RegisterConfig( + key="AUTO_ADD_PUNISH_LEVEL", + value=360, + help="自动提级机制,当周期内处罚次数大于某一特定值就提升惩罚等级", + default_value=360, + type=int, + ), + RegisterConfig( + key="ADD_PUNISH_LEVEL_TO_COUNT", + value=3, + help="在CYCLE_DAYS周期内触发指定惩罚次数后提升惩罚等级", + default_value=3, + type=int, + ), + RegisterConfig( + key="ALAPI_CHECK_FLAG", + value=False, + help="当未检测到已收录的敏感词时,开启ALAPI文本检测并将疑似文本发送给超级用户", + default_value=False, + type=bool, + ), + RegisterConfig( + key="CONTAIN_BLACK_STOP_PROPAGATION", + value=True, + help="当文本包含任意敏感词时,停止向下级插件传递,即不触发ai", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + + +_punish_matcher = on_alconna( + Alconna("设置惩罚", Args["uid", str]["id", int]["punish_level", int]), + priority=1, + permission=SUPERUSER, + block=True, +) + + +_show_matcher = on_alconna( + Alconna( + "记录名单", + Option("-u|--uid", Args["uid", str]), + Option("-g|--group", Args["gid", str]), + Option("-d|--date", Args["date", str]), + Option("-dt|--type", Args["date_type", ["=", ">", "<"]], default="="), + ), + priority=1, + permission=SUPERUSER, + block=True, +) + +_show_punish_matcher = on_alconna( + Alconna("惩罚机制"), aliases={"敏感词检测"}, priority=1, block=True +) + + +@_show_matcher.handle() +async def _( + bot: Bot, uid: Match[str], gid: Match[str], date: Match[str], date_type: Match[str] +): + user_id = None + group_id = None + date_ = None + date_str = None + date_type_ = "=" + if uid.available: + user_id = uid.result + if gid.available: + group_id = gid.result + if date.available: + date_str = date.result + if date_type.available: + date_type_ = date_type.result + if date_str: + try: + date_ = datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + await MessageUtils.build_message("日期格式错误,需要:年-月-日").finish() + result = await show_black_text_image( + user_id, + group_id, + date_, + date_type_, + ) + await MessageUtils.build_message(result).send() + + +@_show_punish_matcher.handle() +async def _(): + text = f""" + ** 惩罚机制 ** + + 惩罚前包含容忍机制,在指定周期内会容忍偶尔少次数的敏感词只会进行警告提醒 + + 多次触发同级惩罚会使惩罚等级提高,即惩罚自动提级机制 + + 目前公开的惩罚等级: + + 1级:永久ban + + 2级:删除好友 + + 3级:ban指定/随机天数 + + 4级:ban指定/随机时长 + + 5级:警告 + + 备注: + + 该功能为测试阶段,如果你有被误封情况,请联系管理员,会从数据库中提取出你的数据进行审核后判断 + + 目前该功能暂不完善,部分情况会由管理员鉴定,请注意对真寻的发言 + + 关于敏感词: + + 记住不要骂{NICKNAME}就对了! + """.strip() + max_width = 0 + for m in text.split("\n"): + max_width = len(m) * 20 if len(m) * 20 > max_width else max_width + max_height = len(text.split("\n")) * 24 + A = BuildImage( + max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1" + ) + await A.text((10, 10), text) + await MessageUtils.build_message(A).send() + + +@_punish_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + uid: str, + id: int, + punish_level: int, +): + result = await set_user_punish( + bot, uid, session.id2 or session.id3, id, punish_level + ) + await MessageUtils.build_message(result).send(reply_to=True) + logger.info( + f"设置惩罚 uid:{uid} id_:{id} punish_level:{punish_level} --> {result}", + arparma.header_result, + session=session, + ) diff --git a/zhenxun/plugins/black_word/data_source.py b/zhenxun/plugins/black_word/data_source.py new file mode 100644 index 00000000..e985facc --- /dev/null +++ b/zhenxun/plugins/black_word/data_source.py @@ -0,0 +1,103 @@ +from datetime import datetime + +from nonebot.adapters import Bot + +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.utils.image_utils import BuildImage, ImageTemplate + +from .model import BlackWord +from .utils import Config, _get_punish + + +async def show_black_text_image( + user_id: str | None, + group_id: str | None, + date: datetime | None, + data_type: str = "=", +) -> BuildImage: + """展示记录名单 + + 参数: + bot: bot + user: 用户id + group_id: 群组id + date: 日期 + data_type: 日期搜索类型 + + 返回: + BuildImage: 数据图片 + """ + data_list = await BlackWord.get_black_data(user_id, group_id, date, data_type) + column_name = [ + "ID", + "昵称", + "UID", + "GID", + "文本", + "检测内容", + "检测等级", + "惩罚", + "平台", + "记录日期", + ] + column_list = [] + uid_list = [u for u in data_list] + uid2name = { + u.user_id: u.user_name for u in await FriendUser.filter(user_id__in=uid_list) + } + for i, data in enumerate(data_list): + uname = uid2name.get(data.user_id) + if not uname: + if u := await GroupInfoUser.get_or_none( + user_id=data.user_id, group_id=data.group_id + ): + uname = u.user_name + if len(data.plant_text) > 30: + data.plant_text = data.plant_text[:30] + "..." + column_list.append( + [ + i, + uname or data.user_id, + data.user_id, + data.group_id, + data.plant_text, + data.black_word, + data.punish_level, + data.punish, + data.platform, + data.create_time, + ] + ) + A = await ImageTemplate.table_page( + "记录名单", "一个都不放过!", column_name, column_list + ) + return A + + +async def set_user_punish( + bot: Bot, user_id: str, group_id: str | None, id_: int, punish_level: int +) -> str: + """设置惩罚 + + 参数: + user_id: 用户id + group_id: 群组id或频道id + id_: 记录下标 + punish_level: 惩罚等级 + + 返回: + str: 结果 + """ + result = await _get_punish(bot, punish_level, user_id, group_id) + punish = { + 1: "永久ban", + 2: "删除好友", + 3: f"ban {result} 天", + 4: f"ban {result} 分钟", + 5: "口头警告", + } + if await BlackWord.set_user_punish(user_id, punish[punish_level], id_=id_): + return f"已对 USER {user_id} 进行 {punish[punish_level]} 处罚。" + else: + return "操作失败,可能未找到用户,id或敏感词" diff --git a/plugins/black_word/model.py b/zhenxun/plugins/black_word/model.py similarity index 71% rename from plugins/black_word/model.py rename to zhenxun/plugins/black_word/model.py index d3cd3afa..ef81c0ba 100644 --- a/plugins/black_word/model.py +++ b/zhenxun/plugins/black_word/model.py @@ -1,9 +1,10 @@ from datetime import datetime, timedelta -from typing import List, Optional +from email.policy import default +import pytz from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class BlackWord(Model): @@ -24,6 +25,8 @@ class BlackWord(Model): """惩罚等级""" create_time = fields.DatetimeField(auto_now_add=True) """创建时间""" + platform = fields.CharField(255, null=True) + """平台""" class Meta: table = "black_word" @@ -34,17 +37,16 @@ class BlackWord(Model): cls, user_id: str, punish: str, - black_word: Optional[str] = None, - id_: Optional[int] = None, + black_word: str | None = None, + id_: int | None = None, ) -> bool: - """ - 说明: - 设置处罚 + """设置处罚 + 参数: - :param user_id: 用户id - :param punish: 处罚 - :param black_word: 黑名单词汇 - :param id_: 记录下标 + user_id: 用户id + punish: 处罚 + black_word: 黑名单词汇 + id_: 记录下标 """ user = None if (not black_word and id_ is None) or not punish: @@ -68,15 +70,14 @@ class BlackWord(Model): @classmethod async def get_user_count( - cls, user_id: str, days: int = 7, punish_level: Optional[int] = None + cls, user_id: str, days: int = 7, punish_level: int | None = None ) -> int: - """ - 说明: - 获取用户规定周期内的犯事次数 + """获取用户规定周期内的犯事次数 + 参数: - :param user_id: 用户id - :param days: 周期天数 - :param punish_level: 惩罚等级 + user_id: 用户id + days: 周期天数 + punish_level: 惩罚等级 """ query = cls.filter( user_id=user_id, @@ -88,13 +89,12 @@ class BlackWord(Model): return await query.count() @classmethod - async def get_user_punish_level(cls, user_id: str, days: int = 7) -> Optional[int]: - """ - 说明: - 获取用户最近一次的惩罚记录等级 + async def get_user_punish_level(cls, user_id: str, days: int = 7) -> int | None: + """获取用户最近一次的惩罚记录等级 + 参数: - :param user_id: 用户id - :param days: 周期天数 + user_id: 用户id + days: 周期天数 """ if ( user := await cls.filter( @@ -110,19 +110,18 @@ class BlackWord(Model): @classmethod async def get_black_data( cls, - user_id: Optional[str], - group_id: Optional[str], - date: Optional[datetime], + user_id: str | None, + group_id: str | None, + date: datetime | None, date_type: str = "=", - ) -> List["BlackWord"]: - """ - 说明: - 通过指定条件查询数据 + ) -> list["BlackWord"]: + """通过指定条件查询数据 + 参数: - :param user_id: 用户id - :param group_id: 群号 - :param date: 日期 - :param date_type: 日期查询类型 + user_id: 用户id + group_id: 群号 + date: 日期 + date_type: 日期查询类型 """ query = cls if user_id: @@ -138,7 +137,12 @@ class BlackWord(Model): query = query.filter(create_time__gte=date) elif date_type == "<": query = query.filter(create_time__lte=date) - return await query.all().order_by("id") # type: ignore + data_list = await query.all().order_by("id") + for data in data_list: + data.create_time = data.create_time.astimezone( + pytz.timezone("Asia/Shanghai") + ) + return data_list # type: ignore @classmethod async def _run_script(cls): @@ -146,4 +150,5 @@ class BlackWord(Model): "ALTER TABLE black_word RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id "ALTER TABLE black_word ALTER COLUMN user_id TYPE character varying(255);", "ALTER TABLE black_word ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE black_word ADD COLUMN platform character varying(255);", ] diff --git a/plugins/black_word/utils.py b/zhenxun/plugins/black_word/utils.py similarity index 66% rename from plugins/black_word/utils.py rename to zhenxun/plugins/black_word/utils.py index f1b5842d..53526bd0 100644 --- a/plugins/black_word/utils.py +++ b/zhenxun/plugins/black_word/utils.py @@ -1,27 +1,24 @@ import random from pathlib import Path -from typing import Optional, Tuple, Union +import ujson as json +from nonebot.adapters import Bot from nonebot.adapters.onebot.v11 import ActionFailed +from nonebot_plugin_session import EventSession -from configs.config import Config -from configs.path_config import DATA_PATH -from models.ban_user import BanUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.utils import cn2py, get_bot +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.utils import cn2py from .model import BlackWord -try: - import ujson as json -except ModuleNotFoundError: - import json - class BlackWordManager: - """ 敏感词管理( 拒绝恶意 """ @@ -83,33 +80,46 @@ class BlackWordManager: ) async def check( - self, user_id: str, group_id: Optional[str], message: str - ) -> Optional[Union[str, bool]]: + self, bot: Bot, session: EventSession, message: str + ) -> str | bool | None: + """检查是否包含黑名单词汇 + + 参数: + bot: Bot + session: EventSession + message: 消息 """ - 检查是否包含黑名单词汇 - :param user_id: 用户id - :param group_id: 群号 - :param message: 消息 - """ - logger.debug(f"检查文本是否含有黑名单词汇: {message}", "敏感词检测", user_id, group_id) - if data := self._check(message): - if data[0]: - await _add_user_black_word( - user_id, group_id, data[0], message, int(data[1]) + logger.debug( + f"检查文本是否含有黑名单词汇: {message}", "敏感词检测", session=session + ) + if session.id1: + if data := self._check(message): + if data[0]: + await _add_user_black_word( + bot, + session.id1, + session.id2 or session.id3, + data[0], + message, + int(data[1]), + ) + return True + if Config.get_config( + "black_word", "ALAPI_CHECK_FLAG" + ) and not await check_text(message): + await send_msg( + bot, + "", + None, + f"用户 {session.id1} 群组 {session.id3 or session.id2} ALAPI 疑似检测:{message}", ) - return True - if Config.get_config("black_word", "ALAPI_CHECK_FLAG") and not await check_text( - message - ): - await send_msg( - 0, None, f"USER {user_id} GROUP {group_id} ALAPI 疑似检测:{message}" - ) return False - def _check(self, message: str) -> Tuple[Optional[str], int]: - """ - 检测文本是否违规 - :param message: 检测消息 + def _check(self, message: str) -> tuple[str | None, int]: + """检测文本是否违规 + + 参数: + message: 检测消息 """ # 移除空格 message = message.replace(" ", "") @@ -129,19 +139,22 @@ class BlackWordManager: async def _add_user_black_word( + bot: Bot, user_id: str, - group_id: Optional[str], + group_id: str | None, black_word: str, message: str, punish_level: int, ): - """ - 添加敏感词数据 - :param user_id: 用户id - :param group_id: 群号 - :param black_word: 触发的黑名单词汇 - :param message: 原始文本 - :param punish_level: 惩罚等级 + """添加敏感词数据 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id或频道id + black_word: 触发的黑名单词汇 + message: 原始文本 + punish_level: 惩罚等级 """ cycle_days = Config.get_config("black_word", "CYCLE_DAYS") or 7 user_count = await BlackWord.get_user_count(user_id, cycle_days, punish_level) @@ -160,23 +173,31 @@ async def _add_user_black_word( plant_text=message, black_word=black_word, punish_level=punish_level, + platform=PlatformUtils.get_platform(bot), ) logger.info( f"已将 USER {user_id} GROUP {group_id} 添加至黑名单词汇记录 Black_word:{black_word} Plant_text:{message}" ) # 自动惩罚 if Config.get_config("black_word", "AUTO_PUNISH") and punish_level != -1: - await _punish_handle(user_id, group_id, punish_level, black_word) + await _punish_handle(bot, user_id, group_id, punish_level, black_word) async def _punish_handle( - user_id: str, group_id: Optional[str], punish_level: int, black_word: str + bot: Bot, + user_id: str, + group_id: str | None, + punish_level: int, + black_word: str, ): - """ - 惩罚措施,级别越低惩罚越严 - :param user_id: 用户id - :param group_id: 群号 - :param black_word: 触发的黑名单词汇 + """惩罚措施,级别越低惩罚越严 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id或频道id + black_word: 触发的黑名单词汇 + channel_id: 频道id """ logger.info(f"BlackWord USER {user_id} 触发 {punish_level} 级惩罚...") # 周期天数 @@ -193,27 +214,30 @@ async def _punish_handle( tolerate_count = [5, 2, 2, 2, 2] if punish_level == 1 and user_count > tolerate_count[punish_level - 1]: # 永久ban - await _get_punish(1, user_id, group_id) + await _get_punish(bot, 1, user_id, group_id) await BlackWord.set_user_punish(user_id, "永久ban 删除好友", black_word) elif punish_level == 2 and user_count > tolerate_count[punish_level - 1]: # 删除好友 - await _get_punish(2, user_id, group_id) + await _get_punish(bot, 2, user_id, group_id) await BlackWord.set_user_punish(user_id, "删除好友", black_word) elif punish_level == 3 and user_count > tolerate_count[punish_level - 1]: # 永久ban - ban_day = await _get_punish(3, user_id, group_id) + ban_day = await _get_punish(bot, 3, user_id, group_id) await BlackWord.set_user_punish(user_id, f"ban {ban_day} 天", black_word) elif punish_level == 4 and user_count > tolerate_count[punish_level - 1]: # ban指定时长 - ban_time = await _get_punish(4, user_id, group_id) + ban_time = await _get_punish(bot, 4, user_id, group_id) await BlackWord.set_user_punish(user_id, f"ban {ban_time} 分钟", black_word) elif punish_level == 5 and user_count > tolerate_count[punish_level - 1]: # 口头警告 - warning_result = await _get_punish(5, user_id, group_id) - await BlackWord.set_user_punish(user_id, f"口头警告:{warning_result}", black_word) + warning_result = await _get_punish(bot, 5, user_id, group_id) + await BlackWord.set_user_punish( + user_id, f"口头警告:{warning_result}", black_word + ) else: await BlackWord.set_user_punish(user_id, f"提示!", black_word) await send_msg( + bot, user_id, group_id, f"BlackWordChecker:该条发言已被记录,目前你在{cycle_days}天内的发表{punish_level}级" @@ -223,15 +247,19 @@ async def _punish_handle( async def _get_punish( - id_: int, user_id: str, group_id: Optional[str] = None -) -> Optional[Union[int, str]]: + bot: Bot, + id_: int, + user_id: str, + group_id: str | None = None, +) -> int | str | None: + """通过id_获取惩罚 + + 参数: + bot: Bot + id_: id + user_id: 用户id + group_id: 群组id或频道id """ - 通过id_获取惩罚 - :param id_: id - :param user_id: 用户id - :param group_id: 群号 - """ - bot = get_bot() # 忽略的群聊 # _ignore_group = Config.get_config("black_word", "IGNORE_GROUP") # 处罚 id 4 ban 时间:int,List[int] @@ -247,9 +275,12 @@ async def _get_punish( # 永久ban if id_ == 1: if str(user_id) not in bot.config.superusers: - await BanUser.ban(user_id, 10, 99999999) + await BanConsole.ban(user_id, group_id, 10, -1, None) await send_msg( - user_id, group_id, f"BlackWordChecker 永久ban USER {uname}({user_id})" + bot, + user_id, + group_id, + f"BlackWordChecker 永久ban USER {uname}({user_id})", ) logger.info(f"BlackWord 永久封禁 USER {user_id}...") # 删除好友(有的话 @@ -258,7 +289,10 @@ async def _get_punish( try: await bot.delete_friend(user_id=user_id) await send_msg( - user_id, group_id, f"BlackWordChecker 删除好友 USER {uname}({user_id})" + bot, + user_id, + group_id, + f"BlackWordChecker 删除好友 USER {uname}({user_id})", ) logger.info(f"BlackWord 删除好友 {user_id}...") except ActionFailed: @@ -267,8 +301,9 @@ async def _get_punish( elif id_ == 3: if isinstance(ban_3_duration, list): ban_3_duration = random.randint(ban_3_duration[0], ban_3_duration[1]) - await BanUser.ban(user_id, 9, ban_4_duration * 60 * 60 * 24) + await BanConsole.ban(user_id, group_id, 9, ban_4_duration * 60 * 24) await send_msg( + bot, user_id, group_id, f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_3_duration} 天处罚。", @@ -279,8 +314,9 @@ async def _get_punish( elif id_ == 4: if isinstance(ban_4_duration, list): ban_4_duration = random.randint(ban_4_duration[0], ban_4_duration[1]) - await BanUser.ban(user_id, 9, ban_4_duration * 60) + await BanConsole.ban(user_id, group_id, 9, ban_4_duration * 60) await send_msg( + bot, user_id, group_id, f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_4_duration} 分钟处罚。", @@ -289,37 +325,32 @@ async def _get_punish( return ban_4_duration # 口头警告 elif id_ == 5: - if group_id: - await bot.send_group_msg(group_id=int(group_id), message=warning_result) - else: - await bot.send_private_msg(user_id=int(user_id), message=warning_result) + await PlatformUtils.send_message(bot, user_id, group_id, warning_result) logger.info(f"BlackWord 口头警告 USER {user_id}") return warning_result return None -async def send_msg( - user_id: Union[str, int], group_id: Optional[Union[str, int]], message: str -): +async def send_msg(bot: Bot, user_id: str, group_id: str | None, message: str): + """发送消息 + + 参数: + bot: Bot + user_id: user_id + group_id: group_id + message: message """ - 发送消息 - :param user_id: user_id - :param group_id: group_id - :param message: message - """ - if bot := get_bot(): - if not user_id: - user_id = list(bot.config.superusers)[0] - if group_id: - await bot.send_group_msg(group_id=int(group_id), message=message) - else: - await bot.send_private_msg(user_id=int(user_id), message=message) + if not user_id: + platform = PlatformUtils.get_platform(bot) + user_id = bot.config.platform_superusers[platform][0] + await PlatformUtils.send_message(bot, user_id, group_id, message) async def check_text(text: str) -> bool: - """ - ALAPI文本检测,检测输入违规 - :param text: 回复 + """ALAPI文本检测,检测输入违规 + + 参数: + text: 回复 """ if not Config.get_config("alapi", "ALAPI_TOKEN"): return True @@ -333,7 +364,7 @@ async def check_text(text: str) -> bool: if data["code"] == 200: return data["data"]["conclusion_type"] == 2 except Exception as e: - logger.error(f"检测违规文本错误...{type(e)}:{e}") + logger.error(f"检测违规文本错误...", e=e) return True diff --git a/zhenxun/plugins/bt/__init__.py b/zhenxun/plugins/bt/__init__.py new file mode 100644 index 00000000..3aff4f8b --- /dev/null +++ b/zhenxun/plugins/bt/__init__.py @@ -0,0 +1,78 @@ +from asyncio.exceptions import TimeoutError + +from httpx import ConnectTimeout +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import ensure_private + +from .data_source import get_bt_info + +__plugin_meta__ = PluginMetadata( + name="磁力搜索", + description="bt(磁力搜索)[仅支持私聊,懂的都懂]", + usage=""" + * 拒绝反冲斗士! * + 指令: + bt [关键词] ?[页数] + 示例: bt 钢铁侠 + 示例: bt 钢铁侠 3 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + key="BT_MAX_NUM", + value=10, + help="单次BT搜索返回最大消息数量", + default_value=10, + type=int, + ), + ], + ).dict(), +) + + +_matcher = on_alconna( + Alconna("bt", Args["keyword", str]["page?", int]), + rule=ensure_private, + priority=5, + block=True, +) + + +@_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + keyword: str, + page: Match[int], +): + send_flag = False + try: + async for title, type_, create_time, file_size, link in get_bt_info( + keyword, page.result if page.available else 1 + ): + await MessageUtils.build_message( + f"标题:{title}\n" + f"类型:{type_}\n" + f"创建时间:{create_time}\n" + f"文件大小:{file_size}\n" + f"种子:{link}" + ).send() + send_flag = True + except (TimeoutError, ConnectTimeout): + await MessageUtils.build_message(f"搜索 {keyword} 超时...").finish() + except Exception as e: + logger.error(f"bt 错误", arparma.header_result, session=session, e=e) + await MessageUtils.build_message(f"bt 其他未知错误..").finish() + if not send_flag: + await MessageUtils.build_message(f"{keyword} 未搜索到...").send() + logger.info( + f"BT搜索 {keyword} 第 {page} 页", arparma.header_result, session=session + ) diff --git a/plugins/bt/data_source.py b/zhenxun/plugins/bt/data_source.py old mode 100755 new mode 100644 similarity index 58% rename from plugins/bt/data_source.py rename to zhenxun/plugins/bt/data_source.py index 1b3ba7cd..ad02a5d5 --- a/plugins/bt/data_source.py +++ b/zhenxun/plugins/bt/data_source.py @@ -1,42 +1,54 @@ -from bs4 import BeautifulSoup - -from configs.config import Config -from utils.http_utils import AsyncHttpx - -url = "http://www.eclzz.ink" - - -async def get_bt_info(keyword: str, page: int): - """ - 获取资源信息 - :param keyword: 关键词 - :param page: 页数 - """ - text = (await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=5)).text - if "大约0条结果" in text: - return - soup = BeautifulSoup(text, "lxml") - item_lst = soup.find_all("div", {"class": "search-item"}) - bt_max_num = Config.get_config("bt", "BT_MAX_NUM") or 10 - bt_max_num = bt_max_num if bt_max_num < len(item_lst) else len(item_lst) - for item in item_lst[:bt_max_num]: - divs = item.find_all("div") - title = ( - str(divs[0].find("a").text).replace("", "").replace("", "").strip() - ) - spans = divs[2].find_all("span") - type_ = spans[0].text - create_time = spans[1].find("b").text - file_size = spans[2].find("b").text - link = await get_download_link(divs[0].find("a")["href"]) - yield title, type_, create_time, file_size, link - - -async def get_download_link(_url: str) -> str: - """ - 获取资源下载地址 - :param _url: 链接 - """ - text = (await AsyncHttpx.get(f"{url}{_url}")).text - soup = BeautifulSoup(text, "lxml") - return soup.find("a", {"id": "down-url"})["href"] +from bs4 import BeautifulSoup + +from zhenxun.configs.config import Config +from zhenxun.utils.http_utils import AsyncHttpx, AsyncPlaywright + +url = "http://www.eclzz.ink" + + +async def get_bt_info(keyword: str, page: int): + """获取资源信息 + + 参数: + keyword: 关键词 + page: 页数 + """ + global url + text = (await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=30)).text + if "301 Moved Permanently" in text: + async with AsyncPlaywright.new_page() as _page: + await _page.goto(url) + url = _page.url + text = ( + await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=30) + ).text + if "大约0条结果" in text: + return + soup = BeautifulSoup(text, "lxml") + item_lst = soup.find_all("div", {"class": "search-item"}) + bt_max_num = Config.get_config("bt", "BT_MAX_NUM") or 10 + bt_max_num = bt_max_num if bt_max_num < len(item_lst) else len(item_lst) + for item in item_lst[:bt_max_num]: + divs = item.find_all("div") + title = ( + str(divs[0].find("a").text).replace("", "").replace("", "").strip() + ) + spans = divs[2].find_all("span") + type_ = spans[0].text + create_time = spans[1].find("b").text + file_size = spans[2].find("b").text + link = await get_download_link(divs[0].find("a")["href"]) + yield title, type_, create_time, file_size, link + + +async def get_download_link(_url: str) -> str | None: + """获取资源下载地址 + + 参数: + _url: 链接 + """ + text = (await AsyncHttpx.get(f"{url}{_url}")).text + soup = BeautifulSoup(text, "lxml") + if fd := soup.find("a", {"id": "down-url"}): + return fd["href"] # type: ignore + return None diff --git a/zhenxun/plugins/check/__init__.py b/zhenxun/plugins/check/__init__.py new file mode 100644 index 00000000..ee63b50d --- /dev/null +++ b/zhenxun/plugins/check/__init__.py @@ -0,0 +1,40 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from .data_source import Check + +__plugin_meta__ = PluginMetadata( + name="服务器自我检查", + description="查看服务器当前状态", + usage=""" + 查看服务器当前状态 + 指令: + 自检 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER + ).dict(), +) + + +check = Check() + + +_matcher = on_alconna( + Alconna("自检"), rule=to_me(), permission=SUPERUSER, block=True, priority=1 +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + image = await check.show() + await MessageUtils.build_message(image).send() + logger.info("自检", arparma.header_result, session=session) diff --git a/plugins/check/data_source.py b/zhenxun/plugins/check/data_source.py old mode 100755 new mode 100644 similarity index 61% rename from plugins/check/data_source.py rename to zhenxun/plugins/check/data_source.py index cf24a4dc..78fbe7ba --- a/plugins/check/data_source.py +++ b/zhenxun/plugins/check/data_source.py @@ -1,77 +1,83 @@ -import psutil -import time -from datetime import datetime -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from configs.path_config import IMAGE_PATH -import asyncio -from services.log import logger - - -class Check: - def __init__(self): - self.cpu = None - self.memory = None - self.disk = None - self.user = None - self.baidu = 200 - self.google = 200 - - async def check_all(self): - await self.check_network() - await asyncio.sleep(0.1) - self.check_system() - self.check_user() - - def check_system(self): - self.cpu = psutil.cpu_percent() - self.memory = psutil.virtual_memory().percent - self.disk = psutil.disk_usage("/").percent - - async def check_network(self): - try: - await AsyncHttpx.get("https://www.baidu.com/", timeout=5) - except Exception as e: - logger.warning(f"访问BaiDu失败... {type(e)}: {e}") - self.baidu = 404 - try: - await AsyncHttpx.get("https://www.google.com/", timeout=5) - except Exception as e: - logger.warning(f"访问Google失败... {type(e)}: {e}") - self.google = 404 - - def check_user(self): - rst = "" - for user in psutil.users(): - rst += f'[{user.name}] {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user.started))}\n' - self.user = rst[:-1] - - async def show(self): - await self.check_all() - A = BuildImage(0, 0, font_size=24) - rst = ( - f'[Time] {str(datetime.now()).split(".")[0]}\n' - f"-----System-----\n" - f"[CPU] {self.cpu}%\n" - f"[Memory] {self.memory}%\n" - f"[Disk] {self.disk}%\n" - f"-----Network-----\n" - f"[BaiDu] {self.baidu}\n" - f"[Google] {self.google}\n" - ) - if self.user: - rst += "-----User-----\n" + self.user - width = 0 - height = 0 - for x in rst.split('\n'): - w, h = A.getsize(x) - if w > width: - width = w - height += 30 - A = BuildImage(width + 50, height + 10, font_size=24, font="HWZhongSong.ttf") - A.transparent(1) - A.text((10, 10), rst) - _x = max(width, height) - bk = BuildImage(_x + 100, _x + 100, background=IMAGE_PATH / "background" / "check" / "0.jpg") - bk.paste(A, alpha=True, center_type='center') - return bk.pic2bs4() +import asyncio +import time +from datetime import datetime + +import psutil + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage + + +class Check: + def __init__(self): + self.cpu = None + self.memory = None + self.disk = None + self.user = None + self.baidu = 200 + self.google = 200 + + async def check_all(self): + await self.check_network() + await asyncio.sleep(0.1) + self.check_system() + self.check_user() + + def check_system(self): + self.cpu = psutil.cpu_percent() + self.memory = psutil.virtual_memory().percent + self.disk = psutil.disk_usage("/").percent + + async def check_network(self): + try: + await AsyncHttpx.get("https://www.baidu.com/", timeout=5) + except Exception as e: + logger.warning(f"访问BaiDu失败... {type(e)}: {e}") + self.baidu = 404 + try: + await AsyncHttpx.get("https://www.google.com/", timeout=5) + except Exception as e: + logger.warning(f"访问Google失败... {type(e)}: {e}") + self.google = 404 + + def check_user(self): + result = "" + for user in psutil.users(): + result += f'[{user.name}] {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user.started))}\n' + self.user = result[:-1] + + async def show(self) -> BuildImage: + await self.check_all() + font = BuildImage.load_font(font_size=24) + result = ( + f'[Time] {str(datetime.now()).split(".")[0]}\n' + f"-----System-----\n" + f"[CPU] {self.cpu}%\n" + f"[Memory] {self.memory}%\n" + f"[Disk] {self.disk}%\n" + f"-----Network-----\n" + f"[BaiDu] {self.baidu}\n" + f"[Google] {self.google}\n" + ) + if self.user: + result += "-----User-----\n" + self.user + width = 0 + height = 0 + for x in result.split("\n"): + w, h = BuildImage.get_text_size(x, font) + if w > width: + width = w + height += 30 + A = BuildImage(width + 50, height + 10, font_size=24) + await A.transparent(1) + await A.text((10, 10), result) + max_width = max(width, height) + bk = BuildImage( + max_width + 100, + max_width + 100, + background=IMAGE_PATH / "background" / "check" / "0.jpg", + ) + await bk.paste(A, center_type="center") + return bk diff --git a/zhenxun/plugins/coser.py b/zhenxun/plugins/coser.py new file mode 100644 index 00000000..90062d51 --- /dev/null +++ b/zhenxun/plugins/coser.py @@ -0,0 +1,90 @@ +import time +from typing import Tuple + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.withdraw_manage import WithdrawManager + +__plugin_meta__ = PluginMetadata( + name="coser", + description="三次元也不戳,嘿嘿嘿", + usage=""" + ?N连cos/coser + 示例: cos + 示例: 5连cos (单次请求张数小于9) + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + key="WITHDRAW_COS_MESSAGE", + value=(0, 1), + help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + default_value=(0, 1), + type=Tuple[int, int], + ), + ], + ).dict(), +) + +_matcher = on_alconna(Alconna("get-cos", Args["num", int, 1]), priority=5, block=True) + +_matcher.shortcut( + r"cos", + command="get-cos", + arguments=["1"], + prefix=True, +) + +_matcher.shortcut( + r"(?P\d)(张|个|条|连)cos", + command="get-cos", + arguments=["{num}"], + prefix=True, +) + + +# 纯cos,较慢:https://picture.yinux.workers.dev +# 比较杂,有福利姬,较快:https://api.jrsgslb.cn/cos/url.php?return=img +url = "https://picture.yinux.workers.dev" + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + num: int, +): + withdraw_time = Config.get_config("coser", "WITHDRAW_COS_MESSAGE") + for _ in range(num): + path = TEMP_PATH / f"cos_cc{int(time.time())}.jpeg" + try: + await AsyncHttpx.download_file(url, path) + receipt = await MessageUtils.build_message(path).send() + message_id = receipt.msg_ids[0]["message_id"] + if message_id and WithdrawManager.check(session, withdraw_time): + WithdrawManager.append( + bot, + message_id, + withdraw_time[0], + ) + logger.info(f"发送cos", arparma.header_result, session=session) + except Exception as e: + await MessageUtils.build_message("你cos给我看!").send() + logger.error( + f"cos错误", + arparma.header_result, + session=session, + e=e, + ) diff --git a/zhenxun/plugins/dialogue/__init__.py b/zhenxun/plugins/dialogue/__init__.py new file mode 100644 index 00000000..5524cf9a --- /dev/null +++ b/zhenxun/plugins/dialogue/__init__.py @@ -0,0 +1,161 @@ +import nonebot +from nonebot import on_command +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Target, Text, UniMsg +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +from ._data_source import DialogueManage + +__plugin_meta__ = PluginMetadata( + name="联系管理员", + description="跨越空间与时间跟管理员对话", + usage=""" + 滴滴滴- ?[文本] ?[图片] + 示例:滴滴滴- 我喜欢你 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="联系管理员", + superuser_help=""" + /t: 查看当前存储的消息 + /t [user_id] [group_id] [文本]: 在group回复指定用户 + /t [user_id] [文本]: 私聊用户 + /t -1 [group_id] [文本]: 在group内发送消息 + /t [id] [文本]: 回复指定id的对话,id在 /t 中获取 + 示例:/t 73747222 32848432 你好啊 + 示例:/t 73747222 你好不好 + 示例:/t -1 32848432 我不太好 + 示例:/t 0 我收到你的话了 + """.strip(), + ).dict(), +) + +config = nonebot.get_driver().config + + +_dialogue_matcher = on_command("滴滴滴-", priority=5, block=True) +_reply_matcher = on_command("/t", priority=1, permission=SUPERUSER, block=True) + + +@_dialogue_matcher.handle() +async def _( + bot: Bot, + message: UniMsg, + session: EventSession, + user_info: UserInfo = EventUserInfo(), +): + if session.id1: + message[0] = Text(str(message[0]).replace("滴滴滴-", "", 1)) + platform = PlatformUtils.get_platform(bot) + try: + superuser_id = config.platform_superusers["qq"][0] + if platform == "dodo": + superuser_id = config.platform_superusers["dodo"][0] + if platform == "kaiheila": + superuser_id = config.platform_superusers["kaiheila"][0] + if platform == "discord": + superuser_id = config.platform_superusers["discord"][0] + except IndexError: + await MessageUtils.build_message("管理员失联啦...").finish() + if not superuser_id: + await MessageUtils.build_message("管理员失联啦...").finish() + uname = user_info.user_displayname or user_info.user_name + group_name = "" + gid = session.id3 or session.id2 + if gid: + if g := await GroupConsole.get(group_id=gid): + group_name = g.group_name + logger.info( + f"发送消息至{platform}管理员: {message}", "滴滴滴-", session=session + ) + message.insert(0, Text("消息:\n")) + if gid: + message.insert(0, Text(f"群组: {group_name}({gid})\n")) + message.insert(0, Text(f"昵称: {uname}({session.id1})\n")) + message.insert(0, Text(f"Id: {DialogueManage._index}\n")) + message.insert(0, Text("*****一份交流报告*****\n")) + DialogueManage.add(uname, session.id1, gid, group_name, message, platform) + await message.send(bot=bot, target=Target(superuser_id, private=True)) + await MessageUtils.build_message("已成功发送给管理员啦!").send(reply_to=True) + else: + await MessageUtils.build_message("用户id为空...").send() + + +@_reply_matcher.handle() +async def _( + bot: Bot, + message: UniMsg, + session: EventSession, +): + message[0] = Text(str(message[0]).replace("/t", "", 1).strip()) + if session.id1: + msg = message.extract_plain_text() + if not msg: + platform = PlatformUtils.get_platform(bot) + data = DialogueManage._data + if not data: + await MessageUtils.build_message("暂无待回复消息...").finish() + if platform: + data = [data[d] for d in data if data[d].platform == platform] + for d in data: + await d.message.send( + bot=bot, target=Target(session.id1, private=True) + ) + else: + msg = msg.split() + group_id = "" + user_id = "" + if msg[0].replace("-", "", 1).isdigit(): + if len(msg[0]) < 4: + _id = int(msg[0]) + if _id >= 0: + if model := DialogueManage.get(_id): + user_id = model.user_id + group_id = model.group_id + else: + return MessageUtils.build_message("未获取此id数据").finish() + message[0] = Text(" ".join(str(message[0]).split(" ")[1:])) + else: + user_id = 0 + if msg[1].isdigit(): + group_id = msg[1] + message[0] = Text(" ".join(str(message[0]).split(" ")[2:])) + else: + await MessageUtils.build_message("群组id错误...").finish( + at_sender=True + ) + DialogueManage.remove(_id) + else: + user_id = msg[0] + if msg[1].isdigit() and len(msg[1]) > 5: + group_id = msg[1] + message[0] = Text(" ".join(str(message[0]).split(" ")[2:])) + else: + group_id = 0 + message[0] = Text(" ".join(str(message[0]).split(" ")[1:])) + else: + await MessageUtils.build_message("参数错误...").finish(at_sender=True) + if group_id: + if user_id: + message.insert(0, alcAt("user", user_id)) + message.insert(1, Text("\n管理员回复\n=======\n")) + await message.send(Target(group_id), bot) + await MessageUtils.build_message("消息发送成功!").finish(at_sender=True) + elif user_id: + await message.send(Target(user_id, private=True), bot) + await MessageUtils.build_message("消息发送成功!").finish(at_sender=True) + else: + await MessageUtils.build_message("群组id与用户id为空...").send() + else: + await MessageUtils.build_message("用户id为空...").send() diff --git a/zhenxun/plugins/dialogue/_data_source.py b/zhenxun/plugins/dialogue/_data_source.py new file mode 100644 index 00000000..440c8176 --- /dev/null +++ b/zhenxun/plugins/dialogue/_data_source.py @@ -0,0 +1,55 @@ +from typing import Dict + +from nonebot_plugin_alconna import UniMsg +from pydantic import BaseModel + + +class DialogueData(BaseModel): + + name: str + """用户名称""" + user_id: str + """用户id""" + group_id: str | None + """群组id""" + group_name: str | None + """群组名称""" + message: UniMsg + """UniMsg""" + platform: str | None + """平台""" + + +class DialogueManage: + + _data: Dict[int, DialogueData] = {} + _index = 0 + + @classmethod + def add( + cls, + name: str, + uid: str, + gid: str | None, + group_name: str | None, + message: UniMsg, + platform: str | None, + ): + cls._data[cls._index] = DialogueData( + name=name, + user_id=uid, + group_id=gid, + group_name=group_name, + message=message, + platform=platform, + ) + cls._index += 1 + + @classmethod + def remove(cls, index: int): + if index in cls._data: + del cls._data[index] + + @classmethod + def get(cls, k: int): + return cls._data.get(k) diff --git a/plugins/draw_card/__init__.py b/zhenxun/plugins/draw_card/__init__.py similarity index 64% rename from plugins/draw_card/__init__.py rename to zhenxun/plugins/draw_card/__init__.py index f3864344..2aeeb30e 100644 --- a/plugins/draw_card/__init__.py +++ b/zhenxun/plugins/draw_card/__init__.py @@ -1,305 +1,293 @@ -import asyncio -import traceback -from dataclasses import dataclass -from typing import Any, Optional, Set, Tuple - -import nonebot -from cn2an import cn2an -from configs.config import Config -from nonebot import on_keyword, on_message, on_regex -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.log import logger -from nonebot.matcher import Matcher -from nonebot.params import RegexGroup -from nonebot.permission import SUPERUSER -from nonebot.typing import T_Handler -from nonebot_plugin_apscheduler import scheduler - -from .handles.azur_handle import AzurHandle -from .handles.ba_handle import BaHandle -from .handles.base_handle import BaseHandle -from .handles.fgo_handle import FgoHandle -from .handles.genshin_handle import GenshinHandle -from .handles.guardian_handle import GuardianHandle -from .handles.onmyoji_handle import OnmyojiHandle -from .handles.pcr_handle import PcrHandle -from .handles.pretty_handle import PrettyHandle -from .handles.prts_handle import PrtsHandle -from .rule import rule - -__zx_plugin_name__ = "游戏抽卡" -__plugin_usage__ = """ -usage: - 模拟赛马娘,原神,明日方舟,坎公骑冠剑,公主连结(国/台),碧蓝航线,FGO,阴阳师,碧蓝档案进行抽卡 - 指令: - 原神[1-180]抽: 原神常驻池 - 原神角色[1-180]抽: 原神角色UP池子 - 原神角色2池[1-180]抽: 原神角色UP池子 - 原神武器[1-180]抽: 原神武器UP池子 - 重置原神抽卡: 清空当前卡池的抽卡次数[即从0开始计算UP概率] - 方舟[1-300]抽: 方舟卡池,当有当期UP时指向UP池 - 赛马娘[1-200]抽: 赛马娘卡池,当有当期UP时指向UP池 - 坎公骑冠剑[1-300]抽: 坎公骑冠剑卡池,当有当期UP时指向UP池 - pcr/公主连接[1-300]抽: 公主连接卡池 - 碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池 - fgo[1-300]抽: fgo卡池 - 阴阳师[1-300]抽: 阴阳师卡池 - ba/碧蓝档案[1-200]抽:碧蓝档案卡池 - * 以上指令可以通过 XX一井 来指定最大抽取数量 * - * 示例:原神一井 * -""".strip() -__plugin_superuser_usage__ = """ -usage: - 卡池方面的更新 - 指令: - 更新方舟信息 - 重载方舟卡池 - 更新原神信息 - 重载原神卡池 - 更新赛马娘信息 - 重载赛马娘卡池 - 更新坎公骑冠剑信息 - 更新碧蓝航线信息 - 更新fgo信息 - 更新阴阳师信息 -""".strip() -__plugin_des__ = "就算是模拟抽卡也不能改变自己是个非酋" -__plugin_cmd__ = [ - "原神[1-180]抽", - "原神角色[1-180]抽", - "原神武器[1-180]抽", - "重置原神抽卡", - "方舟[1-300]抽", - "赛马娘[1-200]抽", - "坎公骑冠剑[1-300]抽", - "pcr/公主连接[1-300]抽", - "fgo[1-300]抽", - "阴阳师[1-300]抽", - "碧蓝档案[1-200]抽", - "更新方舟信息 [_superuser]", - "重载方舟卡池 [_superuser]", - "更新原神信息 [_superuser]", - "重载原神卡池 [_superuser]", - "更新赛马娘信息 [_superuser]", - "重载赛马娘卡池 [_superuser]", - "更新坎公骑冠剑信息 [_superuser]", - "更新碧蓝航线信息 [_superuser]", - "更新fgo信息 [_superuser]", - "更新阴阳师信息 [_superuser]", - "更新碧蓝档案信息 [_superuser]", -] -__plugin_type__ = ("抽卡相关", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["游戏抽卡", "抽卡"], -} - - -x = on_message(rule=lambda: False) - - -@dataclass -class Game: - keywords: Set[str] - handle: BaseHandle - flag: bool - config_name: str - max_count: int = 300 # 一次最大抽卡数 - reload_time: Optional[int] = None # 重载UP池时间(小时) - has_other_pool: bool = False - - -games = ( - Game( - {"azur", "碧蓝航线"}, - AzurHandle(), - Config.get_config("draw_card", "AZUR_FLAG", True), - "AZUR_FLAG", - ), - Game( - {"fgo", "命运冠位指定"}, - FgoHandle(), - Config.get_config("draw_card", "FGO_FLAG", True), - "FGO_FLAG", - ), - Game( - {"genshin", "原神"}, - GenshinHandle(), - Config.get_config("draw_card", "GENSHIN_FLAG", True), - "GENSHIN_FLAG", - max_count=180, - reload_time=18, - has_other_pool=True, - ), - Game( - {"guardian", "坎公骑冠剑"}, - GuardianHandle(), - Config.get_config("draw_card", "GUARDIAN_FLAG", True), - "GUARDIAN_FLAG", - reload_time=4, - ), - Game( - {"onmyoji", "阴阳师"}, - OnmyojiHandle(), - Config.get_config("draw_card", "ONMYOJI_FLAG", True), - "ONMYOJI_FLAG", - ), - Game( - {"pcr", "公主连结", "公主连接", "公主链接", "公主焊接"}, - PcrHandle(), - Config.get_config("draw_card", "PCR_FLAG", True), - "PCR_FLAG", - ), - Game( - {"pretty", "马娘", "赛马娘"}, - PrettyHandle(), - Config.get_config("draw_card", "PRETTY_FLAG", True), - "PRETTY_FLAG", - max_count=200, - reload_time=4, - ), - Game( - {"prts", "方舟", "明日方舟"}, - PrtsHandle(), - Config.get_config("draw_card", "PRTS_FLAG", True), - "PRTS_FLAG", - reload_time=4, - ), - Game( - {"ba", "碧蓝档案"}, - BaHandle(), - Config.get_config("draw_card", "BA_FLAG", True), - "BA_FLAG", - ), -) - - -def create_matchers(): - def draw_handler(game: Game) -> T_Handler: - async def handler( - matcher: Matcher, event: MessageEvent, args: Tuple[Any, ...] = RegexGroup() - ): - pool_name, pool_type_, num, unit = args - if num == "单": - num = 1 - else: - try: - num = int(cn2an(num, mode="smart")) - except ValueError: - await matcher.finish("必!须!是!数!字!") - if unit == "井": - num *= game.max_count - if num < 1: - await matcher.finish("虚空抽卡???") - elif num > game.max_count: - await matcher.finish("一井都满不足不了你嘛!快爬开!") - pool_name = ( - pool_name.replace("池", "") - .replace("武器", "arms") - .replace("角色", "char") - .replace("卡牌", "card") - .replace("卡", "card") - ) - try: - if pool_type_ in ["2池", "二池"]: - pool_name = pool_name + "1" - res = game.handle.draw(num, pool_name=pool_name, user_id=event.user_id) - except: - logger.warning(traceback.format_exc()) - await matcher.finish("出错了...") - await matcher.finish(res, at_sender=True) - - return handler - - def update_handler(game: Game) -> T_Handler: - async def handler(matcher: Matcher): - await game.handle.update_info() - await matcher.finish("更新完成!") - - return handler - - def reload_handler(game: Game) -> T_Handler: - async def handler(matcher: Matcher): - res = await game.handle.reload_pool() - if res: - await matcher.finish(res) - - return handler - - def reset_handler(game: Game) -> T_Handler: - async def handler(matcher: Matcher, event: MessageEvent): - if game.handle.reset_count(event.user_id): - await matcher.finish("重置成功!") - - return handler - - def scheduled_job(game: Game) -> T_Handler: - async def handler(): - await game.handle.reload_pool() - - return handler - - for game in games: - pool_pattern = r"([^\s单0-9零一二三四五六七八九百十]{0,3})" - num_pattern = r"(单|[0-9零一二三四五六七八九百十]{1,3})" - unit_pattern = r"([抽|井|连])" - pool_type = "()" - if game.has_other_pool: - pool_type = r"([2二]池)?" - draw_regex = r".*?(?:{})\s*{}\s*{}\s*{}\s*{}".format( - "|".join(game.keywords), pool_pattern, pool_type, num_pattern, unit_pattern - ) - update_keywords = {f"更新{keyword}信息" for keyword in game.keywords} - reload_keywords = {f"重载{keyword}卡池" for keyword in game.keywords} - reset_keywords = {f"重置{keyword}抽卡" for keyword in game.keywords} - on_regex(draw_regex, priority=5, block=True, rule=rule(game)).append_handler( - draw_handler(game) - ) - on_keyword( - update_keywords, priority=1, block=True, permission=SUPERUSER - ).append_handler(update_handler(game)) - on_keyword( - reload_keywords, priority=1, block=True, permission=SUPERUSER - ).append_handler(reload_handler(game)) - on_keyword(reset_keywords, priority=5, block=True).append_handler( - reset_handler(game) - ) - if game.reload_time: - scheduler.add_job( - scheduled_job(game), trigger="cron", hour=game.reload_time, minute=1 - ) - - -create_matchers() - - -# 更新资源 -@scheduler.scheduled_job( - "cron", - hour=4, - minute=1, -) -async def _(): - tasks = [] - for game in games: - if game.flag: - tasks.append(asyncio.ensure_future(game.handle.update_info())) - await asyncio.gather(*tasks) - - -driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - tasks = [] - for game in games: - if game.flag: - game.handle.init_data() - if not game.handle.data_exists(): - tasks.append(asyncio.ensure_future(game.handle.update_info())) - await asyncio.gather(*tasks) +import asyncio +import traceback +from dataclasses import dataclass +from typing import Any + +import nonebot +from cn2an import cn2an +from nonebot import on_keyword, on_message, on_regex +from nonebot.log import logger +from nonebot.matcher import Matcher +from nonebot.params import RegexGroup +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_Handler +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.utils.message import MessageUtils + +from .handles.azur_handle import AzurHandle +from .handles.ba_handle import BaHandle +from .handles.base_handle import BaseHandle +from .handles.fgo_handle import FgoHandle +from .handles.genshin_handle import GenshinHandle +from .handles.guardian_handle import GuardianHandle +from .handles.onmyoji_handle import OnmyojiHandle +from .handles.pcr_handle import PcrHandle +from .handles.pretty_handle import PrettyHandle +from .handles.prts_handle import PrtsHandle +from .rule import rule + +__plugin_meta__ = PluginMetadata( + name="游戏抽卡", + description="就算是模拟抽卡也不能改变自己是个非酋", + usage=""" + usage: + 模拟赛马娘,原神,明日方舟,坎公骑冠剑,公主连结(国/台),碧蓝航线,FGO,阴阳师,碧蓝档案进行抽卡 + 指令: + 原神[1-180]抽: 原神常驻池 + 原神角色[1-180]抽: 原神角色UP池子 + 原神角色2池[1-180]抽: 原神角色UP池子 + 原神武器[1-180]抽: 原神武器UP池子 + 重置原神抽卡: 清空当前卡池的抽卡次数[即从0开始计算UP概率] + 方舟[1-300]抽: 方舟卡池,当有当期UP时指向UP池 + 赛马娘[1-200]抽: 赛马娘卡池,当有当期UP时指向UP池 + 坎公骑冠剑[1-300]抽: 坎公骑冠剑卡池,当有当期UP时指向UP池 + pcr/公主连接[1-300]抽: 公主连接卡池 + 碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池 + fgo[1-300]抽: fgo卡池 (已失效) + 阴阳师[1-300]抽: 阴阳师卡池 + ba/碧蓝档案[1-200]抽:碧蓝档案卡池 + * 以上指令可以通过 XX一井 来指定最大抽取数量 * + * 示例:原神一井 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="抽卡相关", + superuser_help=""" + 更新方舟信息 + 重载方舟卡池 + 更新原神信息 + 重载原神卡池 + 更新赛马娘信息 + 重载赛马娘卡池 + 更新坎公骑冠剑信息 + 更新碧蓝航线信息 + 更新fgo信息 + 更新阴阳师信息 + """, + ).dict(), +) + +_hidden = on_message(rule=lambda: False) + + +@dataclass +class Game: + keywords: set[str] + handle: BaseHandle + flag: bool + config_name: str + max_count: int = 300 # 一次最大抽卡数 + reload_time: int | None = None # 重载UP池时间(小时) + has_other_pool: bool = False + + +games = ( + Game( + {"azur", "碧蓝航线"}, + AzurHandle(), + Config.get_config("draw_card", "AZUR_FLAG", True), + "AZUR_FLAG", + ), + Game( + {"fgo", "命运冠位指定"}, + FgoHandle(), + Config.get_config("draw_card", "FGO_FLAG", True), + "FGO_FLAG", + ), + Game( + {"genshin", "原神"}, + GenshinHandle(), + Config.get_config("draw_card", "GENSHIN_FLAG", True), + "GENSHIN_FLAG", + max_count=180, + reload_time=18, + has_other_pool=True, + ), + Game( + {"guardian", "坎公骑冠剑"}, + GuardianHandle(), + Config.get_config("draw_card", "GUARDIAN_FLAG", True), + "GUARDIAN_FLAG", + reload_time=4, + ), + Game( + {"onmyoji", "阴阳师"}, + OnmyojiHandle(), + Config.get_config("draw_card", "ONMYOJI_FLAG", True), + "ONMYOJI_FLAG", + ), + Game( + {"pcr", "公主连结", "公主连接", "公主链接", "公主焊接"}, + PcrHandle(), + Config.get_config("draw_card", "PCR_FLAG", True), + "PCR_FLAG", + ), + Game( + {"pretty", "马娘", "赛马娘"}, + PrettyHandle(), + Config.get_config("draw_card", "PRETTY_FLAG", True), + "PRETTY_FLAG", + max_count=200, + reload_time=4, + ), + Game( + {"prts", "方舟", "明日方舟"}, + PrtsHandle(), + Config.get_config("draw_card", "PRTS_FLAG", True), + "PRTS_FLAG", + reload_time=4, + ), + Game( + {"ba", "碧蓝档案"}, + BaHandle(), + Config.get_config("draw_card", "BA_FLAG", True), + "BA_FLAG", + ), +) + + +def create_matchers(): + def draw_handler(game: Game) -> T_Handler: + async def handler( + session: EventSession, + args: tuple[Any, ...] = RegexGroup(), + ): + pool_name, pool_type_, num, unit = args + if num == "单": + num = 1 + else: + try: + num = int(cn2an(num, mode="smart")) + except ValueError: + await MessageUtils.build_message("必!须!是!数!字!").finish( + reply_to=True + ) + if unit == "井": + num *= game.max_count + if num < 1: + await MessageUtils.build_message("虚空抽卡???").finish(reply_to=True) + elif num > game.max_count: + await MessageUtils.build_message( + "一井都满不足不了你嘛!快爬开!" + ).finish(reply_to=True) + pool_name = ( + pool_name.replace("池", "") + .replace("武器", "arms") + .replace("角色", "char") + .replace("卡牌", "card") + .replace("卡", "card") + ) + try: + if pool_type_ in ["2池", "二池"]: + pool_name = pool_name + "1" + res = await game.handle.draw( + num, pool_name=pool_name, user_id=session.id1 + ) + logger.info( + f"游戏抽卡 类型: {list(game.keywords)[1]} 卡池: {pool_name} 数量: {num}", + "游戏抽卡", + session=session, + ) + except: + logger.warning(traceback.format_exc()) + await MessageUtils.build_message("出错了...").finish(reply_to=True) + await res.send() + + return handler + + def update_handler(game: Game) -> T_Handler: + async def handler(matcher: Matcher): + await game.handle.update_info() + await matcher.finish("更新完成!") + + return handler + + def reload_handler(game: Game) -> T_Handler: + async def handler(matcher: Matcher): + res = await game.handle.reload_pool() + if res: + await res.send() + + return handler + + def reset_handler(game: Game) -> T_Handler: + async def handler(matcher: Matcher, session: EventSession): + if not session.id1: + await MessageUtils.build_message("获取用户id失败...").finish() + if game.handle.reset_count(session.id1): + await MessageUtils.build_message("重置成功!").send() + + return handler + + def scheduled_job(game: Game) -> T_Handler: + async def handler(): + await game.handle.reload_pool() + + return handler + + for game in games: + pool_pattern = r"([^\s单0-9零一二三四五六七八九百十]{0,3})" + num_pattern = r"(单|[0-9零一二三四五六七八九百十]{1,3})" + unit_pattern = r"([抽|井|连])" + pool_type = "()" + if game.has_other_pool: + pool_type = r"([2二]池)?" + draw_regex = r".*?(?:{})\s*{}\s*{}\s*{}\s*{}".format( + "|".join(game.keywords), pool_pattern, pool_type, num_pattern, unit_pattern + ) + update_keywords = {f"更新{keyword}信息" for keyword in game.keywords} + reload_keywords = {f"重载{keyword}卡池" for keyword in game.keywords} + reset_keywords = {f"重置{keyword}抽卡" for keyword in game.keywords} + on_regex(draw_regex, priority=5, block=True, rule=rule(game)).append_handler( + draw_handler(game) + ) + on_keyword( + update_keywords, priority=1, block=True, permission=SUPERUSER + ).append_handler(update_handler(game)) + on_keyword( + reload_keywords, priority=1, block=True, permission=SUPERUSER + ).append_handler(reload_handler(game)) + on_keyword(reset_keywords, priority=5, block=True).append_handler( + reset_handler(game) + ) + if game.reload_time: + scheduler.add_job( + scheduled_job(game), trigger="cron", hour=game.reload_time, minute=1 + ) + + +create_matchers() + + +# 更新资源 +@scheduler.scheduled_job( + "cron", + hour=4, + minute=1, +) +async def _(): + tasks = [] + for game in games: + if game.flag: + tasks.append(asyncio.ensure_future(game.handle.update_info())) + await asyncio.gather(*tasks) + + +driver = nonebot.get_driver() + + +@driver.on_startup +async def _(): + tasks = [] + for game in games: + if game.flag: + game.handle.init_data() + if not game.handle.data_exists(): + tasks.append(asyncio.ensure_future(game.handle.update_info())) + await asyncio.gather(*tasks) diff --git a/plugins/draw_card/config.py b/zhenxun/plugins/draw_card/config.py similarity index 87% rename from plugins/draw_card/config.py rename to zhenxun/plugins/draw_card/config.py index d809b1fd..0aff3ef8 100644 --- a/plugins/draw_card/config.py +++ b/zhenxun/plugins/draw_card/config.py @@ -1,204 +1,203 @@ -from pathlib import Path - -import nonebot -from nonebot.log import logger -from pydantic import BaseModel, Extra, ValidationError - -from configs.config import Config as AConfig -from configs.path_config import DATA_PATH, IMAGE_PATH - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -# 原神 -class GenshinConfig(BaseModel, extra=Extra.ignore): - GENSHIN_FIVE_P: float = 0.006 - GENSHIN_FOUR_P: float = 0.051 - GENSHIN_THREE_P: float = 0.43 - GENSHIN_G_FIVE_P: float = 0.016 - GENSHIN_G_FOUR_P: float = 0.13 - I72_ADD: float = 0.0585 - - -# 明日方舟 -class PrtsConfig(BaseModel, extra=Extra.ignore): - PRTS_SIX_P: float = 0.02 - PRTS_FIVE_P: float = 0.08 - PRTS_FOUR_P: float = 0.48 - PRTS_THREE_P: float = 0.42 - - -# 赛马娘 -class PrettyConfig(BaseModel, extra=Extra.ignore): - PRETTY_THREE_P: float = 0.03 - PRETTY_TWO_P: float = 0.18 - PRETTY_ONE_P: float = 0.79 - - -# 坎公骑冠剑 -class GuardianConfig(BaseModel, extra=Extra.ignore): - GUARDIAN_THREE_CHAR_P: float = 0.0275 - GUARDIAN_TWO_CHAR_P: float = 0.19 - GUARDIAN_ONE_CHAR_P: float = 0.7825 - GUARDIAN_THREE_CHAR_UP_P: float = 0.01375 - GUARDIAN_THREE_CHAR_OTHER_P: float = 0.01375 - GUARDIAN_EXCLUSIVE_ARMS_P: float = 0.03 - GUARDIAN_FIVE_ARMS_P: float = 0.03 - GUARDIAN_FOUR_ARMS_P: float = 0.09 - GUARDIAN_THREE_ARMS_P: float = 0.27 - GUARDIAN_TWO_ARMS_P: float = 0.58 - GUARDIAN_EXCLUSIVE_ARMS_UP_P: float = 0.01 - GUARDIAN_EXCLUSIVE_ARMS_OTHER_P: float = 0.02 - - -# 公主连结 -class PcrConfig(BaseModel, extra=Extra.ignore): - PCR_THREE_P: float = 0.025 - PCR_TWO_P: float = 0.18 - PCR_ONE_P: float = 0.795 - PCR_G_THREE_P: float = 0.025 - PCR_G_TWO_P: float = 0.975 - - -# 碧蓝航线 -class AzurConfig(BaseModel, extra=Extra.ignore): - AZUR_FIVE_P: float = 0.012 - AZUR_FOUR_P: float = 0.07 - AZUR_THREE_P: float = 0.12 - AZUR_TWO_P: float = 0.51 - AZUR_ONE_P: float = 0.3 - - -# 命运-冠位指定 -class FgoConfig(BaseModel, extra=Extra.ignore): - FGO_SERVANT_FIVE_P: float = 0.01 - FGO_SERVANT_FOUR_P: float = 0.03 - FGO_SERVANT_THREE_P: float = 0.4 - FGO_CARD_FIVE_P: float = 0.04 - FGO_CARD_FOUR_P: float = 0.12 - FGO_CARD_THREE_P: float = 0.4 - - -# 阴阳师 -class OnmyojiConfig(BaseModel, extra=Extra.ignore): - ONMYOJI_SP: float = 0.0025 - ONMYOJI_SSR: float = 0.01 - ONMYOJI_SR: float = 0.2 - ONMYOJI_R: float = 0.7875 - - -# 碧蓝档案 -class BaConfig(BaseModel, extra=Extra.ignore): - BA_THREE_P: float = 0.025 - BA_TWO_P: float = 0.185 - BA_ONE_P: float = 0.79 - BA_G_TWO_P: float = 0.975 - - -class Config(BaseModel, extra=Extra.ignore): - # 开关 - PRTS_FLAG: bool = True - GENSHIN_FLAG: bool = True - PRETTY_FLAG: bool = True - GUARDIAN_FLAG: bool = True - PCR_FLAG: bool = True - AZUR_FLAG: bool = True - FGO_FLAG: bool = True - ONMYOJI_FLAG: bool = True - BA_FLAG: bool = True - - # 其他配置 - PCR_TAI: bool = True - SEMAPHORE: int = 5 - - # 抽卡概率 - prts: PrtsConfig = PrtsConfig() - genshin: GenshinConfig = GenshinConfig() - pretty: PrettyConfig = PrettyConfig() - guardian: GuardianConfig = GuardianConfig() - pcr: PcrConfig = PcrConfig() - azur: AzurConfig = AzurConfig() - fgo: FgoConfig = FgoConfig() - onmyoji: OnmyojiConfig = OnmyojiConfig() - ba: BaConfig = BaConfig() - - -driver = nonebot.get_driver() - -# DRAW_PATH = Path("data/draw_card").absolute() -DRAW_PATH = IMAGE_PATH / "draw_card" -# try: -# DRAW_PATH = Path(global_config.draw_path).absolute() -# except: -# pass -config_path = DATA_PATH / "draw_card" / "draw_card_config" / "draw_card_config.json" - -draw_config: Config = Config() - -for game_flag, game_name in zip( - [ - "PRTS_FLAG", - "GENSHIN_FLAG", - "PRETTY_FLAG", - "GUARDIAN_FLAG", - "PCR_FLAG", - "AZUR_FLAG", - "FGO_FLAG", - "ONMYOJI_FLAG", - "PCR_TAI", - "BA_FLAG", - ], - [ - "明日方舟", - "原神", - "赛马娘", - "坎公骑冠剑", - "公主连结", - "碧蓝航线", - "命运-冠位指定(FGO)", - "阴阳师", - "pcr台服卡池", - "碧蓝档案", - ], -): - AConfig.add_plugin_config( - "draw_card", - game_flag, - True, - name="游戏抽卡", - help_=f"{game_name} 抽卡开关", - default_value=True, - type=bool, - ) -AConfig.add_plugin_config( - "draw_card", "SEMAPHORE", 5, help_=f"异步数据下载数量限制", default_value=5, type=int -) - - -@driver.on_startup -def check_config(): - global draw_config - - if not config_path.exists(): - config_path.parent.mkdir(parents=True, exist_ok=True) - draw_config = Config() - logger.warning("draw_card:配置文件不存在,已重新生成配置文件.....") - else: - with config_path.open("r", encoding="utf8") as fp: - data = json.load(fp) - try: - draw_config = Config.parse_obj({**data}) - except ValidationError: - draw_config = Config() - logger.warning("draw_card:配置文件格式错误,已重新生成配置文件.....") - - with config_path.open("w", encoding="utf8") as fp: - json.dump( - draw_config.dict(), - fp, - indent=4, - ensure_ascii=False, - ) +import nonebot +import ujson as json +from pydantic import BaseModel, Extra, ValidationError + +from zhenxun.configs.config import Config as AConfig +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.services.log import logger + + +# 原神 +class GenshinConfig(BaseModel, extra=Extra.ignore): + GENSHIN_FIVE_P: float = 0.006 + GENSHIN_FOUR_P: float = 0.051 + GENSHIN_THREE_P: float = 0.43 + GENSHIN_G_FIVE_P: float = 0.016 + GENSHIN_G_FOUR_P: float = 0.13 + I72_ADD: float = 0.0585 + + +# 明日方舟 +class PrtsConfig(BaseModel, extra=Extra.ignore): + PRTS_SIX_P: float = 0.02 + PRTS_FIVE_P: float = 0.08 + PRTS_FOUR_P: float = 0.48 + PRTS_THREE_P: float = 0.42 + + +# 赛马娘 +class PrettyConfig(BaseModel, extra=Extra.ignore): + PRETTY_THREE_P: float = 0.03 + PRETTY_TWO_P: float = 0.18 + PRETTY_ONE_P: float = 0.79 + + +# 坎公骑冠剑 +class GuardianConfig(BaseModel, extra=Extra.ignore): + GUARDIAN_THREE_CHAR_P: float = 0.0275 + GUARDIAN_TWO_CHAR_P: float = 0.19 + GUARDIAN_ONE_CHAR_P: float = 0.7825 + GUARDIAN_THREE_CHAR_UP_P: float = 0.01375 + GUARDIAN_THREE_CHAR_OTHER_P: float = 0.01375 + GUARDIAN_EXCLUSIVE_ARMS_P: float = 0.03 + GUARDIAN_FIVE_ARMS_P: float = 0.03 + GUARDIAN_FOUR_ARMS_P: float = 0.09 + GUARDIAN_THREE_ARMS_P: float = 0.27 + GUARDIAN_TWO_ARMS_P: float = 0.58 + GUARDIAN_EXCLUSIVE_ARMS_UP_P: float = 0.01 + GUARDIAN_EXCLUSIVE_ARMS_OTHER_P: float = 0.02 + + +# 公主连结 +class PcrConfig(BaseModel, extra=Extra.ignore): + PCR_THREE_P: float = 0.025 + PCR_TWO_P: float = 0.18 + PCR_ONE_P: float = 0.795 + PCR_G_THREE_P: float = 0.025 + PCR_G_TWO_P: float = 0.975 + + +# 碧蓝航线 +class AzurConfig(BaseModel, extra=Extra.ignore): + AZUR_FIVE_P: float = 0.012 + AZUR_FOUR_P: float = 0.07 + AZUR_THREE_P: float = 0.12 + AZUR_TWO_P: float = 0.51 + AZUR_ONE_P: float = 0.3 + + +# 命运-冠位指定 +class FgoConfig(BaseModel, extra=Extra.ignore): + FGO_SERVANT_FIVE_P: float = 0.01 + FGO_SERVANT_FOUR_P: float = 0.03 + FGO_SERVANT_THREE_P: float = 0.4 + FGO_CARD_FIVE_P: float = 0.04 + FGO_CARD_FOUR_P: float = 0.12 + FGO_CARD_THREE_P: float = 0.4 + + +# 阴阳师 +class OnmyojiConfig(BaseModel, extra=Extra.ignore): + ONMYOJI_SP: float = 0.0025 + ONMYOJI_SSR: float = 0.01 + ONMYOJI_SR: float = 0.2 + ONMYOJI_R: float = 0.7875 + + +# 碧蓝档案 +class BaConfig(BaseModel, extra=Extra.ignore): + BA_THREE_P: float = 0.025 + BA_TWO_P: float = 0.185 + BA_ONE_P: float = 0.79 + BA_G_TWO_P: float = 0.975 + + +class Config(BaseModel, extra=Extra.ignore): + # 开关 + PRTS_FLAG: bool = True + GENSHIN_FLAG: bool = True + PRETTY_FLAG: bool = True + GUARDIAN_FLAG: bool = True + PCR_FLAG: bool = True + AZUR_FLAG: bool = True + FGO_FLAG: bool = True + ONMYOJI_FLAG: bool = True + BA_FLAG: bool = True + + # 其他配置 + PCR_TAI: bool = False + SEMAPHORE: int = 5 + + # 抽卡概率 + prts: PrtsConfig = PrtsConfig() + genshin: GenshinConfig = GenshinConfig() + pretty: PrettyConfig = PrettyConfig() + guardian: GuardianConfig = GuardianConfig() + pcr: PcrConfig = PcrConfig() + azur: AzurConfig = AzurConfig() + fgo: FgoConfig = FgoConfig() + onmyoji: OnmyojiConfig = OnmyojiConfig() + ba: BaConfig = BaConfig() + + +driver = nonebot.get_driver() + +# DRAW_PATH = Path("data/draw_card").absolute() +DRAW_PATH = IMAGE_PATH / "draw_card" +# try: +# DRAW_PATH = Path(global_config.draw_path).absolute() +# except: +# pass +config_path = DATA_PATH / "draw_card" / "draw_card_config" / "draw_card_config.json" + +draw_config: Config = Config() + +for game_flag, game_name in zip( + [ + "PRTS_FLAG", + "GENSHIN_FLAG", + "PRETTY_FLAG", + "GUARDIAN_FLAG", + "PCR_FLAG", + "AZUR_FLAG", + "FGO_FLAG", + "ONMYOJI_FLAG", + "PCR_TAI", + "BA_FLAG", + ], + [ + "明日方舟", + "原神", + "赛马娘", + "坎公骑冠剑", + "公主连结", + "碧蓝航线", + "命运-冠位指定(FGO)", + "阴阳师", + "pcr台服卡池", + "碧蓝档案", + ], +): + AConfig.add_plugin_config( + "draw_card", + game_flag, + True, + help=f"{game_name} 抽卡开关", + default_value=True, + type=bool, + ) +AConfig.add_plugin_config( + "draw_card", + "SEMAPHORE", + 5, + help=f"异步数据下载数量限制", + default_value=5, + type=int, +) +AConfig.set_name("draw_card", "游戏抽卡") + + +@driver.on_startup +def check_config(): + global draw_config + + if not config_path.exists(): + config_path.parent.mkdir(parents=True, exist_ok=True) + draw_config = Config() + logger.warning("draw_card:配置文件不存在,已重新生成配置文件...") + else: + with config_path.open("r", encoding="utf8") as fp: + data = json.load(fp) + try: + draw_config = Config.parse_obj({**data}) + except ValidationError: + draw_config = Config() + logger.warning("draw_card:配置文件格式错误,已重新生成配置文件...") + + with config_path.open("w", encoding="utf8") as fp: + json.dump( + draw_config.dict(), + fp, + indent=4, + ensure_ascii=False, + ) diff --git a/plugins/draw_card/count_manager.py b/zhenxun/plugins/draw_card/count_manager.py similarity index 100% rename from plugins/draw_card/count_manager.py rename to zhenxun/plugins/draw_card/count_manager.py diff --git a/plugins/draw_card/handles/azur_handle.py b/zhenxun/plugins/draw_card/handles/azur_handle.py similarity index 85% rename from plugins/draw_card/handles/azur_handle.py rename to zhenxun/plugins/draw_card/handles/azur_handle.py index 0ae2dade..67242a77 100644 --- a/plugins/draw_card/handles/azur_handle.py +++ b/zhenxun/plugins/draw_card/handles/azur_handle.py @@ -1,304 +1,308 @@ -import random -import dateparser -from lxml import etree -from typing import List, Optional, Tuple -from PIL import ImageDraw -from urllib.parse import unquote -from pydantic import ValidationError -from nonebot.log import logger -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from utils.message_builder import image -from .base_handle import BaseHandle, BaseData, UpEvent as _UpEvent, UpChar as _UpChar -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -class AzurChar(BaseData): - type_: str # 舰娘类型 - - @property - def star_str(self) -> str: - return ["白", "蓝", "紫", "金"][self.star - 1] - - -class UpChar(_UpChar): - type_: str # 舰娘类型 - - -class UpEvent(_UpEvent): - up_char: List[UpChar] # up对象 - - -class AzurHandle(BaseHandle[AzurChar]): - def __init__(self): - super().__init__("azur", "碧蓝航线") - self.max_star = 4 - self.config = draw_config.azur - self.ALL_CHAR: List[AzurChar] = [] - self.UP_EVENT: Optional[UpEvent] = None - - def get_card(self, pool_name: str, **kwargs) -> AzurChar: - if pool_name == "轻型": - type_ = ["驱逐", "轻巡", "维修"] - elif pool_name == "重型": - type_ = ["重巡", "战列", "战巡", "重炮"] - else: - type_ = ["维修", "潜艇", "重巡", "轻航", "航母"] - up_pool_flag = pool_name == "活动" - # Up - up_ship = ( - [x for x in self.UP_EVENT.up_char if x.zoom > 0] if self.UP_EVENT else [] - ) - # print(up_ship) - acquire_char = None - if up_ship and up_pool_flag: - up_zoom: List[Tuple[float, float]] = [(0, up_ship[0].zoom / 100)] - # 初始化概率 - cur_ = up_ship[0].zoom / 100 - for i in range(len(up_ship)): - try: - up_zoom.append((cur_, cur_ + up_ship[i + 1].zoom / 100)) - cur_ += up_ship[i + 1].zoom / 100 - except IndexError: - pass - rand = random.random() - # 抽取up - for i, zoom in enumerate(up_zoom): - if zoom[0] <= rand <= zoom[1]: - try: - acquire_char = [ - x for x in self.ALL_CHAR if x.name == up_ship[i].name - ][0] - except IndexError: - pass - # 没有up或者未抽取到up - if not acquire_char: - star = self.get_star( - [4, 3, 2, 1], - [ - self.config.AZUR_FOUR_P, - self.config.AZUR_THREE_P, - self.config.AZUR_TWO_P, - self.config.AZUR_ONE_P, - ], - ) - acquire_char = random.choice( - [ - x - for x in self.ALL_CHAR - if x.star == star and x.type_ in type_ and not x.limited - ] - ) - return acquire_char - - def draw(self, count: int, **kwargs) -> Message: - index2card = self.get_cards(count, **kwargs) - cards = [card[0] for card in index2card] - up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] - result = self.format_result(index2card, **{**kwargs, "up_list": up_list}) - return image(b64=self.generate_img(cards).pic2bs4()) + result - - def generate_card_img(self, card: AzurChar) -> BuildImage: - sep_w = 5 - sep_t = 5 - sep_b = 20 - w = 100 - h = 100 - bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) - frame_path = str(self.img_path / f"{card.star}_star.png") - frame = BuildImage(w, h, background=frame_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(w, h, background=img_path) - # 加圆角 - frame.circle_corner(6) - img.circle_corner(6) - bg.paste(img, (sep_w, sep_t), alpha=True) - bg.paste(frame, (sep_w, sep_t), alpha=True) - # 加名字 - text = card.name[:6] + "..." if len(card.name) > 7 else card.name - font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), - text, - font=font, - fill=["#808080", "#3b8bff", "#8000ff", "#c90", "#ee494c"][card.star - 1], - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - AzurChar( - name=value["名称"], - star=int(value["星级"]), - limited="可以建造" not in value["获取途径"], - type_=value["类型"], - ) - for value in self.load_data().values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_EVENT: - data = {"char": json.loads(self.UP_EVENT.json())} - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - info = {} - # 更新图鉴 - url = "https://wiki.biligame.com/blhx/舰娘图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - contents = dom.xpath( - "//div[@class='mw-body-content mw-content-ltr']/div[@class='mw-parser-output']" - ) - for index, content in enumerate(contents): - char_list = content.xpath("./div[@id='CardSelectTr']/div") - for char in char_list: - try: - name = char.xpath("./div/a/@title")[0] - frame = char.xpath("./div/div/a/img/@alt")[0] - avatar = char.xpath("./div/a/img/@srcset")[0] - except IndexError: - continue - member_dict = { - "名称": remove_prohibited_str(name), - "头像": unquote(str(avatar).split(" ")[-2]), - "星级": self.parse_star(frame), - "类型": char.xpath("./@data-param1")[0].split(",")[1], - } - info[member_dict["名称"]] = member_dict - # 更新额外信息 - for key in info.keys(): - url = f"https://wiki.biligame.com/blhx/{key}" - result = await self.get_url(url) - if not result: - info[key]["获取途径"] = [] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - time = dom.xpath( - "//table[@class='wikitable sv-general']/tbody[1]/tr[4]/td[2]//text()" - )[0] - sources = [] - if "无法建造" in time: - sources.append("无法建造") - elif "活动已关闭" in time: - sources.append("活动限定") - else: - sources.append("可以建造") - info[key]["获取途径"] = sources - except IndexError: - info[key]["获取途径"] = [] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载头像框 - idx = 1 - BLHX_URL = "https://patchwiki.biligame.com/images/blhx" - for url in [ - "/1/15/pxho13xsnkyb546tftvh49etzdh74cf.png", - "/a/a9/k8t7nx6c8pan5vyr8z21txp45jxeo66.png", - "/a/a5/5whkzvt200zwhhx0h0iz9qo1kldnidj.png", - "/a/a2/ptog1j220x5q02hytpwc8al7f229qk9.png", - "/6/6d/qqv5oy3xs40d3055cco6bsm0j4k4gzk.png", - ]: - await self.download_img(BLHX_URL + url, f"{idx}_star") - idx += 1 - await self.update_up_char() - - @staticmethod - def parse_star(star: str) -> int: - if star in ["舰娘头像外框普通.png", "舰娘头像外框白色.png"]: - return 1 - elif star in ["舰娘头像外框稀有.png", "舰娘头像外框蓝色.png"]: - return 2 - elif star in ["舰娘头像外框精锐.png", "舰娘头像外框紫色.png"]: - return 3 - elif star in ["舰娘头像外框超稀有.png", "舰娘头像外框金色.png"]: - return 4 - elif star in ["舰娘头像外框海上传奇.png", "舰娘头像外框彩色.png"]: - return 5 - elif star in [ - "舰娘头像外框最高方案.png", - "舰娘头像外框决战方案.png", - "舰娘头像外框超稀有META.png", - "舰娘头像外框精锐META.png", - ]: - return 6 - else: - return 6 - - async def update_up_char(self): - url = "https://wiki.biligame.com/blhx/游戏活动表" - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取活动表出错") - return - try: - dom = etree.HTML(result, etree.HTMLParser()) - dd = dom.xpath("//div[@class='timeline2']/dl/dd/a")[0] - url = "https://wiki.biligame.com" + dd.xpath("./@href")[0] - title = dd.xpath("string(.)") - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取活动页面出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - timer = dom.xpath("//span[@class='eventTimer']")[0] - start_time = dateparser.parse(timer.xpath("./@data-start")[0]) - end_time = dateparser.parse(timer.xpath("./@data-end")[0]) - ships = dom.xpath("//table[@class='shipinfo']") - up_chars = [] - for ship in ships: - name = ship.xpath("./tbody/tr/td[2]/p/a/@title")[0] - type_ = ship.xpath("./tbody/tr/td[2]/p/small/text()")[0] # 舰船类型 - try: - p = float(str(ship.xpath(".//sup/text()")[0]).strip("%")) - except (IndexError, ValueError): - p = 0 - star = self.parse_star( - ship.xpath("./tbody/tr/td[1]/div/div/div/a/img/@alt")[0] - ) - up_chars.append( - UpChar(name=name, star=star, limited=False, zoom=p, type_=type_) - ) - self.UP_EVENT = UpEvent( - title=title, - pool_img="", - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.dump_up_char() - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_EVENT: - return Message(f"重载成功!\n当前活动:{self.UP_EVENT.title}") +import random +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from nonebot_plugin_alconna import UniMessage +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle +from .base_handle import UpChar as _UpChar +from .base_handle import UpEvent as _UpEvent + + +class AzurChar(BaseData): + type_: str # 舰娘类型 + + @property + def star_str(self) -> str: + return ["白", "蓝", "紫", "金"][self.star - 1] + + +class UpChar(_UpChar): + type_: str # 舰娘类型 + + +class UpEvent(_UpEvent): + up_char: list[UpChar] # up对象 + + +class AzurHandle(BaseHandle[AzurChar]): + def __init__(self): + super().__init__("azur", "碧蓝航线") + self.max_star = 4 + self.config = draw_config.azur + self.ALL_CHAR: list[AzurChar] = [] + self.UP_EVENT: UpEvent | None = None + + def get_card(self, pool_name: str, **kwargs) -> AzurChar: + if pool_name == "轻型": + type_ = ["驱逐", "轻巡", "维修"] + elif pool_name == "重型": + type_ = ["重巡", "战列", "战巡", "重炮"] + else: + type_ = ["维修", "潜艇", "重巡", "轻航", "航母"] + up_pool_flag = pool_name == "活动" + # Up + up_ship = ( + [x for x in self.UP_EVENT.up_char if x.zoom > 0] if self.UP_EVENT else [] + ) + # print(up_ship) + acquire_char = None + if up_ship and up_pool_flag: + up_zoom: list[tuple[float, float]] = [(0, up_ship[0].zoom / 100)] + # 初始化概率 + cur_ = up_ship[0].zoom / 100 + for i in range(len(up_ship)): + try: + up_zoom.append((cur_, cur_ + up_ship[i + 1].zoom / 100)) + cur_ += up_ship[i + 1].zoom / 100 + except IndexError: + pass + rand = random.random() + # 抽取up + for i, zoom in enumerate(up_zoom): + if zoom[0] <= rand <= zoom[1]: + try: + acquire_char = [ + x for x in self.ALL_CHAR if x.name == up_ship[i].name + ][0] + except IndexError: + pass + # 没有up或者未抽取到up + if not acquire_char: + star = self.get_star( + # [4, 3, 2, 1], + [4, 3, 2, 2], + [ + self.config.AZUR_FOUR_P, + self.config.AZUR_THREE_P, + self.config.AZUR_TWO_P, + self.config.AZUR_ONE_P, + ], + ) + acquire_char = random.choice( + [ + x + for x in self.ALL_CHAR + if x.star == star and x.type_ in type_ and not x.limited + ] + ) + return acquire_char + + async def draw(self, count: int, **kwargs) -> UniMessage: + index2card = self.get_cards(count, **kwargs) + cards = [card[0] for card in index2card] + up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] + result = self.format_result(index2card, **{**kwargs, "up_list": up_list}) + gen_img = await self.generate_img(cards) + return MessageUtils.build_message([gen_img.pic2bytes(), result]) + + async def generate_card_img(self, card: AzurChar) -> BuildImage: + sep_w = 5 + sep_t = 5 + sep_b = 20 + w = 100 + h = 100 + bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) + frame_path = str(self.img_path / f"{card.star}_star.png") + frame = BuildImage(w, h, background=frame_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(w, h, background=img_path) + # 加圆角 + await frame.circle_corner(6) + await img.circle_corner(6) + await bg.paste(img, (sep_w, sep_t)) + await bg.paste(frame, (sep_w, sep_t)) + # 加名字 + text = card.name[:6] + "..." if len(card.name) > 7 else card.name + font = load_font(fontsize=14) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), + text, + font=font, + fill=["#808080", "#3b8bff", "#8000ff", "#c90", "#ee494c"][card.star - 1], + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + AzurChar( + name=value["名称"], + star=int(value["星级"]), + limited="可以建造" not in value["获取途径"], + type_=value["类型"], + ) + for value in self.load_data().values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_EVENT: + data = {"char": json.loads(self.UP_EVENT.json())} + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + info = {} + # 更新图鉴 + url = "https://wiki.biligame.com/blhx/舰娘图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + contents = dom.xpath( + "//div[@class='mw-body-content mw-content-ltr']/div[@class='mw-parser-output']" + ) + for index, content in enumerate(contents): + char_list = content.xpath("./div[@id='CardSelectTr']/div") + for char in char_list: + try: + name = char.xpath("./span/a/text()")[0] + frame = char.xpath("./div/div/a/img/@alt")[0] + avatar = char.xpath("./div/img/@srcset")[0] + except IndexError: + continue + member_dict = { + "名称": remove_prohibited_str(name), + "头像": unquote(str(avatar).split(" ")[-2]), + "星级": self.parse_star(frame), + "类型": char.xpath("./@data-param1")[0].split(",")[-1], + } + info[member_dict["名称"]] = member_dict + # 更新额外信息 + for key in info.keys(): + # TODO: 各种舰娘·改获取错误 + url = f"https://wiki.biligame.com/blhx/{key}" + result = await self.get_url(url) + if not result: + info[key]["获取途径"] = [] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + time = dom.xpath( + "//table[@class='wikitable sv-general']/tbody[1]/tr[4]/td[2]//text()" + )[0] + sources = [] + if "无法建造" in time: + sources.append("无法建造") + elif "活动已关闭" in time: + sources.append("活动限定") + else: + sources.append("可以建造") + info[key]["获取途径"] = sources + except IndexError: + info[key]["获取途径"] = [] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载头像框 + idx = 1 + BLHX_URL = "https://patchwiki.biligame.com/images/blhx" + for url in [ + "/1/15/pxho13xsnkyb546tftvh49etzdh74cf.png", + "/a/a9/k8t7nx6c8pan5vyr8z21txp45jxeo66.png", + "/a/a5/5whkzvt200zwhhx0h0iz9qo1kldnidj.png", + "/a/a2/ptog1j220x5q02hytpwc8al7f229qk9.png", + "/6/6d/qqv5oy3xs40d3055cco6bsm0j4k4gzk.png", + ]: + await self.download_img(BLHX_URL + url, f"{idx}_star") + idx += 1 + await self.update_up_char() + + @staticmethod + def parse_star(star: str) -> int: + if star in ["舰娘头像外框普通.png", "舰娘头像外框白色.png"]: + return 1 + elif star in ["舰娘头像外框稀有.png", "舰娘头像外框蓝色.png"]: + return 2 + elif star in ["舰娘头像外框精锐.png", "舰娘头像外框紫色.png"]: + return 3 + elif star in ["舰娘头像外框超稀有.png", "舰娘头像外框金色.png"]: + return 4 + elif star in ["舰娘头像外框海上传奇.png", "舰娘头像外框彩色.png"]: + return 5 + elif star in [ + "舰娘头像外框最高方案.png", + "舰娘头像外框决战方案.png", + "舰娘头像外框超稀有META.png", + "舰娘头像外框精锐META.png", + ]: + return 6 + else: + return 6 + + async def update_up_char(self): + url = "https://wiki.biligame.com/blhx/游戏活动表" + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取活动表出错") + return + try: + dom = etree.HTML(result, etree.HTMLParser()) + dd = dom.xpath("//div[@class='timeline2']/dl/dd/a")[0] + url = "https://wiki.biligame.com" + dd.xpath("./@href")[0] + title = dd.xpath("string(.)") + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取活动页面出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + timer = dom.xpath("//span[@class='eventTimer']")[0] + start_time = dateparser.parse(timer.xpath("./@data-start")[0]) + end_time = dateparser.parse(timer.xpath("./@data-end")[0]) + ships = dom.xpath("//table[@class='shipinfo']") + up_chars = [] + for ship in ships: + name = ship.xpath("./tbody/tr/td[2]/p/a/@title")[0] + type_ = ship.xpath("./tbody/tr/td[2]/p/small/text()")[0] # 舰船类型 + try: + p = float(str(ship.xpath(".//sup/text()")[0]).strip("%")) + except (IndexError, ValueError): + p = 0 + star = self.parse_star( + ship.xpath("./tbody/tr/td[1]/div/div/div/a/img/@alt")[0] + ) + up_chars.append( + UpChar(name=name, star=star, limited=False, zoom=p, type_=type_) + ) + self.UP_EVENT = UpEvent( + title=title, + pool_img="", + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.dump_up_char() + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错", e=e) + + async def _reload_pool(self) -> UniMessage | None: + await self.update_up_char() + self.load_up_char() + if self.UP_EVENT: + return MessageUtils.build_message( + f"重载成功!\n当前活动:{self.UP_EVENT.title}" + ) diff --git a/plugins/draw_card/handles/ba_handle.py b/zhenxun/plugins/draw_card/handles/ba_handle.py similarity index 84% rename from plugins/draw_card/handles/ba_handle.py rename to zhenxun/plugins/draw_card/handles/ba_handle.py index 658bb2df..d347504a 100644 --- a/plugins/draw_card/handles/ba_handle.py +++ b/zhenxun/plugins/draw_card/handles/ba_handle.py @@ -1,15 +1,13 @@ import random -from typing import List, Tuple -from urllib.parse import unquote -from lxml import etree -from nonebot.log import logger from PIL import ImageDraw -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage from ..config import draw_config -from ..util import cn2py, load_font, remove_prohibited_str +from ..util import cn2py, load_font from .base_handle import BaseData, BaseHandle @@ -22,7 +20,7 @@ class BaHandle(BaseHandle[BaChar]): super().__init__("ba", "碧蓝档案") self.max_star = 3 self.config = draw_config.ba - self.ALL_CHAR: List[BaChar] = [] + self.ALL_CHAR: list[BaChar] = [] def get_card(self, mode: int = 1) -> BaChar: if mode == 2: @@ -37,7 +35,7 @@ class BaHandle(BaseHandle[BaChar]): chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] return random.choice(chars) - def get_cards(self, count: int, **kwargs) -> List[Tuple[BaChar, int]]: + def get_cards(self, count: int, **kwargs) -> list[tuple[BaChar, int]]: card_list = [] card_count = 0 # 保底计算 for i in range(count): @@ -53,7 +51,7 @@ class BaHandle(BaseHandle[BaChar]): card_list.append((card, i + 1)) return card_list - def generate_card_img(self, card: BaChar) -> BuildImage: + async def generate_card_img(self, card: BaChar) -> BuildImage: sep_w = 5 sep_h = 5 star_h = 15 @@ -63,11 +61,13 @@ class BaHandle(BaseHandle[BaChar]): bar_h = 20 bar_w = 90 bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) + img_path = self.img_path / f"{cn2py(card.name)}.png" + img = BuildImage( + img_w, img_h, background=img_path if img_path.exists() else None + ) bar = BuildImage(bar_w, bar_h, color="#6495ED") - bg.paste(img, (sep_w, sep_h), alpha=True) - bg.paste(bar, (sep_w, img_h - bar_h + sep_h), alpha=True) + await bg.paste(img, (sep_w, sep_h)) + await bg.paste(bar, (sep_w, img_h - bar_h + sep_h)) if card.star == 1: star_path = str(self.img_path / "star-1.png") star_w = 15 @@ -78,12 +78,10 @@ class BaHandle(BaseHandle[BaChar]): star_path = str(self.img_path / "star-3.png") star_w = 45 star = BuildImage(star_w, star_h, background=star_path) - bg.paste( - star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h), alpha=True - ) + await bg.paste(star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h)) text = card.name[:5] + "..." if len(card.name) > 6 else card.name font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) + text_w, text_h = BuildImage.get_text_size(text, font) draw = ImageDraw.Draw(bg.markImg) draw.text( (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), @@ -112,6 +110,7 @@ class BaHandle(BaseHandle[BaChar]): return 1 async def _update_info(self): + # TODO: ba获取链接失效 info = {} url = "https://schale.gg/data/cn/students.min.json?v=49" result = (await AsyncHttpx.get(url)).json() @@ -129,6 +128,7 @@ class BaHandle(BaseHandle[BaChar]): + ".webp" ) star = char["StarGrade"] + star = char["StarGrade"] except IndexError: continue member_dict = { diff --git a/plugins/draw_card/handles/base_handle.py b/zhenxun/plugins/draw_card/handles/base_handle.py similarity index 72% rename from plugins/draw_card/handles/base_handle.py rename to zhenxun/plugins/draw_card/handles/base_handle.py index ac0ee79e..3483d246 100644 --- a/plugins/draw_card/handles/base_handle.py +++ b/zhenxun/plugins/draw_card/handles/base_handle.py @@ -1,27 +1,23 @@ -import math -import anyio -import random -import aiohttp import asyncio -from PIL import Image -from datetime import datetime -from pydantic import BaseModel, Extra +import random from asyncio.exceptions import TimeoutError -from typing import Dict, List, Optional, TypeVar, Generic, Tuple -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger +from datetime import datetime +from typing import Generic, TypeVar -from configs.path_config import DATA_PATH -from utils.message_builder import image +import aiohttp +import anyio +import ujson as json +from nonebot_plugin_alconna import UniMessage +from PIL import Image +from pydantic import BaseModel, Extra -try: - import ujson as json -except ModuleNotFoundError: - import json +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils -from utils.image_utils import BuildImage from ..config import DRAW_PATH, draw_config -from ..util import cn2py, circled_number +from ..util import circled_number, cn2py class BaseData(BaseModel, extra=Extra.ignore): @@ -47,9 +43,9 @@ class UpChar(BaseData): class UpEvent(BaseModel): title: str # up池标题 pool_img: str # up池封面 - start_time: Optional[datetime] # 开始时间 - end_time: Optional[datetime] # 结束时间 - up_char: List[UpChar] # up对象 + start_time: datetime | None # 开始时间 + end_time: datetime | None # 结束时间 + up_char: list[UpChar] # up对象 TC = TypeVar("TC", bound="BaseData") @@ -66,27 +62,28 @@ class BaseHandle(Generic[TC]): self.up_path = DATA_PATH / "draw_card" / "draw_card_up" self.img_path.mkdir(parents=True, exist_ok=True) self.up_path.mkdir(parents=True, exist_ok=True) - self.data_files: List[str] = [f"{self.game_name}.json"] + self.data_files: list[str] = [f"{self.game_name}.json"] - def draw(self, count: int, **kwargs) -> Message: + async def draw(self, count: int, **kwargs) -> UniMessage: index2card = self.get_cards(count, **kwargs) cards = [card[0] for card in index2card] result = self.format_result(index2card) - return image(b64=self.generate_img(cards).pic2bs4()) + result + gen_img = await self.generate_img(cards) + return MessageUtils.build_message([gen_img, result]) # 抽取卡池 def get_card(self, **kwargs) -> TC: raise NotImplementedError - def get_cards(self, count: int, **kwargs) -> List[Tuple[TC, int]]: + def get_cards(self, count: int, **kwargs) -> list[tuple[TC, int]]: return [(self.get_card(**kwargs), i) for i in range(count)] # 获取星级 @staticmethod - def get_star(star_list: List[int], probability_list: List[float]) -> int: + def get_star(star_list: list[int], probability_list: list[float]) -> int: return random.choices(star_list, weights=probability_list, k=1)[0] - def format_result(self, index2card: List[Tuple[TC, int]], **kwargs) -> str: + def format_result(self, index2card: list[tuple[TC, int]], **kwargs) -> str: card_list = [card[0] for card in index2card] results = [ self.format_star_result(card_list, **kwargs), @@ -96,8 +93,8 @@ class BaseHandle(Generic[TC]): results = [rst for rst in results if rst] return "\n".join(results) - def format_star_result(self, card_list: List[TC], **kwargs) -> str: - star_dict: Dict[str, int] = {} # 记录星级及其次数 + def format_star_result(self, card_list: list[TC], **kwargs) -> str: + star_dict: dict[str, int] = {} # 记录星级及其次数 card_list_sorted = sorted(card_list, key=lambda c: c.star, reverse=True) for card in card_list_sorted: @@ -112,7 +109,7 @@ class BaseHandle(Generic[TC]): return rst.strip() def format_max_star( - self, card_list: List[Tuple[TC, int]], up_list: List[str] = [], **kwargs + self, card_list: list[tuple[TC, int]], up_list: list[str] = [], **kwargs ) -> str: up_list = up_list or kwargs.get("up_list", []) rst = "" @@ -124,8 +121,8 @@ class BaseHandle(Generic[TC]): rst += f"第 {index} 抽获取 {card.name}\n" return rst.strip() - def format_max_card(self, card_list: List[TC], **kwargs) -> str: - card_dict: Dict[TC, int] = {} # 记录卡牌抽取次数 + def format_max_card(self, card_list: list[TC], **kwargs) -> str: + card_dict: dict[TC, int] = {} # 记录卡牌抽取次数 for card in card_list: try: @@ -139,22 +136,22 @@ class BaseHandle(Generic[TC]): return "" return f"抽取到最多的是{max_card.name},共抽取了{max_count}次" - def generate_img( + async def generate_img( self, - cards: List[TC], + cards: list[TC], num_per_line: int = 5, - max_per_line: Tuple[int, int] = (40, 10), + max_per_line: tuple[int, int] = (40, 10), ) -> BuildImage: """ 生成统计图片 - :param cards: 卡牌列表 - :param num_per_line: 单行角色显示数量 - :param max_per_line: 当card_list超过一定数值时,更改单行数量 + cards: 卡牌列表 + num_per_line: 单行角色显示数量 + max_per_line: 当card_list超过一定数值时,更改单行数量 """ if len(cards) > max_per_line[0]: num_per_line = max_per_line[1] if len(cards) > 90: - card_dict: Dict[TC, int] = {} # 记录卡牌抽取次数 + card_dict: dict[TC, int] = {} # 记录卡牌抽取次数 for card in cards: try: card_dict[card] += 1 @@ -166,37 +163,37 @@ class BaseHandle(Generic[TC]): card_list = cards num_list = [1] * len(cards) - card_imgs: List[BuildImage] = [] + card_imgs: list[BuildImage] = [] for card, num in zip(card_list, num_list): - card_img = self.generate_card_img(card) + card_img = await self.generate_card_img(card) # 数量 > 1 时加数字上标 if num > 1: label = circled_number(num) - label_w = int(min(card_img.w, card_img.h) / 7) + label_w = int(min(card_img.width, card_img.height) / 7) label = label.resize( ( int(label_w * label.width / label.height), label_w, ), - Image.ANTIALIAS, + Image.ANTIALIAS, # type: ignore ) - card_img.paste(label, alpha=True) + await card_img.paste(label) card_imgs.append(card_img) - img_w = card_imgs[0].w - img_h = card_imgs[0].h - if len(card_imgs) < num_per_line: - w = img_w * len(card_imgs) - else: - w = img_w * num_per_line - h = img_h * math.ceil(len(card_imgs) / num_per_line) - img = BuildImage(w, h, img_w, img_h, color=self.game_card_color) - for card_img in card_imgs: - img.paste(card_img) - return img + # img_w = card_imgs[0].width + # img_h = card_imgs[0].height + # if len(card_imgs) < num_per_line: + # w = img_w * len(card_imgs) + # else: + # w = img_w * num_per_line + # h = img_h * math.ceil(len(card_imgs) / num_per_line) + # img = BuildImage(w, h, img_w, img_h, color=self.game_card_color) + # for card_img in card_imgs: + # await img.paste(card_img) + return await BuildImage.auto_paste(card_imgs, 10, color=self.game_card_color) # type: ignore - def generate_card_img(self, card: TC) -> BuildImage: + async def generate_card_img(self, card: TC) -> BuildImage: img = str(self.img_path / f"{cn2py(card.name)}.png") return BuildImage(100, 100, background=img) @@ -273,22 +270,26 @@ class BaseHandle(Generic[TC]): await f.write(await response.read()) return True except TimeoutError: - logger.warning(f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}") + logger.warning( + f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}" + ) return False except: - logger.warning(f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}") + logger.warning( + f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}" + ) return False - async def _reload_pool(self) -> Optional[Message]: + async def _reload_pool(self) -> UniMessage | None: return None - async def reload_pool(self) -> Optional[Message]: + async def reload_pool(self) -> UniMessage | None: try: async with self.client() as session: self.session = session return await self._reload_pool() except Exception as e: - logger.warning(f"{self.game_name_cn} 重载UP池错误:{type(e)}:{e}") + logger.warning(f"{self.game_name_cn} 重载UP池错误", e=e) - def reset_count(self, user_id: int) -> bool: + def reset_count(self, user_id: str) -> bool: return False diff --git a/plugins/draw_card/handles/fgo_handle.py b/zhenxun/plugins/draw_card/handles/fgo_handle.py similarity index 88% rename from plugins/draw_card/handles/fgo_handle.py rename to zhenxun/plugins/draw_card/handles/fgo_handle.py index a491b906..5acb8c5f 100644 --- a/plugins/draw_card/handles/fgo_handle.py +++ b/zhenxun/plugins/draw_card/handles/fgo_handle.py @@ -1,221 +1,223 @@ -import random -from lxml import etree -from typing import List, Tuple -from PIL import ImageDraw -from nonebot.log import logger - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class FgoData(BaseData): - pass - - -class FgoChar(FgoData): - pass - - -class FgoCard(FgoData): - pass - - -class FgoHandle(BaseHandle[FgoData]): - def __init__(self): - super().__init__("fgo", "命运-冠位指定") - self.data_files.append("fgo_card.json") - self.max_star = 5 - self.config = draw_config.fgo - self.ALL_CHAR: List[FgoChar] = [] - self.ALL_CARD: List[FgoCard] = [] - - def get_card(self, mode: int = 1) -> FgoData: - if mode == 1: - star = self.get_star( - [8, 7, 6, 5, 4, 3], - [ - self.config.FGO_SERVANT_FIVE_P, - self.config.FGO_SERVANT_FOUR_P, - self.config.FGO_SERVANT_THREE_P, - self.config.FGO_CARD_FIVE_P, - self.config.FGO_CARD_FOUR_P, - self.config.FGO_CARD_THREE_P, - ], - ) - elif mode == 2: - star = self.get_star( - [5, 4], [self.config.FGO_CARD_FIVE_P, self.config.FGO_CARD_FOUR_P] - ) - else: - star = self.get_star( - [8, 7, 6], - [ - self.config.FGO_SERVANT_FIVE_P, - self.config.FGO_SERVANT_FOUR_P, - self.config.FGO_SERVANT_THREE_P, - ], - ) - if star > 5: - star -= 3 - chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] - else: - chars = [x for x in self.ALL_CARD if x.star == star and not x.limited] - return random.choice(chars) - - def get_cards(self, count: int, **kwargs) -> List[Tuple[FgoData, int]]: - card_list = [] # 获取所有角色 - servant_count = 0 # 保底计算 - card_count = 0 # 保底计算 - for i in range(count): - servant_count += 1 - card_count += 1 - if card_count == 9: # 四星卡片保底 - mode = 2 - elif servant_count == 10: # 三星从者保底 - mode = 3 - else: # 普通抽 - mode = 1 - card = self.get_card(mode) - if isinstance(card, FgoCard) and card.star > self.max_star - 2: - card_count = 0 - if isinstance(card, FgoChar): - servant_count = 0 - card_list.append((card, i + 1)) - return card_list - - def generate_card_img(self, card: FgoData) -> BuildImage: - sep_w = 5 - sep_t = 5 - sep_b = 20 - w = 128 - h = 140 - bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(w, h, background=img_path) - bg.paste(img, (sep_w, sep_t), alpha=True) - # 加名字 - text = card.name[:6] + "..." if len(card.name) > 7 else card.name - font = load_font(fontsize=16) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), - text, - font=font, - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - FgoChar( - name=value["名称"], - star=int(value["星级"]), - limited=True - if not ("圣晶石召唤" in value["入手方式"] or "圣晶石召唤(Story卡池)" in value["入手方式"]) - else False, - ) - for value in self.load_data().values() - ] - self.ALL_CARD = [ - FgoCard(name=value["名称"], star=int(value["星级"]), limited=False) - for value in self.load_data("fgo_card.json").values() - ] - - async def _update_info(self): - # fgo.json - fgo_info = {} - for i in range(500): - url = f"http://fgo.vgtime.com/servant/ajax?card=&wd=&ids=&sort=12777&o=desc&pn={i}" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} page {i} 出错") - continue - fgo_data = json.loads(result) - if int(fgo_data["nums"]) <= 0: - break - for x in fgo_data["data"]: - name = remove_prohibited_str(x["name"]) - member_dict = { - "id": x["id"], - "card_id": x["charid"], - "头像": x["icon"], - "名称": remove_prohibited_str(x["name"]), - "职阶": x["classes"], - "星级": int(x["star"]), - "hp": x["lvmax4hp"], - "atk": x["lvmax4atk"], - "card_quick": x["cardquick"], - "card_arts": x["cardarts"], - "card_buster": x["cardbuster"], - "宝具": x["tprop"], - } - fgo_info[name] = member_dict - # 更新额外信息 - for key in fgo_info.keys(): - url = f'http://fgo.vgtime.com/servant/{fgo_info[key]["id"]}' - result = await self.get_url(url) - if not result: - fgo_info[key]["入手方式"] = ["圣晶石召唤"] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - obtain = dom.xpath( - "//table[contains(string(.),'入手方式')]/tr[8]/td[3]/text()" - )[0] - obtain = str(obtain).strip() - if "限时活动免费获取 活动结束后无法获得" in obtain: - obtain = ["活动获取"] - elif "非限时UP无法获得" in obtain: - obtain = ["限时召唤"] - else: - if "&" in obtain: - obtain = obtain.split("&") - else: - obtain = obtain.split(" ") - obtain = [s.strip() for s in obtain if s.strip()] - fgo_info[key]["入手方式"] = obtain - except IndexError: - fgo_info[key]["入手方式"] = ["圣晶石召唤"] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - self.dump_data(fgo_info) - logger.info(f"{self.game_name_cn} 更新成功") - # fgo_card.json - fgo_card_info = {} - for i in range(500): - url = f"http://fgo.vgtime.com/equipment/ajax?wd=&ids=&sort=12958&o=desc&pn={i}" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn}卡牌 page {i} 出错") - continue - fgo_data = json.loads(result) - if int(fgo_data["nums"]) <= 0: - break - for x in fgo_data["data"]: - name = remove_prohibited_str(x["name"]) - member_dict = { - "id": x["id"], - "card_id": x["equipid"], - "头像": x["icon"], - "名称": name, - "星级": int(x["star"]), - "hp": x["lvmax_hp"], - "atk": x["lvmax_atk"], - "skill_e": str(x["skill_e"]).split("
")[:-1], - } - fgo_card_info[name] = member_dict - self.dump_data(fgo_card_info, "fgo_card.json") - logger.info(f"{self.game_name_cn} 卡牌更新成功") - # 下载头像 - for value in fgo_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in fgo_card_info.values(): - await self.download_img(value["头像"], value["名称"]) +import random + +import ujson as json +from lxml import etree +from PIL import ImageDraw + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle + + +class FgoData(BaseData): + pass + + +class FgoChar(FgoData): + pass + + +class FgoCard(FgoData): + pass + + +class FgoHandle(BaseHandle[FgoData]): + def __init__(self): + super().__init__("fgo", "命运-冠位指定") + self.data_files.append("fgo_card.json") + self.max_star = 5 + self.config = draw_config.fgo + self.ALL_CHAR: list[FgoChar] = [] + self.ALL_CARD: list[FgoCard] = [] + + def get_card(self, mode: int = 1) -> FgoData: + if mode == 1: + star = self.get_star( + [8, 7, 6, 5, 4, 3], + [ + self.config.FGO_SERVANT_FIVE_P, + self.config.FGO_SERVANT_FOUR_P, + self.config.FGO_SERVANT_THREE_P, + self.config.FGO_CARD_FIVE_P, + self.config.FGO_CARD_FOUR_P, + self.config.FGO_CARD_THREE_P, + ], + ) + elif mode == 2: + star = self.get_star( + [5, 4], [self.config.FGO_CARD_FIVE_P, self.config.FGO_CARD_FOUR_P] + ) + else: + star = self.get_star( + [8, 7, 6], + [ + self.config.FGO_SERVANT_FIVE_P, + self.config.FGO_SERVANT_FOUR_P, + self.config.FGO_SERVANT_THREE_P, + ], + ) + if star > 5: + star -= 3 + chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] + else: + chars = [x for x in self.ALL_CARD if x.star == star and not x.limited] + return random.choice(chars) + + def get_cards(self, count: int, **kwargs) -> list[tuple[FgoData, int]]: + card_list = [] # 获取所有角色 + servant_count = 0 # 保底计算 + card_count = 0 # 保底计算 + for i in range(count): + servant_count += 1 + card_count += 1 + if card_count == 9: # 四星卡片保底 + mode = 2 + elif servant_count == 10: # 三星从者保底 + mode = 3 + else: # 普通抽 + mode = 1 + card = self.get_card(mode) + if isinstance(card, FgoCard) and card.star > self.max_star - 2: + card_count = 0 + if isinstance(card, FgoChar): + servant_count = 0 + card_list.append((card, i + 1)) + return card_list + + async def generate_card_img(self, card: FgoData) -> BuildImage: + sep_w = 5 + sep_t = 5 + sep_b = 20 + w = 128 + h = 140 + bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(w, h, background=img_path) + await bg.paste(img, (sep_w, sep_t)) + # 加名字 + text = card.name[:6] + "..." if len(card.name) > 7 else card.name + font = load_font(fontsize=16) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), + text, + font=font, + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + FgoChar( + name=value["名称"], + star=int(value["星级"]), + limited=( + True + if not ( + "圣晶石召唤" in value["入手方式"] + or "圣晶石召唤(Story卡池)" in value["入手方式"] + ) + else False + ), + ) + for value in self.load_data().values() + ] + self.ALL_CARD = [ + FgoCard(name=value["名称"], star=int(value["星级"]), limited=False) + for value in self.load_data("fgo_card.json").values() + ] + + async def _update_info(self): + # TODO: fgo获取链接失效 + fgo_info = {} + for i in range(500): + url = f"http://fgo.vgtime.com/servant/ajax?card=&wd=&ids=&sort=12777&o=desc&pn={i}" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} page {i} 出错") + continue + fgo_data = json.loads(result) + if int(fgo_data["nums"]) <= 0: + break + for x in fgo_data["data"]: + name = remove_prohibited_str(x["name"]) + member_dict = { + "id": x["id"], + "card_id": x["charid"], + "头像": x["icon"], + "名称": remove_prohibited_str(x["name"]), + "职阶": x["classes"], + "星级": int(x["star"]), + "hp": x["lvmax4hp"], + "atk": x["lvmax4atk"], + "card_quick": x["cardquick"], + "card_arts": x["cardarts"], + "card_buster": x["cardbuster"], + "宝具": x["tprop"], + } + fgo_info[name] = member_dict + # 更新额外信息 + for key in fgo_info.keys(): + url = f'http://fgo.vgtime.com/servant/{fgo_info[key]["id"]}' + result = await self.get_url(url) + if not result: + fgo_info[key]["入手方式"] = ["圣晶石召唤"] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + obtain = dom.xpath( + "//table[contains(string(.),'入手方式')]/tr[8]/td[3]/text()" + )[0] + obtain = str(obtain).strip() + if "限时活动免费获取 活动结束后无法获得" in obtain: + obtain = ["活动获取"] + elif "非限时UP无法获得" in obtain: + obtain = ["限时召唤"] + else: + if "&" in obtain: + obtain = obtain.split("&") + else: + obtain = obtain.split(" ") + obtain = [s.strip() for s in obtain if s.strip()] + fgo_info[key]["入手方式"] = obtain + except IndexError: + fgo_info[key]["入手方式"] = ["圣晶石召唤"] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + self.dump_data(fgo_info) + logger.info(f"{self.game_name_cn} 更新成功") + # fgo_card.json + fgo_card_info = {} + for i in range(500): + url = f"http://fgo.vgtime.com/equipment/ajax?wd=&ids=&sort=12958&o=desc&pn={i}" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn}卡牌 page {i} 出错") + continue + fgo_data = json.loads(result) + if int(fgo_data["nums"]) <= 0: + break + for x in fgo_data["data"]: + name = remove_prohibited_str(x["name"]) + member_dict = { + "id": x["id"], + "card_id": x["equipid"], + "头像": x["icon"], + "名称": name, + "星级": int(x["star"]), + "hp": x["lvmax_hp"], + "atk": x["lvmax_atk"], + "skill_e": str(x["skill_e"]).split("
")[:-1], + } + fgo_card_info[name] = member_dict + self.dump_data(fgo_card_info, "fgo_card.json") + logger.info(f"{self.game_name_cn} 卡牌更新成功") + # 下载头像 + for value in fgo_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in fgo_card_info.values(): + await self.download_img(value["头像"], value["名称"]) diff --git a/plugins/draw_card/handles/genshin_handle.py b/zhenxun/plugins/draw_card/handles/genshin_handle.py similarity index 84% rename from plugins/draw_card/handles/genshin_handle.py rename to zhenxun/plugins/draw_card/handles/genshin_handle.py index 71e79544..61edcf30 100644 --- a/plugins/draw_card/handles/genshin_handle.py +++ b/zhenxun/plugins/draw_card/handles/genshin_handle.py @@ -1,448 +1,461 @@ -import random -import dateparser -from lxml import etree -from PIL import Image, ImageDraw -from urllib.parse import unquote -from typing import List, Optional, Tuple -from pydantic import ValidationError -from datetime import datetime, timedelta -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..count_manager import GenshinCountManager -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class GenshinData(BaseData): - pass - - -class GenshinChar(GenshinData): - pass - - -class GenshinArms(GenshinData): - pass - - -class GenshinHandle(BaseHandle[GenshinData]): - def __init__(self): - super().__init__("genshin", "原神") - self.data_files.append("genshin_arms.json") - self.max_star = 5 - self.game_card_color = "#ebebeb" - self.config = draw_config.genshin - - self.ALL_CHAR: List[GenshinData] = [] - self.ALL_ARMS: List[GenshinData] = [] - self.UP_CHAR: Optional[UpEvent] = None - self.UP_CHAR_LIST: Optional[UpEvent] = [] - self.UP_ARMS: Optional[UpEvent] = None - - self.count_manager = GenshinCountManager((10, 90), ("4", "5"), 180) - - # 抽取卡池 - def get_card( - self, pool_name: str, mode: int = 1, add: float = 0.0, is_up: bool = False, card_index: int = 0 - ): - """ - mode 1:普通抽 2:四星保底 3:五星保底 - """ - if mode == 1: - star = self.get_star( - [5, 4, 3], - [ - self.config.GENSHIN_FIVE_P + add, - self.config.GENSHIN_FOUR_P, - self.config.GENSHIN_THREE_P, - ], - ) - elif mode == 2: - star = self.get_star( - [5, 4], - [self.config.GENSHIN_G_FIVE_P + add, self.config.GENSHIN_G_FOUR_P], - ) - else: - star = 5 - - if pool_name == "char": - up_event = self.UP_CHAR_LIST[card_index] - all_list = self.ALL_CHAR + [ - x for x in self.ALL_ARMS if x.star == star and x.star < 5 - ] - elif pool_name == "arms": - up_event = self.UP_ARMS - all_list = self.ALL_ARMS + [ - x for x in self.ALL_CHAR if x.star == star and x.star < 5 - ] - else: - up_event = None - all_list = self.ALL_ARMS + self.ALL_CHAR - - acquire_char = None - # 是否UP - if up_event and star > 3: - # 获取up角色列表 - up_list = [x.name for x in up_event.up_char if x.star == star] - # 成功获取up角色 - if random.random() < 0.5 or is_up: - up_name = random.choice(up_list) - try: - acquire_char = [x for x in all_list if x.name == up_name][0] - except IndexError: - pass - if not acquire_char: - chars = [x for x in all_list if x.star == star and not x.limited] - acquire_char = random.choice(chars) - return acquire_char - - def get_cards( - self, count: int, user_id: int, pool_name: str, card_index: int = 0 - ) -> List[Tuple[GenshinData, int]]: - card_list = [] # 获取角色列表 - add = 0.0 - count_manager = self.count_manager - count_manager.check_count(user_id, count) # 检查次数累计 - pool = self.UP_CHAR_LIST[card_index] if pool_name == "char" else self.UP_ARMS - for i in range(count): - count_manager.increase(user_id) - star = count_manager.check(user_id) # 是否有四星或五星保底 - if ( - count_manager.get_user_count(user_id) - - count_manager.get_user_five_index(user_id) - ) % count_manager.get_max_guarantee() >= 72: - add += draw_config.genshin.I72_ADD - if star: - if star == 4: - card = self.get_card(pool_name, 2, add=add, card_index=card_index) - else: - card = self.get_card( - pool_name, 3, add, count_manager.is_up(user_id), card_index=card_index - ) - else: - card = self.get_card(pool_name, 1, add, count_manager.is_up(user_id), card_index=card_index) - # print(f"{count_manager.get_user_count(user_id)}:", - # count_manager.get_user_five_index(user_id), star, card.star, add) - # 四星角色 - if card.star == 4: - count_manager.mark_four_index(user_id) - # 五星角色 - elif card.star == self.max_star: - add = 0 - count_manager.mark_five_index(user_id) # 记录五星保底 - count_manager.mark_four_index(user_id) # 记录四星保底 - if pool and card.name in [ - x.name for x in pool.up_char if x.star == self.max_star - ]: - count_manager.set_is_up(user_id, True) - else: - count_manager.set_is_up(user_id, False) - card_list.append((card, count_manager.get_user_count(user_id))) - return card_list - - def generate_card_img(self, card: GenshinData) -> BuildImage: - sep_w = 10 - sep_h = 5 - frame_w = 112 - frame_h = 132 - img_w = 106 - img_h = 106 - bg = BuildImage(frame_w + sep_w * 2, frame_h + sep_h * 2, color="#EBEBEB") - frame_path = str(self.img_path / "avatar_frame.png") - frame = Image.open(frame_path) - # 加名字 - text = card.name - font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(frame) - draw.text( - ((frame_w - text_w) / 2, frame_h - 15 - text_h / 2), - text, - font=font, - fill="gray", - ) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - if isinstance(card, GenshinArms): - # 武器卡背景不是透明的,切去上方两个圆弧 - r = 12 - circle = Image.new("L", (r * 2, r * 2), 0) - alpha = Image.new("L", img.size, 255) - alpha.paste(circle, (-r - 3, -r - 3)) # 左上角 - alpha.paste(circle, (img_h - r + 3, -r - 3)) # 右上角 - img.markImg.putalpha(alpha) - star_path = str(self.img_path / f"{card.star}_star.png") - star = Image.open(star_path) - bg.paste(frame, (sep_w, sep_h), alpha=True) - bg.paste(img, (sep_w + 3, sep_h + 3), alpha=True) - bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6), alpha=True) - return bg - - def format_pool_info(self, pool_name: str, card_index: int = 0) -> str: - info = "" - up_event = None - if pool_name == "char": - up_event = self.UP_CHAR_LIST[card_index] - elif pool_name == "arms": - up_event = self.UP_ARMS - if up_event: - star5_list = [x.name for x in up_event.up_char if x.star == 5] - star4_list = [x.name for x in up_event.up_char if x.star == 4] - if star5_list: - info += f"五星UP:{' '.join(star5_list)}\n" - if star4_list: - info += f"四星UP:{' '.join(star4_list)}\n" - info = f"当前up池:{up_event.title}\n{info}" - return info.strip() - - def draw(self, count: int, user_id: int, pool_name: str = "", **kwargs) -> Message: - card_index = 0 - if "1" in pool_name: - card_index = 1 - pool_name = pool_name.replace("1", "") - index2cards = self.get_cards(count, user_id, pool_name, card_index) - cards = [card[0] for card in index2cards] - up_event = None - if pool_name == "char": - if card_index == 1 and len(self.UP_CHAR_LIST) == 1: - return Message("当前没有第二个角色UP池") - up_event = self.UP_CHAR_LIST[card_index] - elif pool_name == "arms": - up_event = self.UP_ARMS - up_list = [x.name for x in up_event.up_char] if up_event else [] - result = self.format_star_result(cards) - result += ( - "\n" + max_star_str - if (max_star_str := self.format_max_star(index2cards, up_list=up_list)) - else "" - ) - result += f"\n距离保底发还剩 {self.count_manager.get_user_guarantee_count(user_id)} 抽" - # result += "\n【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】" - pool_info = self.format_pool_info(pool_name, card_index) - img = self.generate_img(cards) - bk = BuildImage(img.w, img.h + 50, font_size=20, color="#ebebeb") - bk.paste(img) - bk.text((0, img.h + 10), "【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】") - return pool_info + image(b64=bk.pic2bs4()) + result - - def _init_data(self): - self.ALL_CHAR = [ - GenshinChar( - name=value["名称"], - star=int(value["星级"]), - limited=value["常驻/限定"] == "限定UP", - ) - for key, value in self.load_data().items() - if "旅行者" not in key - ] - self.ALL_ARMS = [ - GenshinArms( - name=value["名称"], - star=int(value["星级"]), - limited="祈愿" not in value["获取途径"], - ) - for value in self.load_data("genshin_arms.json").values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char", {}))) - self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char1", {}))) - self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_CHAR_LIST and self.UP_ARMS: - data = { - "char": json.loads(self.UP_CHAR_LIST[0].json()), - "arms": json.loads(self.UP_ARMS.json()), - } - if len(self.UP_CHAR_LIST) > 1: - data['char1'] = json.loads(self.UP_CHAR_LIST[1].json()) - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - # genshin.json - char_info = {} - url = "https://wiki.biligame.com/ys/角色筛选" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = char.xpath("./td[3]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()[:1]), - } - char_info[member_dict["名称"]] = member_dict - # 更新额外信息 - for key in char_info.keys(): - result = await self.get_url(f"https://wiki.biligame.com/ys/{key}") - if not result: - char_info[key]["常驻/限定"] = "未知" - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - limit = dom.xpath( - "//table[contains(string(.),'常驻/限定')]/tbody/tr[6]/td/text()" - )[0] - char_info[key]["常驻/限定"] = str(limit).strip() - except IndexError: - char_info[key]["常驻/限定"] = "未知" - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - self.dump_data(char_info) - logger.info(f"{self.game_name_cn} 更新成功") - # genshin_arms.json - arms_info = {} - url = "https://wiki.biligame.com/ys/武器图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = char.xpath("./td[4]/img/@alt")[0] - sources = str(char.xpath("./td[5]/text()")[0]).split(",") - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()[:1]), - "获取途径": [s.strip() for s in sources if s.strip()], - } - arms_info[member_dict["名称"]] = member_dict - self.dump_data(arms_info, "genshin_arms.json") - logger.info(f"{self.game_name_cn} 武器更新成功") - # 下载头像 - for value in char_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in arms_info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - idx = 1 - YS_URL = "https://patchwiki.biligame.com/images/ys" - for url in [ - "/1/13/7xzg7tgf8dsr2hjpmdbm5gn9wvzt2on.png", - "/b/bc/sd2ige6d7lvj7ugfumue3yjg8gyi0d1.png", - "/e/ec/l3mnhy56pyailhn3v7r873htf2nofau.png", - "/9/9c/sklp02ffk3aqszzvh8k1c3139s0awpd.png", - "/c/c7/qu6xcndgj6t14oxvv7yz2warcukqv1m.png", - ]: - await self.download_img(YS_URL + url, f"{idx}_star") - idx += 1 - # 下载头像框 - await self.download_img( - YS_URL + "/2/2e/opbcst4xbtcq0i4lwerucmosawn29ti.png", f"avatar_frame" - ) - await self.update_up_char() - - async def update_up_char(self): - self.UP_CHAR_LIST = [] - url = "https://wiki.biligame.com/ys/祈愿" - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取祈愿页面出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - tables = dom.xpath( - "//div[@class='mw-parser-output']/div[@class='row']/div/table[@class='wikitable']/tbody" - ) - if not tables or len(tables) < 2: - logger.warning(f"{self.game_name_cn}获取活动祈愿出错") - return - try: - for index, table in enumerate(tables): - title = table.xpath("./tr[1]/th/img/@title")[0] - title = str(title).split("」")[0] + "」" if "」" in title else title - pool_img = str(table.xpath("./tr[1]/th/img/@srcset")[0]).split(" ")[-2] - time = table.xpath("./tr[2]/td/text()")[0] - star5_list = table.xpath("./tr[3]/td/a/@title") - star4_list = table.xpath("./tr[4]/td/a/@title") - start, end = str(time).split("~") - start_time = dateparser.parse(start) - end_time = dateparser.parse(end) - if not start_time and end_time: - start_time = end_time - timedelta(days=20) - if start_time and end_time and start_time <= datetime.now() <= end_time: - up_event = UpEvent( - title=title, - pool_img=pool_img, - start_time=start_time, - end_time=end_time, - up_char=[ - UpChar(name=name, star=5, limited=False, zoom=50) - for name in star5_list - ] - + [ - UpChar(name=name, star=4, limited=False, zoom=50) - for name in star4_list - ], - ) - if '神铸赋形' not in title: - self.UP_CHAR_LIST.append(up_event) - else: - self.UP_ARMS = up_event - if self.UP_CHAR_LIST and self.UP_ARMS: - self.dump_up_char() - char_title = " & ".join([x.title for x in self.UP_CHAR_LIST]) - logger.info( - f"成功获取{self.game_name_cn}当前up信息...当前up池: {char_title} & {self.UP_ARMS.title}" - ) - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - def reset_count(self, user_id: int) -> bool: - self.count_manager.reset(user_id) - return True - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_CHAR_LIST and self.UP_ARMS: - if len(self.UP_CHAR_LIST) > 1: - return Message( - Message.template("重载成功!\n当前UP池子:{} & {} & {}{:image}{:image}{:image}").format( - self.UP_CHAR_LIST[0].title, - self.UP_CHAR_LIST[1].title, - self.UP_ARMS.title, - self.UP_CHAR_LIST[0].pool_img, - self.UP_CHAR_LIST[1].pool_img, - self.UP_ARMS.pool_img, - ) - ) - return Message( - Message.template("重载成功!\n当前UP池子:{} & {}{:image}{:image}").format( - char_title, - self.UP_ARMS.title, - self.UP_CHAR_LIST[0].pool_img, - self.UP_ARMS.pool_img, - ) - ) +import random +from datetime import datetime, timedelta +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from nonebot_plugin_alconna import UniMessage +from PIL import Image, ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +from ..config import draw_config +from ..count_manager import GenshinCountManager +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class GenshinData(BaseData): + pass + + +class GenshinChar(GenshinData): + pass + + +class GenshinArms(GenshinData): + pass + + +class GenshinHandle(BaseHandle[GenshinData]): + def __init__(self): + super().__init__("genshin", "原神") + self.data_files.append("genshin_arms.json") + self.max_star = 5 + self.game_card_color = "#ebebeb" + self.config = draw_config.genshin + + self.ALL_CHAR: list[GenshinData] = [] + self.ALL_ARMS: list[GenshinData] = [] + self.UP_CHAR: UpEvent | None = None + self.UP_CHAR_LIST: UpEvent | None = [] + self.UP_ARMS: UpEvent | None = None + + self.count_manager = GenshinCountManager((10, 90), ("4", "5"), 180) + + # 抽取卡池 + def get_card( + self, + pool_name: str, + mode: int = 1, + add: float = 0.0, + is_up: bool = False, + card_index: int = 0, + ): + """ + mode 1:普通抽 2:四星保底 3:五星保底 + """ + if mode == 1: + star = self.get_star( + [5, 4, 3], + [ + self.config.GENSHIN_FIVE_P + add, + self.config.GENSHIN_FOUR_P, + self.config.GENSHIN_THREE_P, + ], + ) + elif mode == 2: + star = self.get_star( + [5, 4], + [self.config.GENSHIN_G_FIVE_P + add, self.config.GENSHIN_G_FOUR_P], + ) + else: + star = 5 + + if pool_name == "char": + up_event = self.UP_CHAR_LIST[card_index] + all_list = self.ALL_CHAR + [ + x for x in self.ALL_ARMS if x.star == star and x.star < 5 + ] + elif pool_name == "arms": + up_event = self.UP_ARMS + all_list = self.ALL_ARMS + [ + x for x in self.ALL_CHAR if x.star == star and x.star < 5 + ] + else: + up_event = None + all_list = self.ALL_ARMS + self.ALL_CHAR + + acquire_char = None + # 是否UP + if up_event and star > 3: + # 获取up角色列表 + up_list = [x.name for x in up_event.up_char if x.star == star] + # 成功获取up角色 + if random.random() < 0.5 or is_up: + up_name = random.choice(up_list) + try: + acquire_char = [x for x in all_list if x.name == up_name][0] + except IndexError: + pass + if not acquire_char: + chars = [x for x in all_list if x.star == star and not x.limited] + acquire_char = random.choice(chars) + return acquire_char + + def get_cards( + self, count: int, user_id: int, pool_name: str, card_index: int = 0 + ) -> list[tuple[GenshinData, int]]: + card_list = [] # 获取角色列表 + add = 0.0 + count_manager = self.count_manager + count_manager.check_count(user_id, count) # 检查次数累计 + pool = self.UP_CHAR_LIST[card_index] if pool_name == "char" else self.UP_ARMS + for i in range(count): + count_manager.increase(user_id) + star = count_manager.check(user_id) # 是否有四星或五星保底 + if ( + count_manager.get_user_count(user_id) + - count_manager.get_user_five_index(user_id) + ) % count_manager.get_max_guarantee() >= 72: + add += draw_config.genshin.I72_ADD + if star: + if star == 4: + card = self.get_card(pool_name, 2, add=add, card_index=card_index) + else: + card = self.get_card( + pool_name, + 3, + add, + count_manager.is_up(user_id), + card_index=card_index, + ) + else: + card = self.get_card( + pool_name, + 1, + add, + count_manager.is_up(user_id), + card_index=card_index, + ) + # print(f"{count_manager.get_user_count(user_id)}:", + # count_manager.get_user_five_index(user_id), star, card.star, add) + # 四星角色 + if card.star == 4: + count_manager.mark_four_index(user_id) + # 五星角色 + elif card.star == self.max_star: + add = 0 + count_manager.mark_five_index(user_id) # 记录五星保底 + count_manager.mark_four_index(user_id) # 记录四星保底 + if pool and card.name in [ + x.name for x in pool.up_char if x.star == self.max_star + ]: + count_manager.set_is_up(user_id, True) + else: + count_manager.set_is_up(user_id, False) + card_list.append((card, count_manager.get_user_count(user_id))) + return card_list + + async def generate_card_img(self, card: GenshinData) -> BuildImage: + sep_w = 10 + sep_h = 5 + frame_w = 112 + frame_h = 132 + img_w = 106 + img_h = 106 + bg = BuildImage(frame_w + sep_w * 2, frame_h + sep_h * 2, color="#EBEBEB") + frame_path = str(self.img_path / "avatar_frame.png") + frame = Image.open(frame_path) + # 加名字 + text = card.name + font = load_font(fontsize=14) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(frame) + draw.text( + ((frame_w - text_w) / 2, frame_h - 15 - text_h / 2), + text, + font=font, + fill="gray", + ) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + if isinstance(card, GenshinArms): + # 武器卡背景不是透明的,切去上方两个圆弧 + r = 12 + circle = Image.new("L", (r * 2, r * 2), 0) + alpha = Image.new("L", img.size, 255) + alpha.paste(circle, (-r - 3, -r - 3)) # 左上角 + alpha.paste(circle, (img_h - r + 3, -r - 3)) # 右上角 + img.markImg.putalpha(alpha) + star_path = str(self.img_path / f"{card.star}_star.png") + star = Image.open(star_path) + await bg.paste(frame, (sep_w, sep_h)) + await bg.paste(img, (sep_w + 3, sep_h + 3)) + await bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6)) + return bg + + def format_pool_info(self, pool_name: str, card_index: int = 0) -> str: + info = "" + up_event = None + if pool_name == "char": + up_event = self.UP_CHAR_LIST[card_index] + elif pool_name == "arms": + up_event = self.UP_ARMS + if up_event: + star5_list = [x.name for x in up_event.up_char if x.star == 5] + star4_list = [x.name for x in up_event.up_char if x.star == 4] + if star5_list: + info += f"五星UP:{' '.join(star5_list)}\n" + if star4_list: + info += f"四星UP:{' '.join(star4_list)}\n" + info = f"当前up池:{up_event.title}\n{info}" + return info.strip() + + async def draw( + self, count: int, user_id: int, pool_name: str = "", **kwargs + ) -> UniMessage: + card_index = 0 + if "1" in pool_name: + card_index = 1 + pool_name = pool_name.replace("1", "") + index2cards = self.get_cards(count, user_id, pool_name, card_index) + cards = [card[0] for card in index2cards] + up_event = None + if pool_name == "char": + if card_index == 1 and len(self.UP_CHAR_LIST) == 1: + return MessageUtils.build_message("当前没有第二个角色UP池") + up_event = self.UP_CHAR_LIST[card_index] + elif pool_name == "arms": + up_event = self.UP_ARMS + up_list = [x.name for x in up_event.up_char] if up_event else [] + result = self.format_star_result(cards) + result += ( + "\n" + max_star_str + if (max_star_str := self.format_max_star(index2cards, up_list=up_list)) + else "" + ) + result += f"\n距离保底发还剩 {self.count_manager.get_user_guarantee_count(user_id)} 抽" + # result += "\n【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】" + pool_info = self.format_pool_info(pool_name, card_index) + img = await self.generate_img(cards) + bk = BuildImage(img.width, img.height + 50, font_size=20, color="#ebebeb") + await bk.paste(img) + await bk.text( + (0, img.height + 10), + "【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】", + ) + return MessageUtils.build_message([pool_info, bk, result]) + + def _init_data(self): + self.ALL_CHAR = [ + GenshinChar( + name=value["名称"], + star=int(value["星级"]), + limited=value["常驻/限定"] == "限定UP", + ) + for key, value in self.load_data().items() + if "旅行者" not in key + ] + self.ALL_ARMS = [ + GenshinArms( + name=value["名称"], + star=int(value["星级"]), + limited="祈愿" not in value["获取途径"], + ) + for value in self.load_data("genshin_arms.json").values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char", {}))) + self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char1", {}))) + self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_CHAR_LIST and self.UP_ARMS: + data = { + "char": json.loads(self.UP_CHAR_LIST[0].json()), + "arms": json.loads(self.UP_ARMS.json()), + } + if len(self.UP_CHAR_LIST) > 1: + data["char1"] = json.loads(self.UP_CHAR_LIST[1].json()) + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + # genshin.json + char_info = {} + url = "https://wiki.biligame.com/ys/角色筛选" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = char.xpath("./td[3]/text()")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()[:1]), + } + char_info[member_dict["名称"]] = member_dict + # 更新额外信息 + for key in char_info.keys(): + result = await self.get_url(f"https://wiki.biligame.com/ys/{key}") + if not result: + char_info[key]["常驻/限定"] = "未知" + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + limit = dom.xpath( + "//table[contains(string(.),'常驻/限定')]/tbody/tr[6]/td/text()" + )[0] + char_info[key]["常驻/限定"] = str(limit).strip() + except IndexError: + char_info[key]["常驻/限定"] = "未知" + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + self.dump_data(char_info) + logger.info(f"{self.game_name_cn} 更新成功") + # genshin_arms.json + arms_info = {} + url = "https://wiki.biligame.com/ys/武器图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = char.xpath("./td[4]/img/@alt")[0] + sources = str(char.xpath("./td[5]/text()")[0]).split(",") + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()[:1]), + "获取途径": [s.strip() for s in sources if s.strip()], + } + arms_info[member_dict["名称"]] = member_dict + self.dump_data(arms_info, "genshin_arms.json") + logger.info(f"{self.game_name_cn} 武器更新成功") + # 下载头像 + for value in char_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in arms_info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + idx = 1 + YS_URL = "https://patchwiki.biligame.com/images/ys" + for url in [ + "/1/13/7xzg7tgf8dsr2hjpmdbm5gn9wvzt2on.png", + "/b/bc/sd2ige6d7lvj7ugfumue3yjg8gyi0d1.png", + "/e/ec/l3mnhy56pyailhn3v7r873htf2nofau.png", + "/9/9c/sklp02ffk3aqszzvh8k1c3139s0awpd.png", + "/c/c7/qu6xcndgj6t14oxvv7yz2warcukqv1m.png", + ]: + await self.download_img(YS_URL + url, f"{idx}_star") + idx += 1 + # 下载头像框 + await self.download_img( + YS_URL + "/2/2e/opbcst4xbtcq0i4lwerucmosawn29ti.png", f"avatar_frame" + ) + await self.update_up_char() + + async def update_up_char(self): + self.UP_CHAR_LIST = [] + url = "https://wiki.biligame.com/ys/祈愿" + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取祈愿页面出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + tables = dom.xpath( + "//div[@class='mw-parser-output']/div[@class='row']/div/table[@class='wikitable']/tbody" + ) + if not tables or len(tables) < 2: + logger.warning(f"{self.game_name_cn}获取活动祈愿出错") + return + try: + for index, table in enumerate(tables): + title = table.xpath("./tr[1]/th/img/@title")[0] + title = str(title).split("」")[0] + "」" if "」" in title else title + pool_img = str(table.xpath("./tr[1]/th/img/@srcset")[0]).split(" ")[-2] + time = table.xpath("./tr[2]/td/text()")[0] + star5_list = table.xpath("./tr[3]/td/a/@title") + star4_list = table.xpath("./tr[4]/td/a/@title") + start, end = str(time).split("~") + start_time = dateparser.parse(start) + end_time = dateparser.parse(end) + if not start_time and end_time: + start_time = end_time - timedelta(days=20) + if start_time and end_time and start_time <= datetime.now() <= end_time: + up_event = UpEvent( + title=title, + pool_img=pool_img, + start_time=start_time, + end_time=end_time, + up_char=[ + UpChar(name=name, star=5, limited=False, zoom=50) + for name in star5_list + ] + + [ + UpChar(name=name, star=4, limited=False, zoom=50) + for name in star4_list + ], + ) + if "神铸赋形" not in title: + self.UP_CHAR_LIST.append(up_event) + else: + self.UP_ARMS = up_event + if self.UP_CHAR_LIST and self.UP_ARMS: + self.dump_up_char() + char_title = " & ".join([x.title for x in self.UP_CHAR_LIST]) + logger.info( + f"成功获取{self.game_name_cn}当前up信息...当前up池: {char_title} & {self.UP_ARMS.title}" + ) + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错", e=e) + + def reset_count(self, user_id: str) -> bool: + self.count_manager.reset(user_id) + return True + + async def _reload_pool(self) -> UniMessage | None: + await self.update_up_char() + self.load_up_char() + if self.UP_CHAR_LIST and self.UP_ARMS: + if len(self.UP_CHAR_LIST) > 1: + return MessageUtils.build_message( + [ + f"重载成功!\n当前UP池子:{self.UP_CHAR_LIST[0].title} & {self.UP_CHAR_LIST[1].title} & {self.UP_ARMS.title}", + self.UP_CHAR_LIST[0].pool_img, + self.UP_CHAR_LIST[1].pool_img, + self.UP_ARMS.pool_img, + ] + ) + return UniMessage( + [ + f"重载成功!\n当前UP池子:{char_title} & {self.UP_ARMS.title}", + self.UP_CHAR_LIST[0].pool_img, + self.UP_ARMS.pool_img, + ] + ) diff --git a/plugins/draw_card/handles/guardian_handle.py b/zhenxun/plugins/draw_card/handles/guardian_handle.py similarity index 88% rename from plugins/draw_card/handles/guardian_handle.py rename to zhenxun/plugins/draw_card/handles/guardian_handle.py index 33e9449b..517f126d 100644 --- a/plugins/draw_card/handles/guardian_handle.py +++ b/zhenxun/plugins/draw_card/handles/guardian_handle.py @@ -1,400 +1,400 @@ -import re -import random -import dateparser -from lxml import etree -from PIL import ImageDraw -from datetime import datetime -from urllib.parse import unquote -from typing import List, Optional, Tuple -from pydantic import ValidationError -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class GuardianData(BaseData): - pass - - -class GuardianChar(GuardianData): - pass - - -class GuardianArms(GuardianData): - pass - - -class GuardianHandle(BaseHandle[GuardianData]): - def __init__(self): - super().__init__("guardian", "坎公骑冠剑") - self.data_files.append("guardian_arms.json") - self.config = draw_config.guardian - - self.ALL_CHAR: List[GuardianChar] = [] - self.ALL_ARMS: List[GuardianArms] = [] - self.UP_CHAR: Optional[UpEvent] = None - self.UP_ARMS: Optional[UpEvent] = None - - def get_card(self, pool_name: str, mode: int = 1) -> GuardianData: - if pool_name == "char": - if mode == 1: - star = self.get_star( - [3, 2, 1], - [ - self.config.GUARDIAN_THREE_CHAR_P, - self.config.GUARDIAN_TWO_CHAR_P, - self.config.GUARDIAN_ONE_CHAR_P, - ], - ) - else: - star = self.get_star( - [3, 2], - [ - self.config.GUARDIAN_THREE_CHAR_P, - self.config.GUARDIAN_TWO_CHAR_P, - ], - ) - up_event = self.UP_CHAR - self.max_star = 3 - all_data = self.ALL_CHAR - else: - if mode == 1: - star = self.get_star( - [5, 4, 3, 2], - [ - self.config.GUARDIAN_FIVE_ARMS_P, - self.config.GUARDIAN_FOUR_ARMS_P, - self.config.GUARDIAN_THREE_ARMS_P, - self.config.GUARDIAN_TWO_ARMS_P, - ], - ) - else: - star = self.get_star( - [5, 4], - [ - self.config.GUARDIAN_FIVE_ARMS_P, - self.config.GUARDIAN_FOUR_ARMS_P, - ], - ) - up_event = self.UP_ARMS - self.max_star = 5 - all_data = self.ALL_ARMS - - acquire_char = None - # 是否UP - if up_event and star == self.max_star and pool_name: - # 获取up角色列表 - up_list = [x.name for x in up_event.up_char if x.star == star] - # 成功获取up角色 - if random.random() < 0.5: - up_name = random.choice(up_list) - try: - acquire_char = [x for x in all_data if x.name == up_name][0] - except IndexError: - pass - if not acquire_char: - chars = [x for x in all_data if x.star == star and not x.limited] - acquire_char = random.choice(chars) - return acquire_char - - def get_cards(self, count: int, pool_name: str) -> List[Tuple[GuardianData, int]]: - card_list = [] - card_count = 0 # 保底计算 - for i in range(count): - card_count += 1 - # 十连保底 - if card_count == 10: - card = self.get_card(pool_name, 2) - card_count = 0 - else: - card = self.get_card(pool_name, 1) - if card.star > self.max_star - 2: - card_count = 0 - card_list.append((card, i + 1)) - return card_list - - def format_pool_info(self, pool_name: str) -> str: - info = "" - up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS - if up_event: - if pool_name == "char": - up_list = [x.name for x in up_event.up_char if x.star == 3] - info += f'三星UP:{" ".join(up_list)}\n' - else: - up_list = [x.name for x in up_event.up_char if x.star == 5] - info += f'五星UP:{" ".join(up_list)}\n' - info = f"当前up池:{up_event.title}\n{info}" - return info.strip() - - def draw(self, count: int, pool_name: str, **kwargs) -> Message: - index2card = self.get_cards(count, pool_name) - cards = [card[0] for card in index2card] - up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS - up_list = [x.name for x in up_event.up_char] if up_event else [] - result = self.format_result(index2card, up_list=up_list) - pool_info = self.format_pool_info(pool_name) - return pool_info + image(b64=self.generate_img(cards).pic2bs4()) + result - - def generate_card_img(self, card: GuardianData) -> BuildImage: - sep_w = 1 - sep_h = 1 - block_w = 170 - block_h = 90 - img_w = 90 - img_h = 90 - if isinstance(card, GuardianChar): - block_color = "#2e2923" - font_color = "#e2ccad" - star_w = 90 - star_h = 30 - star_name = f"{card.star}_star.png" - frame_path = "" - else: - block_color = "#EEE4D5" - font_color = "#A65400" - star_w = 45 - star_h = 45 - star_name = f"{card.star}_star_rank.png" - frame_path = str(self.img_path / "avatar_frame.png") - bg = BuildImage(block_w + sep_w * 2, block_h + sep_h * 2, color="#F6F4ED") - block = BuildImage(block_w, block_h, color=block_color) - star_path = str(self.img_path / star_name) - star = BuildImage(star_w, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - block.paste(img, (0, 0), alpha=True) - if frame_path: - frame = BuildImage(img_w, img_h, background=frame_path) - block.paste(frame, (0, 0), alpha=True) - block.paste( - star, - (int((block_w + img_w - star_w) / 2), block_h - star_h - 30), - alpha=True, - ) - # 加名字 - text = card.name[:4] + "..." if len(card.name) > 5 else card.name - font = load_font(fontsize=14) - text_w, _ = font.getsize(text) - draw = ImageDraw.Draw(block.markImg) - draw.text( - ((block_w + img_w - text_w) / 2, 55), - text, - font=font, - fill=font_color, - ) - bg.paste(block, (sep_w, sep_h)) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - GuardianChar(name=value["名称"], star=int(value["星级"]), limited=False) - for value in self.load_data().values() - ] - self.ALL_ARMS = [ - GuardianArms(name=value["名称"], star=int(value["星级"]), limited=False) - for value in self.load_data("guardian_arms.json").values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) - self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_CHAR and self.UP_ARMS: - data = { - "char": json.loads(self.UP_CHAR.json()), - "arms": json.loads(self.UP_ARMS.json()), - } - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - # guardian.json - guardian_info = {} - url = "https://wiki.biligame.com/gt/英雄筛选表" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - # name = char.xpath("./td[1]/a/@title")[0] - # avatar = char.xpath("./td[1]/a/img/@src")[0] - # star = char.xpath("./td[1]/span/img/@alt")[0] - name = char.xpath("./th[1]/a[1]/@title")[0] - avatar = char.xpath("./th[1]/a/img/@src")[0] - star = char.xpath("./th[1]/span/img/@alt")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar)), - "名称": remove_prohibited_str(name), - "星级": int(str(star).split(" ")[0].replace("Rank", "")), - } - guardian_info[member_dict["名称"]] = member_dict - self.dump_data(guardian_info) - logger.info(f"{self.game_name_cn} 更新成功") - # guardian_arms.json - guardian_arms_info = {} - url = "https://wiki.biligame.com/gt/武器" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 武器出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath( - "//div[@class='resp-tabs-container']/div[1]/div/table[2]/tbody/tr" - ) - for char in char_list: - try: - name = char.xpath("./td[2]/a/@title")[0] - avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] - star = char.xpath("./td[3]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar)), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()), - } - guardian_arms_info[member_dict["名称"]] = member_dict - self.dump_data(guardian_arms_info, "guardian_arms.json") - logger.info(f"{self.game_name_cn} 武器更新成功") - url = "https://wiki.biligame.com/gt/盾牌" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 盾牌出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath( - "//div[@class='resp-tabs-container']/div[2]/div/table[1]/tbody/tr" - ) - for char in char_list: - try: - name = char.xpath("./td[2]/a/@title")[0] - avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] - star = char.xpath("./td[3]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar)), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()), - } - guardian_arms_info[member_dict["名称"]] = member_dict - self.dump_data(guardian_arms_info, "guardian_arms.json") - logger.info(f"{self.game_name_cn} 盾牌更新成功") - # 下载头像 - for value in guardian_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in guardian_arms_info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - idx = 1 - GT_URL = "https://patchwiki.biligame.com/images/gt" - for url in [ - "/4/4b/ardr3bi2yf95u4zomm263tc1vke6i3i.png", - "/5/55/6vow7lh76gzus6b2g9cfn325d1sugca.png", - "/b/b9/du8egrd2vyewg0cuyra9t8jh0srl0ds.png", - ]: - await self.download_img(GT_URL + url, f"{idx}_star") - idx += 1 - # 另一种星星 - idx = 1 - for url in [ - "/6/66/4e2tfa9kvhfcbikzlyei76i9crva145.png", - "/1/10/r9ihsuvycgvsseyneqz4xs22t53026m.png", - "/7/7a/o0k86ru9k915y04azc26hilxead7xp1.png", - "/c/c9/rxz99asysz0rg391j3b02ta09mnpa7v.png", - "/2/2a/sfxz0ucv1s6ewxveycz9mnmrqs2rw60.png", - ]: - await self.download_img(GT_URL + url, f"{idx}_star_rank") - idx += 1 - # 头像框 - await self.download_img( - GT_URL + "/8/8e/ogbqslbhuykjhnc8trtoa0p0nhfzohs.png", f"avatar_frame" - ) - await self.update_up_char() - - async def update_up_char(self): - url = "https://wiki.biligame.com/gt/首页" - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告出错") - return - try: - dom = etree.HTML(result, etree.HTMLParser()) - announcement = dom.xpath( - "//div[@class='mw-parser-output']/div/div[3]/div[2]/div/div[2]/div[3]" - )[0] - title = announcement.xpath("./font/p/b/text()")[0] - match = re.search(r"从(.*?)开始.*?至(.*?)结束", title) - if not match: - logger.warning(f"{self.game_name_cn}找不到UP时间") - return - start, end = match.groups() - start_time = dateparser.parse(start.replace("月", "/").replace("日", "")) - end_time = dateparser.parse(end.replace("月", "/").replace("日", "")) - if not (start_time and end_time) or not ( - start_time <= datetime.now() <= end_time - ): - return - divs = announcement.xpath("./font/div") - char_index = 0 - arms_index = 0 - for index, div in enumerate(divs): - if div.xpath("string(.)") == "角色": - char_index = index - elif div.xpath("string(.)") == "武器": - arms_index = index - chars = divs[char_index + 1 : arms_index] - arms = divs[arms_index + 1 :] - up_chars = [] - up_arms = [] - for char in chars: - name = char.xpath("./p/a/@title")[0] - up_chars.append(UpChar(name=name, star=3, limited=False, zoom=0)) - for arm in arms: - name = arm.xpath("./p/a/@title")[0] - up_arms.append(UpChar(name=name, star=5, limited=False, zoom=0)) - self.UP_CHAR = UpEvent( - title=title, - pool_img="", - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.UP_ARMS = UpEvent( - title=title, - pool_img="", - start_time=start_time, - end_time=end_time, - up_char=up_arms, - ) - self.dump_up_char() - logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_CHAR and self.UP_ARMS: - return Message(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}") +import random +import re +from datetime import datetime +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from nonebot_plugin_alconna import UniMessage +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class GuardianData(BaseData): + pass + + +class GuardianChar(GuardianData): + pass + + +class GuardianArms(GuardianData): + pass + + +class GuardianHandle(BaseHandle[GuardianData]): + def __init__(self): + super().__init__("guardian", "坎公骑冠剑") + self.data_files.append("guardian_arms.json") + self.config = draw_config.guardian + + self.ALL_CHAR: list[GuardianChar] = [] + self.ALL_ARMS: list[GuardianArms] = [] + self.UP_CHAR: UpEvent | None = None + self.UP_ARMS: UpEvent | None = None + + def get_card(self, pool_name: str, mode: int = 1) -> GuardianData: + if pool_name == "char": + if mode == 1: + star = self.get_star( + [3, 2, 1], + [ + self.config.GUARDIAN_THREE_CHAR_P, + self.config.GUARDIAN_TWO_CHAR_P, + self.config.GUARDIAN_ONE_CHAR_P, + ], + ) + else: + star = self.get_star( + [3, 2], + [ + self.config.GUARDIAN_THREE_CHAR_P, + self.config.GUARDIAN_TWO_CHAR_P, + ], + ) + up_event = self.UP_CHAR + self.max_star = 3 + all_data = self.ALL_CHAR + else: + if mode == 1: + star = self.get_star( + [5, 4, 3, 2], + [ + self.config.GUARDIAN_FIVE_ARMS_P, + self.config.GUARDIAN_FOUR_ARMS_P, + self.config.GUARDIAN_THREE_ARMS_P, + self.config.GUARDIAN_TWO_ARMS_P, + ], + ) + else: + star = self.get_star( + [5, 4], + [ + self.config.GUARDIAN_FIVE_ARMS_P, + self.config.GUARDIAN_FOUR_ARMS_P, + ], + ) + up_event = self.UP_ARMS + self.max_star = 5 + all_data = self.ALL_ARMS + + acquire_char = None + # 是否UP + if up_event and star == self.max_star and pool_name: + # 获取up角色列表 + up_list = [x.name for x in up_event.up_char if x.star == star] + # 成功获取up角色 + if random.random() < 0.5: + up_name = random.choice(up_list) + try: + acquire_char = [x for x in all_data if x.name == up_name][0] + except IndexError: + pass + if not acquire_char: + chars = [x for x in all_data if x.star == star and not x.limited] + acquire_char = random.choice(chars) + return acquire_char + + def get_cards(self, count: int, pool_name: str) -> list[tuple[GuardianData, int]]: + card_list = [] + card_count = 0 # 保底计算 + for i in range(count): + card_count += 1 + # 十连保底 + if card_count == 10: + card = self.get_card(pool_name, 2) + card_count = 0 + else: + card = self.get_card(pool_name, 1) + if card.star > self.max_star - 2: + card_count = 0 + card_list.append((card, i + 1)) + return card_list + + def format_pool_info(self, pool_name: str) -> str: + info = "" + up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS + if up_event: + if pool_name == "char": + up_list = [x.name for x in up_event.up_char if x.star == 3] + info += f'三星UP:{" ".join(up_list)}\n' + else: + up_list = [x.name for x in up_event.up_char if x.star == 5] + info += f'五星UP:{" ".join(up_list)}\n' + info = f"当前up池:{up_event.title}\n{info}" + return info.strip() + + async def draw(self, count: int, pool_name: str, **kwargs) -> UniMessage: + index2card = self.get_cards(count, pool_name) + cards = [card[0] for card in index2card] + up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS + up_list = [x.name for x in up_event.up_char] if up_event else [] + result = self.format_result(index2card, up_list=up_list) + pool_info = self.format_pool_info(pool_name) + img = await self.generate_img(cards) + return MessageUtils.build_message([pool_info, img, result]) + + async def generate_card_img(self, card: GuardianData) -> BuildImage: + sep_w = 1 + sep_h = 1 + block_w = 170 + block_h = 90 + img_w = 90 + img_h = 90 + if isinstance(card, GuardianChar): + block_color = "#2e2923" + font_color = "#e2ccad" + star_w = 90 + star_h = 30 + star_name = f"{card.star}_star.png" + frame_path = "" + else: + block_color = "#EEE4D5" + font_color = "#A65400" + star_w = 45 + star_h = 45 + star_name = f"{card.star}_star_rank.png" + frame_path = str(self.img_path / "avatar_frame.png") + bg = BuildImage(block_w + sep_w * 2, block_h + sep_h * 2, color="#F6F4ED") + block = BuildImage(block_w, block_h, color=block_color) + star_path = str(self.img_path / star_name) + star = BuildImage(star_w, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await block.paste(img, (0, 0)) + if frame_path: + frame = BuildImage(img_w, img_h, background=frame_path) + await block.paste(frame, (0, 0)) + await block.paste( + star, + (int((block_w + img_w - star_w) / 2), block_h - star_h - 30), + ) + # 加名字 + text = card.name[:4] + "..." if len(card.name) > 5 else card.name + font = load_font(fontsize=14) + text_w, _ = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(block.markImg) + draw.text( + ((block_w + img_w - text_w) / 2, 55), + text, + font=font, + fill=font_color, + ) + await bg.paste(block, (sep_w, sep_h)) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + GuardianChar(name=value["名称"], star=int(value["星级"]), limited=False) + for value in self.load_data().values() + ] + self.ALL_ARMS = [ + GuardianArms(name=value["名称"], star=int(value["星级"]), limited=False) + for value in self.load_data("guardian_arms.json").values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) + self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_CHAR and self.UP_ARMS: + data = { + "char": json.loads(self.UP_CHAR.json()), + "arms": json.loads(self.UP_ARMS.json()), + } + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + # guardian.json + guardian_info = {} + url = "https://wiki.biligame.com/gt/英雄筛选表" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + # name = char.xpath("./td[1]/a/@title")[0] + # avatar = char.xpath("./td[1]/a/img/@src")[0] + # star = char.xpath("./td[1]/span/img/@alt")[0] + name = char.xpath("./th[1]/a[1]/@title")[0] + avatar = char.xpath("./th[1]/a/img/@src")[0] + star = char.xpath("./th[1]/span/img/@alt")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar)), + "名称": remove_prohibited_str(name), + "星级": int(str(star).split(" ")[0].replace("Rank", "")), + } + guardian_info[member_dict["名称"]] = member_dict + self.dump_data(guardian_info) + logger.info(f"{self.game_name_cn} 更新成功") + # guardian_arms.json + guardian_arms_info = {} + url = "https://wiki.biligame.com/gt/武器" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 武器出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[2]/a/@title")[0] + avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] + url = char.xpath("./td[3]/img/@srcset")[0] + if r := re.search(r"Rank-mini-star_(\d).png", url): + star = r.group(1) + else: + continue + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar)), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()), + } + guardian_arms_info[member_dict["名称"]] = member_dict + self.dump_data(guardian_arms_info, "guardian_arms.json") + logger.info(f"{self.game_name_cn} 武器更新成功") + url = "https://wiki.biligame.com/gt/盾牌" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 盾牌出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath( + "//div[@class='resp-tabs-container']/div[2]/div/table[1]/tbody/tr" + ) + for char in char_list: + try: + name = char.xpath("./td[2]/a/@title")[0] + avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] + star = char.xpath("./td[3]/text()")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar)), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()), + } + guardian_arms_info[member_dict["名称"]] = member_dict + self.dump_data(guardian_arms_info, "guardian_arms.json") + logger.info(f"{self.game_name_cn} 盾牌更新成功") + # 下载头像 + for value in guardian_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in guardian_arms_info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + idx = 1 + GT_URL = "https://patchwiki.biligame.com/images/gt" + for url in [ + "/4/4b/ardr3bi2yf95u4zomm263tc1vke6i3i.png", + "/5/55/6vow7lh76gzus6b2g9cfn325d1sugca.png", + "/b/b9/du8egrd2vyewg0cuyra9t8jh0srl0ds.png", + ]: + await self.download_img(GT_URL + url, f"{idx}_star") + idx += 1 + # 另一种星星 + idx = 1 + for url in [ + "/6/66/4e2tfa9kvhfcbikzlyei76i9crva145.png", + "/1/10/r9ihsuvycgvsseyneqz4xs22t53026m.png", + "/7/7a/o0k86ru9k915y04azc26hilxead7xp1.png", + "/c/c9/rxz99asysz0rg391j3b02ta09mnpa7v.png", + "/2/2a/sfxz0ucv1s6ewxveycz9mnmrqs2rw60.png", + ]: + await self.download_img(GT_URL + url, f"{idx}_star_rank") + idx += 1 + # 头像框 + await self.download_img( + GT_URL + "/8/8e/ogbqslbhuykjhnc8trtoa0p0nhfzohs.png", f"avatar_frame" + ) + await self.update_up_char() + + async def update_up_char(self): + url = "https://wiki.biligame.com/gt/首页" + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告出错") + return + try: + dom = etree.HTML(result, etree.HTMLParser()) + announcement = dom.xpath( + "//div[@class='mw-parser-output']/div/div[3]/div[2]/div/div[2]/div[3]" + )[0] + title = announcement.xpath("./font/p/b/text()")[0] + match = re.search(r"从(.*?)开始.*?至(.*?)结束", title) + if not match: + logger.warning(f"{self.game_name_cn}找不到UP时间") + return + start, end = match.groups() + start_time = dateparser.parse(start.replace("月", "/").replace("日", "")) + end_time = dateparser.parse(end.replace("月", "/").replace("日", "")) + if not (start_time and end_time) or not ( + start_time <= datetime.now() <= end_time + ): + return + divs = announcement.xpath("./font/div") + char_index = 0 + arms_index = 0 + for index, div in enumerate(divs): + if div.xpath("string(.)") == "角色": + char_index = index + elif div.xpath("string(.)") == "武器": + arms_index = index + chars = divs[char_index + 1 : arms_index] + arms = divs[arms_index + 1 :] + up_chars = [] + up_arms = [] + for char in chars: + name = char.xpath("./p/a/@title")[0] + up_chars.append(UpChar(name=name, star=3, limited=False, zoom=0)) + for arm in arms: + name = arm.xpath("./p/a/@title")[0] + up_arms.append(UpChar(name=name, star=5, limited=False, zoom=0)) + self.UP_CHAR = UpEvent( + title=title, + pool_img="", + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.UP_ARMS = UpEvent( + title=title, + pool_img="", + start_time=start_time, + end_time=end_time, + up_char=up_arms, + ) + self.dump_up_char() + logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") + + async def _reload_pool(self) -> UniMessage | None: + await self.update_up_char() + self.load_up_char() + if self.UP_CHAR and self.UP_ARMS: + return MessageUtils.build_message( + f"重载成功!\n当前UP池子:{self.UP_CHAR.title}" + ) diff --git a/plugins/draw_card/handles/onmyoji_handle.py b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py similarity index 78% rename from plugins/draw_card/handles/onmyoji_handle.py rename to zhenxun/plugins/draw_card/handles/onmyoji_handle.py index 5797e830..25d05c38 100644 --- a/plugins/draw_card/handles/onmyoji_handle.py +++ b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py @@ -1,179 +1,178 @@ -import random -from lxml import etree -from typing import List, Tuple -from nonebot.log import logger -from PIL import Image, ImageDraw -from PIL.Image import Image as IMG - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class OnmyojiChar(BaseData): - @property - def star_str(self) -> str: - return ["N", "R", "SR", "SSR", "SP"][self.star - 1] - - -class OnmyojiHandle(BaseHandle[OnmyojiChar]): - def __init__(self): - super().__init__("onmyoji", "阴阳师") - self.max_star = 5 - self.config = draw_config.onmyoji - self.ALL_CHAR: List[OnmyojiChar] = [] - - def get_card(self, **kwargs) -> OnmyojiChar: - star = self.get_star( - [5, 4, 3, 2], - [ - self.config.ONMYOJI_SP, - self.config.ONMYOJI_SSR, - self.config.ONMYOJI_SR, - self.config.ONMYOJI_R, - ], - ) - chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] - return random.choice(chars) - - def format_max_star(self, card_list: List[Tuple[OnmyojiChar, int]]) -> str: - rst = "" - for card, index in card_list: - if card.star == self.max_star: - rst += f"第 {index} 抽获取SP {card.name}\n" - elif card.star == self.max_star - 1: - rst += f"第 {index} 抽获取SSR {card.name}\n" - return rst.strip() - - @staticmethod - def star_label(star: int) -> IMG: - text, color1, color2 = [ - ("N", "#7E7E82", "#F5F6F7"), - ("R", "#014FA8", "#37C6FD"), - ("SR", "#6E0AA4", "#E94EFD"), - ("SSR", "#E5511D", "#FAF905"), - ("SP", "#FA1F2D", "#FFBBAF"), - ][star - 1] - w = 200 - h = 110 - # 制作渐变色图片 - base = Image.new("RGBA", (w, h), color1) - top = Image.new("RGBA", (w, h), color2) - mask = Image.new("L", (w, h)) - mask_data = [] - for y in range(h): - mask_data.extend([int(255 * (y / h))] * w) - mask.putdata(mask_data) - base.paste(top, (0, 0), mask) - # 透明图层 - font = load_font("gorga.otf", 100) - alpha = Image.new("L", (w, h)) - draw = ImageDraw.Draw(alpha) - draw.text((20, -30), text, fill="white", font=font) - base.putalpha(alpha) - # stroke - bg = Image.new("RGBA", (w, h)) - draw = ImageDraw.Draw(bg) - draw.text( - (20, -30), - text, - font=font, - fill="gray", - stroke_width=3, - stroke_fill="gray", - ) - bg.paste(base, (0, 0), base) - return bg - - def generate_img(self, card_list: List[OnmyojiChar]) -> BuildImage: - return super().generate_img(card_list, num_per_line=10) - - def generate_card_img(self, card: OnmyojiChar) -> BuildImage: - bg = BuildImage(73, 240, color="#F1EFE9") - img_path = str(self.img_path / f"{cn2py(card.name)}_mark_btn.png") - img = BuildImage(0, 0, background=img_path) - img = Image.open(img_path).convert("RGBA") - label = self.star_label(card.star).resize((60, 33), Image.ANTIALIAS) - bg.paste(img, (0, 0), alpha=True) - bg.paste(label, (0, 135), alpha=True) - font = load_font("msyh.ttf", 16) - draw = ImageDraw.Draw(bg.markImg) - text = "\n".join([t for t in card.name[:4]]) - _, text_h = font.getsize_multiline(text, spacing=0) - draw.text( - (40, 150 + (90 - text_h) / 2), text, font=font, fill="gray", spacing=0 - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - OnmyojiChar( - name=value["名称"], - star=["N", "R", "SR", "SSR", "SP"].index(value["星级"]) + 1, - limited=True - if key - in [ - "奴良陆生", - "卖药郎", - "鬼灯", - "阿香", - "蜜桃&芥子", - "犬夜叉", - "杀生丸", - "桔梗", - "朽木露琪亚", - "黑崎一护", - "灶门祢豆子", - "灶门炭治郎", - ] - else False, - ) - for key, value in self.load_data().items() - ] - - async def _update_info(self): - info = {} - url = "https://yys.res.netease.com/pc/zt/20161108171335/js/app/all_shishen.json?v74=" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - data = json.loads(result) - for x in data: - name = remove_prohibited_str(x["name"]) - member_dict = { - "id": x["id"], - "名称": name, - "星级": x["level"], - } - info[name] = member_dict - # logger.info(f"{name} is update...") - # 更新头像 - for key in info.keys(): - url = f'https://yys.163.com/shishen/{info[key]["id"]}.html' - result = await self.get_url(url) - if not result: - info[key]["头像"] = "" - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - avatar = dom.xpath("//div[@class='pic_wrap']/img/@src")[0] - avatar = "https:" + avatar - info[key]["头像"] = avatar - except IndexError: - info[key]["头像"] = "" - logger.warning(f"{self.game_name_cn} 获取头像错误 {key}") - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载书签形式的头像 - url = f"https://yys.res.netease.com/pc/zt/20161108171335/data/mark_btn/{value['id']}.png" - await self.download_img(url, value["名称"] + "_mark_btn") +import random + +import ujson as json +from lxml import etree +from PIL import Image, ImageDraw +from PIL.Image import Image as IMG + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle + + +class OnmyojiChar(BaseData): + @property + def star_str(self) -> str: + return ["N", "R", "SR", "SSR", "SP"][self.star - 1] + + +class OnmyojiHandle(BaseHandle[OnmyojiChar]): + def __init__(self): + super().__init__("onmyoji", "阴阳师") + self.max_star = 5 + self.config = draw_config.onmyoji + self.ALL_CHAR: list[OnmyojiChar] = [] + + def get_card(self, **kwargs) -> OnmyojiChar: + star = self.get_star( + [5, 4, 3, 2], + [ + self.config.ONMYOJI_SP, + self.config.ONMYOJI_SSR, + self.config.ONMYOJI_SR, + self.config.ONMYOJI_R, + ], + ) + chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] + return random.choice(chars) + + def format_max_star(self, card_list: list[tuple[OnmyojiChar, int]]) -> str: + rst = "" + for card, index in card_list: + if card.star == self.max_star: + rst += f"第 {index} 抽获取SP {card.name}\n" + elif card.star == self.max_star - 1: + rst += f"第 {index} 抽获取SSR {card.name}\n" + return rst.strip() + + @staticmethod + def star_label(star: int) -> IMG: + text, color1, color2 = [ + ("N", "#7E7E82", "#F5F6F7"), + ("R", "#014FA8", "#37C6FD"), + ("SR", "#6E0AA4", "#E94EFD"), + ("SSR", "#E5511D", "#FAF905"), + ("SP", "#FA1F2D", "#FFBBAF"), + ][star - 1] + w = 200 + h = 110 + # 制作渐变色图片 + base = Image.new("RGBA", (w, h), color1) + top = Image.new("RGBA", (w, h), color2) + mask = Image.new("L", (w, h)) + mask_data = [] + for y in range(h): + mask_data.extend([int(255 * (y / h))] * w) + mask.putdata(mask_data) + base.paste(top, (0, 0), mask) + # 透明图层 + font = load_font("gorga.otf", 100) + alpha = Image.new("L", (w, h)) + draw = ImageDraw.Draw(alpha) + draw.text((20, -30), text, fill="white", font=font) + base.putalpha(alpha) + # stroke + bg = Image.new("RGBA", (w, h)) + draw = ImageDraw.Draw(bg) + draw.text( + (20, -30), + text, + font=font, + fill="gray", + stroke_width=3, + stroke_fill="gray", + ) + bg.paste(base, (0, 0), base) + return bg + + async def generate_img(self, card_list: list[OnmyojiChar]) -> BuildImage: + return await super().generate_img(card_list, num_per_line=10) + + async def generate_card_img(self, card: OnmyojiChar) -> BuildImage: + bg = BuildImage(73, 240, color="#F1EFE9") + img_path = str(self.img_path / f"{cn2py(card.name)}_mark_btn.png") + img = BuildImage(0, 0, background=img_path) + img = Image.open(img_path).convert("RGBA") + label = self.star_label(card.star).resize((60, 33), Image.ANTIALIAS) + await bg.paste(img, (0, 0)) + await bg.paste(label, (0, 135)) + font = load_font("msyh.ttf", 16) + draw = ImageDraw.Draw(bg.markImg) + text = "\n".join([t for t in card.name[:4]]) + _, text_h = font.getsize_multiline(text, spacing=0) + draw.text( + (40, 150 + (90 - text_h) / 2), text, font=font, fill="gray", spacing=0 + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + OnmyojiChar( + name=value["名称"], + star=["N", "R", "SR", "SSR", "SP"].index(value["星级"]) + 1, + limited=( + True + if key + in [ + "奴良陆生", + "卖药郎", + "鬼灯", + "阿香", + "蜜桃&芥子", + "犬夜叉", + "杀生丸", + "桔梗", + "朽木露琪亚", + "黑崎一护", + "灶门祢豆子", + "灶门炭治郎", + ] + else False + ), + ) + for key, value in self.load_data().items() + ] + + async def _update_info(self): + info = {} + url = "https://yys.res.netease.com/pc/zt/20161108171335/js/app/all_shishen.json?v74=" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + data = json.loads(result) + for x in data: + name = remove_prohibited_str(x["name"]) + member_dict = { + "id": x["id"], + "名称": name, + "星级": x["level"], + } + info[name] = member_dict + # logger.info(f"{name} is update...") + # 更新头像 + for key in info.keys(): + url = f'https://yys.163.com/shishen/{info[key]["id"]}.html' + result = await self.get_url(url) + if not result: + info[key]["头像"] = "" + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + avatar = dom.xpath("//div[@class='pic_wrap']/img/@src")[0] + avatar = "https:" + avatar + info[key]["头像"] = avatar + except IndexError: + info[key]["头像"] = "" + logger.warning(f"{self.game_name_cn} 获取头像错误 {key}") + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载书签形式的头像 + url = f"https://yys.res.netease.com/pc/zt/20161108171335/data/mark_btn/{value['id']}.png" + await self.download_img(url, value["名称"] + "_mark_btn") diff --git a/plugins/draw_card/handles/pcr_handle.py b/zhenxun/plugins/draw_card/handles/pcr_handle.py similarity index 86% rename from plugins/draw_card/handles/pcr_handle.py rename to zhenxun/plugins/draw_card/handles/pcr_handle.py index d82ee27a..666a6842 100644 --- a/plugins/draw_card/handles/pcr_handle.py +++ b/zhenxun/plugins/draw_card/handles/pcr_handle.py @@ -1,147 +1,149 @@ -import random -from lxml import etree -from typing import List, Tuple -from PIL import ImageDraw -from urllib.parse import unquote -from nonebot.log import logger - -from .base_handle import BaseHandle, BaseData -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class PcrChar(BaseData): - pass - - -class PcrHandle(BaseHandle[PcrChar]): - def __init__(self): - super().__init__("pcr", "公主连结") - self.max_star = 3 - self.config = draw_config.pcr - self.ALL_CHAR: List[PcrChar] = [] - - def get_card(self, mode: int = 1) -> PcrChar: - if mode == 2: - star = self.get_star( - [3, 2], [self.config.PCR_G_THREE_P, self.config.PCR_G_TWO_P] - ) - else: - star = self.get_star( - [3, 2, 1], - [self.config.PCR_THREE_P, self.config.PCR_TWO_P, self.config.PCR_ONE_P], - ) - chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] - return random.choice(chars) - - def get_cards(self, count: int, **kwargs) -> List[Tuple[PcrChar, int]]: - card_list = [] - card_count = 0 # 保底计算 - for i in range(count): - card_count += 1 - # 十连保底 - if card_count == 10: - card = self.get_card(2) - card_count = 0 - else: - card = self.get_card(1) - if card.star > self.max_star - 2: - card_count = 0 - card_list.append((card, i + 1)) - return card_list - - def generate_card_img(self, card: PcrChar) -> BuildImage: - sep_w = 5 - sep_h = 5 - star_h = 15 - img_w = 90 - img_h = 90 - font_h = 20 - bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") - star_path = str(self.img_path / "star.png") - star = BuildImage(star_h, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - bg.paste(img, (sep_w, sep_h), alpha=True) - for i in range(card.star): - bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h), alpha=True) - # 加名字 - text = card.name[:5] + "..." if len(card.name) > 6 else card.name - font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), - text, - font=font, - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - PcrChar( - name=value["名称"], - star=int(value["星级"]), - limited=True if "(" in key else False, - ) - for key, value in self.load_data().items() - ] - - async def _update_info(self): - info = {} - if draw_config.PCR_TAI: - url = "https://wiki.biligame.com/pcr/角色图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath( - "//div[@class='resp-tab-content']/div[@class='unit-icon']" - ) - for char in char_list: - try: - name = char.xpath("./a/@title")[0] - avatar = char.xpath("./a/img/@srcset")[0] - star = len(char.xpath("./div[1]/img")) - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": star, - } - info[member_dict["名称"]] = member_dict - else: - url = "https://wiki.biligame.com/pcr/角色筛选表" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = char.xpath("./td[4]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()), - } - info[member_dict["名称"]] = member_dict - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - await self.download_img( - "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", - "star", - ) +import random +from urllib.parse import unquote + +from lxml import etree +from PIL import ImageDraw + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle + + +class PcrChar(BaseData): + pass + + +class PcrHandle(BaseHandle[PcrChar]): + def __init__(self): + super().__init__("pcr", "公主连结") + self.max_star = 3 + self.config = draw_config.pcr + self.ALL_CHAR: list[PcrChar] = [] + + def get_card(self, mode: int = 1) -> PcrChar: + if mode == 2: + star = self.get_star( + [3, 2], [self.config.PCR_G_THREE_P, self.config.PCR_G_TWO_P] + ) + else: + star = self.get_star( + [3, 2, 1], + [self.config.PCR_THREE_P, self.config.PCR_TWO_P, self.config.PCR_ONE_P], + ) + chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] + return random.choice(chars) + + def get_cards(self, count: int, **kwargs) -> list[tuple[PcrChar, int]]: + card_list = [] + card_count = 0 # 保底计算 + for i in range(count): + card_count += 1 + # 十连保底 + if card_count == 10: + card = self.get_card(2) + card_count = 0 + else: + card = self.get_card(1) + if card.star > self.max_star - 2: + card_count = 0 + card_list.append((card, i + 1)) + return card_list + + async def generate_card_img(self, card: PcrChar) -> BuildImage: + sep_w = 5 + sep_h = 5 + star_h = 15 + img_w = 90 + img_h = 90 + font_h = 20 + bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") + star_path = str(self.img_path / "star.png") + star = BuildImage(star_h, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await bg.paste(img, (sep_w, sep_h)) + for i in range(card.star): + await bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h)) + # 加名字 + text = card.name[:5] + "..." if len(card.name) > 6 else card.name + font = load_font(fontsize=14) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), + text, + font=font, + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + PcrChar( + name=value["名称"], + star=int(value["星级"]), + limited=True if "(" in key else False, + ) + for key, value in self.load_data().items() + ] + + async def _update_info(self): + info = {} + if draw_config.PCR_TAI: + url = "https://wiki.biligame.com/pcr/角色图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + # TODO: PCR台湾更新失败 + char_list = dom.xpath( + "//*[@id='CardSelectCard']/div[@class='unit-icon trcard']" + ) + for char in char_list: + try: + name = char.xpath("./a/@title")[0] + avatar = char.xpath("./a/img/@srcset")[0] + star = len(char.xpath("./div[1]/img")) + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": star, + } + info[member_dict["名称"]] = member_dict + else: + url = "https://wiki.biligame.com/pcr/角色筛选表" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = char.xpath("./td[4]/text()")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()), + } + info[member_dict["名称"]] = member_dict + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + await self.download_img( + "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", + "star", + ) diff --git a/plugins/draw_card/handles/pretty_handle.py b/zhenxun/plugins/draw_card/handles/pretty_handle.py similarity index 88% rename from plugins/draw_card/handles/pretty_handle.py rename to zhenxun/plugins/draw_card/handles/pretty_handle.py index 5558e268..535e2b19 100644 --- a/plugins/draw_card/handles/pretty_handle.py +++ b/zhenxun/plugins/draw_card/handles/pretty_handle.py @@ -1,424 +1,423 @@ -import re -import random -import dateparser -from lxml import etree -from PIL import ImageDraw -from bs4 import BeautifulSoup -from datetime import datetime -from urllib.parse import unquote -from typing import List, Optional, Tuple -from pydantic import ValidationError -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class PrettyData(BaseData): - pass - - -class PrettyChar(PrettyData): - pass - - -class PrettyCard(PrettyData): - @property - def star_str(self) -> str: - return ["R", "SR", "SSR"][self.star - 1] - - -class PrettyHandle(BaseHandle[PrettyData]): - def __init__(self): - super().__init__("pretty", "赛马娘") - self.data_files.append("pretty_card.json") - self.max_star = 3 - self.game_card_color = "#eff2f5" - self.config = draw_config.pretty - - self.ALL_CHAR: List[PrettyChar] = [] - self.ALL_CARD: List[PrettyCard] = [] - self.UP_CHAR: Optional[UpEvent] = None - self.UP_CARD: Optional[UpEvent] = None - - def get_card(self, pool_name: str, mode: int = 1) -> PrettyData: - if mode == 1: - star = self.get_star( - [3, 2, 1], - [ - self.config.PRETTY_THREE_P, - self.config.PRETTY_TWO_P, - self.config.PRETTY_ONE_P, - ], - ) - else: - star = self.get_star( - [3, 2], [self.config.PRETTY_THREE_P, self.config.PRETTY_TWO_P] - ) - up_pool = None - if pool_name == "char": - up_pool = self.UP_CHAR - all_list = self.ALL_CHAR - else: - up_pool = self.UP_CARD - all_list = self.ALL_CARD - - all_char = [x for x in all_list if x.star == star and not x.limited] - acquire_char = None - # 有UP池子 - if up_pool and star in [x.star for x in up_pool.up_char]: - up_list = [x.name for x in up_pool.up_char if x.star == star] - # 抽到UP - if random.random() < 1 / len(all_char) * (0.7 / 0.1385): - up_name = random.choice(up_list) - try: - acquire_char = [x for x in all_list if x.name == up_name][0] - except IndexError: - pass - if not acquire_char: - acquire_char = random.choice(all_char) - return acquire_char - - def get_cards(self, count: int, pool_name: str) -> List[Tuple[PrettyData, int]]: - card_list = [] - card_count = 0 # 保底计算 - for i in range(count): - card_count += 1 - # 十连保底 - if card_count == 10: - card = self.get_card(pool_name, 2) - card_count = 0 - else: - card = self.get_card(pool_name, 1) - if card.star > self.max_star - 2: - card_count = 0 - card_list.append((card, i + 1)) - return card_list - - def format_pool_info(self, pool_name: str) -> str: - info = "" - up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD - if up_event: - star3_list = [x.name for x in up_event.up_char if x.star == 3] - star2_list = [x.name for x in up_event.up_char if x.star == 2] - star1_list = [x.name for x in up_event.up_char if x.star == 1] - if star3_list: - if pool_name == "char": - info += f'三星UP:{" ".join(star3_list)}\n' - else: - info += f'SSR UP:{" ".join(star3_list)}\n' - if star2_list: - if pool_name == "char": - info += f'二星UP:{" ".join(star2_list)}\n' - else: - info += f'SR UP:{" ".join(star2_list)}\n' - if star1_list: - if pool_name == "char": - info += f'一星UP:{" ".join(star1_list)}\n' - else: - info += f'R UP:{" ".join(star1_list)}\n' - info = f"当前up池:{up_event.title}\n{info}" - return info.strip() - - def draw(self, count: int, pool_name: str, **kwargs) -> Message: - pool_name = "char" if not pool_name else pool_name - index2card = self.get_cards(count, pool_name) - cards = [card[0] for card in index2card] - up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD - up_list = [x.name for x in up_event.up_char] if up_event else [] - result = self.format_result(index2card, up_list=up_list) - pool_info = self.format_pool_info(pool_name) - return pool_info + image(b64=self.generate_img(cards).pic2bs4()) + result - - def generate_card_img(self, card: PrettyData) -> BuildImage: - if isinstance(card, PrettyChar): - star_h = 30 - img_w = 200 - img_h = 219 - font_h = 50 - bg = BuildImage(img_w, img_h + font_h, color="#EFF2F5") - star_path = str(self.img_path / "star.png") - star = BuildImage(star_h, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - star_w = star_h * card.star - for i in range(card.star): - bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0), alpha=True) - bg.paste(img, (0, 0), alpha=True) - # 加名字 - text = card.name[:5] + "..." if len(card.name) > 6 else card.name - font = load_font(fontsize=30) - text_w, _ = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - ((img_w - text_w) / 2, img_h), - text, - font=font, - fill="gray", - ) - return bg - else: - sep_w = 10 - img_w = 200 - img_h = 267 - font_h = 75 - bg = BuildImage(img_w + sep_w * 2, img_h + font_h, color="#EFF2F5") - label_path = str(self.img_path / f"{card.star}_label.png") - label = BuildImage(40, 40, background=label_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - bg.paste(img, (sep_w, 0), alpha=True) - bg.paste(label, (30, 3), alpha=True) - # 加名字 - text = "" - texts = [] - font = load_font(fontsize=25) - for t in card.name: - if font.getsize(text + t)[0] > 190: - texts.append(text) - text = "" - if len(texts) >= 2: - texts[-1] += "..." - break - else: - text += t - if text: - texts.append(text) - text = "\n".join(texts) - text_w, _ = font.getsize_multiline(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - ((img_w - text_w) / 2, img_h), - text, - font=font, - align="center", - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - PrettyChar( - name=value["名称"], - star=int(value["初始星级"]), - limited=False, - ) - for value in self.load_data().values() - ] - self.ALL_CARD = [ - PrettyCard( - name=value["中文名"], - star=["R", "SR", "SSR"].index(value["稀有度"]) + 1, - limited=True if "卡池" not in value["获取方式"] else False, - ) - for value in self.load_data("pretty_card.json").values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) - self.UP_CARD = UpEvent.parse_obj(data.get("card", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_CHAR and self.UP_CARD: - data = { - "char": json.loads(self.UP_CHAR.json()), - "card": json.loads(self.UP_CARD.json()), - } - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - # pretty.json - pretty_info = {} - url = "https://wiki.biligame.com/umamusume/赛马娘图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = len(char.xpath("./td[3]/img")) - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "初始星级": star, - } - pretty_info[member_dict["名称"]] = member_dict - self.dump_data(pretty_info) - logger.info(f"{self.game_name_cn} 更新成功") - # pretty_card.json - pretty_card_info = {} - url = "https://wiki.biligame.com/umamusume/支援卡图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 卡牌出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/div/a/@title")[0] - name_cn = char.xpath("./td[3]/a/text()")[0] - avatar = char.xpath("./td[1]/div/a/img/@srcset")[0] - star = str(char.xpath("./td[5]/text()")[0]).strip() - sources = str(char.xpath("./td[7]/text()")[0]).strip() - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "中文名": remove_prohibited_str(name_cn), - "稀有度": star, - "获取方式": [sources] if sources else [], - } - pretty_card_info[member_dict["中文名"]] = member_dict - self.dump_data(pretty_card_info, "pretty_card.json") - logger.info(f"{self.game_name_cn} 卡牌更新成功") - # 下载头像 - for value in pretty_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in pretty_card_info.values(): - await self.download_img(value["头像"], value["中文名"]) - # 下载星星 - PRETTY_URL = "https://patchwiki.biligame.com/images/umamusume" - await self.download_img( - PRETTY_URL + "/1/13/e1hwjz4vmhtvk8wlyb7c0x3ld1s2ata.png", "star" - ) - # 下载稀有度标志 - idx = 1 - for url in [ - "/f/f7/afqs7h4snmvovsrlifq5ib8vlpu2wvk.png", - "/3/3b/d1jmpwrsk4irkes1gdvoos4ic6rmuht.png", - "/0/06/q23szwkbtd7pfkqrk3wcjlxxt9z595o.png", - ]: - await self.download_img(PRETTY_URL + url, f"{idx}_label") - idx += 1 - await self.update_up_char() - - async def update_up_char(self): - announcement_url = "https://wiki.biligame.com/umamusume/公告" - result = await self.get_url(announcement_url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - announcements = dom.xpath("//div[@id='mw-content-text']/div/div/span/a") - title = "" - url = "" - for announcement in announcements: - try: - title = announcement.xpath("./@title")[0] - url = "https://wiki.biligame.com/" + announcement.xpath("./@href")[0] - if re.match(r".*?\d{8}$", title) or re.match( - r"^\d{1,2}月\d{1,2}日.*?", title - ): - break - except IndexError: - continue - if not title: - logger.warning(f"{self.game_name_cn}未找到新UP公告") - return - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取UP公告出错") - return - try: - start_time = None - end_time = None - char_img = "" - card_img = "" - up_chars = [] - up_cards = [] - soup = BeautifulSoup(result, "lxml") - heads = soup.find_all("span", {"class": "mw-headline"}) - for head in heads: - if "时间" in head.text: - time = head.find_next("p").text.split("\n")[0] - if "~" in time: - start, end = time.split("~") - start_time = dateparser.parse(start) - end_time = dateparser.parse(end) - elif "赛马娘" in head.text: - char_img = head.find_next("a", {"class": "image"}).find("img")[ - "src" - ] - lines = str(head.find_next("p").text).split("\n") - chars = [ - line - for line in lines - if "★" in line and "(" in line and ")" in line - ] - for char in chars: - star = char.count("★") - name = re.split(r"[()]", char)[-2].strip() - up_chars.append( - UpChar(name=name, star=star, limited=False, zoom=70) - ) - elif "支援卡" in head.text: - card_img = head.find_next("a", {"class": "image"}).find("img")[ - "src" - ] - lines = str(head.find_next("p").text).split("\n") - cards = [ - line - for line in lines - if "R" in line and "(" in line and ")" in line - ] - for card in cards: - star = 3 if "SSR" in card else 2 if "SR" in card else 1 - name = re.split(r"[()]", card)[-2].strip() - up_cards.append( - UpChar(name=name, star=star, limited=False, zoom=70) - ) - if start_time and end_time: - if start_time <= datetime.now() <= end_time: - self.UP_CHAR = UpEvent( - title=title, - pool_img=char_img, - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.UP_CARD = UpEvent( - title=title, - pool_img=card_img, - start_time=start_time, - end_time=end_time, - up_char=up_cards, - ) - self.dump_up_char() - logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_CHAR and self.UP_CARD: - return Message( - Message.template("重载成功!\n当前UP池子:{}{:image}{:image}").format( - self.UP_CHAR.title, - self.UP_CHAR.pool_img, - self.UP_CARD.pool_img, - ) - ) +import random +import re +from datetime import datetime +from urllib.parse import unquote + +import dateparser +import ujson as json +from bs4 import BeautifulSoup +from lxml import etree +from nonebot_plugin_alconna import UniMessage +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class PrettyData(BaseData): + pass + + +class PrettyChar(PrettyData): + pass + + +class PrettyCard(PrettyData): + @property + def star_str(self) -> str: + return ["R", "SR", "SSR"][self.star - 1] + + +class PrettyHandle(BaseHandle[PrettyData]): + def __init__(self): + super().__init__("pretty", "赛马娘") + self.data_files.append("pretty_card.json") + self.max_star = 3 + self.game_card_color = "#eff2f5" + self.config = draw_config.pretty + + self.ALL_CHAR: list[PrettyChar] = [] + self.ALL_CARD: list[PrettyCard] = [] + self.UP_CHAR: UpEvent | None = None + self.UP_CARD: UpEvent | None = None + + def get_card(self, pool_name: str, mode: int = 1) -> PrettyData: + if mode == 1: + star = self.get_star( + [3, 2, 1], + [ + self.config.PRETTY_THREE_P, + self.config.PRETTY_TWO_P, + self.config.PRETTY_ONE_P, + ], + ) + else: + star = self.get_star( + [3, 2], [self.config.PRETTY_THREE_P, self.config.PRETTY_TWO_P] + ) + up_pool = None + if pool_name == "char": + up_pool = self.UP_CHAR + all_list = self.ALL_CHAR + else: + up_pool = self.UP_CARD + all_list = self.ALL_CARD + + all_char = [x for x in all_list if x.star == star and not x.limited] + acquire_char = None + # 有UP池子 + if up_pool and star in [x.star for x in up_pool.up_char]: + up_list = [x.name for x in up_pool.up_char if x.star == star] + # 抽到UP + if random.random() < 1 / len(all_char) * (0.7 / 0.1385): + up_name = random.choice(up_list) + try: + acquire_char = [x for x in all_list if x.name == up_name][0] + except IndexError: + pass + if not acquire_char: + acquire_char = random.choice(all_char) + return acquire_char + + def get_cards(self, count: int, pool_name: str) -> list[tuple[PrettyData, int]]: + card_list = [] + card_count = 0 # 保底计算 + for i in range(count): + card_count += 1 + # 十连保底 + if card_count == 10: + card = self.get_card(pool_name, 2) + card_count = 0 + else: + card = self.get_card(pool_name, 1) + if card.star > self.max_star - 2: + card_count = 0 + card_list.append((card, i + 1)) + return card_list + + def format_pool_info(self, pool_name: str) -> str: + info = "" + up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD + if up_event: + star3_list = [x.name for x in up_event.up_char if x.star == 3] + star2_list = [x.name for x in up_event.up_char if x.star == 2] + star1_list = [x.name for x in up_event.up_char if x.star == 1] + if star3_list: + if pool_name == "char": + info += f'三星UP:{" ".join(star3_list)}\n' + else: + info += f'SSR UP:{" ".join(star3_list)}\n' + if star2_list: + if pool_name == "char": + info += f'二星UP:{" ".join(star2_list)}\n' + else: + info += f'SR UP:{" ".join(star2_list)}\n' + if star1_list: + if pool_name == "char": + info += f'一星UP:{" ".join(star1_list)}\n' + else: + info += f'R UP:{" ".join(star1_list)}\n' + info = f"当前up池:{up_event.title}\n{info}" + return info.strip() + + async def draw(self, count: int, pool_name: str, **kwargs) -> UniMessage: + pool_name = "char" if not pool_name else pool_name + index2card = self.get_cards(count, pool_name) + cards = [card[0] for card in index2card] + up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD + up_list = [x.name for x in up_event.up_char] if up_event else [] + result = self.format_result(index2card, up_list=up_list) + pool_info = self.format_pool_info(pool_name) + img = await self.generate_img(cards) + return MessageUtils.build_message([pool_info, img, result]) + + async def generate_card_img(self, card: PrettyData) -> BuildImage: + if isinstance(card, PrettyChar): + star_h = 30 + img_w = 200 + img_h = 219 + font_h = 50 + bg = BuildImage(img_w, img_h + font_h, color="#EFF2F5") + star_path = str(self.img_path / "star.png") + star = BuildImage(star_h, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + star_w = star_h * card.star + for i in range(card.star): + await bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0)) + await bg.paste(img, (0, 0)) + # 加名字 + text = card.name[:5] + "..." if len(card.name) > 6 else card.name + font = load_font(fontsize=30) + text_w, _ = font.getsize(text) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + ((img_w - text_w) / 2, img_h), + text, + font=font, + fill="gray", + ) + return bg + else: + sep_w = 10 + img_w = 200 + img_h = 267 + font_h = 75 + bg = BuildImage(img_w + sep_w * 2, img_h + font_h, color="#EFF2F5") + label_path = str(self.img_path / f"{card.star}_label.png") + label = BuildImage(40, 40, background=label_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await bg.paste(img, (sep_w, 0)) + await bg.paste(label, (30, 3)) + # 加名字 + text = "" + texts = [] + font = load_font(fontsize=25) + for t in card.name: + if BuildImage.get_text_size((text + t), font)[0] > 190: + texts.append(text) + text = "" + if len(texts) >= 2: + texts[-1] += "..." + break + else: + text += t + if text: + texts.append(text) + text = "\n".join(texts) + text_w, _ = font.getsize_multiline(text) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + ((img_w - text_w) / 2, img_h), + text, + font=font, + align="center", + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + PrettyChar( + name=value["名称"], + star=int(value["初始星级"]), + limited=False, + ) + for value in self.load_data().values() + ] + self.ALL_CARD = [ + PrettyCard( + name=value["中文名"], + star=["R", "SR", "SSR"].index(value["稀有度"]) + 1, + limited=True if "卡池" not in value["获取方式"] else False, + ) + for value in self.load_data("pretty_card.json").values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) + self.UP_CARD = UpEvent.parse_obj(data.get("card", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_CHAR and self.UP_CARD: + data = { + "char": json.loads(self.UP_CHAR.json()), + "card": json.loads(self.UP_CARD.json()), + } + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + # pretty.json + pretty_info = {} + url = "https://wiki.biligame.com/umamusume/赛马娘图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = len(char.xpath("./td[3]/img")) + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "初始星级": star, + } + pretty_info[member_dict["名称"]] = member_dict + self.dump_data(pretty_info) + logger.info(f"{self.game_name_cn} 更新成功") + # pretty_card.json + pretty_card_info = {} + url = "https://wiki.biligame.com/umamusume/支援卡图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 卡牌出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/div/a/@title")[0] + name_cn = char.xpath("./td[3]/a/text()")[0] + avatar = char.xpath("./td[1]/div/a/img/@srcset")[0] + star = str(char.xpath("./td[5]/text()")[0]).strip() + sources = str(char.xpath("./td[7]/text()")[0]).strip() + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "中文名": remove_prohibited_str(name_cn), + "稀有度": star, + "获取方式": [sources] if sources else [], + } + pretty_card_info[member_dict["中文名"]] = member_dict + self.dump_data(pretty_card_info, "pretty_card.json") + logger.info(f"{self.game_name_cn} 卡牌更新成功") + # 下载头像 + for value in pretty_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in pretty_card_info.values(): + await self.download_img(value["头像"], value["中文名"]) + # 下载星星 + PRETTY_URL = "https://patchwiki.biligame.com/images/umamusume" + await self.download_img( + PRETTY_URL + "/1/13/e1hwjz4vmhtvk8wlyb7c0x3ld1s2ata.png", "star" + ) + # 下载稀有度标志 + idx = 1 + for url in [ + "/f/f7/afqs7h4snmvovsrlifq5ib8vlpu2wvk.png", + "/3/3b/d1jmpwrsk4irkes1gdvoos4ic6rmuht.png", + "/0/06/q23szwkbtd7pfkqrk3wcjlxxt9z595o.png", + ]: + await self.download_img(PRETTY_URL + url, f"{idx}_label") + idx += 1 + await self.update_up_char() + + async def update_up_char(self): + announcement_url = "https://wiki.biligame.com/umamusume/公告" + result = await self.get_url(announcement_url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + announcements = dom.xpath("//div[@id='mw-content-text']/div/div/span/a") + title = "" + url = "" + for announcement in announcements: + try: + title = announcement.xpath("./@title")[0] + url = "https://wiki.biligame.com/" + announcement.xpath("./@href")[0] + if re.match(r".*?\d{8}$", title) or re.match( + r"^\d{1,2}月\d{1,2}日.*?", title + ): + break + except IndexError: + continue + if not title: + logger.warning(f"{self.game_name_cn}未找到新UP公告") + return + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取UP公告出错") + return + try: + start_time = None + end_time = None + char_img = "" + card_img = "" + up_chars = [] + up_cards = [] + soup = BeautifulSoup(result, "lxml") + heads = soup.find_all("span", {"class": "mw-headline"}) + for head in heads: + if "时间" in head.text: + time = head.find_next("p").text.split("\n")[0] + if "~" in time: + start, end = time.split("~") + start_time = dateparser.parse(start) + end_time = dateparser.parse(end) + elif "赛马娘" in head.text: + char_img = head.find_next("a", {"class": "image"}).find("img")[ + "src" + ] + lines = str(head.find_next("p").text).split("\n") + chars = [ + line + for line in lines + if "★" in line and "(" in line and ")" in line + ] + for char in chars: + star = char.count("★") + name = re.split(r"[()]", char)[-2].strip() + up_chars.append( + UpChar(name=name, star=star, limited=False, zoom=70) + ) + elif "支援卡" in head.text: + card_img = head.find_next("a", {"class": "image"}).find("img")[ + "src" + ] + lines = str(head.find_next("p").text).split("\n") + cards = [ + line + for line in lines + if "R" in line and "(" in line and ")" in line + ] + for card in cards: + star = 3 if "SSR" in card else 2 if "SR" in card else 1 + name = re.split(r"[()]", card)[-2].strip() + up_cards.append( + UpChar(name=name, star=star, limited=False, zoom=70) + ) + if start_time and end_time: + if start_time <= datetime.now() <= end_time: + self.UP_CHAR = UpEvent( + title=title, + pool_img=char_img, + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.UP_CARD = UpEvent( + title=title, + pool_img=card_img, + start_time=start_time, + end_time=end_time, + up_char=up_cards, + ) + self.dump_up_char() + logger.info( + f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}" + ) + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错", e=e) + + async def _reload_pool(self) -> UniMessage | None: + await self.update_up_char() + self.load_up_char() + if self.UP_CHAR and self.UP_CARD: + return MessageUtils.build_message( + [ + f"重载成功!\n当前UP池子:{self.UP_CHAR.title}", + self.UP_CHAR.pool_img, + self.UP_CARD.pool_img, + ] + ) diff --git a/plugins/draw_card/handles/prts_handle.py b/zhenxun/plugins/draw_card/handles/prts_handle.py similarity index 74% rename from plugins/draw_card/handles/prts_handle.py rename to zhenxun/plugins/draw_card/handles/prts_handle.py index af0c5be2..18a86fc3 100644 --- a/plugins/draw_card/handles/prts_handle.py +++ b/zhenxun/plugins/draw_card/handles/prts_handle.py @@ -1,325 +1,344 @@ -import random -import re -from datetime import datetime -from typing import List, Optional, Tuple -from urllib.parse import unquote - -import dateparser -from PIL import ImageDraw -from lxml import etree -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger -from pydantic import ValidationError - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class Operator(BaseData): - recruit_only: bool # 公招限定 - event_only: bool # 活动获得干员 - core_only:bool #中坚干员 - # special_only: bool # 升变/异格干员 - - -class PrtsHandle(BaseHandle[Operator]): - def __init__(self): - super().__init__(game_name="prts", game_name_cn="明日方舟") - self.max_star = 6 - self.game_card_color = "#eff2f5" - self.config = draw_config.prts - - self.ALL_OPERATOR: List[Operator] = [] - self.UP_EVENT: Optional[UpEvent] = None - - def get_card(self, add: float) -> Operator: - star = self.get_star( - star_list=[6, 5, 4, 3], - probability_list=[ - self.config.PRTS_SIX_P + add, - self.config.PRTS_FIVE_P, - self.config.PRTS_FOUR_P, - self.config.PRTS_THREE_P, - ], - ) - - all_operators = [ - x - for x in self.ALL_OPERATOR - if x.star == star and not any([x.limited, x.recruit_only, x.event_only,x.core_only]) - ] - acquire_operator = None - - if self.UP_EVENT: - up_operators = [x for x in self.UP_EVENT.up_char if x.star == star] - # UPs - try: - zooms = [x.zoom for x in up_operators] - zoom_sum = sum(zooms) - if random.random() < zoom_sum: - up_name = random.choices(up_operators, weights=zooms, k=1)[0].name - acquire_operator = [ - x for x in self.ALL_OPERATOR if x.name == up_name - ][0] - except IndexError: - pass - if not acquire_operator: - acquire_operator = random.choice(all_operators) - return acquire_operator - - def get_cards(self, count: int, **kwargs) -> List[Tuple[Operator, int]]: - card_list = [] # 获取所有角色 - add = 0.0 - count_idx = 0 - for i in range(count): - count_idx += 1 - card = self.get_card(add) - if card.star == self.max_star: - add = 0.0 - count_idx = 0 - elif count_idx > 50: - add += 0.02 - card_list.append((card, i + 1)) - return card_list - - def format_pool_info(self) -> str: - info = "" - if self.UP_EVENT: - star6_list = [x.name for x in self.UP_EVENT.up_char if x.star == 6] - star5_list = [x.name for x in self.UP_EVENT.up_char if x.star == 5] - star4_list = [x.name for x in self.UP_EVENT.up_char if x.star == 4] - if star6_list: - info += f"六星UP:{' '.join(star6_list)}\n" - if star5_list: - info += f"五星UP:{' '.join(star5_list)}\n" - if star4_list: - info += f"四星UP:{' '.join(star4_list)}\n" - info = f"当前up池: {self.UP_EVENT.title}\n{info}" - return info.strip() - - def draw(self, count: int, **kwargs) -> Message: - index2card = self.get_cards(count) - """这里cards修复了抽卡图文不符的bug""" - cards = [card[0] for card in index2card] - up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] - result = self.format_result(index2card, up_list=up_list) - pool_info = self.format_pool_info() - return ( - pool_info - + image(b64=self.generate_img(cards).pic2bs4()) - + result - ) - - def generate_card_img(self, card: Operator) -> BuildImage: - sep_w = 5 - sep_h = 5 - star_h = 15 - img_w = 120 - img_h = 120 - font_h = 20 - bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") - star_path = str(self.img_path / "star.png") - star = BuildImage(star_h, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - bg.paste(img, (sep_w, sep_h), alpha=True) - for i in range(card.star): - bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h), alpha=True) - # 加名字 - text = card.name[:7] + "..." if len(card.name) > 8 else card.name - font = load_font(fontsize=16) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), - text, - font=font, - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_OPERATOR = [ - Operator( - name=value["名称"], - star=int(value["星级"]), - limited="标准寻访" not in value["获取途径"] and "中坚寻访" not in value["获取途径"], - recruit_only=True - if "标准寻访" not in value["获取途径"] and "中坚寻访" not in value["获取途径"] and "公开招募" in value["获取途径"] - else False, - event_only=True - if "活动获取" in value["获取途径"] - else False, - core_only=True - if "标准寻访" not in value["获取途径"] and "中坚寻访" in value["获取途径"] - else False, - ) - for key, value in self.load_data().items() - if "阿米娅" not in key - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - """这里的 waring 有点模糊,更新游戏信息时没有up池的情况下也会报错,所以细分了一下""" - if not data: - logger.warning(f"当前无UP池或 {self.game_name}_up_char.json 文件不存在") - else: - self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_EVENT: - data = {"char": json.loads(self.UP_EVENT.json())} - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - """更新信息""" - info = {} - url = "https://wiki.biligame.com/arknights/干员数据表" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - avatar = char.xpath("./td[1]/div/div/div/a/img/@srcset")[0] - name = char.xpath("./td[2]/a/text()")[0] - star = char.xpath("./td[5]/text()")[0] - """这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因""" - sources = [_.strip('\n') for _ in char.xpath("./td[8]/text()")] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(str(name).strip()), - "星级": int(str(star).strip()), - "获取途径": sources, - } - info[member_dict["名称"]] = member_dict - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - await self.download_img( - "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", - "star", - ) - await self.update_up_char() - - async def update_up_char(self): - """重载卡池""" - announcement_url = "https://ak.hypergryph.com/news.html" - result = await self.get_url(announcement_url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - activity_urls = dom.xpath( - "//ol[@class='articleList' and @data-category-key='ACTIVITY']/li/a/@href" - ) - start_time = None - end_time = None - up_chars = [] - pool_img = "" - for activity_url in activity_urls[:10]: # 减少响应时间, 10个就够了 - activity_url = f"https://ak.hypergryph.com{activity_url}" - result = await self.get_url(activity_url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告 {activity_url} 出错") - continue - - """因为鹰角的前端太自由了,这里重写了匹配规则以尽可能避免因为前端乱七八糟而导致的重载失败""" - dom = etree.HTML(result, etree.HTMLParser()) - contents = dom.xpath( - "//div[@class='article-content']/p/text() | //div[@class='article-content']/p/span/text() | //div[@class='article-content']/div[@class='media-wrap image-wrap']/img/@src" - ) - title = "" - time = "" - chars: List[str] = [] - for index, content in enumerate(contents): - if re.search("(.*)(寻访|复刻).*?开启", content): - title = re.split(r"[【】]", content) - title = "".join(title[1:-1]) if "-" in title else title[1] - lines = [contents[index-2+_] for _ in range(8)] # 从 -2 开始是因为xpath获取的时间有的会在寻访开启这一句之前 - lines.append("") # 防止IndexError,加个空字符串 - for idx, line in enumerate(lines): - match = re.search( - r"(\d{1,2}月\d{1,2}日.*?-.*?\d{1,2}月\d{1,2}日.*?$)", line - ) - if match: - time = match.group(1) - """因为

的诡异排版,所以有了下面的一段""" - if ("★★" in line and "%" in line) or ("★★" in line and "%" in lines[idx + 1]): - chars.append(line) if ("★★" in line and "%" in line) else chars.append(line + lines[idx + 1]) - if not time: - continue - start, end = time.replace("月", "/").replace("日", " ").split("-")[:2] # 日替换为空格是因为有日后面不接空格的情况,导致 split 出问题 - start_time = dateparser.parse(start) - end_time = dateparser.parse(end) - pool_img = contents[index-2] - r"""两类格式:用/分割,用\分割;★+(概率)+名字,★+名字+(概率)""" - for char in chars: - star = char.split("(")[0].count("★") - name = re.split(r"[:(]", char)[1] if "★(" not in char else re.split("):", char)[1] # 有的括号在前面有的在后面 - dual_up = False - if "\\" in name: - names = name.split("\\") - dual_up = True - elif "/" in name: - names = name.split("/") - dual_up = True - else: - names = [name] # 既有用/分割的,又有用\分割的 - - names = [name.replace("[限定]", "").strip() for name in names] - zoom = 1 - if "权值" in char: - zoom = 0.03 - else: - match = re.search(r"(占.*?的.*?(\d+).*?%)", char) - if dual_up == True: - zoom = float(match.group(1))/2 - else: - zoom = float(match.group(1)) - zoom = zoom / 100 if zoom > 1 else zoom - for name in names: - up_chars.append( - UpChar(name=name, star=star, limited=False, zoom=zoom) - ) - break # 这里break会导致个问题:如果一个公告里有两个池子,会漏掉下面的池子,比如 5.19 的定向寻访。但目前我也没啥好想法解决 - if title and start_time and end_time: - if start_time <= datetime.now() <= end_time: - self.UP_EVENT = UpEvent( - title=title, - pool_img=pool_img, - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.dump_up_char() - logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") - break - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_EVENT: - return f"重载成功!\n当前UP池子:{self.UP_EVENT.title}" + MessageSegment.image( - self.UP_EVENT.pool_img - ) +import random +import re +from datetime import datetime +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from lxml.etree import _Element +from nonebot_plugin_alconna import UniMessage +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class Operator(BaseData): + recruit_only: bool # 公招限定 + event_only: bool # 活动获得干员 + core_only: bool # 中坚干员 + # special_only: bool # 升变/异格干员 + + +class PrtsHandle(BaseHandle[Operator]): + def __init__(self): + super().__init__(game_name="prts", game_name_cn="明日方舟") + self.max_star = 6 + self.game_card_color = "#eff2f5" + self.config = draw_config.prts + + self.ALL_OPERATOR: list[Operator] = [] + self.UP_EVENT: UpEvent | None = None + + def get_card(self, add: float) -> Operator: + star = self.get_star( + star_list=[6, 5, 4, 3], + probability_list=[ + self.config.PRTS_SIX_P + add, + self.config.PRTS_FIVE_P, + self.config.PRTS_FOUR_P, + self.config.PRTS_THREE_P, + ], + ) + + all_operators = [ + x + for x in self.ALL_OPERATOR + if x.star == star + and not any([x.limited, x.recruit_only, x.event_only, x.core_only]) + ] + acquire_operator = None + + if self.UP_EVENT: + up_operators = [x for x in self.UP_EVENT.up_char if x.star == star] + # UPs + try: + zooms = [x.zoom for x in up_operators] + zoom_sum = sum(zooms) + if random.random() < zoom_sum: + up_name = random.choices(up_operators, weights=zooms, k=1)[0].name + acquire_operator = [ + x for x in self.ALL_OPERATOR if x.name == up_name + ][0] + except IndexError: + pass + if not acquire_operator: + acquire_operator = random.choice(all_operators) + return acquire_operator + + def get_cards(self, count: int, **kwargs) -> list[tuple[Operator, int]]: + card_list = [] # 获取所有角色 + add = 0.0 + count_idx = 0 + for i in range(count): + count_idx += 1 + card = self.get_card(add) + if card.star == self.max_star: + add = 0.0 + count_idx = 0 + elif count_idx > 50: + add += 0.02 + card_list.append((card, i + 1)) + return card_list + + def format_pool_info(self) -> str: + info = "" + if self.UP_EVENT: + star6_list = [x.name for x in self.UP_EVENT.up_char if x.star == 6] + star5_list = [x.name for x in self.UP_EVENT.up_char if x.star == 5] + star4_list = [x.name for x in self.UP_EVENT.up_char if x.star == 4] + if star6_list: + info += f"六星UP:{' '.join(star6_list)}\n" + if star5_list: + info += f"五星UP:{' '.join(star5_list)}\n" + if star4_list: + info += f"四星UP:{' '.join(star4_list)}\n" + info = f"当前up池: {self.UP_EVENT.title}\n{info}" + return info.strip() + + async def draw(self, count: int, **kwargs) -> UniMessage: + index2card = self.get_cards(count) + """这里cards修复了抽卡图文不符的bug""" + cards = [card[0] for card in index2card] + up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] + result = self.format_result(index2card, up_list=up_list) + pool_info = self.format_pool_info() + img = await self.generate_img(cards) + return MessageUtils.build_message([pool_info, img, result]) + + async def generate_card_img(self, card: Operator) -> BuildImage: + sep_w = 5 + sep_h = 5 + star_h = 15 + img_w = 120 + img_h = 120 + font_h = 20 + bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") + star_path = str(self.img_path / "star.png") + star = BuildImage(star_h, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await bg.paste(img, (sep_w, sep_h)) + for i in range(card.star): + await bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h)) + # 加名字 + text = card.name[:7] + "..." if len(card.name) > 8 else card.name + font = load_font(fontsize=16) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), + text, + font=font, + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_OPERATOR = [ + Operator( + name=value["名称"], + star=int(value["星级"]), + limited="标准寻访" not in value["获取途径"] + and "中坚寻访" not in value["获取途径"], + recruit_only=( + True + if "标准寻访" not in value["获取途径"] + and "中坚寻访" not in value["获取途径"] + and "公开招募" in value["获取途径"] + else False + ), + event_only=True if "活动获取" in value["获取途径"] else False, + core_only=( + True + if "标准寻访" not in value["获取途径"] + and "中坚寻访" in value["获取途径"] + else False + ), + ) + for key, value in self.load_data().items() + if "阿米娅" not in key + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + """这里的 waring 有点模糊,更新游戏信息时没有up池的情况下也会报错,所以细分了一下""" + if not data: + logger.warning(f"当前无UP池或 {self.game_name}_up_char.json 文件不存在") + else: + self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_EVENT: + data = {"char": json.loads(self.UP_EVENT.json())} + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + """更新信息""" + info = {} + url = "https://wiki.biligame.com/arknights/干员数据表" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + char_list: list[_Element] = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + avatar = char.xpath("./td[1]/div/div/div/a/img/@srcset")[0] + name = char.xpath("./td[1]/center/a/text()")[0] + star = char.xpath("./td[2]/text()")[0] + """这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因""" + sources = [_.strip("\n") for _ in char.xpath("./td[7]/text()")] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(str(name).strip()), + "星级": int(str(star).strip()), + "获取途径": sources, + } + info[member_dict["名称"]] = member_dict + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + await self.download_img( + "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", + "star", + ) + await self.update_up_char() + + async def update_up_char(self): + """重载卡池""" + announcement_url = "https://ak.hypergryph.com/news.html" + result = await self.get_url(announcement_url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + activity_urls = dom.xpath( + "//ol[@class='articlelist' and @data-category-key='ACTIVITY']/li/a/@href" + ) + start_time = None + end_time = None + up_chars = [] + pool_img = "" + for activity_url in activity_urls[:10]: # 减少响应时间, 10个就够了 + activity_url = f"https://ak.hypergryph.com{activity_url}" + result = await self.get_url(activity_url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告 {activity_url} 出错") + continue + + """因为鹰角的前端太自由了,这里重写了匹配规则以尽可能避免因为前端乱七八糟而导致的重载失败""" + dom = etree.HTML(result, etree.HTMLParser()) + contents = dom.xpath( + "//div[@class='article-content']/p/text() | //div[@class='article-content']/p/span/text() | //div[@class='article-content']/div[@class='media-wrap image-wrap']/img/@src" + ) + title = "" + time = "" + chars: list[str] = [] + for index, content in enumerate(contents): + if re.search("(.*)(寻访|复刻).*?开启", content): + title = re.split(r"[【】]", content) + title = "".join(title[1:-1]) if "-" in title else title[1] + lines = [ + contents[index - 2 + _] for _ in range(8) + ] # 从 -2 开始是因为xpath获取的时间有的会在寻访开启这一句之前 + lines.append("") # 防止IndexError,加个空字符串 + for idx, line in enumerate(lines): + match = re.search( + r"(\d{1,2}月\d{1,2}日.*?-.*?\d{1,2}月\d{1,2}日.*?$)", line + ) + if match: + time = match.group(1) + """因为

的诡异排版,所以有了下面的一段""" + if ("★★" in line and "%" in line) or ( + "★★" in line and "%" in lines[idx + 1] + ): + ( + chars.append(line) + if ("★★" in line and "%" in line) + else chars.append(line + lines[idx + 1]) + ) + if not time: + continue + start, end = ( + time.replace("月", "/").replace("日", " ").split("-")[:2] + ) # 日替换为空格是因为有日后面不接空格的情况,导致 split 出问题 + start_time = dateparser.parse(start) + end_time = dateparser.parse(end) + pool_img = contents[index - 2] + r"""两类格式:用/分割,用\分割;★+(概率)+名字,★+名字+(概率)""" + for char in chars: + star = char.split("(")[0].count("★") + name = ( + re.split(r"[:(]", char)[1] + if "★(" not in char + else re.split("):", char)[1] + ) # 有的括号在前面有的在后面 + dual_up = False + if "\\" in name: + names = name.split("\\") + dual_up = True + elif "/" in name: + names = name.split("/") + dual_up = True + else: + names = [name] # 既有用/分割的,又有用\分割的 + + names = [name.replace("[限定]", "").strip() for name in names] + zoom = 1 + if "权值" in char: + zoom = 0.03 + else: + match = re.search(r"(占.*?的.*?(\d+).*?%)", char) + if dual_up == True: + zoom = float(match.group(1)) / 2 + else: + zoom = float(match.group(1)) + zoom = zoom / 100 if zoom > 1 else zoom + for name in names: + up_chars.append( + UpChar(name=name, star=star, limited=False, zoom=zoom) + ) + break # 这里break会导致个问题:如果一个公告里有两个池子,会漏掉下面的池子,比如 5.19 的定向寻访。但目前我也没啥好想法解决 + if title and start_time and end_time: + if start_time <= datetime.now() <= end_time: + self.UP_EVENT = UpEvent( + title=title, + pool_img=pool_img, + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.dump_up_char() + logger.info( + f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}" + ) + break + + async def _reload_pool(self) -> UniMessage | None: + await self.update_up_char() + self.load_up_char() + if self.UP_EVENT: + return MessageUtils.build_message( + [ + f"重载成功!\n当前UP池子:{self.UP_EVENT.title}", + self.UP_EVENT.pool_img, + ] + ) diff --git a/plugins/draw_card/rule.py b/zhenxun/plugins/draw_card/rule.py similarity index 81% rename from plugins/draw_card/rule.py rename to zhenxun/plugins/draw_card/rule.py index fbb42096..49746d95 100644 --- a/plugins/draw_card/rule.py +++ b/zhenxun/plugins/draw_card/rule.py @@ -1,6 +1,6 @@ from nonebot.internal.rule import Rule -from configs.config import Config +from zhenxun.configs.config import Config def rule(game) -> Rule: @@ -8,4 +8,3 @@ def rule(game) -> Rule: return Config.get_config("draw_card", game.config_name, True) return Rule(_rule) - diff --git a/plugins/draw_card/util.py b/zhenxun/plugins/draw_card/util.py similarity index 90% rename from plugins/draw_card/util.py rename to zhenxun/plugins/draw_card/util.py index 860e35eb..d0cefc91 100644 --- a/plugins/draw_card/util.py +++ b/zhenxun/plugins/draw_card/util.py @@ -1,60 +1,61 @@ -import platform -import pypinyin -from pathlib import Path -from PIL.ImageFont import FreeTypeFont -from PIL import Image, ImageDraw, ImageFont -from PIL.Image import Image as IMG - -from configs.path_config import FONT_PATH - -dir_path = Path(__file__).parent.absolute() - - -def cn2py(word) -> str: - """保存声调,防止出现类似方舟干员红与吽拼音相同声调不同导致红照片无法保存的问题""" - temp = "" - for i in pypinyin.pinyin(word, style=pypinyin.Style.TONE3): - temp += "".join(i) - return temp - - -# 移除windows和linux下特殊字符 -def remove_prohibited_str(name: str) -> str: - if platform.system().lower() == "windows": - tmp = "" - for i in name: - if i not in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]: - tmp += i - name = tmp - else: - name = name.replace("/", "\\") - return name - - -def load_font(fontname: str = "msyh.ttf", fontsize: int = 16) -> FreeTypeFont: - return ImageFont.truetype( - str(FONT_PATH / f"{fontname}"), fontsize, encoding="utf-8" - ) - - -def circled_number(num: int) -> IMG: - font = load_font(fontsize=450) - text = str(num) - text_w = font.getsize(text)[0] - w = 240 + text_w - w = w if w >= 500 else 500 - img = Image.new("RGBA", (w, 500)) - draw = ImageDraw.Draw(img) - draw.ellipse(((0, 0), (500, 500)), fill="red") - draw.ellipse(((w - 500, 0), (w, 500)), fill="red") - draw.rectangle(((250, 0), (w - 250, 500)), fill="red") - draw.text( - (120, -60), - text, - font=font, - fill="white", - stroke_width=10, - stroke_fill="white", - ) - return img - +import platform +from pathlib import Path + +import pypinyin +from PIL import Image, ImageDraw, ImageFont +from PIL.Image import Image as IMG +from PIL.ImageFont import FreeTypeFont + +from zhenxun.configs.path_config import FONT_PATH +from zhenxun.utils._build_image import BuildImage + +dir_path = Path(__file__).parent.absolute() + + +def cn2py(word) -> str: + """保存声调,防止出现类似方舟干员红与吽拼音相同声调不同导致红照片无法保存的问题""" + temp = "" + for i in pypinyin.pinyin(word, style=pypinyin.Style.TONE3): + temp += "".join(i) + return temp + + +# 移除windows和linux下特殊字符 +def remove_prohibited_str(name: str) -> str: + if platform.system().lower() == "windows": + tmp = "" + for i in name: + if i not in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]: + tmp += i + name = tmp + else: + name = name.replace("/", "\\") + return name + + +def load_font(fontname: str = "msyh.ttf", fontsize: int = 16) -> FreeTypeFont: + return ImageFont.truetype( + str(FONT_PATH / f"{fontname}"), fontsize, encoding="utf-8" + ) + + +def circled_number(num: int) -> IMG: + font = load_font(fontsize=450) + text = str(num) + text_w = BuildImage.get_text_size(text, font=font)[0] + w = 240 + text_w + w = w if w >= 500 else 500 + img = Image.new("RGBA", (w, 500)) + draw = ImageDraw.Draw(img) + draw.ellipse(((0, 0), (500, 500)), fill="red") + draw.ellipse(((w - 500, 0), (w, 500)), fill="red") + draw.rectangle(((250, 0), (w - 250, 500)), fill="red") + draw.text( + (120, -60), + text, + font=font, + fill="white", + stroke_width=10, + stroke_fill="white", + ) + return img diff --git a/zhenxun/plugins/epic/__init__.py b/zhenxun/plugins/epic/__init__.py new file mode 100644 index 00000000..23c084aa --- /dev/null +++ b/zhenxun/plugins/epic/__init__.py @@ -0,0 +1,40 @@ +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, UniMessage, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from .data_source import get_epic_free + +__plugin_meta__ = PluginMetadata( + name="epic免费游戏", + description="可以不玩,不能没有,每日白嫖", + usage=""" + epic + """.strip(), + extra=PluginExtraData( + author="AkashiCoin", + version="0.1", + ).dict(), +) + +_matcher = on_alconna(Alconna("epic"), priority=5, block=True) + + +@_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + type_ = "Group" if gid else "Private" + msg_list, code = await get_epic_free(bot, type_) + if code == 404 and isinstance(msg_list, str): + await MessageUtils.build_message(msg_list).finish() + elif isinstance(bot, (v11Bot, v12Bot)) and isinstance(msg_list, list): + await bot.send_group_forward_msg(group_id=gid, messages=msg_list) + elif isinstance(msg_list, UniMessage): + await msg_list.send() + logger.info(f"获取epic免费游戏", arparma.header_result, session=session) diff --git a/plugins/epic/data_source.py b/zhenxun/plugins/epic/data_source.py old mode 100755 new mode 100644 similarity index 66% rename from plugins/epic/data_source.py rename to zhenxun/plugins/epic/data_source.py index f9b5f4ea..87bc5a63 --- a/plugins/epic/data_source.py +++ b/zhenxun/plugins/epic/data_source.py @@ -1,196 +1,182 @@ -from datetime import datetime -from nonebot.log import logger -from nonebot.adapters.onebot.v11 import Bot -from configs.config import NICKNAME -from utils.http_utils import AsyncHttpx - - -# 获取所有 Epic Game Store 促销游戏 -# 方法参考:RSSHub /epicgames 路由 -# https://github.com/DIYgod/RSSHub/blob/master/lib/v2/epicgames/index.js -async def get_epic_game(): - epic_url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale=zh-CN&country=CN&allowCountries=CN" - headers = { - "Referer": "https://www.epicgames.com/store/zh-CN/", - "Content-Type": "application/json; charset=utf-8", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", - } - try: - res = await AsyncHttpx.get(epic_url, headers=headers, timeout=10) - res_json = res.json() - games = res_json["data"]["Catalog"]["searchStore"]["elements"] - return games - except Exception as e: - logger.error(f"Epic 访问接口错误 {type(e)}:{e}") - return None - -# 此处用于获取游戏简介 -async def get_epic_game_desp(name): - desp_url = "https://store-content-ipv4.ak.epicgames.com/api/zh-CN/content/products/" + str(name) - headers = { - "Referer": "https://store.epicgames.com/zh-CN/p/" + str(name), - "Content-Type": "application/json; charset=utf-8", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", - } - try: - res = await AsyncHttpx.get(desp_url, headers=headers, timeout=10) - res_json = res.json() - gamesDesp = res_json["pages"][0]["data"]["about"] - return gamesDesp - except Exception as e: - logger.error(f"Epic 访问接口错误 {type(e)}:{e}") - return None - -# 获取 Epic Game Store 免费游戏信息 -# 处理免费游戏的信息方法借鉴 pip 包 epicstore_api 示例 -# https://github.com/SD4RK/epicstore_api/blob/master/examples/free_games_example.py -async def get_epic_free(bot: Bot, type_event: str): - games = await get_epic_game() - if not games: - return "Epic 可能又抽风啦,请稍后再试(", 404 - else: - msg_list = [] - for game in games: - game_name = game["title"] - game_corp = game["seller"]["name"] - game_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - # 赋初值以避免 local variable referenced before assignment - game_thumbnail, game_dev, game_pub = None, game_corp, game_corp - try: - game_promotions = game["promotions"]["promotionalOffers"] - upcoming_promotions = game["promotions"]["upcomingPromotionalOffers"] - if not game_promotions and upcoming_promotions: - # 促销暂未上线,但即将上线 - promotion_data = upcoming_promotions[0]["promotionalOffers"][0] - start_date_iso, end_date_iso = ( - promotion_data["startDate"][:-1], - promotion_data["endDate"][:-1], - ) - # 删除字符串中最后一个 "Z" 使 Python datetime 可处理此时间 - start_date = datetime.fromisoformat(start_date_iso).strftime( - "%b.%d %H:%M" - ) - end_date = datetime.fromisoformat(end_date_iso).strftime( - "%b.%d %H:%M" - ) - if type_event == "Group": - _message = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( - game_corp, game_name, game_price, start_date, end_date - ) - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": _message, - }, - } - msg_list.append(data) - else: - msg = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( - game_corp, game_name, game_price, start_date, end_date - ) - msg_list.append(msg) - else: - for image in game["keyImages"]: - if ( - image.get("url") - and not game_thumbnail - and image["type"] - in [ - "Thumbnail", - "VaultOpened", - "DieselStoreFrontWide", - "OfferImageWide", - ] - ): - game_thumbnail = image["url"] - break - for pair in game["customAttributes"]: - if pair["key"] == "developerName": - game_dev = pair["value"] - if pair["key"] == "publisherName": - game_pub = pair["value"] - if game.get("productSlug"): - gamesDesp = await get_epic_game_desp(game["productSlug"]) - try: - #是否存在简短的介绍 - if "shortDescription" in gamesDesp: - game_desp = gamesDesp["shortDescription"] - except KeyError: - game_desp = gamesDesp["description"] - else: - game_desp = game["description"] - try: - end_date_iso = game["promotions"]["promotionalOffers"][0][ - "promotionalOffers" - ][0]["endDate"][:-1] - end_date = datetime.fromisoformat(end_date_iso).strftime( - "%b.%d %H:%M" - ) - except IndexError: - end_date = '未知' - # API 返回不包含游戏商店 URL,此处自行拼接,可能出现少数游戏 404 请反馈 - if game.get("productSlug"): - game_url = "https://store.epicgames.com/zh-CN/p/{}".format( - game["productSlug"].replace("/home", "") - ) - elif game.get("url"): - game_url = game["url"] - else: - slugs = ( - [ - x["pageSlug"] - for x in game.get("offerMappings", []) - if x.get("pageType") == "productHome" - ] - + [ - x["pageSlug"] - for x in game.get("catalogNs", {}).get("mappings", []) - if x.get("pageType") == "productHome" - ] - + [ - x["value"] - for x in game.get("customAttributes", []) - if "productSlug" in x.get("key") - ] - ) - game_url = "https://store.epicgames.com/zh-CN{}".format( - f"/p/{slugs[0]}" if len(slugs) else "" - ) - if type_event == "Group": - _message = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format( - game_thumbnail, - game_name, - game_price, - game_desp, - game_dev, - game_pub, - end_date, - game_url, - ) - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": _message, - }, - } - msg_list.append(data) - else: - msg = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format( - game_thumbnail, - game_name, - game_price, - game_desp, - game_dev, - game_pub, - end_date, - game_url, - ) - msg_list.append(msg) - except TypeError as e: - # logger.info(str(e)) - pass - return msg_list, 200 +from datetime import datetime + +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot_plugin_alconna import Image, UniMessage + +from zhenxun.configs.config import NICKNAME +from zhenxun.services.log import logger +from zhenxun.utils._build_image import BuildImage +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils + + +# 获取所有 Epic Game Store 促销游戏 +# 方法参考:RSSHub /epicgames 路由 +# https://github.com/DIYgod/RSSHub/blob/master/lib/v2/epicgames/index.js +async def get_epic_game() -> dict | None: + epic_url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale=zh-CN&country=CN&allowCountries=CN" + headers = { + "Referer": "https://www.epicgames.com/store/zh-CN/", + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", + } + try: + res = await AsyncHttpx.get(epic_url, headers=headers, timeout=10) + res_json = res.json() + games = res_json["data"]["Catalog"]["searchStore"]["elements"] + return games + except Exception as e: + logger.error(f"Epic 访问接口错误", e=e) + return None + + +# 此处用于获取游戏简介 +async def get_epic_game_desp(name) -> dict | None: + desp_url = ( + "https://store-content-ipv4.ak.epicgames.com/api/zh-CN/content/products/" + + str(name) + ) + headers = { + "Referer": "https://store.epicgames.com/zh-CN/p/" + str(name), + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", + } + try: + res = await AsyncHttpx.get(desp_url, headers=headers, timeout=10) + res_json = res.json() + gamesDesp = res_json["pages"][0]["data"]["about"] + return gamesDesp + except Exception as e: + logger.error(f"Epic 访问接口错误", e=e) + return None + + +# 获取 Epic Game Store 免费游戏信息 +# 处理免费游戏的信息方法借鉴 pip 包 epicstore_api 示例 +# https://github.com/SD4RK/epicstore_api/blob/master/examples/free_games_example.py +async def get_epic_free( + bot: Bot, type_event: str +) -> tuple[UniMessage | list | str, int]: + games = await get_epic_game() + if not games: + return "Epic 可能又抽风啦,请稍后再试(", 404 + else: + msg_list = [] + for game in games: + game_name = game["title"] + game_corp = game["seller"]["name"] + game_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] + # 赋初值以避免 local variable referenced before assignment + game_thumbnail, game_dev, game_pub = None, game_corp, game_corp + try: + game_promotions = game["promotions"]["promotionalOffers"] + upcoming_promotions = game["promotions"]["upcomingPromotionalOffers"] + if not game_promotions and upcoming_promotions: + # 促销暂未上线,但即将上线 + promotion_data = upcoming_promotions[0]["promotionalOffers"][0] + start_date_iso, end_date_iso = ( + promotion_data["startDate"][:-1], + promotion_data["endDate"][:-1], + ) + # 删除字符串中最后一个 "Z" 使 Python datetime 可处理此时间 + start_date = datetime.fromisoformat(start_date_iso).strftime( + "%b.%d %H:%M" + ) + end_date = datetime.fromisoformat(end_date_iso).strftime( + "%b.%d %H:%M" + ) + if type_event == "Group": + _message = f"\n由 {game_corp} 公司发行的游戏 {game_name} ({game_price}) 在 UTC 时间 {start_date} 即将推出免费游玩,预计截至 {end_date}。" + msg_list.append(_message) + else: + msg = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( + game_corp, game_name, game_price, start_date, end_date + ) + msg_list.append(msg) + else: + for image in game["keyImages"]: + if ( + image.get("url") + and not game_thumbnail + and image["type"] + in [ + "Thumbnail", + "VaultOpened", + "DieselStoreFrontWide", + "OfferImageWide", + ] + ): + game_thumbnail = image["url"] + break + for pair in game["customAttributes"]: + if pair["key"] == "developerName": + game_dev = pair["value"] + if pair["key"] == "publisherName": + game_pub = pair["value"] + if game.get("productSlug"): + if gamesDesp := await get_epic_game_desp(game["productSlug"]): + try: + # 是否存在简短的介绍 + if "shortDescription" in gamesDesp: + game_desp = gamesDesp["shortDescription"] + except KeyError: + game_desp = gamesDesp["description"] + else: + game_desp = game["description"] + try: + end_date_iso = game["promotions"]["promotionalOffers"][0][ + "promotionalOffers" + ][0]["endDate"][:-1] + end_date = datetime.fromisoformat(end_date_iso).strftime( + "%b.%d %H:%M" + ) + except IndexError: + end_date = "未知" + # API 返回不包含游戏商店 URL,此处自行拼接,可能出现少数游戏 404 请反馈 + if game.get("productSlug"): + game_url = "https://store.epicgames.com/zh-CN/p/{}".format( + game["productSlug"].replace("/home", "") + ) + elif game.get("url"): + game_url = game["url"] + else: + slugs = ( + [ + x["pageSlug"] + for x in game.get("offerMappings", []) + if x.get("pageType") == "productHome" + ] + + [ + x["pageSlug"] + for x in game.get("catalogNs", {}).get("mappings", []) + if x.get("pageType") == "productHome" + ] + + [ + x["value"] + for x in game.get("customAttributes", []) + if "productSlug" in x.get("key") + ] + ) + game_url = "https://store.epicgames.com/zh-CN{}".format( + f"/p/{slugs[0]}" if len(slugs) else "" + ) + if isinstance(bot, (v11Bot, v12Bot)) and type_event == "Group": + _message = [ + Image(url=game_thumbnail), + f"\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n", + ] + msg_list.append(_message) + else: + _message = [] + if game_thumbnail: + _message.append(Image(url=game_thumbnail)) + _message.append( + f"\n\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n" + ) + return MessageUtils.build_message(_message), 200 + except TypeError as e: + # logger.info(str(e)) + pass + return MessageUtils.template2forward(msg_list, bot.self_id), 200 diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py new file mode 100644 index 00000000..6b1348a0 --- /dev/null +++ b/zhenxun/plugins/fudu.py @@ -0,0 +1,151 @@ +import random + +from nonebot import on_message +from nonebot.adapters import Event +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Image as alcImg +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.models.task_info import TaskInfo +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import get_download_image_hash +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import ensure_group + +__plugin_meta__ = PluginMetadata( + name="复读", + description="群友的本质是什么?是复读机哒!", + usage=""" + usage: + 重复3次相同的消息时会复读 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + plugin_type=PluginType.HIDDEN, + tasks=[Task(module="fudu", name="复读")], + configs=[ + RegisterConfig( + key="FUDU_PROBABILITY", + value=0.7, + help="复读概率", + default_value=0.7, + type=float, + ), + RegisterConfig( + module="_task", + key="DEFAULT_FUDU", + value=True, + help="被动 复读 进群默认开关状态", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + + +class Fudu: + def __init__(self): + self.data = {} + + def append(self, key, content): + self._create(key) + self.data[key]["data"].append(content) + + def clear(self, key): + self._create(key) + self.data[key]["data"] = [] + self.data[key]["is_repeater"] = False + + def size(self, key) -> int: + self._create(key) + return len(self.data[key]["data"]) + + def check(self, key, content) -> bool: + self._create(key) + return self.data[key]["data"][0] == content + + def get(self, key): + self._create(key) + return self.data[key]["data"][0] + + def is_repeater(self, key): + self._create(key) + return self.data[key]["is_repeater"] + + def set_repeater(self, key): + self._create(key) + self.data[key]["is_repeater"] = True + + def _create(self, key): + if self.data.get(key) is None: + self.data[key] = {"is_repeater": False, "data": []} + + +_manage = Fudu() + + +base_config = Config.get("fudu") + + +_matcher = on_message(rule=ensure_group, priority=999) + + +@_matcher.handle() +async def _(message: UniMsg, event: Event, session: EventSession): + group_id = session.id2 or "" + if await TaskInfo.is_block("fudu", group_id): + return + if event.is_tome(): + return + plain_text = message.extract_plain_text() + image_list = [] + for m in message: + if isinstance(m, alcImg): + if m.url: + image_list.append(m.url) + if not plain_text and not image_list: + return + if plain_text and plain_text.startswith(f"@可爱的{NICKNAME}"): + await MessageUtils.build_message("复制粘贴的虚空艾特?").send(reply_to=True) + if image_list: + img_hash = await get_download_image_hash(image_list[0], group_id) + else: + img_hash = "" + add_msg = plain_text + "|-|" + img_hash + if _manage.size(group_id) == 0: + _manage.append(group_id, add_msg) + elif _manage.check(group_id, add_msg): + _manage.append(group_id, add_msg) + else: + _manage.clear(group_id) + _manage.append(group_id, add_msg) + if _manage.size(group_id) > 2: + if random.random() < base_config.get( + "FUDU_PROBABILITY" + ) and not _manage.is_repeater(group_id): + if random.random() < 0.2: + if plain_text.startswith("打断施法"): + await MessageUtils.build_message("打断" + plain_text).finish() + else: + await MessageUtils.build_message("打断施法!").finish() + _manage.set_repeater(group_id) + rst = None + if image_list and plain_text: + rst = MessageUtils.build_message( + [plain_text, TEMP_PATH / f"compare_download_{group_id}_img.jpg"] + ) + elif image_list: + rst = MessageUtils.build_message( + TEMP_PATH / f"compare_download_{group_id}_img.jpg" + ) + elif plain_text: + rst = MessageUtils.build_message(plain_text) + if rst: + await rst.finish() diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py new file mode 100644 index 00000000..f46c6639 --- /dev/null +++ b/zhenxun/plugins/gold_redbag/__init__.py @@ -0,0 +1,349 @@ +import time +import uuid +from datetime import datetime, timedelta + +from apscheduler.jobstores.base import JobLookupError +from nonebot.adapters import Bot +from nonebot.exception import ActionFailed +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Args, Arparma, At, Match, Option, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.depends import GetConfig, UserName +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.rules import ensure_group + +from .config import FESTIVE_KEY, FestiveRedBagManage +from .data_source import RedBagManager + +__plugin_meta__ = PluginMetadata( + name="金币红包", + description="运气项目又来了", + usage=""" + 塞红包 [金币数] ?[红包数=5] ?[at指定人]: 塞入红包 + 开/抢: 打开红包 + 退回红包: 退回未开完的红包,必须在一分钟后使用 + + * 不同群组同一个节日红包用户只能开一次 + + 示例: + 塞红包 1000 + 塞红包 1000 10 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + superuser_help=""" + 节日红包 [金额] [红包数] ?[指定主题文字] ? -g [群id] [群id] ... + + * 不同群组同一个节日红包用户只能开一次 + + 示例: + 节日红包 10000 20 今日出道贺金 + 节日红包 10000 20 明日出道贺金 -g 123123123 + + """, + configs=[ + RegisterConfig( + key="DEFAULT_TIMEOUT", + value=600, + help="普通红包默认超时时间", + default_value=600, + type=int, + ), + RegisterConfig( + key="DEFAULT_INTERVAL", + value=60, + help="用户发送普通红包最小间隔时间", + default_value=60, + type=int, + ), + RegisterConfig( + key="RANK_NUM", + value=10, + help="结算排行显示前N位", + default_value=10, + type=int, + ), + ], + limits=[PluginCdBlock(result="急什么急什么,待会再发!")], + ).dict(), +) + + +# def rule(session: EventSession) -> bool: +# if gid := session.id3 or session.id2: +# if group_red_bag := RedBagManager.get_group_data(gid): +# return group_red_bag.check_open(gid) +# return False + + +# async def rule_group(session: EventSession): +# return rule(session) and ensure_group(session) + + +_red_bag_matcher = on_alconna( + Alconna("塞红包", Args["amount", int]["num", int, 5]["user?", At]), + aliases={"金币红包"}, + priority=5, + block=True, + rule=ensure_group, +) + +_open_matcher = on_alconna( + Alconna("开"), + aliases={"抢", "开红包", "抢红包"}, + priority=5, + block=True, + rule=ensure_group, +) + +_return_matcher = on_alconna( + Alconna("退回红包"), aliases={"退还红包"}, priority=5, block=True, rule=ensure_group +) + +_festive_matcher = on_alconna( + Alconna( + "节日红包", + Args["amount", int]["num", int]["text?", str], + Option("-g|--group", Args["groups", str] / "\n", help_text="指定群"), + ), + priority=1, + block=True, + permission=SUPERUSER, + rule=to_me(), +) + + +@_red_bag_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + amount: int, + num: int, + user: Match[At], + default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), + user_name: str = UserName(), +): + at_user = None + if user.available: + at_user = user.result.target + # group_id = session.id3 or session.id2 + group_id = session.id2 + """以频道id为键""" + user_id = session.id1 + if not user_id: + await MessageUtils.build_message("用户id为空").finish() + if not group_id: + await MessageUtils.build_message("群组id为空").finish() + group_red_bag = RedBagManager.get_group_data(group_id) + # 剩余过期时间 + time_remaining = group_red_bag.check_timeout(user_id) + if time_remaining != -1: + # 判断用户红包是否存在且是否过时覆盖 + if user_red_bag := group_red_bag.get_user_red_bag(user_id): + now = time.time() + if now < user_red_bag.start_time + default_interval: + await MessageUtils.build_message( + f"你的红包还没消化完捏...还剩下 {user_red_bag.num - len(user_red_bag.open_user)} 个! 请等待红包领取完毕..." + f"(或等待{time_remaining}秒红包cd)" + ).finish() + result = await RedBagManager.check_gold(user_id, amount, session.platform) + if result: + await MessageUtils.build_message(result).finish(at_sender=True) + await group_red_bag.add_red_bag( + f"{user_name}的红包", + int(amount), + 1 if at_user else num, + user_name, + user_id, + assigner=at_user, + platform=session.platform, + ) + image = await RedBagManager.random_red_bag_background( + user_id, platform=session.platform + ) + message_list: list = [f"{user_name}发起了金币红包\n金额: {amount}\n数量: {num}\n"] + if at_user: + message_list.append("指定人: ") + message_list.append(At(flag="user", target=at_user)) + message_list.append("\n") + message_list.append(image) + await MessageUtils.build_message(message_list).send() + + logger.info( + f"塞入 {num} 个红包,共 {amount} 金币", arparma.header_result, session=session + ) + + +@_open_matcher.handle() +async def _( + session: EventSession, + rank_num: int = GetConfig(config="RANK_NUM"), +): + # group_id = session.id3 or session.id2 + group_id = session.id2 + """以频道id为键""" + user_id = session.id1 + if not user_id: + await MessageUtils.build_message("用户id为空").finish() + if not group_id: + await MessageUtils.build_message("群组id为空").finish() + if group_red_bag := RedBagManager.get_group_data(group_id): + open_data, settlement_list = await group_red_bag.open(user_id, session.platform) + # send_msg = Text("没有红包给你开!") + send_msg = [] + for _, item in open_data.items(): + amount, red_bag = item + result_image = await RedBagManager.build_open_result_image( + red_bag, user_id, amount, session.platform + ) + send_msg.append(f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n") + send_msg.append(result_image) + send_msg.append("\n") + logger.info( + f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", + "开红包", + session=session, + ) + send_msg = ( + MessageUtils.build_message(send_msg[:-1]) + if send_msg + else MessageUtils.build_message("没有红包给你开!") + ) + await send_msg.send(reply_to=True) + if settlement_list: + for red_bag in settlement_list: + result_image = await red_bag.build_amount_rank( + rank_num, session.platform + ) + await MessageUtils.build_message( + [f"{red_bag.name}已结算\n", result_image] + ).send() + + +@_return_matcher.handle() +async def _( + session: EventSession, + default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), + rank_num: int = GetConfig(config="RANK_NUM"), +): + group_id = session.id3 or session.id2 + user_id = session.id1 + if not user_id: + await MessageUtils.build_message("用户id为空").finish() + if not group_id: + await MessageUtils.build_message("群组id为空").finish() + if group_red_bag := RedBagManager.get_group_data(group_id): + if user_red_bag := group_red_bag.get_user_red_bag(user_id): + now = time.time() + if now - user_red_bag.start_time < default_interval: + await MessageUtils.build_message( + f"你的红包还没有过时, 在 {int(default_interval - now + user_red_bag.start_time)} " + f"秒后可以退回..." + ).finish(reply_to=True) + user_red_bag = group_red_bag.get_user_red_bag(user_id) + if user_red_bag and ( + data := await group_red_bag.settlement(user_id, session.platform) + ): + image_result = await user_red_bag.build_amount_rank( + rank_num, session.platform + ) + logger.info(f"退回了红包 {data[0]} 金币", "红包退回", session=session) + await MessageUtils.build_message( + [ + f"已成功退还了 " f"{data[0]} 金币\n", + image_result, + ] + ).finish(reply_to=True) + await MessageUtils.build_message("目前没有红包可以退回...").finish(reply_to=True) + + +@_festive_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + amount: int, + num: int, + text: Match[str], + groups: Match[str], +): + greetings = "恭喜发财 大吉大利" + if text.available: + greetings = text.result + gl = [] + if groups.available: + gl = groups.result.strip().split() + else: + g_l, platform = await PlatformUtils.get_group_list(bot) + gl = [g.channel_id or g.group_id for g in g_l] + _uuid = str(uuid.uuid1()) + FestiveRedBagManage.add(_uuid) + _suc_cnt = 0 + for g in gl: + if target := PlatformUtils.get_target(bot, group_id=g): + group_red_bag = RedBagManager.get_group_data(g) + if festive_red_bag := group_red_bag.get_festive_red_bag(): + group_red_bag.remove_festive_red_bag() + if festive_red_bag.uuid: + FestiveRedBagManage.remove(festive_red_bag.uuid) + rank_image = await festive_red_bag.build_amount_rank(10, platform) + try: + await MessageUtils.build_message( + [ + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(festive_red_bag.open_user)}" + f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n", + rank_image, + ] + ).send(target=target, bot=bot) + except ActionFailed: + pass + try: + scheduler.remove_job(f"{FESTIVE_KEY}_{g}") + await RedBagManager.end_red_bag( + g, is_festive=True, platform=session.platform + ) + except JobLookupError: + pass + await group_red_bag.add_red_bag( + f"{NICKNAME}的红包", + amount, + num, + NICKNAME, + FESTIVE_KEY, + _uuid, + platform=session.platform, + ) + scheduler.add_job( + RedBagManager._auto_end_festive_red_bag, + "date", + run_date=(datetime.now() + timedelta(hours=24)).replace(microsecond=0), + id=f"{FESTIVE_KEY}_{g}", + args=[bot, g, session.platform], + ) + try: + image_result = await RedBagManager.random_red_bag_background( + bot.self_id, greetings, session.platform + ) + await MessageUtils.build_message( + [ + f"{NICKNAME}发起了节日金币红包\n金额: {amount}\n数量: {num}\n", + image_result, + ] + ).send(target=target, bot=bot) + _suc_cnt += 1 + logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g) + except ActionFailed: + logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g) + if gl: + await MessageUtils.build_message( + f"节日红包发送成功,累计成功发送 {_suc_cnt} 个群组!" + ).send() diff --git a/plugins/gold_redbag/config.py b/zhenxun/plugins/gold_redbag/config.py similarity index 55% rename from plugins/gold_redbag/config.py rename to zhenxun/plugins/gold_redbag/config.py index a2fe4c55..da8c0d39 100644 --- a/plugins/gold_redbag/config.py +++ b/zhenxun/plugins/gold_redbag/config.py @@ -1,23 +1,48 @@ import random import time -from datetime import datetime from io import BytesIO -from typing import Dict, List, Optional, Tuple, Union, overload +from typing import Dict from pydantic import BaseModel -from models.bag_user import BagUser -from models.group_member_info import GroupInfoUser -from plugins.gold_redbag.model import RedbagUser -from utils.image_utils import BuildImage -from utils.utils import get_user_avatar +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.utils import get_user_avatar + +from .model import RedbagUser FESTIVE_KEY = "FESTIVE" """节日红包KEY""" -class RedBag(BaseModel): +class FestiveRedBagManage: + _data: Dict[str, list[str]] = {} + + @classmethod + def add(cls, uuid: str): + cls._data[uuid] = [] + + @classmethod + def open(cls, uuid: str, uid: str): + if uuid in cls._data and uid not in cls._data[uuid]: + cls._data[uuid].append(uid) + + @classmethod + def remove(cls, uuid: str): + if uuid in cls._data: + del cls._data[uuid] + + @classmethod + def check(cls, uuid: str, uid: str): + if uuid in cls._data: + return uid not in cls._data[uuid] + return False + + +class RedBag(BaseModel): """ 红包 """ @@ -38,19 +63,23 @@ class RedBag(BaseModel): """是否为节日红包""" timeout: int """过期时间""" - assigner: Optional[str] = None + assigner: str | None = None """指定人id""" start_time: float """红包发起时间""" open_user: Dict[str, int] = {} """开启用户""" - red_bag_list: List[int] + red_bag_list: list[int] + """红包金额列表""" + uuid: str | None + """uuid""" - async def build_amount_rank(self, num: int = 10) -> BuildImage: + async def build_amount_rank(self, num: int, platform: str) -> BuildImage: """生成结算红包图片 参数: num: 查看的排名数量. + platform: 平台. 返回: BuildImage: 结算红包图片 @@ -68,78 +97,100 @@ class RedBag(BaseModel): for i in range(num): user_background = BuildImage(600, 100, font_size=30) user_id, amount = sort_data[i] - user_ava_bytes = await get_user_avatar(user_id) + user_ava_bytes = await PlatformUtils.get_user_avatar(user_id, platform) user_ava = None if user_ava_bytes: user_ava = BuildImage(80, 80, background=BytesIO(user_ava_bytes)) else: user_ava = BuildImage(80, 80) - await user_ava.acircle_corner(10) - await user_background.apaste(user_ava, (130, 10), True) + await user_ava.circle_corner(10) + await user_background.paste(user_ava, (130, 10)) no_image = BuildImage(100, 100, font_size=65, font="CJGaoDeGuo.otf") - await no_image.atext((0, 0), f"{i+1}", center_type="center") - await no_image.aline((99, 10, 99, 90), "#b9b9b9") - await user_background.apaste(no_image) + await no_image.text((0, 0), f"{i+1}", center_type="center") + await no_image.line((99, 10, 99, 90), "#b9b9b9") + await user_background.paste(no_image) name = [ user.user_name for user in group_user_list if user_id == user.user_id ] - await user_background.atext((225, 15), name[0] if name else "") - amount_image = BuildImage( - 0, 0, plain_text=f"{amount} 元", font_size=30, font_color="#cdac72" + await user_background.text((225, 15), name[0] if name else "") + amount_image = await BuildImage.build_text_image( + f"{amount} 元", size=30, font_color="#cdac72" ) - await user_background.apaste( - amount_image, (user_background.w - amount_image.w - 20, 50), True + await user_background.paste( + amount_image, (user_background.width - amount_image.width - 20, 50) ) - await user_background.aline((225, 99, 590, 99), "#b9b9b9") + await user_background.line((225, 99, 590, 99), "#b9b9b9") user_image_list.append(user_background) background = BuildImage(600, 150 + len(user_image_list) * 100) top = BuildImage(600, 100, color="#f55545", font_size=30) - promoter_ava_bytes = await get_user_avatar(self.promoter_id) + promoter_ava_bytes = await PlatformUtils.get_user_avatar( + self.promoter_id, platform + ) promoter_ava = None if promoter_ava_bytes: promoter_ava = BuildImage(60, 60, background=BytesIO(promoter_ava_bytes)) else: promoter_ava = BuildImage(60, 60) - await promoter_ava.acircle() - await top.apaste(promoter_ava, (10, 0), True, "by_height") - await top.atext((80, 33), self.name, (255, 255, 255)) + await promoter_ava.circle() + await top.paste(promoter_ava, (10, 0), "height") + await top.text((80, 33), self.name, (255, 255, 255)) right_text = BuildImage(150, 100, color="#f55545", font_size=30) - await right_text.atext((10, 33), "结算排行", (255, 255, 255)) - await right_text.aline((4, 10, 4, 90), (255, 255, 255), 2) - await top.apaste(right_text, (460, 0)) - await background.apaste(top) + await right_text.text((10, 33), "结算排行", (255, 255, 255)) + await right_text.line((4, 10, 4, 90), (255, 255, 255), 2) + await top.paste(right_text, (460, 0)) + await background.paste(top) cur_h = 110 for user_image in user_image_list: - await background.apaste(user_image, (0, cur_h)) - cur_h += user_image.h + await background.paste(user_image, (0, cur_h)) + cur_h += user_image.height return background class GroupRedBag: - """ 群组红包管理 """ - def __init__(self, group_id: Union[int, str]): - self.group_id = str(group_id) + def __init__(self, group_id: str): + self.group_id = group_id self._data: Dict[str, RedBag] = {} """红包列表""" - def get_user_red_bag(self, user_id: Union[str, int]) -> Optional[RedBag]: + def remove_festive_red_bag(self): + """删除节日红包""" + _key = None + for k, red_bag in self._data.items(): + if red_bag.is_festival: + _key = k + break + if _key: + del self._data[_key] + + def get_festive_red_bag(self) -> RedBag | None: + """获取节日红包 + + 返回: + RedBag | None: 节日红包 + """ + for _, red_bag in self._data.items(): + if red_bag.is_festival: + return red_bag + return None + + def get_user_red_bag(self, user_id: str) -> RedBag | None: """获取用户塞红包数据 参数: user_id: 用户id 返回: - Optional[RedBag]: RedBag + RedBag | None: RedBag """ return self._data.get(str(user_id)) - def check_open(self, user_id: Union[str, int]) -> bool: + def check_open(self, user_id: str) -> bool: """检查是否有可开启的红包 参数: @@ -158,7 +209,7 @@ class GroupRedBag: return True return False - def check_timeout(self, user_id: Union[int, str]) -> int: + def check_timeout(self, user_id: str) -> int: """判断用户红包是否过期 参数: @@ -167,7 +218,6 @@ class GroupRedBag: 返回: int: 距离过期时间 """ - user_id = str(user_id) if user_id in self._data: reg_bag = self._data[user_id] now = time.time() @@ -176,22 +226,26 @@ class GroupRedBag: return -1 async def open( - self, user_id: Union[int, str] - ) -> Tuple[Dict[str, Tuple[int, RedBag]], List[RedBag]]: + self, user_id: str, platform: str | None = None + ) -> tuple[Dict[str, tuple[int, RedBag]], list[RedBag]]: """开启红包 参数: user_id: 用户id + platform: 所属平台 返回: - Dict[str, Tuple[int, RedBag]]: 键为发起者id, 值为开启金额以及对应RedBag - List[RedBag]: 开完的红包 + Dict[str, tuple[int, RedBag]]: 键为发起者id, 值为开启金额以及对应RedBag + list[RedBag]: 开完的红包 """ - user_id = str(user_id) open_data = {} - settlement_list: List[RedBag] = [] + settlement_list: list[RedBag] = [] for _, red_bag in self._data.items(): if red_bag.num > len(red_bag.open_user): + if red_bag.is_festival and red_bag.uuid: + if not FestiveRedBagManage.check(red_bag.uuid, user_id): + continue + FestiveRedBagManage.open(red_bag.uuid, user_id) is_open = False if red_bag.assigner: is_open = red_bag.assigner == user_id @@ -202,7 +256,9 @@ class GroupRedBag: await RedbagUser.add_redbag_data( user_id, self.group_id, "get", random_amount ) - await BagUser.add_gold(user_id, self.group_id, random_amount) + await UserConsole.add_gold( + user_id, random_amount, "gold_redbag", platform + ) red_bag.open_user[user_id] = random_amount open_data[red_bag.promoter_id] = (random_amount, red_bag) if red_bag.num == len(red_bag.open_user): @@ -214,11 +270,11 @@ class GroupRedBag: del self._data[uid] return open_data, settlement_list - def festive_red_bag_expire(self) -> Optional[RedBag]: + def festive_red_bag_expire(self) -> RedBag | None: """节日红包过期 返回: - Optional[RedBag]: 过期的节日红包 + RedBag | None: 过期的节日红包 """ if FESTIVE_KEY in self._data: red_bag = self._data[FESTIVE_KEY] @@ -227,26 +283,27 @@ class GroupRedBag: return None async def settlement( - self, user_id: Optional[Union[int, str]] = None - ) -> Optional[int]: + self, user_id: str, platform: str | None = None + ) -> tuple[int | None, RedBag | None]: """红包退回 参数: user_id: 用户id, 指定id时结算指定用户红包. + platform: 用户平台 返回: - int: 退回金币 + tuple[int | None, RedBag | None]: 退回金币, 红包 """ - user_id = str(user_id) - if user_id: - if red_bag := self._data.get(user_id): - del self._data[user_id] - if red_bag.red_bag_list: - # 退还剩余金币 - if amount := sum(red_bag.red_bag_list): - await BagUser.add_gold(user_id, self.group_id, amount) - return amount - return None + if red_bag := self._data.get(user_id): + del self._data[user_id] + if red_bag.is_festival and red_bag.uuid: + FestiveRedBagManage.remove(red_bag.uuid) + if red_bag.red_bag_list: + """退还剩余金币""" + if amount := sum(red_bag.red_bag_list): + await UserConsole.add_gold(user_id, amount, "gold_redbag", platform) + return amount, red_bag + return None, None async def add_red_bag( self, @@ -255,9 +312,10 @@ class GroupRedBag: num: int, promoter: str, promoter_id: str, - is_festival: bool = False, + festival_uuid: str | None = None, timeout: int = 60, - assigner: Optional[str] = None, + assigner: str | None = None, + platform: str | None = None, ): """添加红包 @@ -267,17 +325,19 @@ class GroupRedBag: num: 红包数量 promoter: 发起人昵称 promoter_id: 发起人id - is_festival: 是否为节日红包. + festival_uuid: 节日红包uuid. timeout: 超时时间. assigner: 指定人. + platform: 用户平台. """ - user_gold = await BagUser.get_gold(promoter_id, self.group_id) - if not is_festival and (amount < 1 or user_gold < amount): + user = await UserConsole.get_user(promoter_id, platform) + if not festival_uuid and (amount < 1 or user.gold < amount): raise ValueError("红包金币不足或用户金币不足") red_bag_list = self._random_red_bag(amount, num) - if not is_festival: - await BagUser.spend_gold(promoter_id, self.group_id, amount) + if not festival_uuid: + user.gold -= amount await RedbagUser.add_redbag_data(promoter_id, self.group_id, "send", amount) + await user.save(update_fields=["gold"]) self._data[promoter_id] = RedBag( group_id=self.group_id, name=name, @@ -285,14 +345,15 @@ class GroupRedBag: num=num, promoter=promoter, promoter_id=promoter_id, - is_festival=is_festival, + is_festival=bool(festival_uuid), timeout=timeout, start_time=time.time(), assigner=assigner, red_bag_list=red_bag_list, + uuid=festival_uuid, ) - def _random_red_bag(self, amount: int, num: int) -> List[int]: + def _random_red_bag(self, amount: int, num: int) -> list[int]: """初始化红包金币 参数: @@ -300,7 +361,7 @@ class GroupRedBag: num: 红包数量 返回: - List[int]: 红包列表 + list[int]: 红包列表 """ red_bag_list = [] for _ in range(num - 1): diff --git a/zhenxun/plugins/gold_redbag/data_source.py b/zhenxun/plugins/gold_redbag/data_source.py new file mode 100644 index 00000000..ec903100 --- /dev/null +++ b/zhenxun/plugins/gold_redbag/data_source.py @@ -0,0 +1,234 @@ +import asyncio +import os +import random +from io import BytesIO +from typing import Dict + +from nonebot.adapters import Bot +from nonebot.exception import ActionFailed +from nonebot_plugin_alconna import UniMessage + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +from .config import FestiveRedBagManage, GroupRedBag, RedBag + + +class RedBagManager: + + _data: Dict[str, GroupRedBag] = {} + + @classmethod + def get_group_data(cls, group_id: str) -> GroupRedBag: + """获取群组红包数据 + + 参数: + group_id: 群组id + + 返回: + GroupRedBag | None: GroupRedBag + """ + if group_id not in cls._data: + cls._data[group_id] = GroupRedBag(group_id) + return cls._data[group_id] + + @classmethod + async def _auto_end_festive_red_bag(cls, bot: Bot, group_id: str, platform: str): + """自动结算节日红包 + + 参数: + bot: Bot + group_id: 群组id + platform: 平台 + """ + if target := PlatformUtils.get_target(bot, group_id=group_id): + rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10 + group_red_bag = cls.get_group_data(group_id) + red_bag = group_red_bag.get_festive_red_bag() + if not red_bag: + return + rank_image = await red_bag.build_amount_rank(rank_num, platform) + if red_bag.is_festival and red_bag.uuid: + FestiveRedBagManage.remove(red_bag.uuid) + await asyncio.sleep(random.randint(1, 5)) + try: + await MessageUtils.build_message( + [ + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(red_bag.open_user)}" + f" 个红包,共 {sum(red_bag.open_user.values())} 金币\n", + rank_image, + ] + ).send(target=target, bot=bot) + except ActionFailed: + pass + + @classmethod + async def end_red_bag( + cls, + group_id: str, + user_id: str | None = None, + is_festive: bool = False, + platform: str = "", + ) -> UniMessage | None: + """结算红包 + + 参数: + group_id: 群组id或频道id + user_id: 用户id + is_festive: 是否节日红包 + platform: 用户平台 + """ + rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10 + group_red_bag = cls.get_group_data(group_id) + if not group_red_bag: + return None + if is_festive: + if festive_red_bag := group_red_bag.festive_red_bag_expire(): + rank_image = await festive_red_bag.build_amount_rank(rank_num, platform) + return MessageUtils.build_message( + [ + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(festive_red_bag.open_user)}" + f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n", + rank_image, + ] + ) + else: + if not user_id: + return None + return_gold, red_bag = await group_red_bag.settlement(user_id, platform) + if red_bag: + rank_image = await red_bag.build_amount_rank(rank_num, platform) + return MessageUtils.build_message( + [ + f"已成功退还了 " f"{return_gold} 金币\n", + rank_image.pic2bytes(), + ] + ) + + @classmethod + async def check_gold(cls, user_id: str, amount: int, platform: str) -> str | None: + """检查金币数量是否合法 + + 参数: + user_id: 用户id + amount: 金币数量 + platform: 所属平台 + + 返回: + tuple[bool, str]: 是否合法以及提示语 + """ + user = await UserConsole.get_user(user_id, platform) + if amount < 1: + return "小气鬼,要别人倒贴金币给你嘛!" + if user.gold < amount: + return "没有金币的话请不要发红包..." + return None + + @classmethod + async def random_red_bag_background( + cls, user_id: str, msg: str = "恭喜发财 大吉大利", platform: str = "" + ) -> BuildImage: + """构造发送红包图片 + + 参数: + user_id: 用户id + msg: 红包消息. + platform: 平台. + + 异常: + ValueError: 图片背景列表为空 + + 返回: + BuildImage: 构造后的图片 + """ + background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_2") + if not background_list: + raise ValueError("prts/redbag_1 背景图列表为空...") + random_redbag = random.choice(background_list) + redbag = BuildImage( + 0, + 0, + font_size=38, + background=IMAGE_PATH / "prts" / "redbag_2" / random_redbag, + ) + ava_byte = await PlatformUtils.get_user_avatar(user_id, platform) + ava = None + if ava_byte: + ava = BuildImage(65, 65, background=BytesIO(ava_byte)) + else: + ava = BuildImage(65, 65, color=(0, 0, 0)) + await ava.circle() + await redbag.text( + (int((redbag.size[0] - redbag.getsize(msg)[0]) / 2), 210), + msg, + (240, 218, 164), + ) + await redbag.paste(ava, (int((redbag.size[0] - ava.size[0]) / 2), 130)) + return redbag + + @classmethod + async def build_open_result_image( + cls, red_bag: RedBag, user_id: str, amount: int, platform: str + ) -> BuildImage: + """构造红包开启图片 + + 参数: + red_bag: RedBag + user_id: 开启红包用户id + amount: 开启红包获取的金额 + platform: 平台 + + 异常: + ValueError: 图片背景列表为空 + + 返回: + BuildImage: 构造后的图片 + """ + background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_1") + if not background_list: + raise ValueError("prts/redbag_1 背景图列表为空...") + random_redbag = random.choice(background_list) + head = BuildImage( + 1000, + 980, + font_size=30, + background=IMAGE_PATH / "prts" / "redbag_1" / random_redbag, + ) + size = BuildImage.get_text_size(red_bag.name, font_size=50) + ava_bk = BuildImage(100 + size[0], 66, (255, 255, 255, 0), font_size=50) + + ava_byte = await PlatformUtils.get_user_avatar(user_id, platform) + ava = None + if ava_byte: + ava = BuildImage(66, 66, background=BytesIO(ava_byte)) + else: + ava = BuildImage(66, 66, color=(0, 0, 0)) + await ava_bk.paste(ava) + await ava_bk.text((100, 7), red_bag.name) + ava_bk_w, ava_bk_h = ava_bk.size + await head.paste(ava_bk, (int((1000 - ava_bk_w) / 2), 300)) + size = BuildImage.get_text_size(str(amount), font_size=150) + amount_image = BuildImage(size[0], size[1], (255, 255, 255, 0), font_size=150) + await amount_image.text((0, 0), str(amount), fill=(209, 171, 108)) + # 金币中文 + await head.paste(amount_image, (int((1000 - size[0]) / 2) - 50, 460)) + await head.text( + (int((1000 - size[0]) / 2 + size[0]) - 50, 500 + size[1] - 70), + "金币", + fill=(209, 171, 108), + ) + # 剩余数量和金额 + text = ( + f"已领取" + f"{red_bag.num - len(red_bag.open_user)}" + f"/{red_bag.num}个," + f"共{sum(red_bag.open_user.values())}/{red_bag.amount}金币" + ) + await head.text((350, 900), text, (198, 198, 198)) + return head diff --git a/plugins/gold_redbag/model.py b/zhenxun/plugins/gold_redbag/model.py old mode 100755 new mode 100644 similarity index 86% rename from plugins/gold_redbag/model.py rename to zhenxun/plugins/gold_redbag/model.py index 76755b0d..a8e9359a --- a/plugins/gold_redbag/model.py +++ b/zhenxun/plugins/gold_redbag/model.py @@ -1,8 +1,6 @@ -from typing import List - from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class RedbagUser(Model): @@ -31,14 +29,13 @@ class RedbagUser(Model): async def add_redbag_data( cls, user_id: str, group_id: str, i_type: str, money: int ): - """ - 说明: - 添加收发红包数据 + """添加收发红包数据 + 参数: - :param user_id: 用户id - :param group_id: 群号 - :param i_type: 收或发 - :param money: 金钱数量 + user_id: 用户id + group_id: 群号 + i_type: 收或发 + money: 金钱数量 """ user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) diff --git a/zhenxun/plugins/group_welcome_msg.py b/zhenxun/plugins/group_welcome_msg.py new file mode 100644 index 00000000..7148e8e9 --- /dev/null +++ b/zhenxun/plugins/group_welcome_msg.py @@ -0,0 +1,62 @@ +import re + +import ujson as json +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import ensure_group + +__plugin_meta__ = PluginMetadata( + name="查看群欢迎消息", + description="查看群欢迎消息", + usage=""" + usage: + 查看群欢迎消息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + ).dict(), +) + +_matcher = on_alconna(Alconna("群欢迎消息"), rule=ensure_group, priority=5, block=True) + + +BASE_PATH = DATA_PATH / "welcome_message" + + +@_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, +): + path = BASE_PATH / f"{session.platform or session.bot_type}" / f"{session.id2}" + if session.id3: + path = ( + BASE_PATH + / f"{session.platform or session.bot_type}" + / f"{session.id3}" + / f"{session.id2}" + ) + file = path / "text.json" + if not file.exists(): + await MessageUtils.build_message("未设置群欢迎消息...").finish(reply_to=True) + message = json.load(open(file, encoding="utf8"))["message"] + message_split = re.split(r"\[image:\d+\]", message) + if len(message_split) == 1: + await MessageUtils.build_message(message_split[0]).finish(reply_to=True) + idx = 0 + data_list = [] + for msg in message_split[:-1]: + data_list.append(msg) + data_list.append(path / f"{idx}.png") + idx += 1 + data_list.append(message_split[-1]) + await MessageUtils.build_message(data_list).send(reply_to=True) + logger.info("查看群欢迎消息", arparma.header_result, session=session) diff --git a/plugins/image_management/__init__.py b/zhenxun/plugins/image_management/__init__.py old mode 100755 new mode 100644 similarity index 57% rename from plugins/image_management/__init__.py rename to zhenxun/plugins/image_management/__init__.py index 16a9738f..8f24386d --- a/plugins/image_management/__init__.py +++ b/zhenxun/plugins/image_management/__init__.py @@ -3,15 +3,14 @@ from typing import List, Tuple import nonebot -from configs.config import Config -from configs.path_config import IMAGE_PATH +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH Config.add_plugin_config( "image_management", "IMAGE_DIR_LIST", ["美图", "萝莉", "壁纸"], - name="图库操作", - help_="公开图库列表,可自定义添加 [如果含有send_setu插件,请不要添加色图库]", + help="公开图库列表,可自定义添加 [如果含有send_setu插件,请不要添加色图库]", default_value=[], type=List[str], ) @@ -20,35 +19,34 @@ Config.add_plugin_config( "image_management", "WITHDRAW_IMAGE_MESSAGE", (0, 1), - name="图库操作", - help_="自动撤回,参1:延迟撤回发送图库图片的时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + help="自动撤回,参1:延迟撤回发送图库图片的时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", default_value=(0, 1), type=Tuple[int, int], ) Config.add_plugin_config( "image_management:delete_image", - "DELETE_IMAGE_LEVEL [LEVEL]", + "DELETE_IMAGE_LEVEL", 7, - help_="删除图库图片需要的管理员等级", + help="删除图库图片需要的管理员等级", default_value=7, type=int, ) Config.add_plugin_config( "image_management:move_image", - "MOVE_IMAGE_LEVEL [LEVEL]", + "MOVE_IMAGE_LEVEL", 7, - help_="移动图库图片需要的管理员等级", + help="移动图库图片需要的管理员等级", default_value=7, type=int, ) Config.add_plugin_config( "image_management:upload_image", - "UPLOAD_IMAGE_LEVEL [LEVEL]", + "UPLOAD_IMAGE_LEVEL", 6, - help_="上传图库图片需要的管理员等级", + help="上传图库图片需要的管理员等级", default_value=6, type=int, ) @@ -57,11 +55,13 @@ Config.add_plugin_config( "image_management", "SHOW_ID", True, - help_="是否消息显示图片下标id", + help="是否消息显示图片下标id", default_value=True, type=bool, ) +Config.set_name("image_management", "图库操作") + (IMAGE_PATH / "image_management").mkdir(parents=True, exist_ok=True) diff --git a/zhenxun/plugins/image_management/_config.py b/zhenxun/plugins/image_management/_config.py new file mode 100644 index 00000000..d5e01f58 --- /dev/null +++ b/zhenxun/plugins/image_management/_config.py @@ -0,0 +1,14 @@ +from strenum import StrEnum + + +class ImageHandleType(StrEnum): + """ + 图片处理类型 + """ + + UPLOAD = "UPLOAD" + """上传""" + DELETE = "DELETE" + """删除""" + MOVE = "MOVE" + """移动""" diff --git a/zhenxun/plugins/image_management/_data_source.py b/zhenxun/plugins/image_management/_data_source.py new file mode 100644 index 00000000..bc26b74f --- /dev/null +++ b/zhenxun/plugins/image_management/_data_source.py @@ -0,0 +1,196 @@ +import os +import random +from pathlib import Path + +import aiofiles + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.utils import cn2py + +from .image_management_log import ImageHandleType, ImageManagementLog + +BASE_PATH = IMAGE_PATH / "image_management" + + +class ImageManagementManage: + + @classmethod + async def random_image(cls, name: str, file_id: int | None = None) -> Path | None: + """随机图片 + + 参数: + name: 图库名称 + file_id: 图片id. + + 返回: + Path | None: 图片路径 + """ + path = BASE_PATH / name + file_name = f"{file_id}.jpg" + if file_id is None: + if file_list := os.listdir(path): + file_name = random.choice(file_list) + _file = path / file_name + if not _file.exists(): + return None + return _file + + @classmethod + async def upload_image( + cls, + image_data: bytes | str, + name: str, + user_id: str, + platform: str | None = None, + ) -> str | None: + """上传图片 + + 参数: + image_data: 图片bytes + name: 图库名称 + user_id: 用户id + platform: 所属平台 + + 返回: + str | None: 文件名称 + """ + path = BASE_PATH / cn2py(name) + path.mkdir(exist_ok=True, parents=True) + _file_name = 0 + if file_list := os.listdir(path): + file_list.sort() + _file_name = int(file_list[-1].split(".")[0]) + 1 + _file_path = path / f"{_file_name}.jpg" + try: + await ImageManagementLog.create( + user_id=user_id, + path=_file_path, + handle_type=ImageHandleType.UPLOAD, + platform=platform, + ) + if isinstance(image_data, str): + await AsyncHttpx.download_file(image_data, _file_path) + else: + async with aiofiles.open(_file_path, "wb") as f: + await f.write(image_data) + logger.info( + f"上传图片至 {name}, 路径: {_file_path}", + "上传图片", + session=user_id, + ) + return f"{_file_name}.jpg" + except Exception as e: + logger.error("上传图片错误", "上传图片", e=e) + return None + + @classmethod + async def delete_image( + cls, name: str, file_id: int, user_id: str, platform: str | None = None + ) -> bool: + """删除图片 + + 参数: + name: 图库名称 + file_id: 图片id + user_id: 用户id + platform: 所属平台. + + 返回: + bool: 是否删除成功 + """ + path = BASE_PATH / cn2py(name) + if not path.exists(): + return False + _file_path = path / f"{file_id}.jpg" + if not _file_path.exists(): + return False + try: + await ImageManagementLog.create( + user_id=user_id, + path=_file_path, + handle_type=ImageHandleType.DELETE, + platform=platform, + ) + _file_path.unlink() + logger.info( + f"图库: {name}, 删除图片路径: {_file_path}", "删除图片", session=user_id + ) + if file_list := os.listdir(path): + file_list.sort() + _file_name = file_list[-1].split(".")[0] + _move_file = path / f"{_file_name}.jpg" + _move_file.rename(_file_path) + logger.info( + f"图库: {name}, 移动图片名称: {_file_name}.jpg -> {file_id}.jpg", + "删除图片", + session=user_id, + ) + except Exception as e: + logger.error("删除图片错误", "删除图片", e=e) + return False + return True + + @classmethod + async def move_image( + cls, + a_name: str, + b_name: str, + file_id: int, + user_id: str, + platform: str | None = None, + ) -> str | None: + """移动图片 + + 参数: + a_name: 源图库 + b_name: 模板图库 + file_id: 图片id + user_id: 用户id + platform: 所属平台. + + 返回: + bool: 是否移动成功 + """ + source_path = BASE_PATH / cn2py(a_name) + if not source_path.exists(): + return None + destination_path = BASE_PATH / cn2py(b_name) + destination_path.mkdir(exist_ok=True, parents=True) + source_file = source_path / f"{file_id}.jpg" + if not source_file.exists(): + return None + _destination_name = 0 + if file_list := os.listdir(destination_path): + file_list.sort() + _destination_name = int(file_list[-1].split(".")[0]) + 1 + destination_file = destination_path / f"{_destination_name}.jpg" + try: + await ImageManagementLog.create( + user_id=user_id, + path=source_file, + move=destination_file, + handle_type=ImageHandleType.MOVE, + platform=platform, + ) + source_file.rename(destination_file) + logger.info( + f"图库: {a_name} -> {b_name}, 移动图片路径: {source_file} -> {destination_file}", + "移动图片", + session=user_id, + ) + if file_list := os.listdir(source_path): + file_list.sort() + _file_name = file_list[-1].split(".")[0] + _move_file = source_path / f"{_file_name}.jpg" + _move_file.rename(source_file) + logger.info( + f"图库: {a_name}, 移动图片名称: {_file_name}.jpg -> {file_id}.jpg", + "移动图片", + session=user_id, + ) + except Exception as e: + logger.error("移动图片错误", "移动图片", e=e) + return None + return f"{source_file} -> {destination_file}" diff --git a/zhenxun/plugins/image_management/delete_image.py b/zhenxun/plugins/image_management/delete_image.py new file mode 100644 index 00000000..6cabb8e9 --- /dev/null +++ b/zhenxun/plugins/image_management/delete_image.py @@ -0,0 +1,107 @@ +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.typing import T_State +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from ._data_source import ImageManagementManage + +base_config = Config.get("image_management") + +__plugin_meta__ = PluginMetadata( + name="删除图片", + description="不好看的图片删掉删掉!", + usage=""" + 指令: + 删除图片 [图库] [id] + 查看图库 + 示例:删除图片 美图 666 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("DELETE_IMAGE_LEVEL"), + ).dict(), +) + + +_matcher = on_alconna( + Alconna("删除图片", Args["name?", str]["index?", str]), + rule=to_me(), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _( + name: Match[str], + index: Match[str], + state: T_State, +): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await MessageUtils.build_message("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if name.available: + _matcher.set_path_arg("name", name.result) + if index.available: + _matcher.set_path_arg("index", index.result) + + +@_matcher.got_path( + "name", + prompt=UniMessage.template( + "请输入要删除的目标图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(name: str): + if name in ["取消", "算了"]: + await MessageUtils.build_message("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if name.isdigit(): + index = int(name) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _matcher.reject_path("name", "此目录不正确,请重新输入目录!") + _matcher.set_path_arg("name", name) + + +@_matcher.got_path("index", "请输入要删除的图片id?【发送'取消', '算了'来取消操作】") +async def _( + session: EventSession, + arparma: Arparma, + index: str, +): + if index in ["取消", "算了"]: + await MessageUtils.build_message("已取消操作...").finish() + if not index.isdigit(): + await _matcher.reject_path("index", "图片id需要输入数字...") + name = _matcher.get_path_arg("name", None) + if not name: + await MessageUtils.build_message("图库名称为空...").finish() + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if file_name := await ImageManagementManage.delete_image( + name, int(index), session.id1, session.platform + ): + logger.info( + f"删除图片成功 图库: {name} --- 名称: {file_name}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message( + f"删除图片成功!\n图库: {name}\n名称: {index}.jpg" + ).finish() + await MessageUtils.build_message("图片删除失败...").finish() diff --git a/zhenxun/plugins/image_management/image_management_log.py b/zhenxun/plugins/image_management/image_management_log.py new file mode 100644 index 00000000..756e58f2 --- /dev/null +++ b/zhenxun/plugins/image_management/image_management_log.py @@ -0,0 +1,27 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + +from ._config import ImageHandleType + + +class ImageManagementLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, description="用户id") + """用户id""" + path = fields.TextField(description="图片路径") + """图片路径""" + move = fields.TextField(null=True, description="移动路径") + """移动路径""" + handle_type = fields.CharEnumField(ImageHandleType, description="操作类型") + """操作类型""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "image_management_log" + table_description = "画廊操作记录" diff --git a/zhenxun/plugins/image_management/move_image.py b/zhenxun/plugins/image_management/move_image.py new file mode 100644 index 00000000..6c1ec5e9 --- /dev/null +++ b/zhenxun/plugins/image_management/move_image.py @@ -0,0 +1,137 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.typing import T_State +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from ._data_source import ImageManagementManage + +base_config = Config.get("image_management") + +__plugin_meta__ = PluginMetadata( + name="移动图片", + description="图库间的图片移动操作", + usage=""" + 指令: + 移动图片 [源图库] [目标图库] [id] + 查看图库 + 示例:移动图片 萝莉 美图 234 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("MOVE_IMAGE_LEVEL"), + ).dict(), +) + + +_matcher = on_alconna( + Alconna("移动图片", Args["source?", str]["destination?", str]["index?", str]), + rule=to_me(), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + source: Match[str], + destination: Match[str], + index: Match[str], + state: T_State, +): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await MessageUtils.build_message("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if source.available: + _matcher.set_path_arg("source", source.result) + if destination.available: + _matcher.set_path_arg("destination", destination.result) + if index.available: + _matcher.set_path_arg("index", index.result) + + +@_matcher.got_path( + "source", + prompt=UniMessage.template( + "要从哪个图库移出?【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(source: str): + if source in ["取消", "算了"]: + await MessageUtils.build_message("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if source.isdigit(): + index = int(source) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _matcher.reject_path("source", "此目录不正确,请重新输入目录!") + _matcher.set_path_arg("source", name) + + +@_matcher.got_path( + "destination", + prompt=UniMessage.template( + "要移动到哪个图库?【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(destination: str): + if destination in ["取消", "算了"]: + await MessageUtils.build_message("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + name = None + if destination.isdigit(): + index = int(destination) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _matcher.reject_path("destination", "此目录不正确,请重新输入目录!") + _matcher.set_path_arg("destination", name) + + +@_matcher.got_path("index", "要移动的图片id是?【发送'取消', '算了'来取消操作】") +async def _( + session: EventSession, + arparma: Arparma, + index: str, +): + if index in ["取消", "算了"]: + await MessageUtils.build_message("已取消操作...").finish() + if not index.isdigit(): + await _matcher.reject_path("index", "图片id需要输入数字...") + source = _matcher.get_path_arg("source", None) + destination = _matcher.get_path_arg("destination", None) + if not source: + await MessageUtils.build_message("转出图库名称为空...").finish() + if not destination: + await MessageUtils.build_message("转入图库名称为空...").finish() + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if file_name := await ImageManagementManage.move_image( + source, destination, int(index), session.id1, session.platform + ): + logger.info( + f"移动图片成功 图库: {source} -> {destination} --- 名称: {file_name}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message( + f"移动图片成功!\n图库: {source} -> {destination}" + ).finish() + await MessageUtils.build_message("图片删除失败...").finish() diff --git a/plugins/__init__.py b/zhenxun/plugins/image_management/send_image.py old mode 100755 new mode 100644 similarity index 100% rename from plugins/__init__.py rename to zhenxun/plugins/image_management/send_image.py diff --git a/zhenxun/plugins/image_management/upload_image.py b/zhenxun/plugins/image_management/upload_image.py new file mode 100644 index 00000000..b69281a4 --- /dev/null +++ b/zhenxun/plugins/image_management/upload_image.py @@ -0,0 +1,195 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.typing import T_State +from nonebot_plugin_alconna import Alconna, Args, Arparma +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Match, UniMessage, UniMsg, image_fetch, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils + +from ._data_source import ImageManagementManage + +base_config = Config.get("image_management") + +__plugin_meta__ = PluginMetadata( + name="上传图片", + description="上传图片至指定图库", + usage=""" + 指令: + 查看图库 + 上传图片 [图库] [图片] + 示例:上传图片 美图 [图片] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("UPLOAD_IMAGE_LEVEL"), + ).dict(), +) + + +_upload_matcher = on_alconna( + Alconna("上传图片", Args["name?", str]["img?", alcImage]), + rule=to_me(), + priority=5, + block=True, +) + +_continuous_upload_matcher = on_alconna( + Alconna("连续上传图片", Args["name?", str]), + rule=to_me(), + priority=5, + block=True, +) + +_show_matcher = on_alconna(Alconna("查看公开图库"), priority=1, block=True) + + +@_show_matcher.handle() +async def _(): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await MessageUtils.build_message("未发现任何图库").finish() + text = "公开图库列表:\n" + for i, e in enumerate(image_dir_list): + text += f"\t{i+1}.{e}\n" + await MessageUtils.build_message(text[:-1]).send() + + +@_upload_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + name: Match[str], + img: Match[bytes], + state: T_State, +): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await MessageUtils.build_message("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if name.available: + _upload_matcher.set_path_arg("name", name.result) + if img.available: + result = await AsyncHttpx.get(img.result.url) # type: ignore + _upload_matcher.set_path_arg("img", result.content) + + +@_continuous_upload_matcher.handle() +async def _(bot: Bot, state: T_State, name: Match[str]): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await MessageUtils.build_message("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if name.available: + _upload_matcher.set_path_arg("name", name.result) + + +@_continuous_upload_matcher.got_path( + "name", + prompt=UniMessage.template( + "请选择要上传的图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +@_upload_matcher.got_path( + "name", + prompt=UniMessage.template( + "请选择要上传的图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(name: str, state: T_State): + if name in ["取消", "算了"]: + await MessageUtils.build_message("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if name.isdigit(): + index = int(name) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _upload_matcher.reject_path("name", "此目录不正确,请重新输入目录!") + _upload_matcher.set_path_arg("name", name) + + +@_upload_matcher.got_path("img", "图呢图呢图呢图呢!GKD!", image_fetch) +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + img: bytes, +): + name = _upload_matcher.get_path_arg("name", None) + if not name: + await MessageUtils.build_message("图库名称为空...").finish() + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if file_name := await ImageManagementManage.upload_image( + img, name, session.id1, session.platform + ): + logger.info( + f"图库: {name} --- 名称: {file_name}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message( + f"上传图片成功!\n图库: {name}\n名称: {file_name}" + ).finish() + await MessageUtils.build_message("图片上传失败...").finish() + + +@_continuous_upload_matcher.got( + "img", "图呢图呢图呢图呢!GKD!【在最后一张图片中+‘stop’为停止】" +) +async def _( + bot: Bot, + arparma: Arparma, + session: EventSession, + state: T_State, + message: UniMsg, +): + name = _continuous_upload_matcher.get_path_arg("name", None) + if not name: + await MessageUtils.build_message("图库名称为空...").finish() + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not state.get("img_list"): + state["img_list"] = [] + msg = message.extract_plain_text().strip().replace(arparma.header_result, "", 1) + if msg in ["取消", "算了"]: + await MessageUtils.build_message("已取消操作...").finish() + if msg != "stop": + for msg in message: + if isinstance(msg, alcImage): + state["img_list"].append(msg.url) + await _continuous_upload_matcher.reject("图再来!!【发送‘stop’为停止】") + if state["img_list"]: + await MessageUtils.build_message("正在下载, 请稍后...").send() + file_list = [] + for img in state["img_list"]: + if file_name := await ImageManagementManage.upload_image( + img, name, session.id1, session.platform + ): + file_list.append(img) + logger.info( + f"图库: {name} --- 名称: {file_name}", + "上传图片", + session=session, + ) + await MessageUtils.build_message( + f"上传图片成功!共上传了{len(file_list)}张图片\n图库: {name}\n名称: {', '.join(file_list)}" + ).finish() + await MessageUtils.build_message("图片上传失败...").finish() diff --git a/zhenxun/plugins/luxun.py b/zhenxun/plugins/luxun.py new file mode 100644 index 00000000..1c1ab09c --- /dev/null +++ b/zhenxun/plugins/luxun.py @@ -0,0 +1,74 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import BaseBlock, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="鲁迅说", + description="鲁迅说了啥?", + usage=""" + 鲁迅说 [文本] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + limits=[BaseBlock(result="你的鲁迅正在说,等会")], + ).dict(), +) + +_matcher = on_alconna( + Alconna("luxun", Args["content", str]), + priority=5, + block=True, +) + +_matcher.shortcut( + "鲁迅说", + command="luxun", + arguments=["{%0}"], + prefix=True, +) + + +_sign = None + + +@_matcher.handle() +async def _(content: Match[str]): + if content.available: + _matcher.set_path_arg("content", content.result) + + +@_matcher.got_path("content", prompt="你让鲁迅说点啥?") +async def _(content: str, session: EventSession, arparma: Arparma): + global _sign + if content.startswith(",") or content.startswith(","): + content = content[1:] + A = BuildImage( + font_size=37, background=f"{IMAGE_PATH}/other/luxun.jpg", font="msyh.ttf" + ) + text = "" + if len(content) > 40: + await MessageUtils.build_message("太长了,鲁迅说不完...").finish() + while A.getsize(content)[0] > A.width - 50: + n = int(len(content) / 2) + text += content[:n] + "\n" + content = content[n:] + text += content + if len(text.split("\n")) > 2: + await MessageUtils.build_message("太长了,鲁迅说不完...").finish() + await A.text( + (int((480 - A.getsize(text.split("\n")[0])[0]) / 2), 300), text, (255, 255, 255) + ) + if not _sign: + _sign = await BuildImage.build_text_image( + "--鲁迅", "msyh.ttf", 30, (255, 255, 255) + ) + await A.paste(_sign, (320, 400)) + await MessageUtils.build_message(A).send() + logger.info(f"鲁迅说: {content}", arparma.header_result, session=session) diff --git a/zhenxun/plugins/mute/__init__.py b/zhenxun/plugins/mute/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/plugins/mute/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/mute/_data_source.py b/zhenxun/plugins/mute/_data_source.py new file mode 100644 index 00000000..206de400 --- /dev/null +++ b/zhenxun/plugins/mute/_data_source.py @@ -0,0 +1,124 @@ +import time + +import ujson as json +from pydantic import BaseModel + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH + +base_config = Config.get("mute_setting") + + +class GroupData(BaseModel): + + count: int + """次数""" + time: int + """检测时长""" + duration: int + """禁言时长""" + message_data: dict = {} + """消息存储""" + + +class MuteManage: + + file = DATA_PATH / "group_mute_data.json" + + def __init__(self) -> None: + self._group_data: dict[str, GroupData] = {} + if self.file.exists(): + _data = json.load(open(self.file)) + for gid in _data: + self._group_data[gid] = GroupData( + count=_data[gid]["count"], + time=_data[gid]["time"], + duration=_data[gid]["duration"], + ) + + def get_group_data(self, group_id: str) -> GroupData: + """获取群组数据 + + 参数: + group_id: 群组id + + 返回: + GroupData: GroupData + """ + if group_id not in self._group_data: + self._group_data[group_id] = GroupData( + count=base_config.get("MUTE_DEFAULT_COUNT"), + time=base_config.get("MUTE_DEFAULT_TIME"), + duration=base_config.get("MUTE_DEFAULT_DURATION"), + ) + return self._group_data[group_id] + + def reset(self, user_id: str, group_id: str): + """重置用户检查次数 + + 参数: + user_id: 用户id + group_id: 群组id + """ + if group_data := self._group_data.get(group_id): + if user_id in group_data.message_data: + group_data.message_data[user_id]["count"] = 0 + + def save_data(self): + """保存数据""" + data = {} + for gid in self._group_data: + data[gid] = { + "count": self._group_data[gid].count, + "time": self._group_data[gid].time, + "duration": self._group_data[gid].duration, + } + with open(self.file, "w") as f: + json.dump(data, f, indent=4, ensure_ascii=False) + + def add_message(self, user_id: str, group_id: str, message: str) -> int: + """添加消息 + + 参数: + user_id: 用户id + group_id: 群组id + message: 消息内容 + + 返回: + int: 禁言时长 + """ + if group_id not in self._group_data: + self._group_data[group_id] = GroupData( + count=base_config.get("MUTE_DEFAULT_COUNT"), + time=base_config.get("MUTE_DEFAULT_TIME"), + duration=base_config.get("MUTE_DEFAULT_DURATION"), + ) + group_data = self._group_data[group_id] + if group_data.duration == 0: + return 0 + message_data = group_data.message_data + if not message_data.get(user_id): + message_data[user_id] = { + "time": time.time(), + "count": 1, + "message": message, + } + else: + if message.find(message_data[user_id]["message"]) != -1: + message_data[user_id]["count"] += 1 + else: + message_data[user_id]["time"] = time.time() + message_data[user_id]["count"] = 1 + message_data[user_id]["message"] = message + if time.time() - message_data[user_id]["time"] > group_data.time: + message_data[user_id]["time"] = time.time() + message_data[user_id]["count"] = 1 + if ( + message_data[user_id]["count"] > group_data.count + and time.time() - message_data[user_id]["time"] < group_data.time + ): + return group_data.duration + return 0 + + +mute_manage = MuteManage() diff --git a/zhenxun/plugins/mute/mute_message.py b/zhenxun/plugins/mute/mute_message.py new file mode 100644 index 00000000..c2c476f0 --- /dev/null +++ b/zhenxun/plugins/mute/mute_message.py @@ -0,0 +1,53 @@ +from nonebot import on_message +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import get_download_image_hash +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +from ._data_source import mute_manage + +__plugin_meta__ = PluginMetadata( + name="刷屏监听", + description="", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + plugin_type=PluginType.HIDDEN, + ).dict(), +) + +_matcher = on_message(priority=1, block=False) + + +@_matcher.handle() +async def _(bot: Bot, session: EventSession, message: UniMsg): + group_id = session.id2 + if not session.id1 or not group_id: + return + plain_text = message.extract_plain_text() + image_list = [m.url for m in message if isinstance(m, alcImage) and m.url] + img_hash = "" + for url in image_list: + img_hash += await get_download_image_hash(url, "_mute_") + _message = plain_text + img_hash + if duration := mute_manage.add_message(session.id1, group_id, _message): + try: + await PlatformUtils.ban_user(bot, session.id1, group_id, duration) + await MessageUtils.build_message( + f"检测到恶意刷屏,{NICKNAME}要把你关进小黑屋!" + ).send(at_sender=True) + mute_manage.reset(session.id1, group_id) + logger.info(f"检测刷屏 被禁言 {duration} 分钟", "禁言检查", session=session) + except Exception as e: + logger.error("禁言发送错误", "禁言检测", session=session, e=e) diff --git a/zhenxun/plugins/mute/mute_setting.py b/zhenxun/plugins/mute/mute_setting.py new file mode 100644 index 00000000..f723f075 --- /dev/null +++ b/zhenxun/plugins/mute/mute_setting.py @@ -0,0 +1,117 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.rules import ensure_group + +from ._data_source import base_config, mute_manage + +__plugin_meta__ = PluginMetadata( + name="刷屏禁言", + description="刷屏禁言相关操作", + usage=""" + 刷屏禁言相关操作,需要 {NICKNAME} 有群管理员权限 + 指令: + 设置刷屏: 查看当前设置 + -c [count]: 检测最大次数 + -t [time]: 规定时间内 + -d [duration]: 禁言时长 + 示例: + 设置刷屏 -c 10: 设置最大次数为10 + 设置刷屏 -t 100 -d 20: 设置规定时间和禁言时长 + 设置刷屏 -d 10: 设置禁言时长为10 + * 即 X 秒内发送同样消息 N 次,禁言 M 分钟 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("MUTE_LEVEL", 5), + configs=[ + RegisterConfig( + key="MUTE_LEVEL", + value=5, + help="更改禁言设置的管理权限", + default_value=5, + type=int, + ), + RegisterConfig( + key="MUTE_DEFAULT_COUNT", + value=10, + help="刷屏禁言默认检测次数", + default_value=10, + type=int, + ), + RegisterConfig( + key="MUTE_DEFAULT_TIME", + value=7, + help="刷屏检测默认规定时间", + default_value=7, + type=int, + ), + RegisterConfig( + key="MUTE_DEFAULT_DURATION", + value=10, + help="刷屏检测默禁言时长(分钟)", + default_value=10, + type=int, + ), + ], + ).dict(), +) + + +_setting_matcher = on_alconna( + Alconna( + "刷屏设置", + Option("-t|--time", Args["time", int], help_text="检测时长"), + Option("-c|--count", Args["count", int], help_text="检测次数"), + Option("-d|--duration", Args["duration", int], help_text="禁言时长"), + ), + rule=ensure_group, + block=True, + priority=5, +) + + +@_setting_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + time: Match[int], + count: Match[int], + duration: Match[int], +): + group_id = session.id2 + if not session.id1 or not group_id: + return + _time = time.result if time.available else None + _count = count.result if count.available else None + _duration = duration.result if duration.available else None + group_data = mute_manage.get_group_data(group_id) + if _time is None and _count is None and _duration is None: + await MessageUtils.build_message( + f"最大次数:{group_data.count} 次\n" + f"规定时间:{group_data.time} 秒\n" + f"禁言时长:{group_data.duration:.2f} 分钟\n" + f"【在规定时间内发送相同消息超过最大次数则禁言\n当禁言时长为0时关闭此功能】" + ).finish(reply_to=True) + if _time is not None: + group_data.time = _time + if _count is not None: + group_data.count = _count + if _duration is not None: + group_data.duration = _duration + await MessageUtils.build_message("设置成功!").send(reply_to=True) + logger.info( + f"设置禁言配置 time: {_time}, count: {_count}, duration: {_duration}", + arparma.header_result, + session=session, + ) + mute_manage.save_data() diff --git a/zhenxun/plugins/nbnhhsh.py b/zhenxun/plugins/nbnhhsh.py new file mode 100644 index 00000000..7ab78aaa --- /dev/null +++ b/zhenxun/plugins/nbnhhsh.py @@ -0,0 +1,62 @@ +import ujson as json +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="能不能好好说话", + description="能不能好好说话,说人话", + usage=""" + 说人话 + 指令: + nbnhhsh [文本] + 能不能好好说话 [文本] + 示例: + nbnhhsh xsx + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1", aliases={"nbnhhsh"}).dict(), +) + +URL = "https://lab.magiconch.com/api/nbnhhsh/guess" + +_matcher = on_alconna( + Alconna("nbnhhsh", Args["text", str]), + aliases={"能不能好好说话"}, + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma, text: str): + response = await AsyncHttpx.post( + URL, + data=json.dumps({"text": text}), # type: ignore + timeout=5, + headers={"content-type": "application/json"}, + ) + try: + data = response.json() + tmp = "" + result = "" + for x in data: + trans = "" + if x.get("trans"): + trans = x["trans"][0] + elif x.get("inputting"): + trans = ",".join(x["inputting"]) + tmp += f'{x["name"]} -> {trans}\n' + result += trans + logger.info( + f" 发送能不能好好说话: {text} -> {result}", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message(f"{tmp}={result}").send(reply_to=True) + except (IndexError, KeyError): + await MessageUtils.build_message("没有找到对应的翻译....").send() diff --git a/zhenxun/plugins/one_friend/__init__.py b/zhenxun/plugins/one_friend/__init__.py new file mode 100644 index 00000000..0191084f --- /dev/null +++ b/zhenxun/plugins/one_friend/__init__.py @@ -0,0 +1,79 @@ +import random +from io import BytesIO + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +__plugin_meta__ = PluginMetadata( + name="我有一个朋友", + description="我有一个朋友想问问...", + usage=""" + 指令: + 我有一个朋友想问问 [文本] ?[at]: 当at时你的朋友就是艾特对象 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("one-friend", Args["text", str]["at?", alcAt]), priority=5, block=True +) + +_matcher.shortcut( + "^我.{0,4}朋友.{0,2}(?:想问问|说|让我问问|想问|让我问|想知道|让我帮他问问|让我帮他问|让我帮忙问|让我帮忙问问|问)(?P.{0,30})$", + command="one-friend", + arguments=["{content}"], + prefix=True, +) + + +@_matcher.handle() +async def _(bot: Bot, text: str, at: Match[alcAt], session: EventSession): + gid = session.id3 or session.id2 + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + at_user = None + if at.available: + at_user = at.result.target + user = None + if at_user: + user = await PlatformUtils.get_user(bot, at_user, gid) + else: + if member_list := await PlatformUtils.get_group_member_list(bot, gid): + text = text.replace("他", "我").replace("她", "我").replace("它", "我") + user = random.choice(member_list) + if user: + ava_data = None + if PlatformUtils.get_platform(bot) == "qq": + ava_data = await PlatformUtils.get_user_avatar(user.user_id, "qq") + elif user.avatar_url: + ava_data = (await AsyncHttpx.get(user.avatar_url)).content + ava_img = BuildImage(200, 100, color=(0, 0, 0, 0)) + if ava_data: + ava_img = BuildImage(200, 100, background=BytesIO(ava_data)) + await ava_img.circle() + user_name = "朋友" + content = BuildImage(400, 30, font_size=30) + await content.text((0, 0), user_name) + A = BuildImage(700, 150, font_size=25, color="white") + await A.paste(ava_img, (30, 25)) + await A.paste(content, (150, 38)) + await A.text((150, 85), text, (125, 125, 125)) + logger.info(f"发送有一个朋友: {text}", "我有一个朋友", session=session) + await MessageUtils.build_message(A).finish() + await MessageUtils.build_message("获取用户信息失败...").send() diff --git a/zhenxun/plugins/open_cases/__init__.py b/zhenxun/plugins/open_cases/__init__.py new file mode 100644 index 00000000..2798942e --- /dev/null +++ b/zhenxun/plugins/open_cases/__init__.py @@ -0,0 +1,329 @@ +import asyncio +import random +from datetime import datetime, timedelta +from typing import List + +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Arparma, Match +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig, Task +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import text2image +from zhenxun.utils.message import MessageUtils + +from .command import ( + _group_open_matcher, + _knifes_matcher, + _multiple_matcher, + _my_open_matcher, + _open_matcher, + _price_matcher, + _reload_matcher, + _show_case_matcher, + _update_image_matcher, + _update_matcher, +) +from .open_cases_c import ( + auto_update, + get_my_knifes, + group_statistics, + open_case, + open_multiple_case, + total_open_statistics, +) +from .utils import ( + CASE2ID, + KNIFE2ID, + CaseManager, + build_case_image, + download_image, + get_skin_case, + init_skin_trends, + reset_count_daily, + update_skin_data, +) + +__plugin_meta__ = PluginMetadata( + name="CSGO开箱", + description="csgo模拟开箱[戒赌]", + usage=""" + 指令: + 开箱 ?[武器箱] + [1-30]连开箱 ?[武器箱] + 我的开箱 + 我的金色 + 群开箱统计 + 查看武器箱?[武器箱] + * 不包含[武器箱]时随机开箱 * + 示例: 查看武器箱 + 示例: 查看武器箱英勇 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + superuser_help=""" + 更新皮肤指令 + 重置开箱: 重置今日开箱所有次数 + 指令: + 更新武器箱 ?[武器箱/ALL] + 更新皮肤 ?[名称/ALL1] + 更新皮肤 ?[名称/ALL1] -S: (必定更新罕见皮肤所属箱子) + 更新武器箱图片 + * 不指定武器箱时则全部更新 * + * 过多的爬取会导致账号API被封 * + """.strip(), + menu_type="抽卡相关", + tasks=[Task(module="open_case_reset_remind", name="每日开箱重置提醒")], + limits=[PluginCdBlock(result="着什么急啊,慢慢来!")], + configs=[ + RegisterConfig( + key="INITIAL_OPEN_CASE_COUNT", + value=20, + help="初始每日开箱次数", + default_value=20, + type=int, + ), + RegisterConfig( + key="EACH_IMPRESSION_ADD_COUNT", + value=3, + help="每 * 点好感度额外增加开箱次数", + default_value=3, + type=int, + ), + RegisterConfig(key="COOKIE", value=None, help="BUFF的cookie"), + RegisterConfig( + key="DAILY_UPDATE", + value=None, + help="每日自动更新的武器箱,存在'ALL'时则更新全部武器箱", + type=List[str], + ), + RegisterConfig( + key="DEFAULT_OPEN_CASE_RESET_REMIND", + module="_task", + value=True, + help="被动 每日开箱重置提醒 进群默认开关状态", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + + +@_price_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + name: str, + skin: str, + abrasion: str, + day: Match[int], +): + name = name.replace("武器箱", "").strip() + _day = 7 + if day.available: + _day = day.result + if _day > 180: + await MessageUtils.build_message("天数必须大于0且小于180").finish() + result = await init_skin_trends(name, skin, abrasion, _day) + if not result: + await MessageUtils.build_message("未查询到数据...").finish(reply_to=True) + await MessageUtils.build_message(result).send() + logger.info( + f"查看 [{name}:{skin}({abrasion})] 价格趋势", + arparma.header_result, + session=session, + ) + + +@_reload_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + await reset_count_daily() + logger.info("重置开箱次数", arparma.header_result, session=session) + + +@_open_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + gid = session.id3 or session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + case_name = None + if name.available: + case_name = name.result.replace("武器箱", "").strip() + result = await open_case(session.id1, gid, case_name, session) + await result.finish(reply_to=True) + + +@_my_open_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + await MessageUtils.build_message( + await total_open_statistics(session.id1, gid), + ).send(reply_to=True) + logger.info("查询我的开箱", arparma.header_result, session=session) + + +@_group_open_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + result = await group_statistics(gid) + await MessageUtils.build_message(result).send(reply_to=True) + logger.info("查询群开箱统计", arparma.header_result, session=session) + + +@_knifes_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + result = await get_my_knifes(session.id1, gid) + await result.send(reply_to=True) + logger.info("查询我的金色", arparma.header_result, session=session) + + +@_multiple_matcher.handle() +async def _(session: EventSession, arparma: Arparma, num: int, name: Match[str]): + gid = session.id3 or session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + if num > 30: + await MessageUtils.build_message("开箱次数不要超过30啊笨蛋!").finish() + if num < 0: + await MessageUtils.build_message("再负开箱就扣你明天开箱数了!").finish() + case_name = None + if name.available: + case_name = name.result.replace("武器箱", "").strip() + result = await open_multiple_case(session.id1, gid, case_name, num, session) + await result.send(reply_to=True) + logger.info(f"{num}连开箱", arparma.header_result, session=session) + + +@_update_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + case_name = None + if name.available: + case_name = name.result.strip() + if not case_name: + case_list = [] + skin_list = [] + for i, case_name in enumerate(CASE2ID): + if case_name in CaseManager.CURRENT_CASES: + case_list.append(f"{i+1}.{case_name} [已更新]") + else: + case_list.append(f"{i+1}.{case_name}") + for skin_name in KNIFE2ID: + skin_list.append(f"{skin_name}") + text = "武器箱:\n" + "\n".join(case_list) + "\n皮肤:\n" + ", ".join(skin_list) + img = await text2image(text, padding=20, color="#f9f6f2") + await MessageUtils.build_message( + ["未指定武器箱, 当前已包含武器箱/皮肤\n", img] + ).finish() + if case_name in ["ALL", "ALL1"]: + if case_name == "ALL": + case_list = list(CASE2ID.keys()) + type_ = "武器箱" + else: + case_list = list(KNIFE2ID.keys()) + type_ = "罕见皮肤" + await MessageUtils.build_message(f"即将更新所有{type_}, 请稍等").send() + for i, case_name in enumerate(case_list): + try: + info = await update_skin_data(case_name, arparma.find("s")) + if "请先登录" in info: + await MessageUtils.build_message( + f"未登录, 已停止更新, 请配置BUFF token..." + ).send() + return + rand = random.randint(300, 500) + result = f"更新全部{type_}完成" + if i < len(case_list) - 1: + next_case = case_list[i + 1] + result = f"将在 {rand} 秒后更新下一{type_}: {next_case}" + await MessageUtils.build_message(f"{info}, {result}").send() + logger.info(f"info, {result}", "更新武器箱", session=session) + await asyncio.sleep(rand) + except Exception as e: + logger.error(f"更新{type_}: {case_name}", session=session, e=e) + await MessageUtils.build_message( + f"更新{type_}: {case_name} 发生错误: {type(e)}: {e}" + ).send() + await MessageUtils.build_message(f"更新全部{type_}完成").send() + else: + await MessageUtils.build_message( + f"开始{arparma.header_result}: {case_name}, 请稍等" + ).send() + try: + await MessageUtils.build_message( + await update_skin_data(case_name, arparma.find("s")) + ).send(at_sender=True) + except Exception as e: + logger.error(f"{arparma.header_result}: {case_name}", session=session, e=e) + await MessageUtils.build_message( + f"成功{arparma.header_result}: {case_name} 发生错误: {type(e)}: {e}" + ).send() + + +@_show_case_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + case_name = None + if name.available: + case_name = name.result.strip() + result = await build_case_image(case_name) + if isinstance(result, str): + await MessageUtils.build_message(result).send() + else: + await MessageUtils.build_message(result).send() + logger.info("查看武器箱", arparma.header_result, session=session) + + +@_update_image_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + case_name = None + if name.available: + case_name = name.result.strip() + await MessageUtils.build_message("开始更新图片...").send(reply_to=True) + await download_image(case_name) + await MessageUtils.build_message("更新图片完成...").send(at_sender=True) + logger.info("更新武器箱图片", arparma.header_result, session=session) + + +# 重置开箱 +@scheduler.scheduled_job( + "cron", + hour=0, + minute=1, +) +async def _(): + await reset_count_daily() + + +@scheduler.scheduled_job( + "cron", + hour=0, + minute=10, +) +async def _(): + now = datetime.now() + hour = random.choice([0, 1, 2, 3]) + date = now + timedelta(hours=hour) + logger.debug(f"将在 {date} 时自动更新武器箱...", "更新武器箱") + scheduler.add_job( + auto_update, + "date", + run_date=date.replace(microsecond=0), + id=f"auto_update_csgo_cases", + ) diff --git a/zhenxun/plugins/open_cases/build_image.py b/zhenxun/plugins/open_cases/build_image.py new file mode 100644 index 00000000..8b8db8e2 --- /dev/null +++ b/zhenxun/plugins/open_cases/build_image.py @@ -0,0 +1,155 @@ +from datetime import timedelta, timezone + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import cn2py + +from .config import COLOR2COLOR, COLOR2NAME +from .models.buff_skin import BuffSkin + +BASE_PATH = IMAGE_PATH / "csgo_cases" + +ICON_PATH = IMAGE_PATH / "_icon" + + +async def draw_card(skin: BuffSkin, rand: str) -> BuildImage: + """构造抽取图片 + + 参数: + skin (BuffSkin): BuffSkin + rand (str): 磨损 + + 返回: + BuildImage: BuildImage + """ + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" + if not file_path.exists(): + logger.warning(f"皮肤图片: {name} 不存在", "开箱") + skin_bk = BuildImage( + 460, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" + ) + if file_path.exists(): + skin_image = BuildImage(205, 153, background=file_path) + await skin_bk.paste(skin_image, (10, 30)) + await skin_bk.line((220, 10, 220, 180)) + await skin_bk.text((10, 10), skin.name, (255, 255, 255)) + name_icon = BuildImage(20, 20, background=ICON_PATH / "name_white.png") + await skin_bk.paste(name_icon, (240, 13)) + await skin_bk.text((265, 15), f"名称:", (255, 255, 255), font_size=20) + await skin_bk.text( + (310, 15), + f"{skin.skin_name + ('(St)' if skin.is_stattrak else '')}", + (255, 255, 255), + ) + tone_icon = BuildImage(20, 20, background=ICON_PATH / "tone_white.png") + await skin_bk.paste(tone_icon, (240, 45)) + await skin_bk.text((265, 45), "品质:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 45), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color]) + type_icon = BuildImage(20, 20, background=ICON_PATH / "type_white.png") + await skin_bk.paste(type_icon, (240, 73)) + await skin_bk.text((265, 75), "类型:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 75), skin.weapon_type, (255, 255, 255)) + price_icon = BuildImage(20, 20, background=ICON_PATH / "price_white.png") + await skin_bk.paste(price_icon, (240, 103)) + await skin_bk.text((265, 105), "价格:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 105), str(skin.sell_min_price), (0, 255, 98)) + abrasion_icon = BuildImage(20, 20, background=ICON_PATH / "abrasion_white.png") + await skin_bk.paste(abrasion_icon, (240, 133)) + await skin_bk.text((265, 135), "磨损:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 135), skin.abrasion, (255, 255, 255)) + await skin_bk.text((228, 165), f"({rand})", (255, 255, 255)) + return skin_bk + + +async def generate_skin(skin: BuffSkin, update_count: int) -> BuildImage | None: + """构造皮肤图片 + + 参数: + skin (BuffSkin): BuffSkin + + 返回: + BuildImage | None: 图片 + """ + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" + if not file_path.exists(): + logger.warning(f"皮肤图片: {name} 不存在", "查看武器箱") + if skin.color == "CASE": + case_bk = BuildImage( + 700, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" + ) + if file_path.exists(): + skin_img = BuildImage(200, 200, background=file_path) + await case_bk.paste(skin_img, (10, 10)) + await case_bk.line((250, 10, 250, 190)) + await case_bk.line((280, 160, 660, 160)) + name_icon = BuildImage(30, 30, background=ICON_PATH / "box_white.png") + await case_bk.paste(name_icon, (260, 25)) + await case_bk.text((295, 30), "名称:", (255, 255, 255)) + await case_bk.text((345, 30), skin.case_name, (255, 0, 38), font_size=30) + + type_icon = BuildImage(30, 30, background=ICON_PATH / "type_white.png") + await case_bk.paste(type_icon, (260, 70)) + await case_bk.text((295, 75), "类型:", (255, 255, 255)) + await case_bk.text((345, 75), "武器箱", (0, 157, 255), font_size=30) + + price_icon = BuildImage(30, 30, background=ICON_PATH / "price_white.png") + await case_bk.paste(price_icon, (260, 114)) + await case_bk.text((295, 120), "单价:", (255, 255, 255)) + await case_bk.text( + (340, 120), str(skin.sell_min_price), (0, 255, 98), font_size=30 + ) + + update_count_icon = BuildImage( + 40, 40, background=ICON_PATH / "reload_white.png" + ) + await case_bk.paste(update_count_icon, (575, 10)) + await case_bk.text((625, 12), str(update_count), (255, 255, 255), font_size=45) + + num_icon = BuildImage(30, 30, background=ICON_PATH / "num_white.png") + await case_bk.paste(num_icon, (455, 70)) + await case_bk.text((490, 75), "在售:", (255, 255, 255)) + await case_bk.text((535, 75), str(skin.sell_num), (144, 0, 255), font_size=30) + + want_buy_icon = BuildImage(30, 30, background=ICON_PATH / "want_buy_white.png") + await case_bk.paste(want_buy_icon, (455, 114)) + await case_bk.text((490, 120), "求购:", (255, 255, 255)) + await case_bk.text((535, 120), str(skin.buy_num), (144, 0, 255), font_size=30) + + await case_bk.text((275, 165), "更新时间", (255, 255, 255), font_size=22) + date = str( + skin.update_time.replace(microsecond=0).astimezone( + timezone(timedelta(hours=8)) + ) + ).split("+")[0] + await case_bk.text( + (350, 165), + date, + (255, 255, 255), + font_size=30, + ) + return case_bk + else: + skin_bk = BuildImage( + 235, 250, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" + ) + if file_path.exists(): + skin_image = BuildImage(205, 153, background=file_path) + await skin_bk.paste(skin_image, (10, 30)) + update_count_icon = BuildImage( + 35, 35, background=ICON_PATH / "reload_white.png" + ) + await skin_bk.line((10, 180, 220, 180)) + await skin_bk.text((10, 10), skin.name, (255, 255, 255)) + await skin_bk.paste(update_count_icon, (140, 10)) + await skin_bk.text((175, 15), str(update_count), (255, 255, 255)) + await skin_bk.text((10, 185), f"{skin.skin_name}", (255, 255, 255), "width") + await skin_bk.text((10, 218), "品质:", (255, 255, 255)) + await skin_bk.text( + (55, 218), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color] + ) + await skin_bk.text((100, 218), "类型:", (255, 255, 255)) + await skin_bk.text((145, 218), skin.weapon_type, (255, 255, 255)) + return skin_bk diff --git a/zhenxun/plugins/open_cases/command.py b/zhenxun/plugins/open_cases/command.py new file mode 100644 index 00000000..ea86c2fc --- /dev/null +++ b/zhenxun/plugins/open_cases/command.py @@ -0,0 +1,75 @@ +from nonebot.permission import SUPERUSER +from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna, store_true + +from zhenxun.utils.rules import ensure_group + +_open_matcher = on_alconna( + Alconna("开箱", Args["name?", str]), priority=5, block=True, rule=ensure_group +) + +_reload_matcher = on_alconna( + Alconna("重置开箱"), priority=5, block=True, permission=SUPERUSER, rule=ensure_group +) + +_my_open_matcher = on_alconna( + Alconna("我的开箱"), + aliases={"开箱统计", "开箱查询", "查询开箱"}, + priority=5, + block=True, + rule=ensure_group, +) + +_group_open_matcher = on_alconna( + Alconna("群开箱统计"), priority=5, block=True, rule=ensure_group +) + +_multiple_matcher = on_alconna( + Alconna("multiple-open", Args["num", int]["name?", str]), + priority=5, + block=True, + rule=ensure_group, +) + +_multiple_matcher.shortcut( + r"(?P\d+)连开箱(?P.*?)", + command="multiple-open", + arguments=["{num}", "{name}"], + prefix=True, +) + +_update_matcher = on_alconna( + Alconna( + "更新武器箱", + Args["name?", str], + Option("-s", action=store_true, help_text="是否必定更新所属箱子"), + ), + aliases={"更新皮肤"}, + priority=1, + permission=SUPERUSER, + block=True, +) + +_update_image_matcher = on_alconna( + Alconna("更新武器箱图片", Args["name?", str]), + priority=1, + permission=SUPERUSER, + block=True, +) + +_show_case_matcher = on_alconna( + Alconna("查看武器箱", Args["name?", str]), priority=5, block=True +) + +_knifes_matcher = on_alconna( + Alconna("我的金色"), priority=5, block=True, rule=ensure_group +) + +_show_skin_matcher = on_alconna(Alconna("查看皮肤"), priority=5, block=True) + +_price_matcher = on_alconna( + Alconna( + "价格趋势", Args["name", str]["skin", str]["abrasion", str]["day?", int, 7] + ), + priority=5, + block=True, +) diff --git a/plugins/open_cases/config.py b/zhenxun/plugins/open_cases/config.py old mode 100755 new mode 100644 similarity index 92% rename from plugins/open_cases/config.py rename to zhenxun/plugins/open_cases/config.py index 43afbecb..cefa7384 --- a/plugins/open_cases/config.py +++ b/zhenxun/plugins/open_cases/config.py @@ -1,255 +1,253 @@ -import random -from enum import Enum -from typing import List, Tuple - -from configs.path_config import IMAGE_PATH -from services.log import logger - -from .models.buff_skin import BuffSkin - -BLUE = 0.7981 -BLUE_ST = 0.0699 -PURPLE = 0.1626 -PURPLE_ST = 0.0164 -PINK = 0.0315 -PINK_ST = 0.0048 -RED = 0.0057 -RED_ST = 0.00021 -KNIFE = 0.0021 -KNIFE_ST = 0.000041 - -# 崭新 -FACTORY_NEW_S = 0 -FACTORY_NEW_E = 0.0699999 -# 略磨 -MINIMAL_WEAR_S = 0.07 -MINIMAL_WEAR_E = 0.14999 -# 久经 -FIELD_TESTED_S = 0.15 -FIELD_TESTED_E = 0.37999 -# 破损 -WELL_WORN_S = 0.38 -WELL_WORN_E = 0.44999 -# 战痕 -BATTLE_SCARED_S = 0.45 -BATTLE_SCARED_E = 0.99999 - - -class UpdateType(Enum): - - """ - 更新类型 - """ - - CASE = "case" - WEAPON_TYPE = "weapon_type" - - -NAME2COLOR = { - "消费级": "WHITE", - "工业级": "LIGHTBLUE", - "军规级": "BLUE", - "受限": "PURPLE", - "保密": "PINK", - "隐秘": "RED", - "非凡": "KNIFE", -} - -COLOR2NAME = { - "WHITE": "消费级", - "LIGHTBLUE": "工业级", - "BLUE": "军规级", - "PURPLE": "受限", - "PINK": "保密", - "RED": "隐秘", - "KNIFE": "非凡", -} - -COLOR2COLOR = { - "WHITE": (255, 255, 255), - "LIGHTBLUE": (0, 179, 255), - "BLUE": (0, 85, 255), - "PURPLE": (149, 0, 255), - "PINK": (255, 0, 162), - "RED": (255, 34, 0), - "KNIFE": (255, 225, 0), -} - -ABRASION_SORT = ["崭新出厂", "略有磨损", "久经沙场", "破损不堪", "战横累累"] - -CASE_BACKGROUND = IMAGE_PATH / "csgo_cases" / "_background" / "shu" - -# 刀 -KNIFE2ID = { - "鲍伊猎刀": "weapon_knife_survival_bowie", - "蝴蝶刀": "weapon_knife_butterfly", - "弯刀": "weapon_knife_falchion", - "折叠刀": "weapon_knife_flip", - "穿肠刀": "weapon_knife_gut", - "猎杀者匕首": "weapon_knife_tactical", - "M9刺刀": "weapon_knife_m9_bayonet", - "刺刀": "weapon_bayonet", - "爪子刀": "weapon_knife_karambit", - "暗影双匕": "weapon_knife_push", - "短剑": "weapon_knife_stiletto", - "熊刀": "weapon_knife_ursus", - "折刀": "weapon_knife_gypsy_jackknife", - "锯齿爪刀": "weapon_knife_widowmaker", - "海豹短刀": "weapon_knife_css", - "系绳匕首": "weapon_knife_cord", - "求生匕首": "weapon_knife_canis", - "流浪者匕首": "weapon_knife_outdoor", - "骷髅匕首": "weapon_knife_skeleton", - "血猎手套": "weapon_bloodhound_gloves", - "驾驶手套": "weapon_driver_gloves", - "手部束带": "weapon_hand_wraps", - "摩托手套": "weapon_moto_gloves", - "专业手套": "weapon_specialist_gloves", - "运动手套": "weapon_sport_gloves", - "九头蛇手套": "weapon_hydra_gloves", - "狂牙手套": "weapon_brokenfang_gloves", -} - -WEAPON2ID = {} - -# 武器箱 -CASE2ID = { - "变革": "set_community_32", - "反冲": "set_community_31", - "梦魇": "set_community_30", - "激流": "set_community_29", - "蛇噬": "set_community_28", - "狂牙大行动": "set_community_27", - "裂空": "set_community_26", - "棱彩2号": "set_community_25", - "CS20": "set_community_24", - "裂网大行动": "set_community_23", - "棱彩": "set_community_22", - "头号特训": "set_community_21", - "地平线": "set_community_20", - "命悬一线": "set_community_19", - "光谱2号": "set_community_18", - "九头蛇大行动": "set_community_17", - "光谱": "set_community_16", - "手套": "set_community_15", - "伽玛2号": "set_gamma_2", - "伽玛": "set_community_13", - "幻彩3号": "set_community_12", - "野火大行动": "set_community_11", - "左轮": "set_community_10", - "暗影": "set_community_9", - "弯曲猎手": "set_community_8", - "幻彩2号": "set_community_7", - "幻彩": "set_community_6", - "先锋": "set_community_5", - "电竞2014夏季": "set_esports_iii", - "突围大行动": "set_community_4", - "猎杀者": "set_community_3", - "凤凰": "set_community_2", - "电竞2013冬季": "set_esports_ii", - "冬季攻势": "set_community_1", - "军火交易3号": "set_weapons_iii", - "英勇": "set_bravo_i", - "电竞2013": "set_esports", - "军火交易2号": "set_weapons_ii", - "军火交易": "set_weapons_i", -} - - -def get_wear(rand: float) -> str: - """判断磨损度 - - Args: - rand (float): 随机rand - - Returns: - str: 磨损名称 - """ - if rand <= FACTORY_NEW_E: - return "崭新出厂" - if MINIMAL_WEAR_S <= rand <= MINIMAL_WEAR_E: - return "略有磨损" - if FIELD_TESTED_S <= rand <= FIELD_TESTED_E: - return "久经沙场" - if WELL_WORN_S <= rand <= WELL_WORN_E: - return "破损不堪" - return "战痕累累" - - -def random_color_and_st(rand: float) -> Tuple[str, bool]: - """获取皮肤品质及是否暗金 - - Args: - rand (float): 随机rand - - Returns: - Tuple[str, bool]: 品质,是否暗金 - """ - if rand <= KNIFE: - if random.random() <= KNIFE_ST: - return ("KNIFE", True) - return ("KNIFE", False) - elif KNIFE < rand <= RED: - if random.random() <= RED_ST: - return ("RED", True) - return ("RED", False) - elif RED < rand <= PINK: - if random.random() <= PINK_ST: - return ("PINK", True) - return ("PINK", False) - elif PINK < rand <= PURPLE: - if random.random() <= PURPLE_ST: - return ("PURPLE", True) - return ("PURPLE", False) - else: - if random.random() <= BLUE_ST: - return ("BLUE", True) - return ("BLUE", False) - - -async def random_skin(num: int, case_name: str) -> List[Tuple[BuffSkin, float]]: - """ - 随机抽取皮肤 - """ - case_name = case_name.replace("武器箱", "").replace(" ", "") - color_map = {} - for _ in range(num): - rand = random.random() - # 尝试降低磨损 - if rand > MINIMAL_WEAR_E: - for _ in range(2): - if random.random() < 0.5: - logger.debug(f"[START]开箱随机磨损触发降低磨损条件: {rand}") - if random.random() < 0.2: - rand /= 3 - else: - rand /= 2 - logger.debug(f"[END]开箱随机磨损触发降低磨损条件: {rand}") - break - abrasion = get_wear(rand) - logger.debug(f"开箱随机磨损: {rand} | {abrasion}") - color, is_stattrak = random_color_and_st(rand) - if not color_map.get(color): - color_map[color] = {} - if is_stattrak: - if not color_map[color].get(f"{abrasion}_st"): - color_map[color][f"{abrasion}_st"] = [] - color_map[color][f"{abrasion}_st"].append(rand) - else: - if not color_map[color].get(abrasion): - color_map[color][f"{abrasion}"] = [] - color_map[color][f"{abrasion}"].append(rand) - skin_list = [] - for color in color_map: - for abrasion in color_map[color]: - rand_list = color_map[color][abrasion] - is_stattrak = "_st" in abrasion - abrasion = abrasion.replace("_st", "") - skin_list_ = await BuffSkin.random_skin( - len(rand_list), color, abrasion, is_stattrak, case_name - ) - skin_list += [(skin, rand) for skin, rand in zip(skin_list_, rand_list)] - return skin_list - - -# M249(StatTrak™) | 等高线 +import random +from enum import Enum + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger + +from .models.buff_skin import BuffSkin + +BLUE = 0.7981 +BLUE_ST = 0.0699 +PURPLE = 0.1626 +PURPLE_ST = 0.0164 +PINK = 0.0315 +PINK_ST = 0.0048 +RED = 0.0057 +RED_ST = 0.00021 +KNIFE = 0.0021 +KNIFE_ST = 0.000041 + +# 崭新 +FACTORY_NEW_S = 0 +FACTORY_NEW_E = 0.0699999 +# 略磨 +MINIMAL_WEAR_S = 0.07 +MINIMAL_WEAR_E = 0.14999 +# 久经 +FIELD_TESTED_S = 0.15 +FIELD_TESTED_E = 0.37999 +# 破损 +WELL_WORN_S = 0.38 +WELL_WORN_E = 0.44999 +# 战痕 +BATTLE_SCARED_S = 0.45 +BATTLE_SCARED_E = 0.99999 + + +class UpdateType(Enum): + """ + 更新类型 + """ + + CASE = "case" + WEAPON_TYPE = "weapon_type" + + +NAME2COLOR = { + "消费级": "WHITE", + "工业级": "LIGHTBLUE", + "军规级": "BLUE", + "受限": "PURPLE", + "保密": "PINK", + "隐秘": "RED", + "非凡": "KNIFE", +} + +COLOR2NAME = { + "WHITE": "消费级", + "LIGHTBLUE": "工业级", + "BLUE": "军规级", + "PURPLE": "受限", + "PINK": "保密", + "RED": "隐秘", + "KNIFE": "非凡", +} + +COLOR2COLOR = { + "WHITE": (255, 255, 255), + "LIGHTBLUE": (0, 179, 255), + "BLUE": (0, 85, 255), + "PURPLE": (149, 0, 255), + "PINK": (255, 0, 162), + "RED": (255, 34, 0), + "KNIFE": (255, 225, 0), +} + +ABRASION_SORT = ["崭新出厂", "略有磨损", "久经沙场", "破损不堪", "战横累累"] + +CASE_BACKGROUND = IMAGE_PATH / "csgo_cases" / "_background" / "shu" + +# 刀 +KNIFE2ID = { + "鲍伊猎刀": "weapon_knife_survival_bowie", + "蝴蝶刀": "weapon_knife_butterfly", + "弯刀": "weapon_knife_falchion", + "折叠刀": "weapon_knife_flip", + "穿肠刀": "weapon_knife_gut", + "猎杀者匕首": "weapon_knife_tactical", + "M9刺刀": "weapon_knife_m9_bayonet", + "刺刀": "weapon_bayonet", + "爪子刀": "weapon_knife_karambit", + "暗影双匕": "weapon_knife_push", + "短剑": "weapon_knife_stiletto", + "熊刀": "weapon_knife_ursus", + "折刀": "weapon_knife_gypsy_jackknife", + "锯齿爪刀": "weapon_knife_widowmaker", + "海豹短刀": "weapon_knife_css", + "系绳匕首": "weapon_knife_cord", + "求生匕首": "weapon_knife_canis", + "流浪者匕首": "weapon_knife_outdoor", + "骷髅匕首": "weapon_knife_skeleton", + "血猎手套": "weapon_bloodhound_gloves", + "驾驶手套": "weapon_driver_gloves", + "手部束带": "weapon_hand_wraps", + "摩托手套": "weapon_moto_gloves", + "专业手套": "weapon_specialist_gloves", + "运动手套": "weapon_sport_gloves", + "九头蛇手套": "weapon_hydra_gloves", + "狂牙手套": "weapon_brokenfang_gloves", +} + +WEAPON2ID = {} + +# 武器箱 +CASE2ID = { + "变革": "set_community_32", + "反冲": "set_community_31", + "梦魇": "set_community_30", + "激流": "set_community_29", + "蛇噬": "set_community_28", + "狂牙大行动": "set_community_27", + "裂空": "set_community_26", + "棱彩2号": "set_community_25", + "CS20": "set_community_24", + "裂网大行动": "set_community_23", + "棱彩": "set_community_22", + "头号特训": "set_community_21", + "地平线": "set_community_20", + "命悬一线": "set_community_19", + "光谱2号": "set_community_18", + "九头蛇大行动": "set_community_17", + "光谱": "set_community_16", + "手套": "set_community_15", + "伽玛2号": "set_gamma_2", + "伽玛": "set_community_13", + "幻彩3号": "set_community_12", + "野火大行动": "set_community_11", + "左轮": "set_community_10", + "暗影": "set_community_9", + "弯曲猎手": "set_community_8", + "幻彩2号": "set_community_7", + "幻彩": "set_community_6", + "先锋": "set_community_5", + "电竞2014夏季": "set_esports_iii", + "突围大行动": "set_community_4", + "猎杀者": "set_community_3", + "凤凰": "set_community_2", + "电竞2013冬季": "set_esports_ii", + "冬季攻势": "set_community_1", + "军火交易3号": "set_weapons_iii", + "英勇": "set_bravo_i", + "电竞2013": "set_esports", + "军火交易2号": "set_weapons_ii", + "军火交易": "set_weapons_i", +} + + +def get_wear(rand: float) -> str: + """判断磨损度 + + Args: + rand (float): 随机rand + + Returns: + str: 磨损名称 + """ + if rand <= FACTORY_NEW_E: + return "崭新出厂" + if MINIMAL_WEAR_S <= rand <= MINIMAL_WEAR_E: + return "略有磨损" + if FIELD_TESTED_S <= rand <= FIELD_TESTED_E: + return "久经沙场" + if WELL_WORN_S <= rand <= WELL_WORN_E: + return "破损不堪" + return "战痕累累" + + +def random_color_and_st(rand: float) -> tuple[str, bool]: + """获取皮肤品质及是否暗金 + + 参数: + rand (float): 随机rand + + 返回: + tuple[str, bool]: 品质,是否暗金 + """ + if rand <= KNIFE: + if random.random() <= KNIFE_ST: + return ("KNIFE", True) + return ("KNIFE", False) + elif KNIFE < rand <= RED: + if random.random() <= RED_ST: + return ("RED", True) + return ("RED", False) + elif RED < rand <= PINK: + if random.random() <= PINK_ST: + return ("PINK", True) + return ("PINK", False) + elif PINK < rand <= PURPLE: + if random.random() <= PURPLE_ST: + return ("PURPLE", True) + return ("PURPLE", False) + else: + if random.random() <= BLUE_ST: + return ("BLUE", True) + return ("BLUE", False) + + +async def random_skin(num: int, case_name: str) -> list[tuple[BuffSkin, float]]: + """ + 随机抽取皮肤 + """ + case_name = case_name.replace("武器箱", "").replace(" ", "") + color_map = {} + for _ in range(num): + rand = random.random() + # 尝试降低磨损 + if rand > MINIMAL_WEAR_E: + for _ in range(2): + if random.random() < 0.5: + logger.debug(f"[START]开箱随机磨损触发降低磨损条件: {rand}") + if random.random() < 0.2: + rand /= 3 + else: + rand /= 2 + logger.debug(f"[END]开箱随机磨损触发降低磨损条件: {rand}") + break + abrasion = get_wear(rand) + logger.debug(f"开箱随机磨损: {rand} | {abrasion}") + color, is_stattrak = random_color_and_st(rand) + if not color_map.get(color): + color_map[color] = {} + if is_stattrak: + if not color_map[color].get(f"{abrasion}_st"): + color_map[color][f"{abrasion}_st"] = [] + color_map[color][f"{abrasion}_st"].append(rand) + else: + if not color_map[color].get(abrasion): + color_map[color][f"{abrasion}"] = [] + color_map[color][f"{abrasion}"].append(rand) + skin_list = [] + for color in color_map: + for abrasion in color_map[color]: + rand_list = color_map[color][abrasion] + is_stattrak = "_st" in abrasion + abrasion = abrasion.replace("_st", "") + skin_list_ = await BuffSkin.random_skin( + len(rand_list), color, abrasion, is_stattrak, case_name + ) + skin_list += [(skin, rand) for skin, rand in zip(skin_list_, rand_list)] + return skin_list + + +# M249(StatTrak™) | 等高线 diff --git a/plugins/open_cases/models/__init__.py b/zhenxun/plugins/open_cases/models/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from plugins/open_cases/models/__init__.py rename to zhenxun/plugins/open_cases/models/__init__.py diff --git a/plugins/open_cases/models/buff_prices.py b/zhenxun/plugins/open_cases/models/buff_prices.py old mode 100755 new mode 100644 similarity index 91% rename from plugins/open_cases/models/buff_prices.py rename to zhenxun/plugins/open_cases/models/buff_prices.py index 5402d5e0..9f53de0e --- a/plugins/open_cases/models/buff_prices.py +++ b/zhenxun/plugins/open_cases/models/buff_prices.py @@ -1,7 +1,6 @@ - from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model # 1.狂牙武器箱 diff --git a/plugins/open_cases/models/buff_skin.py b/zhenxun/plugins/open_cases/models/buff_skin.py similarity index 81% rename from plugins/open_cases/models/buff_skin.py rename to zhenxun/plugins/open_cases/models/buff_skin.py index 279f082e..7f51221a 100644 --- a/plugins/open_cases/models/buff_skin.py +++ b/zhenxun/plugins/open_cases/models/buff_skin.py @@ -1,10 +1,7 @@ -from datetime import datetime -from typing import List, Optional - from tortoise import fields from tortoise.contrib.postgres.functions import Random -from services.db_context import Model +from zhenxun.services.db_context import Model class BuffSkin(Model): @@ -28,24 +25,24 @@ class BuffSkin(Model): img_url = fields.CharField(255) """图片url""" - steam_price: float = fields.FloatField(default=0) + steam_price = fields.FloatField(default=0) """steam价格""" weapon_type = fields.CharField(255) """枪械类型""" - buy_max_price: float = fields.FloatField(default=0) + buy_max_price = fields.FloatField(default=0) """最大求购价格""" - buy_num: int = fields.IntField(default=0) + buy_num = fields.IntField(default=0) """求购数量""" - sell_min_price: float = fields.FloatField(default=0) + sell_min_price = fields.FloatField(default=0) """售卖最低价格""" - sell_num: int = fields.IntField(default=0) + sell_num = fields.IntField(default=0) """出售个数""" - sell_reference_price: float = fields.FloatField(default=0) + sell_reference_price = fields.FloatField(default=0) """参考价格""" - create_time: datetime = fields.DatetimeField(auto_add_now=True) + create_time = fields.DatetimeField(auto_add_now=True) """创建日期""" - update_time: datetime = fields.DatetimeField(auto_add=True) + update_time = fields.DatetimeField(auto_add=True) """更新日期""" class Meta: @@ -68,8 +65,20 @@ class BuffSkin(Model): color: str, abrasion: str, is_stattrak: bool = False, - case_name: Optional[str] = None, - ) -> List["BuffSkin"]: # type: ignore + case_name: str | None = None, + ) -> list["BuffSkin"]: # type: ignore + """随机皮肤 + + 参数: + num: 数量 + color: 品质 + abrasion: 磨损度 + is_stattrak: 是否暗金 + case_name: 箱子名称 + + 返回: + list["BuffSkin"]: 皮肤列表 + """ query = cls if case_name: query = query.filter(case_name__contains=case_name) diff --git a/plugins/open_cases/models/buff_skin_log.py b/zhenxun/plugins/open_cases/models/buff_skin_log.py similarity index 94% rename from plugins/open_cases/models/buff_skin_log.py rename to zhenxun/plugins/open_cases/models/buff_skin_log.py index dd67e4c8..ac9fec95 100644 --- a/plugins/open_cases/models/buff_skin_log.py +++ b/zhenxun/plugins/open_cases/models/buff_skin_log.py @@ -1,7 +1,6 @@ from tortoise import fields -from tortoise.contrib.postgres.functions import Random -from services.db_context import Model +from zhenxun.services.db_context import Model class BuffSkinLog(Model): diff --git a/plugins/open_cases/models/open_cases_log.py b/zhenxun/plugins/open_cases/models/open_cases_log.py similarity index 94% rename from plugins/open_cases/models/open_cases_log.py rename to zhenxun/plugins/open_cases/models/open_cases_log.py index ebcaf5bc..0c4f87bb 100644 --- a/plugins/open_cases/models/open_cases_log.py +++ b/zhenxun/plugins/open_cases/models/open_cases_log.py @@ -1,9 +1,7 @@ -from typing import List, Optional - from tortoise import fields from tortoise.contrib.postgres.functions import Random -from services.db_context import Model +from zhenxun.services.db_context import Model class OpenCasesLog(Model): diff --git a/plugins/open_cases/models/open_cases_user.py b/zhenxun/plugins/open_cases/models/open_cases_user.py old mode 100755 new mode 100644 similarity index 60% rename from plugins/open_cases/models/open_cases_user.py rename to zhenxun/plugins/open_cases/models/open_cases_user.py index 3b488717..3ed43937 --- a/plugins/open_cases/models/open_cases_user.py +++ b/zhenxun/plugins/open_cases/models/open_cases_user.py @@ -1,8 +1,6 @@ -from datetime import datetime - from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class OpenCasesUser(Model): @@ -13,37 +11,37 @@ class OpenCasesUser(Model): """用户id""" group_id = fields.CharField(255) """群聊id""" - total_count: int = fields.IntField(default=0) + total_count = fields.IntField(default=0) """总开启次数""" - blue_count: int = fields.IntField(default=0) + blue_count = fields.IntField(default=0) """蓝色""" - blue_st_count: int = fields.IntField(default=0) + blue_st_count = fields.IntField(default=0) """蓝色暗金""" - purple_count: int = fields.IntField(default=0) + purple_count = fields.IntField(default=0) """紫色""" - purple_st_count: int = fields.IntField(default=0) + purple_st_count = fields.IntField(default=0) """紫色暗金""" - pink_count: int = fields.IntField(default=0) + pink_count = fields.IntField(default=0) """粉色""" - pink_st_count: int = fields.IntField(default=0) + pink_st_count = fields.IntField(default=0) """粉色暗金""" - red_count: int = fields.IntField(default=0) + red_count = fields.IntField(default=0) """紫色""" - red_st_count: int = fields.IntField(default=0) + red_st_count = fields.IntField(default=0) """紫色暗金""" - knife_count: int = fields.IntField(default=0) + knife_count = fields.IntField(default=0) """金色""" - knife_st_count: int = fields.IntField(default=0) + knife_st_count = fields.IntField(default=0) """金色暗金""" - spend_money: float = fields.IntField(default=0) + spend_money = fields.IntField(default=0) """花费金币""" - make_money: float = fields.FloatField(default=0) + make_money = fields.FloatField(default=0) """赚取金币""" - today_open_total: int = fields.IntField(default=0) + today_open_total = fields.IntField(default=0) """今日开箱数量""" - open_cases_time_last: datetime = fields.DatetimeField() + open_cases_time_last = fields.DatetimeField() """最后开箱日期""" - knifes_name: str = fields.TextField(default="") + knifes_name = fields.TextField(default="") """已获取金色""" class Meta: diff --git a/plugins/open_cases/open_cases_c.py b/zhenxun/plugins/open_cases/open_cases_c.py old mode 100755 new mode 100644 similarity index 70% rename from plugins/open_cases/open_cases_c.py rename to zhenxun/plugins/open_cases/open_cases_c.py index 1f9409d3..f56aa9fe --- a/plugins/open_cases/open_cases_c.py +++ b/zhenxun/plugins/open_cases/open_cases_c.py @@ -1,461 +1,481 @@ -import asyncio -import random -import re -from datetime import datetime -from typing import Union - -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from models.sign_group_user import SignGroupUser -from services.log import logger -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import cn2py - -from .build_image import draw_card -from .config import * -from .models.open_cases_log import OpenCasesLog -from .models.open_cases_user import OpenCasesUser -from .utils import CaseManager, update_skin_data - -RESULT_MESSAGE = { - "BLUE": ["这样看着才舒服", "是自己人,大伙把刀收好", "非常舒适~"], - "PURPLE": ["还行吧,勉强接受一下下", "居然不是蓝色,太假了", "运气-1-1-1-1-1..."], - "PINK": ["开始不适....", "你妈妈买菜必涨价!涨三倍!", "你最近不适合出门,真的"], - "RED": ["已经非常不适", "好兄弟你开的什么箱子啊,一般箱子不是只有蓝色的吗", "开始拿阳寿开箱子了?"], - "KNIFE": ["你的好运我收到了,你可以去喂鲨鱼了", "最近该吃啥就迟点啥吧,哎,好好的一个人怎么就....哎", "众所周知,欧皇寿命极短."], -} - -COLOR2NAME = {"BLUE": "军规", "PURPLE": "受限", "PINK": "保密", "RED": "隐秘", "KNIFE": "罕见"} - -COLOR2CN = {"BLUE": "蓝", "PURPLE": "紫", "PINK": "粉", "RED": "红", "KNIFE": "金"} - - -def add_count(user: OpenCasesUser, skin: BuffSkin, case_price: float): - if skin.color == "BLUE": - if skin.is_stattrak: - user.blue_st_count += 1 - else: - user.blue_count += 1 - elif skin.color == "PURPLE": - if skin.is_stattrak: - user.purple_st_count += 1 - else: - user.purple_count += 1 - elif skin.color == "PINK": - if skin.is_stattrak: - user.pink_st_count += 1 - else: - user.pink_count += 1 - elif skin.color == "RED": - if skin.is_stattrak: - user.red_st_count += 1 - else: - user.red_count += 1 - elif skin.color == "KNIFE": - if skin.is_stattrak: - user.knife_st_count += 1 - else: - user.knife_count += 1 - user.make_money += skin.sell_min_price - user.spend_money += 17 + case_price - - -async def get_user_max_count( - user_id: Union[int, str], group_id: Union[str, int] -) -> int: - """获取用户每日最大开箱次数 - - Args: - user_id (str): 用户id - group_id (int): 群号 - - Returns: - int: 最大开箱次数 - """ - user, _ = await SignGroupUser.get_or_create( - user_id=str(user_id), group_id=str(group_id) - ) - impression = int(user.impression) - initial_open_case_count = Config.get_config("open_cases", "INITIAL_OPEN_CASE_COUNT") - each_impression_add_count = Config.get_config( - "open_cases", "EACH_IMPRESSION_ADD_COUNT" - ) - return int(initial_open_case_count + impression / each_impression_add_count) # type: ignore - - -async def open_case( - user_id: Union[int, str], group_id: Union[int, str], case_name: str -) -> Union[str, Message]: - """开箱 - - Args: - user_id (str): 用户id - group_id (int): 群号 - case_name (str, optional): 武器箱名称. Defaults to "狂牙大行动". - - Returns: - Union[str, Message]: 回复消息 - """ - user_id = str(user_id) - group_id = str(group_id) - if not CaseManager.CURRENT_CASES: - return "未收录任何武器箱" - if not case_name: - case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore - if case_name not in CaseManager.CURRENT_CASES: - return "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) # type: ignore - logger.debug(f"尝试开启武器箱: {case_name}", "开箱", user_id, group_id) - case = cn2py(case_name) - user = await OpenCasesUser.get_or_none(user_id=user_id, group_id=group_id) - if not user: - user = await OpenCasesUser.create( - user_id=user_id, group_id=group_id, open_cases_time_last=datetime.now() - ) - max_count = await get_user_max_count(user_id, group_id) - # 一天次数上限 - if user.today_open_total >= max_count: - return _handle_is_MAX_COUNT() - skin_list = await random_skin(1, case_name) - if not skin_list: - return "未抽取到任何皮肤..." - skin, rand = skin_list[0] - rand = str(rand)[:11] - case_price = 0 - if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): - case_price = case_skin.sell_min_price - user.today_open_total += 1 - user.total_count += 1 - user.open_cases_time_last = datetime.now() - await user.save( - update_fields=["today_open_total", "total_count", "open_cases_time_last"] - ) - add_count(user, skin, case_price) - ridicule_result = random.choice(RESULT_MESSAGE[skin.color]) - price_result = skin.sell_min_price - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - img_path = IMAGE_PATH / "csgo_cases" / case / f"{cn2py(name)}.jpg" - logger.info( - f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand}] 价格: {skin.sell_min_price}", - "开箱", - user_id, - group_id, - ) - await user.save() - await OpenCasesLog.create( - user_id=user_id, - group_id=group_id, - case_name=case_name, - name=skin.name, - skin_name=skin.skin_name, - is_stattrak=skin.is_stattrak, - abrasion=skin.abrasion, - color=skin.color, - price=skin.sell_min_price, - abrasion_value=rand, - create_time=datetime.now(), - ) - logger.debug(f"添加 1 条开箱日志", "开箱", user_id, group_id) - over_count = max_count - user.today_open_total - img = await draw_card(skin, rand) - return ( - f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n" - + image(img) - + f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}" - ) - - -async def open_multiple_case( - user_id: Union[int, str], group_id: Union[str, int], case_name: str, num: int = 10 -): - """多连开箱 - - Args: - user_id (int): 用户id - group_id (int): 群号 - case_name (str): 箱子名称 - num (int, optional): 数量. Defaults to 10. - - Returns: - _type_: _description_ - """ - user_id = str(user_id) - group_id = str(group_id) - if not CaseManager.CURRENT_CASES: - return "未收录任何武器箱" - if not case_name: - case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore - if case_name not in CaseManager.CURRENT_CASES: - return "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) # type: ignore - user, _ = await OpenCasesUser.get_or_create( - user_id=user_id, - group_id=group_id, - defaults={"open_cases_time_last": datetime.now()}, - ) - max_count = await get_user_max_count(user_id, group_id) - if user.today_open_total >= max_count: - return _handle_is_MAX_COUNT() - if max_count - user.today_open_total < num: - return ( - f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)" - f"\n剩余开箱次数:{max_count - user.today_open_total}" - ) - logger.debug(f"尝试开启武器箱: {case_name}", "开箱", user_id, group_id) - case = cn2py(case_name) - skin_count = {} - img_list = [] - skin_list = await random_skin(num, case_name) - if not skin_list: - return "未抽取到任何皮肤..." - total_price = 0 - log_list = [] - now = datetime.now() - user.today_open_total += num - user.total_count += num - user.open_cases_time_last = datetime.now() - await user.save( - update_fields=["today_open_total", "total_count", "open_cases_time_last"] - ) - case_price = 0 - if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): - case_price = case_skin.sell_min_price - img_w, img_h = 0, 0 - for skin, rand in skin_list: - img = await draw_card(skin, str(rand)[:11]) - img_w, img_h = img.size - total_price += skin.sell_min_price - color_name = COLOR2CN[skin.color] - if not skin_count.get(color_name): - skin_count[color_name] = 0 - skin_count[color_name] += 1 - add_count(user, skin, case_price) - img_list.append(img) - logger.info( - f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand:.11f}] 价格: {skin.sell_min_price}", - "开箱", - user_id, - group_id, - ) - log_list.append( - OpenCasesLog( - user_id=user_id, - group_id=group_id, - case_name=case_name, - name=skin.name, - skin_name=skin.skin_name, - is_stattrak=skin.is_stattrak, - abrasion=skin.abrasion, - color=skin.color, - price=skin.sell_min_price, - abrasion_value=rand, - create_time=now, - ) - ) - await user.save() - if log_list: - await OpenCasesLog.bulk_create(log_list, 10) - logger.debug(f"添加 {len(log_list)} 条开箱日志", "开箱", user_id, group_id) - img_w += 10 - img_h += 10 - w = img_w * 5 - if num < 5: - h = img_h - 10 - w = img_w * num - elif not num % 5: - h = img_h * int(num / 5) - else: - h = img_h * int(num / 5) + img_h - markImg = BuildImage( - w - 10, h - 10, img_w - 10, img_h - 10, 10, color=(255, 255, 255) - ) - for img in img_list: - markImg.paste(img, alpha=True) - over_count = max_count - user.today_open_total - result = "" - for color_name in skin_count: - result += f"[{color_name}:{skin_count[color_name]}] " - return ( - f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n" - + image(markImg) - + "\n" - + result[:-1] - + f"\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}" - ) - - -def _handle_is_MAX_COUNT() -> str: - return f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" - - -async def total_open_statistics( - user_id: Union[str, int], group_id: Union[str, int] -) -> str: - user, _ = await OpenCasesUser.get_or_create( - user_id=str(user_id), group_id=str(group_id) - ) - return ( - f"开箱总数:{user.total_count}\n" - f"今日开箱:{user.today_open_total}\n" - f"蓝色军规:{user.blue_count}\n" - f"蓝色暗金:{user.blue_st_count}\n" - f"紫色受限:{user.purple_count}\n" - f"紫色暗金:{user.purple_st_count}\n" - f"粉色保密:{user.pink_count}\n" - f"粉色暗金:{user.pink_st_count}\n" - f"红色隐秘:{user.red_count}\n" - f"红色暗金:{user.red_st_count}\n" - f"金色罕见:{user.knife_count}\n" - f"金色暗金:{user.knife_st_count}\n" - f"花费金额:{user.spend_money}\n" - f"获取金额:{user.make_money:.2f}\n" - f"最后开箱日期:{user.open_cases_time_last.date()}" - ) - - -async def group_statistics(group_id: Union[int, str]): - user_list = await OpenCasesUser.filter(group_id=str(group_id)).all() - # lan zi fen hong jin pricei - uplist = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0] - for user in user_list: - uplist[0] += user.blue_count - uplist[1] += user.blue_st_count - uplist[2] += user.purple_count - uplist[3] += user.purple_st_count - uplist[4] += user.pink_count - uplist[5] += user.pink_st_count - uplist[6] += user.red_count - uplist[7] += user.red_st_count - uplist[8] += user.knife_count - uplist[9] += user.knife_st_count - uplist[10] += user.make_money - uplist[11] += user.total_count - uplist[12] += user.today_open_total - return ( - f"群开箱总数:{uplist[11]}\n" - f"群今日开箱:{uplist[12]}\n" - f"蓝色军规:{uplist[0]}\n" - f"蓝色暗金:{uplist[1]}\n" - f"紫色受限:{uplist[2]}\n" - f"紫色暗金:{uplist[3]}\n" - f"粉色保密:{uplist[4]}\n" - f"粉色暗金:{uplist[5]}\n" - f"红色隐秘:{uplist[6]}\n" - f"红色暗金:{uplist[7]}\n" - f"金色罕见:{uplist[8]}\n" - f"金色暗金:{uplist[9]}\n" - f"花费金额:{uplist[11] * 17}\n" - f"获取金额:{uplist[10]:.2f}" - ) - - -async def get_my_knifes( - user_id: Union[str, int], group_id: Union[str, int] -) -> Union[str, MessageSegment]: - """获取我的金色 - - Args: - user_id (str): 用户id - group_id (str): 群号 - - Returns: - Union[str, MessageSegment]: 回复消息或图片 - """ - data_list = await get_old_knife(str(user_id), str(group_id)) - data_list += await OpenCasesLog.filter( - user_id=user_id, group_id=group_id, color="KNIFE" - ).all() - if not data_list: - return "您木有开出金色级别的皮肤喔" - length = len(data_list) - if length < 5: - h = 600 - w = length * 540 - elif length % 5 == 0: - h = 600 * int(length / 5) - w = 540 * 5 - else: - h = 600 * int(length / 5) + 600 - w = 540 * 5 - A = BuildImage(w, h, 540, 600) - for skin in data_list: - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - img_path = ( - IMAGE_PATH / "csgo_cases" / cn2py(skin.case_name) / f"{cn2py(name)}.jpg" - ) - knife_img = BuildImage(470, 600, 470, 470, font_size=20) - await knife_img.apaste( - BuildImage(470, 470, background=img_path if img_path.exists() else None), - (0, 0), - True, - ) - await knife_img.atext( - (5, 500), f"\t{skin.name}|{skin.skin_name}({skin.abrasion})" - ) - await knife_img.atext((5, 530), f"\t磨损:{skin.abrasion_value}") - await knife_img.atext((5, 560), f"\t价格:{skin.price}") - await A.apaste(knife_img) - return image(A) - - -async def get_old_knife(user_id: str, group_id: str) -> List[OpenCasesLog]: - """获取旧数据字段 - - Args: - user_id (str): 用户id - group_id (str): 群号 - - Returns: - List[OpenCasesLog]: 旧数据兼容 - """ - user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id) - knifes_name = user.knifes_name - data_list = [] - if knifes_name: - knifes_list = knifes_name[:-1].split(",") - for knife in knifes_list: - try: - if r := re.search( - "(.*)\|\|(.*) \| (.*)\((.*)\) 磨损:(.*), 价格:(.*)", knife - ): - case_name_py = r.group(1) - name = r.group(2) - skin_name = r.group(3) - abrasion = r.group(4) - abrasion_value = r.group(5) - price = r.group(6) - name = name.replace("(StatTrak™)", "") - data_list.append( - OpenCasesLog( - user_id=user_id, - group_id=group_id, - name=name.strip(), - case_name=case_name_py.strip(), - skin_name=skin_name.strip(), - abrasion=abrasion.strip(), - abrasion_value=abrasion_value, - price=price, - ) - ) - except Exception as e: - logger.error(f"获取兼容旧数据错误: {knife}", "我的金色", user_id, group_id, e=e) - return data_list - - -async def auto_update(): - """自动更新武器箱""" - if case_list := Config.get_config("open_cases", "DAILY_UPDATE"): - logger.debug("尝试自动更新武器箱", "更新武器箱") - if "ALL" in case_list: - case_list = CASE2ID.keys() - logger.debug(f"预计自动更新武器箱 {len(case_list)} 个", "更新武器箱") - for case_name in case_list: - logger.debug(f"开始自动更新武器箱: {case_name}", "更新武器箱") - try: - await update_skin_data(case_name) - rand = random.randint(300, 500) - logger.info(f"成功自动更新武器箱: {case_name}, 将在 {rand} 秒后再次更新下一武器箱", "更新武器箱") - await asyncio.sleep(rand) - except Exception as e: - logger.error(f"自动更新武器箱: {case_name}", e=e) +import asyncio +import random +import re +from datetime import datetime + +from nonebot_plugin_alconna import UniMessage +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.sign_user import SignUser +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import cn2py + +from .build_image import draw_card +from .config import * +from .models.open_cases_log import OpenCasesLog +from .models.open_cases_user import OpenCasesUser +from .utils import CaseManager, update_skin_data + +RESULT_MESSAGE = { + "BLUE": ["这样看着才舒服", "是自己人,大伙把刀收好", "非常舒适~"], + "PURPLE": ["还行吧,勉强接受一下下", "居然不是蓝色,太假了", "运气-1-1-1-1-1..."], + "PINK": ["开始不适....", "你妈妈买菜必涨价!涨三倍!", "你最近不适合出门,真的"], + "RED": [ + "已经非常不适", + "好兄弟你开的什么箱子啊,一般箱子不是只有蓝色的吗", + "开始拿阳寿开箱子了?", + ], + "KNIFE": [ + "你的好运我收到了,你可以去喂鲨鱼了", + "最近该吃啥就迟点啥吧,哎,好好的一个人怎么就....哎", + "众所周知,欧皇寿命极短.", + ], +} + +COLOR2NAME = { + "BLUE": "军规", + "PURPLE": "受限", + "PINK": "保密", + "RED": "隐秘", + "KNIFE": "罕见", +} + +COLOR2CN = {"BLUE": "蓝", "PURPLE": "紫", "PINK": "粉", "RED": "红", "KNIFE": "金"} + + +def add_count(user: OpenCasesUser, skin: BuffSkin, case_price: float): + if skin.color == "BLUE": + if skin.is_stattrak: + user.blue_st_count += 1 + else: + user.blue_count += 1 + elif skin.color == "PURPLE": + if skin.is_stattrak: + user.purple_st_count += 1 + else: + user.purple_count += 1 + elif skin.color == "PINK": + if skin.is_stattrak: + user.pink_st_count += 1 + else: + user.pink_count += 1 + elif skin.color == "RED": + if skin.is_stattrak: + user.red_st_count += 1 + else: + user.red_count += 1 + elif skin.color == "KNIFE": + if skin.is_stattrak: + user.knife_st_count += 1 + else: + user.knife_count += 1 + user.make_money += skin.sell_min_price + user.spend_money += int(17 + case_price) + + +async def get_user_max_count(user_id: str) -> int: + """获取用户每日最大开箱次数 + + 参数: + user_id: 用户id + + 返回: + int: 最大开箱次数 + """ + user, _ = await SignUser.get_or_create(user_id=user_id) + impression = int(user.impression) + initial_open_case_count = Config.get_config("open_cases", "INITIAL_OPEN_CASE_COUNT") + each_impression_add_count = Config.get_config( + "open_cases", "EACH_IMPRESSION_ADD_COUNT" + ) + return int(initial_open_case_count + impression / each_impression_add_count) # type: ignore + + +async def open_case( + user_id: str, group_id: str, case_name: str | None, session: EventSession +) -> UniMessage: + """开箱 + + 参数: + user_id: 用户id + group_id : 群号 + case_name: 武器箱名称. Defaults to "狂牙大行动". + session: EventSession + + 返回: + Union[str, Message]: 回复消息 + """ + user_id = str(user_id) + group_id = str(group_id) + if not CaseManager.CURRENT_CASES: + return MessageUtils.build_message("未收录任何武器箱") + if not case_name: + case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore + if case_name not in CaseManager.CURRENT_CASES: + return "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) # type: ignore + logger.debug( + f"尝试开启武器箱: {case_name}", "开箱", session=user_id, group_id=group_id + ) + case = cn2py(case_name) # type: ignore + user = await OpenCasesUser.get_or_none(user_id=user_id, group_id=group_id) + if not user: + user = await OpenCasesUser.create( + user_id=user_id, group_id=group_id, open_cases_time_last=datetime.now() + ) + max_count = await get_user_max_count(user_id) + # 一天次数上限 + if user.today_open_total >= max_count: + return MessageUtils.build_message( + f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" + ) + skin_list = await random_skin(1, case_name) # type: ignore + if not skin_list: + return MessageUtils.build_message("未抽取到任何皮肤") + skin, rand = skin_list[0] + rand = str(rand)[:11] + case_price = 0 + if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): + case_price = case_skin.sell_min_price + user.today_open_total += 1 + user.total_count += 1 + user.open_cases_time_last = datetime.now() + await user.save( + update_fields=["today_open_total", "total_count", "open_cases_time_last"] + ) + add_count(user, skin, case_price) + ridicule_result = random.choice(RESULT_MESSAGE[skin.color]) + price_result = skin.sell_min_price + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + img_path = IMAGE_PATH / "csgo_cases" / case / f"{cn2py(name)}.jpg" + logger.info( + f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand}] 价格: {skin.sell_min_price}", + "开箱", + session=session, + ) + await user.save() + await OpenCasesLog.create( + user_id=user_id, + group_id=group_id, + case_name=case_name, + name=skin.name, + skin_name=skin.skin_name, + is_stattrak=skin.is_stattrak, + abrasion=skin.abrasion, + color=skin.color, + price=skin.sell_min_price, + abrasion_value=rand, + create_time=datetime.now(), + ) + logger.debug(f"添加 1 条开箱日志", "开箱", session=session) + over_count = max_count - user.today_open_total + img = await draw_card(skin, rand) + return MessageUtils.build_message( + [ + f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n", + img, + f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}", + ] + ) + + +async def open_multiple_case( + user_id: str, + group_id: str, + case_name: str | None, + num: int = 10, + session: EventSession | None = None, +) -> UniMessage: + """多连开箱 + + 参数: + user_id (int): 用户id + group_id (int): 群号 + case_name (str): 箱子名称 + num (int, optional): 数量. Defaults to 10. + session: EventSession + + 返回: + _type_: _description_ + """ + user_id = str(user_id) + group_id = str(group_id) + if not CaseManager.CURRENT_CASES: + return MessageUtils.build_message("未收录任何武器箱") + if not case_name: + case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore + if case_name not in CaseManager.CURRENT_CASES: + return MessageUtils.build_message( + "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) + ) + user, _ = await OpenCasesUser.get_or_create( + user_id=user_id, + group_id=group_id, + defaults={"open_cases_time_last": datetime.now()}, + ) + max_count = await get_user_max_count(user_id) + if user.today_open_total >= max_count: + return MessageUtils.build_message( + f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" + ) + if max_count - user.today_open_total < num: + return MessageUtils.build_message( + f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)" + f"\n剩余开箱次数:{max_count - user.today_open_total}" + ) + logger.debug(f"尝试开启武器箱: {case_name}", "开箱", session=session) + case = cn2py(case_name) # type: ignore + skin_count = {} + img_list = [] + skin_list = await random_skin(num, case_name) # type: ignore + if not skin_list: + return MessageUtils.build_message("未抽取到任何皮肤...") + total_price = 0 + log_list = [] + now = datetime.now() + user.today_open_total += num + user.total_count += num + user.open_cases_time_last = datetime.now() + await user.save( + update_fields=["today_open_total", "total_count", "open_cases_time_last"] + ) + case_price = 0 + if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): + case_price = case_skin.sell_min_price + img_w, img_h = 0, 0 + for skin, rand in skin_list: + img = await draw_card(skin, str(rand)[:11]) + img_w, img_h = img.size + total_price += skin.sell_min_price + color_name = COLOR2CN[skin.color] + if not skin_count.get(color_name): + skin_count[color_name] = 0 + skin_count[color_name] += 1 + add_count(user, skin, case_price) + img_list.append(img) + logger.info( + f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand:.11f}] 价格: {skin.sell_min_price}", + "开箱", + session=session, + ) + log_list.append( + OpenCasesLog( + user_id=user_id, + group_id=group_id, + case_name=case_name, + name=skin.name, + skin_name=skin.skin_name, + is_stattrak=skin.is_stattrak, + abrasion=skin.abrasion, + color=skin.color, + price=skin.sell_min_price, + abrasion_value=rand, + create_time=now, + ) + ) + await user.save() + if log_list: + await OpenCasesLog.bulk_create(log_list, 10) + logger.debug(f"添加 {len(log_list)} 条开箱日志", "开箱", session=session) + img_w += 10 + img_h += 10 + w = img_w * 5 + if num < 5: + h = img_h - 10 + w = img_w * num + elif not num % 5: + h = img_h * int(num / 5) + else: + h = img_h * int(num / 5) + img_h + mark_image = BuildImage(w - 10, h - 10, color=(255, 255, 255)) + mark_image = await mark_image.auto_paste(img_list, 5, padding=20) + over_count = max_count - user.today_open_total + result = "" + for color_name in skin_count: + result += f"[{color_name}:{skin_count[color_name]}] " + return MessageUtils.build_message( + [ + f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n", + mark_image, + f"\n{result[:-1]}\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}", + ] + ) + + +async def total_open_statistics(user_id: str, group_id: str) -> str: + user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id) + return ( + f"开箱总数:{user.total_count}\n" + f"今日开箱:{user.today_open_total}\n" + f"蓝色军规:{user.blue_count}\n" + f"蓝色暗金:{user.blue_st_count}\n" + f"紫色受限:{user.purple_count}\n" + f"紫色暗金:{user.purple_st_count}\n" + f"粉色保密:{user.pink_count}\n" + f"粉色暗金:{user.pink_st_count}\n" + f"红色隐秘:{user.red_count}\n" + f"红色暗金:{user.red_st_count}\n" + f"金色罕见:{user.knife_count}\n" + f"金色暗金:{user.knife_st_count}\n" + f"花费金额:{user.spend_money}\n" + f"获取金额:{user.make_money:.2f}\n" + f"最后开箱日期:{user.open_cases_time_last.date()}" + ) + + +async def group_statistics(group_id: str): + user_list = await OpenCasesUser.filter(group_id=str(group_id)).all() + # lan zi fen hong jin pricei + uplist = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0] + for user in user_list: + uplist[0] += user.blue_count + uplist[1] += user.blue_st_count + uplist[2] += user.purple_count + uplist[3] += user.purple_st_count + uplist[4] += user.pink_count + uplist[5] += user.pink_st_count + uplist[6] += user.red_count + uplist[7] += user.red_st_count + uplist[8] += user.knife_count + uplist[9] += user.knife_st_count + uplist[10] += user.make_money + uplist[11] += user.total_count + uplist[12] += user.today_open_total + return ( + f"群开箱总数:{uplist[11]}\n" + f"群今日开箱:{uplist[12]}\n" + f"蓝色军规:{uplist[0]}\n" + f"蓝色暗金:{uplist[1]}\n" + f"紫色受限:{uplist[2]}\n" + f"紫色暗金:{uplist[3]}\n" + f"粉色保密:{uplist[4]}\n" + f"粉色暗金:{uplist[5]}\n" + f"红色隐秘:{uplist[6]}\n" + f"红色暗金:{uplist[7]}\n" + f"金色罕见:{uplist[8]}\n" + f"金色暗金:{uplist[9]}\n" + f"花费金额:{uplist[11] * 17}\n" + f"获取金额:{uplist[10]:.2f}" + ) + + +async def get_my_knifes(user_id: str, group_id: str) -> UniMessage: + """获取我的金色 + + 参数: + user_id (str): 用户id + group_id (str): 群号 + + 返回: + MessageFactory: 回复消息或图片 + """ + data_list = await get_old_knife(str(user_id), str(group_id)) + data_list += await OpenCasesLog.filter( + user_id=user_id, group_id=group_id, color="KNIFE" + ).all() + if not data_list: + return MessageUtils.build_message("您木有开出金色级别的皮肤喔...") + length = len(data_list) + if length < 5: + h = 600 + w = length * 540 + elif length % 5 == 0: + h = 600 * int(length / 5) + w = 540 * 5 + else: + h = 600 * int(length / 5) + 600 + w = 540 * 5 + A = BuildImage(w, h) + image_list = [] + for skin in data_list: + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + img_path = ( + IMAGE_PATH / "csgo_cases" / cn2py(skin.case_name) / f"{cn2py(name)}.jpg" + ) + knife_img = BuildImage(470, 600, font_size=20) + await knife_img.paste( + BuildImage(470, 470, background=img_path if img_path.exists() else None), + (0, 0), + ) + await knife_img.text( + (5, 500), f"\t{skin.name}|{skin.skin_name}({skin.abrasion})" + ) + await knife_img.text((5, 530), f"\t磨损:{skin.abrasion_value}") + await knife_img.text((5, 560), f"\t价格:{skin.price}") + image_list.append(knife_img) + A = await A.auto_paste(image_list, 5) + return MessageUtils.build_message(A) + + +async def get_old_knife(user_id: str, group_id: str) -> list[OpenCasesLog]: + """获取旧数据字段 + + 参数: + user_id (str): 用户id + group_id (str): 群号 + + 返回: + list[OpenCasesLog]: 旧数据兼容 + """ + user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id) + knifes_name = user.knifes_name + data_list = [] + if knifes_name: + knifes_list = knifes_name[:-1].split(",") + for knife in knifes_list: + try: + if r := re.search( + "(.*)\|\|(.*) \| (.*)\((.*)\) 磨损:(.*), 价格:(.*)", knife + ): + case_name_py = r.group(1) + name = r.group(2) + skin_name = r.group(3) + abrasion = r.group(4) + abrasion_value = r.group(5) + price = r.group(6) + name = name.replace("(StatTrak™)", "") + data_list.append( + OpenCasesLog( + user_id=user_id, + group_id=group_id, + name=name.strip(), + case_name=case_name_py.strip(), + skin_name=skin_name.strip(), + abrasion=abrasion.strip(), + abrasion_value=abrasion_value, + price=price, + ) + ) + except Exception as e: + logger.error( + f"获取兼容旧数据错误: {knife}", + "我的金色", + session=user_id, + group_id=group_id, + e=e, + ) + return data_list + + +async def auto_update(): + """自动更新武器箱""" + if case_list := Config.get_config("open_cases", "DAILY_UPDATE"): + logger.debug("尝试自动更新武器箱", "更新武器箱") + if "ALL" in case_list: + case_list = CASE2ID.keys() + logger.debug(f"预计自动更新武器箱 {len(case_list)} 个", "更新武器箱") + for case_name in case_list: + logger.debug(f"开始自动更新武器箱: {case_name}", "更新武器箱") + try: + await update_skin_data(case_name) + rand = random.randint(300, 500) + logger.info( + f"成功自动更新武器箱: {case_name}, 将在 {rand} 秒后再次更新下一武器箱", + "更新武器箱", + ) + await asyncio.sleep(rand) + except Exception as e: + logger.error(f"自动更新武器箱: {case_name}", e=e) diff --git a/plugins/open_cases/utils.py b/zhenxun/plugins/open_cases/utils.py old mode 100755 new mode 100644 similarity index 80% rename from plugins/open_cases/utils.py rename to zhenxun/plugins/open_cases/utils.py index d5e783e8..6fda2265 --- a/plugins/open_cases/utils.py +++ b/zhenxun/plugins/open_cases/utils.py @@ -1,643 +1,657 @@ -import asyncio -import os -import random -import re -import time -from datetime import datetime, timedelta -from typing import List, Optional, Tuple, Union - -import nonebot -from tortoise.functions import Count - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage, BuildMat -from utils.utils import broadcast_group, cn2py - -from .build_image import generate_skin -from .config import ( - CASE2ID, - CASE_BACKGROUND, - COLOR2NAME, - KNIFE2ID, - NAME2COLOR, - UpdateType, -) -from .models.buff_skin import BuffSkin -from .models.buff_skin_log import BuffSkinLog -from .models.open_cases_user import OpenCasesUser - -URL = "https://buff.163.com/api/market/goods" - -SELL_URL = "https://buff.163.com/goods" - - -driver = nonebot.get_driver() - -BASE_PATH = IMAGE_PATH / "csgo_cases" - - -class CaseManager: - - CURRENT_CASES = [] - - @classmethod - async def reload(cls): - cls.CURRENT_CASES = [] - case_list = await BuffSkin.filter(color="CASE").values_list( - "case_name", flat=True - ) - for case_name in ( - await BuffSkin.filter(case_name__not="未知武器箱") - .annotate() - .distinct() - .values_list("case_name", flat=True) - ): - for name in case_name.split(","): # type: ignore - if name not in cls.CURRENT_CASES and name in case_list: - cls.CURRENT_CASES.append(name) - - -async def update_skin_data(name: str, is_update_case_name: bool = False) -> str: - """更新箱子内皮肤数据 - - Args: - name (str): 箱子名称 - is_update_case_name (bool): 是否必定更新所属箱子 - - Returns: - _type_: _description_ - """ - type_ = None - if name in CASE2ID: - type_ = UpdateType.CASE - if name in KNIFE2ID: - type_ = UpdateType.WEAPON_TYPE - if not type_: - return "未在指定武器箱或指定武器类型内" - session = Config.get_config("open_cases", "COOKIE") - if not session: - return "BUFF COOKIE为空捏!" - weapon2case = {} - if type_ == UpdateType.WEAPON_TYPE: - db_data = await BuffSkin.filter(name__contains=name).all() - weapon2case = { - item.name + item.skin_name: item.case_name - for item in db_data - if item.case_name != "未知武器箱" - } - data_list, total = await search_skin_page(name, 1, type_) - if isinstance(data_list, str): - return data_list - for page in range(2, total + 1): - rand_time = random.randint(20, 50) - logger.debug(f"访问随机等待时间: {rand_time}", "开箱更新") - await asyncio.sleep(rand_time) - data_list_, total = await search_skin_page(name, page, type_) - if isinstance(data_list_, list): - data_list += data_list_ - create_list: List[BuffSkin] = [] - update_list: List[BuffSkin] = [] - log_list = [] - now = datetime.now() - exists_id_list = [] - new_weapon2case = {} - for skin in data_list: - if skin.skin_id in exists_id_list: - continue - if skin.case_name: - skin.case_name = ( - skin.case_name.replace("”", "") - .replace("“", "") - .replace("武器箱", "") - .replace(" ", "") - ) - skin.name = skin.name.replace("(★ StatTrak™)", "").replace("(★)", "") - exists_id_list.append(skin.skin_id) - key = skin.name + skin.skin_name - name_ = skin.name + skin.skin_name + skin.abrasion - skin.create_time = now - skin.update_time = now - if UpdateType.WEAPON_TYPE and not skin.case_name: - if is_update_case_name: - case_name = new_weapon2case.get(key) - else: - case_name = weapon2case.get(key) - if not case_name: - if case_list := await get_skin_case(skin.skin_id): - case_name = ",".join(case_list) - rand = random.randint(10, 20) - logger.debug( - f"获取 {skin.name} | {skin.skin_name} 皮肤所属武器箱: {case_name}, 访问随机等待时间: {rand}", - "开箱更新", - ) - await asyncio.sleep(rand) - if not case_name: - case_name = "未知武器箱" - else: - weapon2case[key] = case_name - new_weapon2case[key] = case_name - if skin.case_name == "反恐精英20周年": - skin.case_name = "CS20" - skin.case_name = case_name - if await BuffSkin.exists(skin_id=skin.skin_id): - update_list.append(skin) - else: - create_list.append(skin) - log_list.append( - BuffSkinLog( - name=skin.name, - case_name=skin.case_name, - skin_name=skin.skin_name, - is_stattrak=skin.is_stattrak, - abrasion=skin.abrasion, - color=skin.color, - steam_price=skin.steam_price, - weapon_type=skin.weapon_type, - buy_max_price=skin.buy_max_price, - buy_num=skin.buy_num, - sell_min_price=skin.sell_min_price, - sell_num=skin.sell_num, - sell_reference_price=skin.sell_reference_price, - create_time=now, - ) - ) - name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - for c_name_ in skin.case_name.split(","): - file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" - if not file_path.exists(): - logger.debug(f"下载皮肤 {name} 图片: {skin.img_url}...", "开箱更新") - await AsyncHttpx.download_file(skin.img_url, file_path) - rand_time = random.randint(1, 10) - await asyncio.sleep(rand_time) - logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱更新") - else: - logger.debug(f"皮肤 {name_} 图片已存在...", "开箱更新") - if create_list: - logger.debug(f"更新武器箱/皮肤: [{name}], 创建 {len(create_list)} 个皮肤!") - await BuffSkin.bulk_create(set(create_list), 10) - if update_list: - abrasion_list = [] - name_list = [] - skin_name_list = [] - for skin in update_list: - if skin.abrasion not in abrasion_list: - abrasion_list.append(skin.abrasion) - if skin.name not in name_list: - name_list.append(skin.name) - if skin.skin_name not in skin_name_list: - skin_name_list.append(skin.skin_name) - db_data = await BuffSkin.filter( - case_name__contains=name, - skin_name__in=skin_name_list, - name__in=name_list, - abrasion__in=abrasion_list, - ).all() - _update_list = [] - for data in db_data: - for skin in update_list: - if ( - data.name == skin.name - and data.skin_name == skin.skin_name - and data.abrasion == skin.abrasion - ): - data.steam_price = skin.steam_price - data.buy_max_price = skin.buy_max_price - data.buy_num = skin.buy_num - data.sell_min_price = skin.sell_min_price - data.sell_num = skin.sell_num - data.sell_reference_price = skin.sell_reference_price - data.update_time = skin.update_time - _update_list.append(data) - logger.debug(f"更新武器箱/皮肤: [{name}], 更新 {len(create_list)} 个皮肤!") - await BuffSkin.bulk_update( - _update_list, - [ - "steam_price", - "buy_max_price", - "buy_num", - "sell_min_price", - "sell_num", - "sell_reference_price", - "update_time", - ], - 10, - ) - if log_list: - logger.debug(f"更新武器箱/皮肤: [{name}], 新增 {len(log_list)} 条皮肤日志!") - await BuffSkinLog.bulk_create(log_list) - if name not in CaseManager.CURRENT_CASES: - CaseManager.CURRENT_CASES.append(name) # type: ignore - return f"更新武器箱/皮肤: [{name}] 成功, 共更新 {len(update_list)} 个皮肤, 新创建 {len(create_list)} 个皮肤!" - - -async def search_skin_page( - s_name: str, page_index: int, type_: UpdateType -) -> Tuple[Union[List[BuffSkin], str], int]: - """查询箱子皮肤 - - Args: - s_name (str): 箱子/皮肤名称 - page_index (int): 页数 - - Returns: - Union[List[BuffSkin], str]: BuffSkin - """ - logger.debug( - f"尝试访问武器箱/皮肤: [{s_name}] 页数: [{page_index}]", "开箱更新" - ) - cookie = {"session": Config.get_config("open_cases", "COOKIE")} - params = { - "game": "csgo", - "page_num": page_index, - "page_size": 80, - "_": time.time(), - "use_suggestio": 0, - } - if type_ == UpdateType.CASE: - params["itemset"] = CASE2ID[s_name] - elif type_ == UpdateType.WEAPON_TYPE: - params["category"] = KNIFE2ID[s_name] - proxy = None - if ip := Config.get_config("open_cases", "BUFF_PROXY"): - proxy = {"http://": ip, "https://": ip} - response = None - error = "" - for i in range(3): - try: - response = await AsyncHttpx.get( - URL, - proxy=proxy, - params=params, - cookies=cookie, # type: ignore - ) - if response.status_code == 200: - break - rand = random.randint(3, 7) - logger.debug( - f"尝试访问武器箱/皮肤第 {i+1} 次访问异常, code: {response.status_code}", "开箱更新" - ) - await asyncio.sleep(rand) - except Exception as e: - logger.debug(f"尝试访问武器箱/皮肤第 {i+1} 次访问发生错误 {type(e)}: {e}", "开箱更新") - error = f"{type(e)}: {e}" - if not response: - return f"访问发生异常: {error}", -1 - if response.status_code == 200: - # logger.debug(f"访问BUFF API: {response.text}", "开箱更新") - json_data = response.json() - update_data = [] - if json_data["code"] == "OK": - data_list = json_data["data"]["items"] - for data in data_list: - obj = {} - if type_ == UpdateType.CASE: - obj["case_name"] = s_name - name = data["name"] - try: - logger.debug( - f"武器箱: [{s_name}] 页数: [{page_index}] 正在收录皮肤: [{name}]...", - "开箱更新", - ) - obj["skin_id"] = str(data["id"]) - obj["buy_max_price"] = data["buy_max_price"] # 求购最大金额 - obj["buy_num"] = data["buy_num"] # 当前求购 - goods_info = data["goods_info"] - info = goods_info["info"] - tags = info["tags"] - obj["weapon_type"] = tags["type"]["localized_name"] # 枪械类型 - if obj["weapon_type"] in ["音乐盒", "印花", "探员"]: - continue - elif obj["weapon_type"] in ["匕首", "手套"]: - obj["color"] = "KNIFE" - obj["name"] = data["short_name"].split("(")[0].strip() # 名称 - elif obj["weapon_type"] in ["武器箱"]: - obj["color"] = "CASE" - obj["name"] = data["short_name"] - else: - obj["color"] = NAME2COLOR[tags["rarity"]["localized_name"]] - obj["name"] = tags["weapon"]["localized_name"] # 名称 - if obj["weapon_type"] not in ["武器箱"]: - obj["abrasion"] = tags["exterior"]["localized_name"] # 磨损 - obj["is_stattrak"] = "StatTrak" in tags["quality"]["localized_name"] # type: ignore # 是否暗金 - if not obj["color"]: - obj["color"] = NAME2COLOR[ - tags["rarity"]["localized_name"] - ] # 品质颜色 - else: - obj["abrasion"] = "CASE" - obj["skin_name"] = data["short_name"].split("|")[-1].strip() # 皮肤名称 - obj["img_url"] = goods_info["original_icon_url"] # 图片url - obj["steam_price"] = goods_info["steam_price_cny"] # steam价格 - obj["sell_min_price"] = data["sell_min_price"] # 售卖最低价格 - obj["sell_num"] = data["sell_num"] # 售卖数量 - obj["sell_reference_price"] = data["sell_reference_price"] # 参考价格 - update_data.append(BuffSkin(**obj)) - except Exception as e: - logger.error( - f"更新武器箱: [{s_name}] 皮肤: [{s_name}] 错误", - e=e, - ) - logger.debug( - f"访问武器箱: [{s_name}] 页数: [{page_index}] 成功并收录完成", - "开箱更新", - ) - return update_data, json_data["data"]["total_page"] - else: - logger.warning(f'访问BUFF失败: {json_data["error"]}') - return f'访问失败: {json_data["error"]}', -1 - return f"访问失败, 状态码: {response.status_code}", -1 - - -async def build_case_image(case_name: str) -> Union[BuildImage, str]: - """构造武器箱图片 - - Args: - case_name (str): 名称 - - Returns: - Union[BuildImage, str]: 图片 - """ - background = random.choice(os.listdir(CASE_BACKGROUND)) - background_img = BuildImage(0, 0, background=CASE_BACKGROUND / background) - if case_name: - log_list = ( - await BuffSkinLog.filter(case_name__contains=case_name) - .annotate(count=Count("id")) - .group_by("skin_name") - .values_list("skin_name", "count") - ) - skin_list_ = await BuffSkin.filter(case_name__contains=case_name).all() - skin2count = {item[0]: item[1] for item in log_list} - case = None - skin_list: List[BuffSkin] = [] - exists_name = [] - for skin in skin_list_: - if skin.color == "CASE": - case = skin - else: - name = skin.name + skin.skin_name - if name not in exists_name: - skin_list.append(skin) - exists_name.append(name) - generate_img = {} - for skin in skin_list: - skin_img = await generate_skin(skin, skin2count.get(skin.skin_name, 0)) - if skin_img: - if not generate_img.get(skin.color): - generate_img[skin.color] = [] - generate_img[skin.color].append(skin_img) - skin_image_list = [] - for color in COLOR2NAME: - if generate_img.get(color): - skin_image_list = skin_image_list + generate_img[color] - img = skin_image_list[0] - img_w, img_h = img.size - total_size = (img_w + 25) * (img_h + 10) * len(skin_image_list) # 总面积 - new_size = get_bk_image_size(total_size, background_img.size, img.size, 250) - A = BuildImage( - new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background - ) - await A.afilter("GaussianBlur", 2) - if case: - case_img = await generate_skin(case, skin2count.get(f"{case_name}武器箱", 0)) - if case_img: - A.paste(case_img, (25, 25), True) - w = 25 - h = 230 - skin_image_list.reverse() - for image in skin_image_list: - A.paste(image, (w, h), True) - w += image.w + 20 - if w + image.w - 25 > A.w: - h += image.h + 10 - w = 25 - if h + img_h + 100 < A.h: - await A.acrop((0, 0, A.w, h + img_h + 100)) - return A - else: - log_list = ( - await BuffSkinLog.filter(color="CASE") - .annotate(count=Count("id")) - .group_by("case_name") - .values_list("case_name", "count") - ) - name2count = {item[0]: item[1] for item in log_list} - skin_list = await BuffSkin.filter(color="CASE").all() - image_list: List[BuildImage] = [] - for skin in skin_list: - if img := await generate_skin(skin, name2count[skin.case_name]): - image_list.append(img) - if not image_list: - return "未收录武器箱" - w = 25 - h = 150 - img = image_list[0] - img_w, img_h = img.size - total_size = (img_w + 25) * (img_h + 10) * len(image_list) # 总面积 - - new_size = get_bk_image_size(total_size, background_img.size, img.size, 155) - A = BuildImage( - new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background - ) - await A.afilter("GaussianBlur", 2) - bk_img = BuildImage( - img_w, 120, color=(25, 25, 25, 100), font_size=60, font="CJGaoDeGuo.otf" - ) - await bk_img.atext( - (0, 0), f"已收录 {len(image_list)} 个武器箱", (255, 255, 255), center_type="center" - ) - await A.apaste(bk_img, (10, 10), True, "by_width") - for image in image_list: - A.paste(image, (w, h), True) - w += image.w + 20 - if w + image.w - 25 > A.w: - h += image.h + 10 - w = 25 - if h + img_h + 100 < A.h: - await A.acrop((0, 0, A.w, h + img_h + 100)) - return A - - -def get_bk_image_size( - total_size: int, - base_size: Tuple[int, int], - img_size: Tuple[int, int], - extra_height: int = 0, -): - """获取所需背景大小且不改变图片长宽比 - - Args: - total_size (int): 总面积 - base_size (Tuple[int, int]): 初始背景大小 - img_size (Tuple[int, int]): 贴图大小 - - Returns: - _type_: 满足所有贴图大小 - """ - bk_w, bk_h = base_size - img_w, img_h = img_size - is_add_title_size = False - left_dis = 0 - right_dis = 0 - old_size = (0, 0) - new_size = (0, 0) - ratio = 1.1 - while 1: - w_ = int(ratio * bk_w) - h_ = int(ratio * bk_h) - size = w_ * h_ - if size < total_size: - left_dis = size - else: - right_dis = size - r = w_ / (img_w + 25) - if right_dis and r - int(r) < 0.1: - if not is_add_title_size and extra_height: - total_size = int(total_size + w_ * extra_height) - is_add_title_size = True - right_dis = 0 - continue - if total_size - left_dis > right_dis - total_size: - new_size = (w_, h_) - else: - new_size = old_size - break - old_size = (w_, h_) - ratio += 0.1 - return new_size - - -async def get_skin_case(id_: str) -> Optional[List[str]]: - """获取皮肤所在箱子 - - Args: - id_ (str): 皮肤id - - Returns: - Optional[str]: 武器箱名称 - """ - url = f"{SELL_URL}/{id_}" - proxy = None - if ip := Config.get_config("open_cases", "BUFF_PROXY"): - proxy = {"http://": ip, "https://": ip} - response = await AsyncHttpx.get( - url, - proxy=proxy, - ) - if response.status_code == 200: - text = response.text - if r := re.search('', text): - case_list = [] - for s in r.group(1).split(","): - if "武器箱" in s: - case_list.append( - s.replace("”", "") - .replace("“", "") - .replace('"', "") - .replace("'", "") - .replace("武器箱", "") - .replace(" ", "") - ) - return case_list - else: - logger.debug(f"访问皮肤所属武器箱异常 url: {url} code: {response.status_code}") - return None - - -async def init_skin_trends( - name: str, skin: str, abrasion: str, day: int = 7 -) -> Optional[BuildMat]: - date = datetime.now() - timedelta(days=day) - log_list = ( - await BuffSkinLog.filter( - name__contains=name.upper(), - skin_name=skin, - abrasion__contains=abrasion, - create_time__gt=date, - is_stattrak=False, - ) - .order_by("create_time") - .limit(day * 5) - .all() - ) - if not log_list: - return None - date_list = [] - price_list = [] - for log in log_list: - date = str(log.create_time.date()) - if date not in date_list: - date_list.append(date) - price_list.append(log.sell_min_price) - bar_graph = BuildMat( - y=price_list, - mat_type="line", - title=f"{name}({skin})价格趋势({day})", - x_index=date_list, - x_min_spacing=90, - display_num=True, - x_rotate=30, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - await asyncio.get_event_loop().run_in_executor(None, bar_graph.gen_graph) - return bar_graph - - -async def reset_count_daily(): - """ - 重置每日开箱 - """ - try: - await OpenCasesUser.all().update(today_open_total=0) - await broadcast_group( - "[[_task|open_case_reset_remind]]今日开箱次数重置成功", log_cmd="开箱重置提醒" - ) - except Exception as e: - logger.error(f"开箱重置错误", e=e) - - -async def download_image(case_name: Optional[str] = None): - """下载皮肤图片 - - 参数: - case_name: 箱子名称. - """ - skin_list = ( - await BuffSkin.filter(case_name=case_name).all() - if case_name - else await BuffSkin.all() - ) - for skin in skin_list: - name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - for c_name_ in skin.case_name.split(","): - try: - file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" - if not file_path.exists(): - logger.debug( - f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}...", - "开箱图片更新", - ) - await AsyncHttpx.download_file(skin.img_url, file_path) - rand_time = random.randint(1, 5) - await asyncio.sleep(rand_time) - logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱图片更新") - else: - logger.debug(f"皮肤 {c_name_}/{skin.name} 图片已存在...", "开箱图片更新") - except Exception as e: - logger.error( - f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}", - "开箱图片更新", - e=e, - ) - - -@driver.on_startup -async def _(): - await CaseManager.reload() +import asyncio +import os +import random +import re +import time +from datetime import datetime, timedelta + +import nonebot +from tortoise.functions import Count + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType +from zhenxun.utils.utils import cn2py + +from .build_image import generate_skin +from .config import ( + CASE2ID, + CASE_BACKGROUND, + COLOR2NAME, + KNIFE2ID, + NAME2COLOR, + UpdateType, +) +from .models.buff_skin import BuffSkin +from .models.buff_skin_log import BuffSkinLog +from .models.open_cases_user import OpenCasesUser + +# from zhenxun.utils.utils import broadcast_group, cn2py + + +URL = "https://buff.163.com/api/market/goods" + +SELL_URL = "https://buff.163.com/goods" + + +driver = nonebot.get_driver() + +BASE_PATH = IMAGE_PATH / "csgo_cases" + + +class CaseManager: + + CURRENT_CASES = [] + + @classmethod + async def reload(cls): + cls.CURRENT_CASES = [] + case_list = await BuffSkin.filter(color="CASE").values_list( + "case_name", flat=True + ) + for case_name in ( + await BuffSkin.filter(case_name__not="未知武器箱") + .annotate() + .distinct() + .values_list("case_name", flat=True) + ): + for name in case_name.split(","): # type: ignore + if name not in cls.CURRENT_CASES and name in case_list: + cls.CURRENT_CASES.append(name) + + +async def update_skin_data(name: str, is_update_case_name: bool = False) -> str: + """更新箱子内皮肤数据 + + 参数: + name (str): 箱子名称 + is_update_case_name (bool): 是否必定更新所属箱子 + + 返回: + str: 回复内容 + """ + type_ = None + if name in CASE2ID: + type_ = UpdateType.CASE + if name in KNIFE2ID: + type_ = UpdateType.WEAPON_TYPE + if not type_: + return "未在指定武器箱或指定武器类型内" + session = Config.get_config("open_cases", "COOKIE") + if not session: + return "BUFF COOKIE为空捏!" + weapon2case = {} + if type_ == UpdateType.WEAPON_TYPE: + db_data = await BuffSkin.filter(name__contains=name).all() + weapon2case = { + item.name + item.skin_name: item.case_name + for item in db_data + if item.case_name != "未知武器箱" + } + data_list, total = await search_skin_page(name, 1, type_) + if isinstance(data_list, str): + return data_list + for page in range(2, total + 1): + rand_time = random.randint(20, 50) + logger.debug(f"访问随机等待时间: {rand_time}", "开箱更新") + await asyncio.sleep(rand_time) + data_list_, total = await search_skin_page(name, page, type_) + if isinstance(data_list_, list): + data_list += data_list_ + create_list: list[BuffSkin] = [] + update_list: list[BuffSkin] = [] + log_list = [] + now = datetime.now() + exists_id_list = [] + new_weapon2case = {} + for skin in data_list: + if skin.skin_id in exists_id_list: + continue + if skin.case_name: + skin.case_name = ( + skin.case_name.replace("”", "") + .replace("“", "") + .replace("武器箱", "") + .replace(" ", "") + ) + skin.name = skin.name.replace("(★ StatTrak™)", "").replace("(★)", "") + exists_id_list.append(skin.skin_id) + key = skin.name + skin.skin_name + name_ = skin.name + skin.skin_name + skin.abrasion + skin.create_time = now + skin.update_time = now + if UpdateType.WEAPON_TYPE and not skin.case_name: + if is_update_case_name: + case_name = new_weapon2case.get(key) + else: + case_name = weapon2case.get(key) + if not case_name: + if case_list := await get_skin_case(skin.skin_id): + case_name = ",".join(case_list) + rand = random.randint(10, 20) + logger.debug( + f"获取 {skin.name} | {skin.skin_name} 皮肤所属武器箱: {case_name}, 访问随机等待时间: {rand}", + "开箱更新", + ) + await asyncio.sleep(rand) + if not case_name: + case_name = "未知武器箱" + else: + weapon2case[key] = case_name + new_weapon2case[key] = case_name + if skin.case_name == "反恐精英20周年": + skin.case_name = "CS20" + skin.case_name = case_name + if await BuffSkin.exists(skin_id=skin.skin_id): + update_list.append(skin) + else: + create_list.append(skin) + log_list.append( + BuffSkinLog( + name=skin.name, + case_name=skin.case_name, + skin_name=skin.skin_name, + is_stattrak=skin.is_stattrak, + abrasion=skin.abrasion, + color=skin.color, + steam_price=skin.steam_price, + weapon_type=skin.weapon_type, + buy_max_price=skin.buy_max_price, + buy_num=skin.buy_num, + sell_min_price=skin.sell_min_price, + sell_num=skin.sell_num, + sell_reference_price=skin.sell_reference_price, + create_time=now, + ) + ) + name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + for c_name_ in skin.case_name.split(","): + file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" + if not file_path.exists(): + logger.debug(f"下载皮肤 {name} 图片: {skin.img_url}...", "开箱更新") + await AsyncHttpx.download_file(skin.img_url, file_path) + rand_time = random.randint(1, 10) + await asyncio.sleep(rand_time) + logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱更新") + else: + logger.debug(f"皮肤 {name_} 图片已存在...", "开箱更新") + if create_list: + logger.debug( + f"更新武器箱/皮肤: [{name}], 创建 {len(create_list)} 个皮肤!" + ) + await BuffSkin.bulk_create(set(create_list), 10) + if update_list: + abrasion_list = [] + name_list = [] + skin_name_list = [] + for skin in update_list: + if skin.abrasion not in abrasion_list: + abrasion_list.append(skin.abrasion) + if skin.name not in name_list: + name_list.append(skin.name) + if skin.skin_name not in skin_name_list: + skin_name_list.append(skin.skin_name) + db_data = await BuffSkin.filter( + case_name__contains=name, + skin_name__in=skin_name_list, + name__in=name_list, + abrasion__in=abrasion_list, + ).all() + _update_list = [] + for data in db_data: + for skin in update_list: + if ( + data.name == skin.name + and data.skin_name == skin.skin_name + and data.abrasion == skin.abrasion + ): + data.steam_price = skin.steam_price + data.buy_max_price = skin.buy_max_price + data.buy_num = skin.buy_num + data.sell_min_price = skin.sell_min_price + data.sell_num = skin.sell_num + data.sell_reference_price = skin.sell_reference_price + data.update_time = skin.update_time + _update_list.append(data) + logger.debug( + f"更新武器箱/皮肤: [{name}], 更新 {len(create_list)} 个皮肤!" + ) + await BuffSkin.bulk_update( + _update_list, + [ + "steam_price", + "buy_max_price", + "buy_num", + "sell_min_price", + "sell_num", + "sell_reference_price", + "update_time", + ], + 10, + ) + if log_list: + logger.debug( + f"更新武器箱/皮肤: [{name}], 新增 {len(log_list)} 条皮肤日志!" + ) + await BuffSkinLog.bulk_create(log_list) + if name not in CaseManager.CURRENT_CASES: + CaseManager.CURRENT_CASES.append(name) # type: ignore + return f"更新武器箱/皮肤: [{name}] 成功, 共更新 {len(update_list)} 个皮肤, 新创建 {len(create_list)} 个皮肤!" + + +async def search_skin_page( + s_name: str, page_index: int, type_: UpdateType +) -> tuple[list[BuffSkin] | str, int]: + """查询箱子皮肤 + + 参数: + s_name (str): 箱子/皮肤名称 + page_index (int): 页数 + + 返回: + tuple[list[BuffSkin] | str, int]: BuffSkin + """ + logger.debug( + f"尝试访问武器箱/皮肤: [{s_name}] 页数: [{page_index}]", + "开箱更新", + ) + cookie = {"session": Config.get_config("open_cases", "COOKIE")} + params = { + "game": "csgo", + "page_num": page_index, + "page_size": 80, + "_": time.time(), + "use_suggestio": 0, + } + if type_ == UpdateType.CASE: + params["itemset"] = CASE2ID[s_name] + elif type_ == UpdateType.WEAPON_TYPE: + params["category"] = KNIFE2ID[s_name] + proxy = None + if ip := Config.get_config("open_cases", "BUFF_PROXY"): + proxy = {"http://": ip, "https://": ip} + response = None + error = "" + for i in range(3): + try: + response = await AsyncHttpx.get( + URL, + proxy=proxy, + params=params, + cookies=cookie, # type: ignore + ) + if response.status_code == 200: + break + rand = random.randint(3, 7) + logger.debug( + f"尝试访问武器箱/皮肤第 {i+1} 次访问异常, code: {response.status_code}", + "开箱更新", + ) + await asyncio.sleep(rand) + except Exception as e: + logger.debug( + f"尝试访问武器箱/皮肤第 {i+1} 次访问发生错误 {type(e)}: {e}", "开箱更新" + ) + error = f"{type(e)}: {e}" + if not response: + return f"访问发生异常: {error}", -1 + if response.status_code == 200: + # logger.debug(f"访问BUFF API: {response.text}", "开箱更新") + json_data = response.json() + update_data = [] + if json_data["code"] == "OK": + data_list = json_data["data"]["items"] + for data in data_list: + obj = {} + if type_ == UpdateType.CASE: + obj["case_name"] = s_name + name = data["name"] + try: + logger.debug( + f"武器箱: [{s_name}] 页数: [{page_index}] 正在收录皮肤: [{name}]...", + "开箱更新", + ) + obj["skin_id"] = str(data["id"]) + obj["buy_max_price"] = data["buy_max_price"] # 求购最大金额 + obj["buy_num"] = data["buy_num"] # 当前求购 + goods_info = data["goods_info"] + info = goods_info["info"] + tags = info["tags"] + obj["weapon_type"] = tags["type"]["localized_name"] # 枪械类型 + if obj["weapon_type"] in ["音乐盒", "印花", "探员"]: + continue + elif obj["weapon_type"] in ["匕首", "手套"]: + obj["color"] = "KNIFE" + obj["name"] = data["short_name"].split("(")[0].strip() # 名称 + elif obj["weapon_type"] in ["武器箱"]: + obj["color"] = "CASE" + obj["name"] = data["short_name"] + else: + obj["color"] = NAME2COLOR[tags["rarity"]["localized_name"]] + obj["name"] = tags["weapon"]["localized_name"] # 名称 + if obj["weapon_type"] not in ["武器箱"]: + obj["abrasion"] = tags["exterior"]["localized_name"] # 磨损 + obj["is_stattrak"] = "StatTrak" in tags["quality"]["localized_name"] # type: ignore # 是否暗金 + if not obj["color"]: + obj["color"] = NAME2COLOR[ + tags["rarity"]["localized_name"] + ] # 品质颜色 + else: + obj["abrasion"] = "CASE" + obj["skin_name"] = ( + data["short_name"].split("|")[-1].strip() + ) # 皮肤名称 + obj["img_url"] = goods_info["original_icon_url"] # 图片url + obj["steam_price"] = goods_info["steam_price_cny"] # steam价格 + obj["sell_min_price"] = data["sell_min_price"] # 售卖最低价格 + obj["sell_num"] = data["sell_num"] # 售卖数量 + obj["sell_reference_price"] = data[ + "sell_reference_price" + ] # 参考价格 + update_data.append(BuffSkin(**obj)) + except Exception as e: + logger.error( + f"更新武器箱: [{s_name}] 皮肤: [{s_name}] 错误", + e=e, + ) + logger.debug( + f"访问武器箱: [{s_name}] 页数: [{page_index}] 成功并收录完成", + "开箱更新", + ) + return update_data, json_data["data"]["total_page"] + else: + logger.warning(f'访问BUFF失败: {json_data["error"]}') + return f'访问失败: {json_data["error"]}', -1 + return f"访问失败, 状态码: {response.status_code}", -1 + + +async def build_case_image(case_name: str | None) -> BuildImage | str: + """构造武器箱图片 + + 参数: + case_name (str): 名称 + + 返回: + BuildImage | str: 图片 + """ + background = random.choice(os.listdir(CASE_BACKGROUND)) + background_img = BuildImage(0, 0, background=CASE_BACKGROUND / background) + if case_name: + log_list = ( + await BuffSkinLog.filter(case_name__contains=case_name) + .annotate(count=Count("id")) + .group_by("skin_name") + .values_list("skin_name", "count") + ) + skin_list_ = await BuffSkin.filter(case_name__contains=case_name).all() + skin2count = {item[0]: item[1] for item in log_list} + case = None + skin_list: list[BuffSkin] = [] + exists_name = [] + for skin in skin_list_: + if skin.color == "CASE": + case = skin + else: + name = skin.name + skin.skin_name + if name not in exists_name: + skin_list.append(skin) + exists_name.append(name) + generate_img = {} + for skin in skin_list: + skin_img = await generate_skin(skin, skin2count.get(skin.skin_name, 0)) + if skin_img: + if not generate_img.get(skin.color): + generate_img[skin.color] = [] + generate_img[skin.color].append(skin_img) + skin_image_list = [] + for color in COLOR2NAME: + if generate_img.get(color): + skin_image_list = skin_image_list + generate_img[color] + img = skin_image_list[0] + img_w, img_h = img.size + total_size = (img_w + 25) * (img_h + 10) * len(skin_image_list) # 总面积 + new_size = get_bk_image_size(total_size, background_img.size, img.size, 250) + A = BuildImage( + new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background + ) + await A.filter("GaussianBlur", 2) + if case: + case_img = await generate_skin( + case, skin2count.get(f"{case_name}武器箱", 0) + ) + if case_img: + await A.paste(case_img, (25, 25)) + w = 25 + h = 230 + skin_image_list.reverse() + for image in skin_image_list: + await A.paste(image, (w, h)) + w += image.width + 20 + if w + image.width - 25 > A.width: + h += image.height + 10 + w = 25 + if h + img_h + 100 < A.height: + await A.crop((0, 0, A.width, h + img_h + 100)) + return A + else: + log_list = ( + await BuffSkinLog.filter(color="CASE") + .annotate(count=Count("id")) + .group_by("case_name") + .values_list("case_name", "count") + ) + name2count = {item[0]: item[1] for item in log_list} + skin_list = await BuffSkin.filter(color="CASE").all() + image_list: list[BuildImage] = [] + for skin in skin_list: + if img := await generate_skin(skin, name2count[skin.case_name]): + image_list.append(img) + if not image_list: + return "未收录武器箱" + w = 25 + h = 150 + img = image_list[0] + img_w, img_h = img.size + total_size = (img_w + 25) * (img_h + 10) * len(image_list) # 总面积 + + new_size = get_bk_image_size(total_size, background_img.size, img.size, 155) + A = BuildImage( + new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background + ) + await A.filter("GaussianBlur", 2) + bk_img = BuildImage( + img_w, 120, color=(25, 25, 25, 100), font_size=60, font="CJGaoDeGuo.otf" + ) + await bk_img.text( + (0, 0), + f"已收录 {len(image_list)} 个武器箱", + (255, 255, 255), + center_type="center", + ) + await A.paste(bk_img, (10, 10), "width") + for image in image_list: + await A.paste(image, (w, h)) + w += image.width + 20 + if w + image.width - 25 > A.width: + h += image.height + 10 + w = 25 + if h + img_h + 100 < A.height: + await A.crop((0, 0, A.width, h + img_h + 100)) + return A + + +def get_bk_image_size( + total_size: int, + base_size: tuple[int, int], + img_size: tuple[int, int], + extra_height: int = 0, +) -> tuple[int, int]: + """获取所需背景大小且不改变图片长宽比 + + 参数: + total_size (int): 总面积 + base_size (Tuple[int, int]): 初始背景大小 + img_size (Tuple[int, int]): 贴图大小 + + 返回: + tuple[int, int]: 满足所有贴图大小 + """ + bk_w, bk_h = base_size + img_w, img_h = img_size + is_add_title_size = False + left_dis = 0 + right_dis = 0 + old_size = (0, 0) + new_size = (0, 0) + ratio = 1.1 + while 1: + w_ = int(ratio * bk_w) + h_ = int(ratio * bk_h) + size = w_ * h_ + if size < total_size: + left_dis = size + else: + right_dis = size + r = w_ / (img_w + 25) + if right_dis and r - int(r) < 0.1: + if not is_add_title_size and extra_height: + total_size = int(total_size + w_ * extra_height) + is_add_title_size = True + right_dis = 0 + continue + if total_size - left_dis > right_dis - total_size: + new_size = (w_, h_) + else: + new_size = old_size + break + old_size = (w_, h_) + ratio += 0.1 + return new_size + + +async def get_skin_case(id_: str) -> list[str] | None: + """获取皮肤所在箱子 + + 参数: + id_ (str): 皮肤id + + 返回: + list[str] | None: 武器箱名称 + """ + url = f"{SELL_URL}/{id_}" + proxy = None + if ip := Config.get_config("open_cases", "BUFF_PROXY"): + proxy = {"http://": ip, "https://": ip} + response = await AsyncHttpx.get( + url, + proxy=proxy, + ) + if response.status_code == 200: + text = response.text + if r := re.search('', text): + case_list = [] + for s in r.group(1).split(","): + if "武器箱" in s: + case_list.append( + s.replace("”", "") + .replace("“", "") + .replace('"', "") + .replace("'", "") + .replace("武器箱", "") + .replace(" ", "") + ) + return case_list + else: + logger.debug(f"访问皮肤所属武器箱异常 url: {url} code: {response.status_code}") + return None + + +async def init_skin_trends( + name: str, skin: str, abrasion: str, day: int = 7 +) -> BuildImage | None: + date = datetime.now() - timedelta(days=day) + log_list = ( + await BuffSkinLog.filter( + name__contains=name.upper(), + skin_name=skin, + abrasion__contains=abrasion, + create_time__gt=date, + is_stattrak=False, + ) + .order_by("create_time") + .limit(day * 5) + .all() + ) + if not log_list: + return None + date_list = [] + price_list = [] + for log in log_list: + date = str(log.create_time.date()) + if date not in date_list: + date_list.append(date) + price_list.append(log.sell_min_price) + graph = BuildMat(MatType.LINE) + graph.data = price_list + graph.title = f"{name}({skin})价格趋势({day})" + graph.x_index = date_list + return await graph.build() + + +async def reset_count_daily(): + """ + 重置每日开箱 + """ + try: + await OpenCasesUser.all().update(today_open_total=0) + # await broadcast_group( + # "[[_task|open_case_reset_remind]]今日开箱次数重置成功", + # log_cmd="开箱重置提醒", + # ) + except Exception as e: + logger.error(f"开箱重置错误", e=e) + + +async def download_image(case_name: str | None = None): + """下载皮肤图片 + + 参数: + case_name: 箱子名称. + """ + skin_list = ( + await BuffSkin.filter(case_name=case_name).all() + if case_name + else await BuffSkin.all() + ) + for skin in skin_list: + name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + for c_name_ in skin.case_name.split(","): + try: + pass + # file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" + # if not file_path.exists(): + # logger.debug( + # f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}...", + # "开箱图片更新", + # ) + # await AsyncHttpx.download_file(skin.img_url, file_path) + # rand_time = random.randint(1, 5) + # await asyncio.sleep(rand_time) + # logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱图片更新") + # else: + # logger.debug( + # f"皮肤 {c_name_}/{skin.name} 图片已存在...", "开箱图片更新" + # ) + except Exception as e: + logger.error( + f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}", + "开箱图片更新", + e=e, + ) + + +@driver.on_startup +async def _(): + await CaseManager.reload() diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py new file mode 100644 index 00000000..1d319093 --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -0,0 +1,177 @@ +import re +import time + +import ujson as json +from nonebot import on_message +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Hyper, Image, UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils + +from .information_container import InformationContainer +from .parse_url import parse_bili_url + +__plugin_meta__ = PluginMetadata( + name="B站内容解析", + description="B站内容解析", + usage=""" + usage: + 被动监听插件,解析B站视频、直播、专栏,支持小程序卡片及文本链接,5分钟内不解析相同内容 + """.strip(), + extra=PluginExtraData( + author="leekooyo", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="_task", + key="DEFAULT_BILIBILI_PARSE", + value=True, + default_value=True, + help="被动 B站转发解析 进群默认开关状态", + type=bool, + ) + ], + tasks=[Task(module="bilibili_parse", name="b站转发解析")], + ).dict(), +) + + +async def _rule(session: EventSession) -> bool: + return not await TaskInfo.is_block("bilibili_parse", session.id3 or session.id2) + + +_matcher = on_message(priority=1, block=False, rule=_rule) + +_tmp = {} + + +@_matcher.handle() +async def _(session: EventSession, message: UniMsg): + information_container = InformationContainer() + # 判断文本消息内容是否相关 + match = None + # 判断文本消息和小程序的内容是否指向一个b站链接 + get_url = None + # 判断文本消息是否包含视频相关内容 + vd_flag = False + # 设定时间阈值,阈值之下不会解析重复内容 + repet_second = 300 + # 尝试解析小程序消息 + data = message[0] + if isinstance(data, Hyper) and data.raw: + try: + data = json.loads(data.raw) + except (IndexError, KeyError): + data = None + if data: + # 获取相关数据 + meta_data = data.get("meta", {}) + news_value = meta_data.get("news", {}) + detail_1_value = meta_data.get("detail_1", {}) + qqdocurl_value = detail_1_value.get("qqdocurl", {}) + jumpUrl_value = news_value.get("jumpUrl", {}) + get_url = (qqdocurl_value if qqdocurl_value else jumpUrl_value).split("?")[ + 0 + ] + # 解析文本消息 + elif msg := message.extract_plain_text(): + # 消息中含有视频号 + if "bv" in msg.lower() or "av" in msg.lower(): + match = re.search(r"((?=(?:bv|av))([A-Za-z0-9]+))", msg, re.IGNORECASE) + vd_flag = True + + # 消息中含有b23的链接,包括视频、专栏、动态、直播 + elif "https://b23.tv" in msg: + match = re.search(r"https://b23\.tv/[^?\s]+", msg, re.IGNORECASE) + + # 检查消息中是否含有直播、专栏、动态链接 + elif any( + keyword in msg + for keyword in [ + "https://live.bilibili.com/", + "https://www.bilibili.com/read/", + "https://www.bilibili.com/opus/", + "https://t.bilibili.com/", + ] + ): + pattern = r"https://(live|www\.bilibili\.com/read|www\.bilibili\.com/opus|t\.bilibili\.com)/[^?\s]+" + match = re.search(pattern, msg) + + # 匹配成功,则获取链接 + if match: + if vd_flag: + number = match.group(1) + get_url = f"https://www.bilibili.com/video/{number}" + else: + get_url = match.group() + + if get_url: + # 将链接统一发送给处理函数 + vd_info, live_info, vd_url, live_url, image_info, image_url = ( + await parse_bili_url(get_url, information_container) + ) + if vd_info: + # 判断一定时间内是否解析重复内容,或者是第一次解析 + if ( + vd_url in _tmp.keys() and time.time() - _tmp[vd_url] > repet_second + ) or vd_url not in _tmp.keys(): + pic = vd_info.get("pic", "") # 封面 + aid = vd_info.get("aid", "") # av号 + title = vd_info.get("title", "") # 标题 + author = vd_info.get("owner", {}).get("name", "") # UP主 + reply = vd_info.get("stat", {}).get("reply", "") # 回复 + favorite = vd_info.get("stat", {}).get("favorite", "") # 收藏 + coin = vd_info.get("stat", {}).get("coin", "") # 投币 + like = vd_info.get("stat", {}).get("like", "") # 点赞 + danmuku = vd_info.get("stat", {}).get("danmaku", "") # 弹幕 + ctime = vd_info["ctime"] + date = time.strftime("%Y-%m-%d", time.localtime(ctime)) + logger.info(f"解析bilibili转发 {vd_url}", "b站解析", session=session) + _tmp[vd_url] = time.time() + _path = TEMP_PATH / f"{aid}.jpg" + await AsyncHttpx.download_file(pic, _path) + await MessageUtils.build_message( + [ + _path, + f"av{aid}\n标题:{title}\nUP:{author}\n上传日期:{date}\n回复:{reply},收藏:{favorite},投币:{coin}\n点赞:{like},弹幕:{danmuku}\n{vd_url}", + ] + ).send() + + elif live_info: + if ( + live_url in _tmp.keys() and time.time() - _tmp[live_url] > repet_second + ) or live_url not in _tmp.keys(): + uid = live_info.get("uid", "") # 主播uid + title = live_info.get("title", "") # 直播间标题 + description = live_info.get("description", "") # 简介,可能会出现标签 + user_cover = live_info.get("user_cover", "") # 封面 + keyframe = live_info.get("keyframe", "") # 关键帧画面 + live_time = live_info.get("live_time", "") # 开播时间 + area_name = live_info.get("area_name", "") # 分区 + parent_area_name = live_info.get("parent_area_name", "") # 父分区 + logger.info(f"解析bilibili转发 {live_url}", "b站解析", session=session) + _tmp[live_url] = time.time() + await MessageUtils.build_message( + [ + Image(url=user_cover), + f"开播用户:https://space.bilibili.com/{uid}\n开播时间:{live_time}\n直播分区:{parent_area_name}——>{area_name}\n标题:{title}\n简介:{description}\n直播截图:\n", + Image(url=keyframe), + f"{live_url}", + ] + ).send() + elif image_info: + if ( + image_url in _tmp.keys() + and time.time() - _tmp[image_url] > repet_second + ) or image_url not in _tmp.keys(): + logger.info(f"解析bilibili转发 {image_url}", "b站解析", session=session) + _tmp[image_url] = time.time() + await image_info.send() diff --git a/zhenxun/plugins/parse_bilibili/get_image.py b/zhenxun/plugins/parse_bilibili/get_image.py new file mode 100644 index 00000000..bbc005c5 --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/get_image.py @@ -0,0 +1,108 @@ +import os +import re + +from nonebot_plugin_alconna import UniMessage + +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncPlaywright +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.user_agent import get_user_agent_str + + +async def resize(path: str): + """调整图像大小的异步函数 + + 参数: + path (str): 图像文件路径 + """ + A = BuildImage(background=path) + await A.resize(0.5) + await A.save(path) + + +async def get_image(url) -> UniMessage | None: + """获取Bilibili链接的截图,并返回base64格式的图片 + + 参数: + url (str): Bilibili链接 + + 返回: + Image: Image + """ + cv_match = None + opus_match = None + t_opus_match = None + + cv_number = None + opus_number = None + t_opus_number = None + + # 提取cv、opus、t_opus的编号 + url = url.split("?")[0] + cv_match = re.search(r"read/cv([A-Za-z0-9]+)", url, re.IGNORECASE) + opus_match = re.search(r"opus/([A-Za-z0-9]+)", url, re.IGNORECASE) + t_opus_match = re.search(r"https://t\.bilibili\.com/(\d+)", url, re.IGNORECASE) + + if cv_match: + cv_number = cv_match.group(1) + elif opus_match: + opus_number = opus_match.group(1) + elif t_opus_match: + t_opus_number = t_opus_match.group(1) + + screenshot_path = None + + # 根据编号构建保存路径 + if cv_number: + screenshot_path = f"{TEMP_PATH}/bilibili_cv_{cv_number}.png" + elif opus_number: + screenshot_path = f"{TEMP_PATH}/bilibili_opus_{opus_number}.png" + elif t_opus_number: + screenshot_path = f"{TEMP_PATH}/bilibili_opus_{t_opus_number}.png" + # t.bilibili.com和https://www.bilibili.com/opus在内容上是一样的,为便于维护,调整url至https://www.bilibili.com/opus/ + url = f"https://www.bilibili.com/opus/{t_opus_number}" + + if screenshot_path: + try: + # 如果文件不存在,进行截图 + if not os.path.exists(screenshot_path): + # 创建页面 + # random.choice(),从列表中随机抽取一个对象 + user_agent = get_user_agent_str() + try: + async with AsyncPlaywright.new_page() as page: + await page.set_viewport_size({"width": 5120, "height": 2560}) + # 设置请求拦截器 + await page.route( + re.compile(r"(\.png$)|(\.jpg$)"), + lambda route: route.abort(), + ) + # 访问链接 + await page.goto(url, wait_until="networkidle", timeout=10000) + # 根据不同的链接结构,设置对应的CSS选择器 + if cv_number: + css = "#app > div" + elif opus_number or t_opus_number: + css = "#app > div.opus-detail > div.bili-opus-view" + # 点击对应的元素 + await page.click(css) + # 查询目标元素 + div = await page.query_selector(css) + # 对目标元素进行截图 + await div.screenshot( # type: ignore + path=screenshot_path, + timeout=100000, + animations="disabled", + type="png", + ) + # 异步执行调整截图大小的操作 + await resize(screenshot_path) + except Exception as e: + logger.warning(f"尝试解析bilibili转发失败", e=e) + return None + return MessageUtils.build_message(screenshot_path) + except Exception as e: + logger.error(f"尝试解析bilibili转发失败", e=e) + return None diff --git a/zhenxun/plugins/parse_bilibili/information_container.py b/zhenxun/plugins/parse_bilibili/information_container.py new file mode 100644 index 00000000..1cbf651f --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/information_container.py @@ -0,0 +1,60 @@ +class InformationContainer: + def __init__( + self, + vd_info=None, + live_info=None, + vd_url=None, + live_url=None, + image_info=None, + image_url=None, + ): + self._vd_info = vd_info + self._live_info = live_info + self._vd_url = vd_url + self._live_url = live_url + self._image_info = image_info + self._image_url = image_url + + @property + def vd_info(self): + return self._vd_info + + @property + def live_info(self): + return self._live_info + + @property + def vd_url(self): + return self._vd_url + + @property + def live_url(self): + return self._live_url + + @property + def image_info(self): + return self._image_info + + @property + def image_url(self): + return self._image_url + + def update(self, updates): + """ + 更新多个信息的通用方法 + Args: + updates (dict): 包含信息类型和对应新值的字典 + """ + for info_type, new_value in updates.items(): + if hasattr(self, f"_{info_type}"): + setattr(self, f"_{info_type}", new_value) + + def get_information(self): + return ( + self.vd_info, + self.live_info, + self.vd_url, + self.live_url, + self.image_info, + self.image_url, + ) diff --git a/zhenxun/plugins/parse_bilibili/parse_url.py b/zhenxun/plugins/parse_bilibili/parse_url.py new file mode 100644 index 00000000..b4e2a1fe --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/parse_url.py @@ -0,0 +1,65 @@ +import aiohttp +from bilireq import live, video + +from zhenxun.utils.user_agent import get_user_agent + +from .get_image import get_image +from .information_container import InformationContainer + + +async def parse_bili_url(get_url: str, information_container: InformationContainer): + """解析Bilibili链接,获取相关信息 + + 参数: + get_url (str): 待解析的Bilibili链接 + information_container (InformationContainer): 信息容器 + + 返回: + dict: 包含解析得到的信息的字典 + """ + response_url = "" + + # 去除链接末尾的斜杠 + if get_url[-1] == "/": + get_url = get_url[:-1] + + # 发起HTTP请求,获取重定向后的链接 + async with aiohttp.ClientSession(headers=get_user_agent()) as session: + async with session.get( + get_url, + timeout=7, + ) as response: + response_url = str(response.url).split("?")[0] + + # 去除重定向后链接末尾的斜杠 + if response_url[-1] == "/": + response_url = response_url[:-1] + + # 根据不同类型的链接进行处理 + if response_url.startswith( + ("https://www.bilibili.com/video", "https://m.bilibili.com/video/") + ): + vd_url = response_url + vid = vd_url.split("/")[-1] + vd_info = await video.get_video_base_info(vid) + information_container.update({"vd_info": vd_info, "vd_url": vd_url}) + + elif response_url.startswith("https://live.bilibili.com"): + live_url = response_url + liveid = live_url.split("/")[-1] + live_info = await live.get_room_info_by_id(liveid) + information_container.update({"live_info": live_info, "live_url": live_url}) + + elif response_url.startswith("https://www.bilibili.com/read"): + cv_url = response_url + image_info = await get_image(cv_url) + information_container.update({"image_info": image_info, "image_url": cv_url}) + + elif response_url.startswith( + ("https://www.bilibili.com/opus", "https://t.bilibili.com") + ): + opus_url = response_url + image_info = await get_image(opus_url) + information_container.update({"image_info": image_info, "image_url": opus_url}) + + return information_container.get_information() diff --git a/zhenxun/plugins/pid_search.py b/zhenxun/plugins/pid_search.py new file mode 100644 index 00000000..97fc4d40 --- /dev/null +++ b/zhenxun/plugins/pid_search.py @@ -0,0 +1,125 @@ +from asyncio.exceptions import TimeoutError + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import change_pixiv_image_links +from zhenxun.utils.withdraw_manage import WithdrawManager + +__plugin_meta__ = PluginMetadata( + name="pid搜索", + description="通过 pid 搜索图片", + usage=""" + usage: + 通过 pid 搜索图片 + 指令: + p搜 [pid] + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1").dict(), +) + + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net", +} + +_matcher = on_alconna( + Alconna("p搜", Args["pid", str]), aliases={"P搜"}, priority=5, block=True +) + + +@_matcher.handle() +async def _(pid: Match[int]): + if pid.available: + _matcher.set_path_arg("pid", pid.result) + + +@_matcher.got_path("pid", prompt="需要查询的图片PID是?或发送'取消'结束搜索") +async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): + url = Config.get_config("hibiapi", "HIBIAPI") + "/api/pixiv/illust" + if pid in ["取消", "算了"]: + await Text("已取消操作...").finish() + if not pid.isdigit(): + await Text("pid必须为数字...").finish() + for _ in range(3): + try: + data = ( + await AsyncHttpx.get( + url, + params={"id": pid}, + timeout=5, + ) + ).json() + except TimeoutError: + pass + except Exception as e: + logger.error( + f"pixiv pid 搜索发生了一些错误...", + arparma.header_result, + session=session, + e=e, + ) + await MessageUtils.build_message(f"发生了一些错误..{type(e)}:{e}").finish() + else: + if data.get("error"): + await MessageUtils.build_message(data["error"]["user_message"]).finish( + reply_to=True + ) + data = data["illust"] + if not data["width"] and not data["height"]: + await MessageUtils.build_message( + f"没有搜索到 PID:{pid} 的图片" + ).finish(reply_to=True) + pid = data["id"] + title = data["title"] + author = data["user"]["name"] + author_id = data["user"]["id"] + image_list = [] + try: + image_list.append(data["meta_single_page"]["original_image_url"]) + except KeyError: + for image_url in data["meta_pages"]: + image_list.append(image_url["image_urls"]["original"]) + for i, img_url in enumerate(image_list): + img_url = change_pixiv_image_links(img_url) + if not await AsyncHttpx.download_file( + img_url, + TEMP_PATH / f"pid_search_{session.id1}_{i}.png", + headers=headers, + ): + await MessageUtils.build_message("图片下载失败了...").finish( + reply_to=True + ) + tmp = "" + if session.id3 or session.id2: + tmp = "\n【注】将在30后撤回......" + receipt = await MessageUtils.build_message( + [ + f"title:{title}\n" + f"pid:{pid}\n" + f"author:{author}\n" + f"author_id:{author_id}\n", + TEMP_PATH / f"pid_search_{session.id1}_{i}.png", + f"{tmp}", + ] + ).send() + logger.info( + f" 查询图片 PID:{pid}", arparma.header_result, session=session + ) + if session.id3 or session.id2: + await WithdrawManager.withdraw_message( + bot, receipt.msg_ids[0]["message_id"], 30 # type: ignore + ) + break + else: + await Text("图片下载失败了...").send(reply_to=True) diff --git a/zhenxun/plugins/pix_gallery/__init__.py b/zhenxun/plugins/pix_gallery/__init__.py new file mode 100644 index 00000000..d549a249 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/__init__.py @@ -0,0 +1,62 @@ +from pathlib import Path +from typing import Tuple + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "hibiapi", + "HIBIAPI", + "https://api.obfs.dev", + help="如果没有自建或其他hibiapi请不要修改", + default_value="https://api.obfs.dev", +) +Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pximg.cf", help="Pixiv反向代理") +Config.add_plugin_config( + "pix", + "PIX_IMAGE_SIZE", + "master", + help="PIX图库下载的画质 可能的值:original:原图,master:缩略图(加快发送速度)", + default_value="master", +) +Config.add_plugin_config( + "pix", + "SEARCH_HIBIAPI_BOOKMARKS", + 5000, + help="最低收藏,PIX使用HIBIAPI搜索图片时达到最低收藏才会添加至图库", + default_value=5000, + type=int, +) +Config.add_plugin_config( + "pix", + "WITHDRAW_PIX_MESSAGE", + (0, 1), + help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + default_value=(0, 1), + type=Tuple[int, int], +) +Config.add_plugin_config( + "pix", + "PIX_OMEGA_PIXIV_RATIO", + (10, 0), + help="PIX图库 与 额外图库OmegaPixivIllusts 混合搜索的比例 参1:PIX图库 参2:OmegaPixivIllusts扩展图库(没有此图库请设置为0)", + default_value=(10, 0), + type=Tuple[int, int], +) +Config.add_plugin_config( + "pix", "TIMEOUT", 10, help="下载图片超时限制(秒)", default_value=10, type=int +) + +Config.add_plugin_config( + "pix", + "SHOW_INFO", + True, + help="是否显示图片的基本信息,如PID等", + default_value=True, + type=bool, +) + +Config.set_name("pix", "PIX图库") + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/pix_gallery/_data_source.py b/zhenxun/plugins/pix_gallery/_data_source.py similarity index 77% rename from plugins/pix_gallery/_data_source.py rename to zhenxun/plugins/pix_gallery/_data_source.py index 0b9f92d8..7e9db221 100644 --- a/plugins/pix_gallery/_data_source.py +++ b/zhenxun/plugins/pix_gallery/_data_source.py @@ -1,37 +1,27 @@ import asyncio import math -import platform from asyncio.exceptions import TimeoutError from asyncio.locks import Semaphore from copy import deepcopy -from typing import List, Optional, Tuple +from pathlib import Path import aiofiles -from asyncpg.exceptions import UniqueViolationError +from httpx import ConnectError -from configs.config import Config -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.utils import change_img_md5, change_pixiv_image_links +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import change_img_md5, change_pixiv_image_links from ._model.omega_pixiv_illusts import OmegaPixivIllusts from ._model.pixiv import Pixiv -try: - import ujson as json -except ModuleNotFoundError: - import json - -# if str(platform.system()).lower() == "windows": -# policy = asyncio.WindowsSelectorEventLoopPolicy() -# asyncio.set_event_loop_policy(policy) - headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", + "Referer": "https://www.pixiv.net/", } HIBIAPI = Config.get_config("hibiapi", "HIBIAPI") @@ -41,13 +31,17 @@ HIBIAPI = HIBIAPI[:-1] if HIBIAPI[-1] == "/" else HIBIAPI async def start_update_image_url( - current_keyword: List[str], black_pid: List[str] -) -> "int, int": - """ - 开始更新图片url - :param current_keyword: 关键词 - :param black_pid: 黑名单pid - :return: pid数量和图片数量 + current_keyword: list[str], black_pid: list[str], is_pid: bool +) -> tuple[int, int]: + """开始更新图片url + + 参数: + current_keyword: 关键词 + black_pid: 黑名单pid + is_pid: pid强制更新不受限制 + + 返回: + tuple[int, int]: pid数量和图片数量 """ global HIBIAPI pid_count = 0 @@ -69,7 +63,9 @@ async def start_update_image_url( params = {"word": keyword, "page": page} tasks.append( asyncio.ensure_future( - search_image(url, keyword, params, semaphore, page, black_pid) + search_image( + url, keyword, params, semaphore, page, black_pid, is_pid + ) ) ) if keyword.startswith("pid:"): @@ -87,17 +83,22 @@ async def search_image( params: dict, semaphore: Semaphore, page: int = 1, - black: List[str] = None, -) -> "int, int": - """ - 搜索图片 - :param url: 搜索url - :param keyword: 关键词 - :param params: params参数 - :param semaphore: semaphore - :param page: 页面 - :param black: pid黑名单 - :return: pid数量和图片数量 + black: list[str] = [], + is_pid: bool = False, +) -> tuple[int, int]: + """搜索图片 + + 参数: + url: 搜索url + keyword: 关键词 + params: params参数 + semaphore: semaphore + page: 页面 + black: pid黑名单 + is_pid: pid强制更新不受限制 + + 返回: + tuple[int, int]: pid数量和图片数量 """ tmp_pid = [] pic_count = 0 @@ -149,7 +150,7 @@ async def search_image( ) and len(img_urls) < 10 and _check_black(img_urls, black) - ): + ) or is_pid: img_data[pid] = { "pid": pid, "title": title, @@ -191,12 +192,15 @@ async def search_image( return pid_count, pic_count -async def get_image(img_url: str, user_id: int) -> Optional[str]: - """ - 下载图片 - :param img_url: - :param user_id: - :return: 图片名称 +async def get_image(img_url: str, user_id: str) -> Path | None: + """下载图片 + + 参数: + img_url: 图片url + user_id: 用户id + + 返回: + Path | None: 图片名称 """ if "https://www.pixiv.net/artworks" in img_url: pid = img_url.rsplit("/", maxsplit=1)[-1] @@ -249,14 +253,19 @@ async def get_image(img_url: str, user_id: int) -> Optional[str]: return TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg" except TimeoutError: logger.warning(f"PIX:{img_url} 图片下载超时...") - pass + except ConnectError: + logger.warning(f"PIX:{img_url} 图片下载连接失败...") return None async def uid_pid_exists(id_: str) -> bool: - """ - 检测 pid/uid 是否有效 - :param id_: pid/uid + """检测 pid/uid 是否有效 + + 参数: + id_: pid/uid + + 返回: + bool: 是否有效 """ if id_.startswith("uid:"): url = f"{HIBIAPI}/api/pixiv/member" @@ -271,10 +280,14 @@ async def uid_pid_exists(id_: str) -> bool: return True -async def get_keyword_num(keyword: str) -> Tuple[int, int, int, int, int]: - """ - 查看图片相关 tag 数量 - :param keyword: 关键词tag +async def get_keyword_num(keyword: str) -> tuple[int, int, int, int, int]: + """查看图片相关 tag 数量 + + 参数: + keyword: 关键词tag + + 返回: + tuple[int, int, int, int, int]: 总数, r18数, Omg图库总数, Omg图库色图数, Omg图库r18数 """ count, r18_count = await Pixiv.get_keyword_num(keyword.split()) count_, setu_count, r18_count_ = await OmegaPixivIllusts.get_keyword_num( @@ -283,11 +296,12 @@ async def get_keyword_num(keyword: str) -> Tuple[int, int, int, int, int]: return count, r18_count, count_, setu_count, r18_count_ -async def remove_image(pid: int, img_p: Optional[str]): - """ - 删除置顶图片 - :param pid: pid - :param img_p: 图片 p 如 p0,p1 等 +async def remove_image(pid: int, img_p: str | None): + """删除置顶图片 + + 参数: + pid: pid + img_p: 图片 p 如 p0,p1 等 """ if img_p: if "p" not in img_p: @@ -298,14 +312,18 @@ async def remove_image(pid: int, img_p: Optional[str]): await Pixiv.filter(pid=pid).delete() -def gen_keyword_pic( - _pass_keyword: List[str], not_pass_keyword: List[str], is_superuser: bool -): - """ - 已通过或未通过的所有关键词/uid/pid - :param _pass_keyword: 通过列表 - :param not_pass_keyword: 未通过列表 - :param is_superuser: 是否超级用户 +async def gen_keyword_pic( + _pass_keyword: list[str], not_pass_keyword: list[str], is_superuser: bool +) -> BuildImage: + """已通过或未通过的所有关键词/uid/pid + + 参数: + _pass_keyword: 通过列表 + not_pass_keyword: 未通过列表 + is_superuser: 是否超级用户 + + 返回: + BuildImage: 数据图片 """ _keyword = [ x @@ -362,7 +380,8 @@ def gen_keyword_pic( A = BuildImage(img_width, 1100) for x in list(img_data.keys()): if img_data[x]["data"]: - img = BuildImage(img_data[x]["width"] * 200, 1100, 200, 1100, font_size=40) + # img = BuildImage(img_data[x]["width"] * 200, 1100, 200, 1100, font_size=40) + img = BuildImage(img_data[x]["width"] * 200, 1100, font_size=40) start_index = 0 end_index = 40 total_index = img_data[x]["width"] * 40 @@ -372,30 +391,33 @@ def gen_keyword_pic( key_str = "\n".join( [key for key in img_data[x]["data"][start_index:end_index]] ) - tmp.text((10, 100), key_str) + await tmp.text((10, 100), key_str) if x.find("_n") == -1: - text_img.text((24, 24), "已收录") + await text_img.text((24, 24), "已收录") else: - text_img.text((24, 24), "待收录") - tmp.paste(text_img, (0, 0)) + await text_img.text((24, 24), "待收录") + await tmp.paste(text_img, (0, 0)) start_index += 40 end_index = ( end_index + 40 if end_index + 40 <= total_index else total_index ) background_img = BuildImage(200, 1100, color="#FFE4C4") - background_img.paste(tmp, (1, 1)) - img.paste(background_img) - A.paste(img, (current_width, 0)) + await background_img.paste(tmp, (1, 1)) + await img.paste(background_img) + await A.paste(img, (current_width, 0)) current_width += img_data[x]["width"] * 200 - return A.pic2bs4() + return A -def _check_black(img_urls: List[str], black: List[str]) -> bool: - """ - 检测pid是否在黑名单中 - :param img_urls: 图片img列表 - :param black: 黑名单 - :return: +def _check_black(img_urls: list[str], black: list[str]) -> bool: + """检测pid是否在黑名单中 + + 参数: + img_urls: 图片img列表 + black: 黑名单 + + 返回: + bool: 是否在黑名单中 """ for b in black: for img_url in img_urls: diff --git a/plugins/pix_gallery/_model/__init__.py b/zhenxun/plugins/pix_gallery/_model/__init__.py similarity index 100% rename from plugins/pix_gallery/_model/__init__.py rename to zhenxun/plugins/pix_gallery/_model/__init__.py diff --git a/plugins/pix_gallery/_model/omega_pixiv_illusts.py b/zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py similarity index 72% rename from plugins/pix_gallery/_model/omega_pixiv_illusts.py rename to zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py index c80c5f45..17e2156c 100644 --- a/plugins/pix_gallery/_model/omega_pixiv_illusts.py +++ b/zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py @@ -1,9 +1,8 @@ -from typing import List, Optional, Tuple from tortoise import fields from tortoise.contrib.postgres.functions import Random -from services.db_context import Model +from zhenxun.services.db_context import Model class OmegaPixivIllusts(Model): @@ -39,21 +38,20 @@ class OmegaPixivIllusts(Model): @classmethod async def query_images( cls, - keywords: Optional[List[str]] = None, - uid: Optional[int] = None, - pid: Optional[int] = None, - nsfw_tag: Optional[int] = 0, + keywords: list[str] | None = None, + uid: int | None = None, + pid: int | None = None, + nsfw_tag: int | None = 0, num: int = 100, - ) -> List["OmegaPixivIllusts"]: - """ - 说明: - 查找符合条件的图片 + ) -> list["OmegaPixivIllusts"]: + """查找符合条件的图片 + 参数: - :param keywords: 关键词 - :param uid: 画师uid - :param pid: 图片pid - :param nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18 - :param num: 获取图片数量 + keywords: 关键词 + uid: 画师uid + pid: 图片pid + nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18 + num: 获取图片数量 """ if not num: return [] @@ -72,13 +70,12 @@ class OmegaPixivIllusts(Model): @classmethod async def get_keyword_num( - cls, tags: Optional[List[str]] = None - ) -> Tuple[int, int, int]: - """ - 说明: - 获取相关关键词(keyword, tag)在图库中的数量 + cls, tags: list[str] | None = None + ) -> tuple[int, int, int]: + """获取相关关键词(keyword, tag)在图库中的数量 + 参数: - :param tags: 关键词/Tag + tags: 关键词/Tag """ query = cls if tags: diff --git a/plugins/pix_gallery/_model/pixiv.py b/zhenxun/plugins/pix_gallery/_model/pixiv.py similarity index 71% rename from plugins/pix_gallery/_model/pixiv.py rename to zhenxun/plugins/pix_gallery/_model/pixiv.py index 4af995a5..3451781d 100644 --- a/plugins/pix_gallery/_model/pixiv.py +++ b/zhenxun/plugins/pix_gallery/_model/pixiv.py @@ -1,9 +1,7 @@ -from typing import List, Optional, Tuple - from tortoise import fields from tortoise.contrib.postgres.functions import Random -from services.db_context import Model +from zhenxun.services.db_context import Model class Pixiv(Model): @@ -43,21 +41,20 @@ class Pixiv(Model): @classmethod async def query_images( cls, - keywords: Optional[List[str]] = None, - uid: Optional[int] = None, - pid: Optional[int] = None, - r18: Optional[int] = 0, + keywords: list[str] | None = None, + uid: int | None = None, + pid: int | None = None, + r18: int | None = 0, num: int = 100, - ) -> List[Optional["Pixiv"]]: - """ - 说明: - 查找符合条件的图片 + ) -> list["Pixiv"]: + """查找符合条件的图片 + 参数: - :param keywords: 关键词 - :param uid: 画师uid - :param pid: 图片pid - :param r18: 是否r18,0:非r18 1:r18 2:混合 - :param num: 查找图片的数量 + keywords: 关键词 + uid: 画师uid + pid: 图片pid + r18: 是否r18,0:非r18 1:r18 2:混合 + num: 查找图片的数量 """ if not num: return [] @@ -77,12 +74,11 @@ class Pixiv(Model): return await query.all() # type: ignore @classmethod - async def get_keyword_num(cls, tags: Optional[List[str]] = None) -> Tuple[int, int]: - """ - 说明: - 获取相关关键词(keyword, tag)在图库中的数量 + async def get_keyword_num(cls, tags: list[str] | None = None) -> tuple[int, int]: + """获取相关关键词(keyword, tag)在图库中的数量 + 参数: - :param tags: 关键词/Tag + tags: 关键词/Tag """ query = cls if tags: diff --git a/plugins/pix_gallery/_model/pixiv_keyword_user.py b/zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py similarity index 80% rename from plugins/pix_gallery/_model/pixiv_keyword_user.py rename to zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py index 829a1040..5de544a5 100644 --- a/plugins/pix_gallery/_model/pixiv_keyword_user.py +++ b/zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py @@ -1,8 +1,6 @@ -from typing import List, Set, Tuple - from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class PixivKeywordUser(Model): @@ -23,11 +21,8 @@ class PixivKeywordUser(Model): table_description = "pixiv关键词数据表" @classmethod - async def get_current_keyword(cls) -> Tuple[List[str], List[str]]: - """ - 说明: - 获取当前通过与未通过的关键词 - """ + async def get_current_keyword(cls) -> tuple[list[str], list[str]]: + """获取当前通过与未通过的关键词""" pass_keyword = [] not_pass_keyword = [] for data in await cls.all().values_list("keyword", "is_pass"): @@ -38,11 +33,8 @@ class PixivKeywordUser(Model): return pass_keyword, not_pass_keyword @classmethod - async def get_black_pid(cls) -> List[str]: - """ - 说明: - 获取黑名单PID - """ + async def get_black_pid(cls) -> list[str]: + """获取黑名单PID""" black_pid = [] keyword_list = await cls.filter(user_id="114514").values_list( "keyword", flat=True diff --git a/zhenxun/plugins/pix_gallery/pix.py b/zhenxun/plugins/pix_gallery/pix.py new file mode 100644 index 00000000..2f8d25c3 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix.py @@ -0,0 +1,247 @@ +import random + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.withdraw_manage import WithdrawManager + +from ._data_source import get_image +from ._model.omega_pixiv_illusts import OmegaPixivIllusts +from ._model.pixiv import Pixiv + +__plugin_meta__ = PluginMetadata( + name="PIX", + description="这里是PIX图库!", + usage=""" + 指令: + pix ?*[tags]: 通过 tag 获取相似图片,不含tag时随机抽取 + pid [uid]: 通过uid获取图片 + pix pid[pid]: 查看图库中指定pid图片 + 示例:pix 萝莉 白丝 + 示例:pix 萝莉 白丝 10 (10为数量) + 示例:pix #02 (当tag只有1个tag且为数字时,使用#标记,否则将被判定为数量) + 示例:pix 34582394 (查询指定uid图片) + 示例:pix pid:12323423 (查询指定pid图片) + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + superuser_help=""" + 指令: + pix -s ?*[tags]: 通过tag获取色图,不含tag时随机 + pix -r ?*[tags]: 通过tag获取r18图,不含tag时随机 + """, + menu_type="来点好康的", + limits=[BaseBlock(result="您有PIX图片正在处理,请稍等...")], + configs=[ + RegisterConfig( + key="MAX_ONCE_NUM2FORWARD", + value=None, + help="单次发送的图片数量达到指定值时转发为合并消息", + default_value=None, + type=int, + ), + RegisterConfig( + key="ALLOW_GROUP_SETU", + value=False, + help="允许非超级用户使用-s参数", + default_value=False, + type=bool, + ), + RegisterConfig( + key="ALLOW_GROUP_R18", + value=False, + help="允许非超级用户使用-r参数", + default_value=False, + type=bool, + ), + ], + ).dict(), +) + +# pix = on_command("pix", aliases={"PIX", "Pix"}, priority=5, block=True) + +_matcher = on_alconna( + Alconna( + "pix", + Args["tags?", str] / "\n", + Option("-s", action=store_true, help_text="色图"), + Option("-r", action=store_true, help_text="r18"), + ), + priority=5, + block=True, +) + +PIX_RATIO = None +OMEGA_RATIO = None + + +@_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]): + global PIX_RATIO, OMEGA_RATIO + gid = session.id3 or session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if PIX_RATIO is None: + pix_omega_pixiv_ratio = Config.get_config("pix", "PIX_OMEGA_PIXIV_RATIO") + PIX_RATIO = pix_omega_pixiv_ratio[0] / ( + pix_omega_pixiv_ratio[0] + pix_omega_pixiv_ratio[1] + ) + OMEGA_RATIO = 1 - PIX_RATIO + num = 1 + # keyword = arg.extract_plain_text().strip() + keyword = "" + spt = tags.result.split() if tags.available else [] + if arparma.find("s"): + nsfw_tag = 1 + elif arparma.find("r"): + nsfw_tag = 2 + else: + nsfw_tag = 0 + if session.id1 not in bot.config.superusers: + if (nsfw_tag == 1 and not Config.get_config("pix", "ALLOW_GROUP_SETU")) or ( + nsfw_tag == 2 and not Config.get_config("pix", "ALLOW_GROUP_R18") + ): + await MessageUtils.build_message( + "你不能看这些噢,这些都是是留给管理员看的..." + ).finish() + if (n := len(spt)) == 1: + if str(spt[0]).isdigit() and int(spt[0]) < 100: + num = int(spt[0]) + keyword = "" + elif spt[0].startswith("#"): + keyword = spt[0][1:] + elif n > 1: + if str(spt[-1]).isdigit(): + num = int(spt[-1]) + if num > 10: + if session.id1 not in bot.config.superusers or ( + session.id1 in bot.config.superusers and num > 30 + ): + num = random.randint(1, 10) + await MessageUtils.build_message( + f"太贪心了,就给你发 {num}张 好了" + ).send() + spt = spt[:-1] + keyword = " ".join(spt) + pix_num = int(num * PIX_RATIO) + 15 if PIX_RATIO != 0 else 0 + omega_num = num - pix_num + 15 + if str(keyword).isdigit(): + if num == 1: + pix_num = 15 + omega_num = 15 + all_image = await Pixiv.query_images( + uid=int(keyword), num=pix_num, r18=1 if nsfw_tag == 2 else 0 + ) + await OmegaPixivIllusts.query_images( + uid=int(keyword), num=omega_num, nsfw_tag=nsfw_tag + ) + elif keyword.lower().startswith("pid"): + pid = keyword.replace("pid", "").replace(":", "").replace(":", "") + if not str(pid).isdigit(): + await MessageUtils.build_message("PID必须是数字...").finish(reply_to=True) + all_image = await Pixiv.query_images( + pid=int(pid), r18=1 if nsfw_tag == 2 else 0 + ) + if not all_image: + all_image = await OmegaPixivIllusts.query_images( + pid=int(pid), nsfw_tag=nsfw_tag + ) + num = len(all_image) + else: + tmp = await Pixiv.query_images( + spt, r18=1 if nsfw_tag == 2 else 0, num=pix_num + ) + await OmegaPixivIllusts.query_images(spt, nsfw_tag=nsfw_tag, num=omega_num) + tmp_ = [] + all_image = [] + for x in tmp: + if x.pid not in tmp_: + all_image.append(x) + tmp_.append(x.pid) + if not all_image: + await MessageUtils.build_message( + f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片..." + ).finish(reply_to=True) + msg_list = [] + for _ in range(num): + img_url = None + author = None + if not all_image: + await MessageUtils.build_message("坏了...发完了,没图了...").finish() + img = random.choice(all_image) + all_image.remove(img) # type: ignore + if isinstance(img, OmegaPixivIllusts): + img_url = img.url + author = img.uname + elif isinstance(img, Pixiv): + img_url = img.img_url + author = img.author + pid = img.pid + title = img.title + uid = img.uid + if img_url: + _img = await get_image(img_url, session.id1) + if _img: + if Config.get_config("pix", "SHOW_INFO"): + msg_list.append( + MessageUtils.build_message( + [ + f"title:{title}\n" + f"author:{author}\n" + f"PID:{pid}\nUID:{uid}\n", + _img, + ] + ) + ) + else: + msg_list.append(_img) + logger.info( + f" 查看PIX图库PID: {pid}", arparma.header_result, session=session + ) + else: + msg_list.append(MessageUtils.build_message("这张图似乎下载失败了")) + logger.info( + f" 查看PIX图库PID: {pid},下载图片出错", + arparma.header_result, + session=session, + ) + if ( + Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") + and num >= Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") + and gid + ): + for msg in msg_list: + receipt = await msg.send() + if receipt: + message_id = receipt.msg_ids[0]["message_id"] + await WithdrawManager.withdraw_message( + bot, + str(message_id), + Config.get_config("pix", "WITHDRAW_PIX_MESSAGE"), + session, + ) + else: + for msg in msg_list: + receipt = await msg.send() + if receipt: + message_id = receipt.msg_ids[0]["message_id"] + await WithdrawManager.withdraw_message( + bot, + message_id, + Config.get_config("pix", "WITHDRAW_PIX_MESSAGE"), + session, + ) diff --git a/zhenxun/plugins/pix_gallery/pix_add_keyword.py b/zhenxun/plugins/pix_gallery/pix_add_keyword.py new file mode 100644 index 00000000..452213e3 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix_add_keyword.py @@ -0,0 +1,135 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from ._data_source import uid_pid_exists +from ._model.pixiv import Pixiv +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="PIX添加", + description="PIX关键词/UID/PID添加管理", + usage=""" + 指令: + 添加pix关键词 [Tag]: 添加一个pix搜索收录Tag + pix添加 uid [uid]: 添加一个pix搜索收录uid + pix添加 pid [pid]: 添加一个pix收录pid + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_add_matcher = on_alconna( + Alconna("添加pix关键词", Args["keyword", str]), priority=5, block=True +) + +_uid_matcher = on_alconna( + Alconna( + "pix添加", + Args["add_type", ["uid", "pid"]]["id", str], + Option("-f", action=store_true, help_text="强制收录不检查是否存在"), + ), + priority=5, + block=True, +) + +_black_matcher = on_alconna( + Alconna("添加pix黑名单", Args["pid", str]), priority=5, block=True +) + + +@_add_matcher.handle() +async def _(bot: Bot, session: EventSession, keyword: str, arparma: Arparma): + group_id = session.id3 or session.id2 or -1 + if not await PixivKeywordUser.exists(keyword=keyword): + await PixivKeywordUser.create( + user_id=str(session.id1), + group_id=str(group_id), + keyword=keyword, + is_pass=str(session.id1) in bot.config.superusers, + ) + text = f"已成功添加pixiv搜图关键词:{keyword}" + if session.id1 not in bot.config.superusers: + text += ",请等待管理员通过该关键词!" + await MessageUtils.build_message(text).send(reply_to=True) + logger.info( + f"添加了pixiv搜图关键词: {keyword}", arparma.header_result, session=session + ) + else: + await MessageUtils.build_message(f"该关键词 {keyword} 已存在...").send() + + +@_uid_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, add_type: str, id: str): + group_id = session.id3 or session.id2 or -1 + exists_flag = True + if arparma.find("f") and session.id1 in bot.config.superusers: + exists_flag = False + word = None + if add_type == "uid": + word = f"uid:{id}" + else: + word = f"pid:{id}" + if await Pixiv.get_or_none(pid=int(id), img_p="p0"): + await MessageUtils.build_message(f"该PID:{id}已存在...").finish( + reply_to=True + ) + if not await uid_pid_exists(word) and exists_flag: + await MessageUtils.build_message( + "画师或作品不存在或搜索正在CD,请稍等..." + ).finish(reply_to=True) + if not await PixivKeywordUser.exists(keyword=word): + await PixivKeywordUser.create( + user_id=session.id1, + group_id=str(group_id), + keyword=word, + is_pass=session.id1 in bot.config.superusers, + ) + text = f"已成功添加pixiv搜图UID/PID:{id}" + if session.id1 not in bot.config.superusers: + text += ",请等待管理员通过该关键词!" + await MessageUtils.build_message(text).send(reply_to=True) + else: + await MessageUtils.build_message(f"该UID/PID:{id} 已存在...").send() + + +@_black_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): + img_p = "" + if "p" in pid: + img_p = pid.split("p")[-1] + pid = pid.replace("_", "") + pid = pid[: pid.find("p")] + if not pid.isdigit: + await MessageUtils.build_message("PID必须全部是数字!").finish(reply_to=True) + if not await PixivKeywordUser.exists( + keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}" + ): + await PixivKeywordUser.create( + user_id=114514, + group_id=114514, + keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}", + is_pass=session.id1 in bot.config.superusers, + ) + await MessageUtils.build_message(f"已添加PID:{pid} 至黑名单中...").send() + logger.info( + f" 添加了pixiv搜图黑名单 PID:{pid}", arparma.header_result, session=session + ) + else: + await MessageUtils.build_message( + f"PID:{pid} 已添加黑名单中,添加失败..." + ).send() diff --git a/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py new file mode 100644 index 00000000..9a8f2ea7 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py @@ -0,0 +1,218 @@ +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + At, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +from ._data_source import remove_image +from ._model.pixiv import Pixiv +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="PIX删除", + description="PIX关键词/UID/PID添加管理", + usage=""" + 指令: + pix关键词 [y/n] [关键词/pid/uid] + 删除pix关键词 ['pid'/'uid'/'keyword'] [关键词/pid/uid] + 删除pix图片 *[pid] + 示例:pix关键词 y 萝莉 + 示例:pix关键词 y 12312312 uid + 示例:pix关键词 n 12312312 pid + 示例:删除pix关键词 keyword 萝莉 + 示例:删除pix关键词 uid 123123123 + 示例:删除pix关键词 pid 123123 + 示例:删除pix图片 4223442 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER + ).dict(), +) + + +_pass_matcher = on_alconna( + Alconna( + "pix关键词", Args["status", ["y", "n"]]["keyword", str]["type?", ["uid", "pid"]] + ), + permission=SUPERUSER, + priority=1, + block=True, +) + +_del_matcher = on_alconna( + Alconna("删除pix关键词", Args["type", ["pid", "uid", "keyword"]]["keyword", str]), + permission=SUPERUSER, + priority=1, + block=True, +) + +_del_pic_matcher = on_alconna( + Alconna( + "删除pix图片", + Args["pid", str], + Option("-b|--black", action=store_true, help_text=""), + ), + permission=SUPERUSER, + priority=1, + block=True, +) + + +@_pass_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + status: str, + keyword: str, + type: Match[str], +): + tmp = {"group": {}, "private": {}} + flag = status == "y" + if type.available: + if type.result == "uid": + keyword = f"uid:{keyword}" + else: + keyword = f"pid:{keyword}" + if not keyword[4:].isdigit(): + await MessageUtils.build_message(f"{keyword} 非全数字...").finish( + reply_to=True + ) + data = await PixivKeywordUser.get_or_none(keyword=keyword) + user_id = 0 + group_id = 0 + if data: + data.is_pass = flag + await data.save(update_fields=["is_pass"]) + user_id, group_id = data.user_id, data.group_id + if not user_id: + await MessageUtils.build_message( + f"未找到关键词/UID:{keyword},请检查关键词/UID是否存在..." + ).finish(reply_to=True) + if flag: + if group_id == -1: + if not tmp["private"].get(user_id): + tmp["private"][user_id] = {"keyword": [keyword]} + else: + tmp["private"][user_id]["keyword"].append(keyword) + else: + if not tmp["group"].get(group_id): + tmp["group"][group_id] = {} + if not tmp["group"][group_id].get(user_id): + tmp["group"][group_id][user_id] = {"keyword": [keyword]} + else: + tmp["group"][group_id][user_id]["keyword"].append(keyword) + await MessageUtils.build_message( + f"已成功{'通过' if flag else '拒绝'}搜图关键词:{keyword}..." + ).send() + for user in tmp["private"]: + text = ",".join(tmp["private"][user]["keyword"]) + await PlatformUtils.send_message( + bot, + user, + None, + f"你的关键词/UID/PID {text} 已被管理员通过,将在下一次进行更新...", + ) + # await bot.send_private_msg( + # user_id=user, + # message=f"你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新...", + # ) + for group in tmp["group"]: + for user in tmp["group"][group]: + text = ",".join(tmp["group"][group][user]["keyword"]) + await PlatformUtils.send_message( + bot, + None, + group_id=group, + message=MessageUtils.build_message( + [ + At(flag="user", target=user), + "你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新...", + ] + ), + ) + logger.info( + f" 通过了pixiv搜图关键词/UID: {keyword}", arparma.header_result, session=session + ) + + +@_del_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, type: str, keyword: str): + if type != "keyword": + keyword = f"{type}:{keyword}" + if data := await PixivKeywordUser.get_or_none(keyword=keyword): + await data.delete() + await MessageUtils.build_message( + f"删除搜图关键词/UID:{keyword} 成功..." + ).send() + logger.info( + f" 删除了pixiv搜图关键词: {keyword}", arparma.header_result, session=session + ) + else: + await MessageUtils.build_message( + f"未查询到搜索关键词/UID/PID:{keyword},删除失败!" + ).send() + + +@_del_pic_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, keyword: str): + msg = "" + black_pid = "" + flag = arparma.find("black") + img_p = None + if "p" in keyword: + img_p = keyword.split("p")[-1] + keyword = keyword.replace("_", "") + keyword = keyword[: keyword.find("p")] + elif "ugoira" in keyword: + img_p = keyword.split("ugoira")[-1] + keyword = keyword.replace("_", "") + keyword = keyword[: keyword.find("ugoira")] + if keyword.isdigit(): + if await Pixiv.query_images(pid=int(keyword), r18=2): + if await remove_image(int(keyword), img_p): + msg += f'{keyword}{f"_p{img_p}" if img_p else ""},' + if flag: + if await PixivKeywordUser.exists( + keyword=f"black:{keyword}{f'_p{img_p}' if img_p else ''}" + ): + await PixivKeywordUser.create( + user_id="114514", + group_id="114514", + keyword=f"black:{keyword}{f'_p{img_p}' if img_p else ''}", + is_pass=False, + ) + black_pid += f'{keyword}{f"_p{img_p}" if img_p else ""},' + logger.info( + f" 删除了PIX图片 PID:{keyword}{f'_p{img_p}' if img_p else ''}", + arparma.header_result, + session=session, + ) + else: + await MessageUtils.build_message( + f"PIX:图片pix:{keyword}{f'_p{img_p}' if img_p else ''} 不存在...无法删除.." + ).send() + else: + await MessageUtils.build_message(f"PID必须为数字!pid:{keyword}").send( + reply_to=True + ) + await MessageUtils.build_message(f"PIX:成功删除图片:{msg[:-1]}").send() + if flag: + await MessageUtils.build_message( + f"成功图片PID加入黑名单:{black_pid[:-1]}" + ).send() diff --git a/zhenxun/plugins/pix_gallery/pix_show_info.py b/zhenxun/plugins/pix_gallery/pix_show_info.py new file mode 100644 index 00000000..cb1cbf2a --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix_show_info.py @@ -0,0 +1,85 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from ._data_source import gen_keyword_pic, get_keyword_num +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="查看pix图库", + description="让我看看管理员私藏了多少货", + usage=""" + 指令: + 我的pix关键词 + 显示pix关键词 + 查看pix图库 ?[tag]: 查看指定tag图片数量,为空时查看整个图库 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_my_matcher = on_alconna(Alconna("我的pix关键词"), priority=5, block=True) + +_show_matcher = on_alconna(Alconna("显示pix关键词"), priority=5, block=True) + +_pix_matcher = on_alconna( + Alconna("查看pix图库", Args["keyword?", str]), priority=5, block=True +) + + +@_my_matcher.handle() +async def _(arparma: Arparma, session: EventSession): + data = await PixivKeywordUser.filter(user_id=session.id1).values_list( + "keyword", flat=True + ) + if not data: + await MessageUtils.build_message("您目前没有提供任何Pixiv搜图关键字...").finish( + reply_to=True + ) + await MessageUtils.build_message(f"您目前提供的如下关键字:\n\t" + ",".join(data)).send() # type: ignore + logger.info("查看我的pix关键词", arparma.header_result, session=session) + + +@_show_matcher.handle() +async def _(bot: Bot, arparma: Arparma, session: EventSession): + _pass_keyword, not_pass_keyword = await PixivKeywordUser.get_current_keyword() + if _pass_keyword or not_pass_keyword: + image = await gen_keyword_pic( + _pass_keyword, not_pass_keyword, session.id1 in bot.config.superusers + ) + await MessageUtils.build_message(image).send() # type: ignore + else: + if session.id1 in bot.config.superusers: + await MessageUtils.build_message( + f"目前没有已收录或待收录的搜索关键词..." + ).send() + else: + await MessageUtils.build_message(f"目前没有已收录的搜索关键词...").send() + + +@_pix_matcher.handle() +async def _(bot: Bot, arparma: Arparma, session: EventSession, keyword: Match[str]): + _keyword = "" + if keyword.available: + _keyword = keyword.result + count, r18_count, count_, setu_count, r18_count_ = await get_keyword_num(_keyword) + await MessageUtils.build_message( + f"PIX图库:{_keyword}\n" + f"总数:{count + r18_count}\n" + f"美图:{count}\n" + f"R18:{r18_count}\n" + f"---------------\n" + f"Omega图库:{_keyword}\n" + f"总数:{count_ + setu_count + r18_count_}\n" + f"美图:{count_}\n" + f"色图:{setu_count}\n" + f"R18:{r18_count_}" + ).send() + logger.info("查看pix图库", arparma.header_result, session=session) diff --git a/plugins/pix_gallery/pix_update.py b/zhenxun/plugins/pix_gallery/pix_update.py old mode 100755 new mode 100644 similarity index 60% rename from plugins/pix_gallery/pix_update.py rename to zhenxun/plugins/pix_gallery/pix_update.py index ce19604f..b0f209dc --- a/plugins/pix_gallery/pix_update.py +++ b/zhenxun/plugins/pix_gallery/pix_update.py @@ -1,207 +1,225 @@ -import asyncio -import os -import re -import time -from pathlib import Path -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Message -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.utils import is_number - -from ._data_source import start_update_image_url -from ._model.omega_pixiv_illusts import OmegaPixivIllusts -from ._model.pixiv import Pixiv -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "pix检查更新 [Superuser]" -__plugin_usage__ = """ -usage: - 更新pix收录的所有或指定数量的 关键词/uid/pid - 指令: - 更新pix关键词 *[keyword/uid/pid] [num=max]: 更新仅keyword/uid/pid或全部 - pix检测更新:检测从未更新过的uid和pid - 示例:更新pix关键词keyword - 示例:更新pix关键词uid 10 -""".strip() -__plugin_des__ = "pix图库收录数据检查更新" -__plugin_cmd__ = ["更新pix关键词 *[keyword/uid/pid] [num=max]", "pix检测更新"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -start_update = on_command( - "更新pix关键词", aliases={"更新pix关键字"}, permission=SUPERUSER, priority=1, block=True -) - -check_not_update_uid_pid = on_command( - "pix检测更新", - aliases={"pix检查更新"}, - permission=SUPERUSER, - priority=1, - block=True, -) - -check_omega = on_command("检测omega图库", permission=SUPERUSER, priority=1, block=True) - - -@start_update.handle() -async def _(arg: Message = CommandArg()): - msg_sp = arg.extract_plain_text().strip().split() - _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() - _pass_keyword.reverse() - black_pid = await PixivKeywordUser.get_black_pid() - _keyword = [ - x - for x in _pass_keyword - if not x.startswith("uid:") - and not x.startswith("pid:") - and not x.startswith("black:") - ] - _uid = [x for x in _pass_keyword if x.startswith("uid:")] - _pid = [x for x in _pass_keyword if x.startswith("pid:")] - num = 9999 - msg = msg_sp[0] if len(msg_sp) else "" - if len(msg_sp) == 2: - if is_number(msg_sp[1]): - num = int(msg_sp[1]) - else: - await start_update.finish("参数错误...第二参数必须为数字") - if num < 10000: - keyword_str = ",".join( - _keyword[: num if num < len(_keyword) else len(_keyword)] - ) - uid_str = ",".join(_uid[: num if num < len(_uid) else len(_uid)]) - pid_str = ",".join(_pid[: num if num < len(_pid) else len(_pid)]) - if msg.lower() == "pid": - update_lst = _pid - info = f"开始更新Pixiv搜图PID:\n{pid_str}" - elif msg.lower() == "uid": - update_lst = _uid - info = f"开始更新Pixiv搜图UID:\n{uid_str}" - elif msg.lower() == "keyword": - update_lst = _keyword - info = f"开始更新Pixiv搜图关键词:\n{keyword_str}" - else: - update_lst = _pass_keyword - info = f"开始更新Pixiv搜图关键词:\n{keyword_str}\n更新UID:{uid_str}\n更新PID:{pid_str}" - num = num if num < len(update_lst) else len(update_lst) - else: - if msg.lower() == "pid": - update_lst = [f"pid:{num}"] - info = f"开始更新Pixiv搜图UID:\npid:{num}" - else: - update_lst = [f"uid:{num}"] - info = f"开始更新Pixiv搜图UID:\nuid:{num}" - await start_update.send(info) - start_time = time.time() - pid_count, pic_count = await start_update_image_url(update_lst[:num], black_pid) - await start_update.send( - f"Pixiv搜图关键词搜图更新完成...\n" - f"累计更新PID {pid_count} 个\n" - f"累计更新图片 {pic_count} 张" + "\n耗时:{:.2f}秒".format((time.time() - start_time)) - ) - - -@check_not_update_uid_pid.handle() -async def _(arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - flag = False - if msg == "update": - flag = True - _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() - x_uid = [] - x_pid = [] - _uid = [int(x[4:]) for x in _pass_keyword if x.startswith("uid:")] - _pid = [int(x[4:]) for x in _pass_keyword if x.startswith("pid:")] - all_images = await Pixiv.query_images(r18=2) - for img in all_images: - if img.pid not in x_pid: - x_pid.append(img.pid) - if img.uid not in x_uid: - x_uid.append(img.uid) - await check_not_update_uid_pid.send( - "从未更新过的UID:" - + ",".join([f"uid:{x}" for x in _uid if x not in x_uid]) - + "\n" - + "从未更新过的PID:" - + ",".join([f"pid:{x}" for x in _pid if x not in x_pid]) - ) - if flag: - await check_not_update_uid_pid.send("开始自动自动更新PID....") - update_lst = [f"pid:{x}" for x in _uid if x not in x_uid] - black_pid = await PixivKeywordUser.get_black_pid() - start_time = time.time() - pid_count, pic_count = await start_update_image_url(update_lst, black_pid) - await check_not_update_uid_pid.send( - f"Pixiv搜图关键词搜图更新完成...\n" - f"累计更新PID {pid_count} 个\n" - f"累计更新图片 {pic_count} 张" + "\n耗时:{:.2f}秒".format((time.time() - start_time)) - ) - - -@check_omega.handle() -async def _(): - async def _tasks(line: str, all_pid: List[int], length: int, index: int): - data = line.split("VALUES", maxsplit=1)[-1].strip()[1:-2] - num_list = re.findall(r"(\d+)", data) - pid = int(num_list[1]) - uid = int(num_list[2]) - id_ = 3 - while num_list[id_] not in ["0", "1"]: - id_ += 1 - classified = int(num_list[id_]) - nsfw_tag = int(num_list[id_ + 1]) - width = int(num_list[id_ + 2]) - height = int(num_list[id_ + 3]) - str_list = re.findall(r"'(.*?)',", data) - title = str_list[0] - uname = str_list[1] - tags = str_list[2] - url = str_list[3] - if pid in all_pid: - logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") - return - _, is_create = await OmegaPixivIllusts.get_or_create( - pid=pid, - title=title, - width=width, - height=height, - url=url, - uid=uid, - nsfw_tag=nsfw_tag, - tags=tags, - uname=uname, - classified=classified, - ) - if is_create: - logger.info( - f"成功添加OmegaPixivIllusts图库数据 pid:{pid} 本次预计存储 {length} 张,已更新第 {index} 张" - ) - else: - logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") - - omega_pixiv_illusts = None - for file in os.listdir("."): - if "omega_pixiv_artwork" in file and ".sql" in file: - omega_pixiv_illusts = Path() / file - if omega_pixiv_illusts: - with open(omega_pixiv_illusts, "r", encoding="utf8") as f: - lines = f.readlines() - tasks = [] - length = len([x for x in lines if "INSERT INTO" in x.upper()]) - all_pid = await OmegaPixivIllusts.all().values_list("pid", flat=True) - index = 0 - logger.info("检测到OmegaPixivIllusts数据库,准备开始更新....") - for line in lines: - if "INSERT INTO" in line.upper(): - index += 1 - logger.info(f"line: {line} 加入更新计划") - tasks.append( - asyncio.ensure_future(_tasks(line, all_pid, length, index)) - ) - await asyncio.gather(*tasks) - omega_pixiv_illusts.unlink() +import asyncio +import os +import re +import time +from pathlib import Path + +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from ._data_source import start_update_image_url +from ._model.omega_pixiv_illusts import OmegaPixivIllusts +from ._model.pixiv import Pixiv +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="pix检查更新", + description="pix图库收录数据检查更新", + usage=""" + 指令: + 更新pix关键词 *[keyword/uid/pid] [num=max]: 更新仅keyword/uid/pid或全部 + pix检测更新:检测从未更新过的uid和pid + 示例:更新pix关键词keyword + 示例:更新pix关键词uid 10 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER + ).dict(), +) + + +_update_matcher = on_alconna( + Alconna("更新pix关键词", Args["type", ["uid", "pid", "keyword"]]["num?", int]), + permission=SUPERUSER, + priority=1, + block=True, +) + +_check_matcher = on_alconna( + Alconna( + "pix检测更新", Option("-u|--update", action=store_true, help_text="是否更新") + ), + permission=SUPERUSER, + priority=1, + block=True, +) + +_omega_matcher = on_alconna( + Alconna("检测omega图库"), permission=SUPERUSER, priority=1, block=True +) + + +@_update_matcher.handle() +async def _(arparma: Arparma, session: EventSession, type: str, num: Match[int]): + _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() + _pass_keyword.reverse() + black_pid = await PixivKeywordUser.get_black_pid() + _keyword = [ + x + for x in _pass_keyword + if not x.startswith("uid:") + and not x.startswith("pid:") + and not x.startswith("black:") + ] + _uid = [x for x in _pass_keyword if x.startswith("uid:")] + _pid = [x for x in _pass_keyword if x.startswith("pid:")] + _num = num.result if num.available else 9999 + if _num < 10000: + keyword_str = ",".join( + _keyword[: _num if _num < len(_keyword) else len(_keyword)] + ) + uid_str = ",".join(_uid[: _num if _num < len(_uid) else len(_uid)]) + pid_str = ",".join(_pid[: _num if _num < len(_pid) else len(_pid)]) + if type == "pid": + update_lst = _pid + info = f"开始更新Pixiv搜图PID:\n{pid_str}" + elif type == "uid": + update_lst = _uid + info = f"开始更新Pixiv搜图UID:\n{uid_str}" + elif type == "keyword": + update_lst = _keyword + info = f"开始更新Pixiv搜图关键词:\n{keyword_str}" + else: + update_lst = _pass_keyword + info = f"开始更新Pixiv搜图关键词:\n{keyword_str}\n更新UID:{uid_str}\n更新PID:{pid_str}" + _num = _num if _num < len(update_lst) else len(update_lst) + else: + if type == "pid": + update_lst = [f"pid:{_num}"] + info = f"开始更新Pixiv搜图UID:\npid:{_num}" + else: + update_lst = [f"uid:{_num}"] + info = f"开始更新Pixiv搜图UID:\nuid:{_num}" + await MessageUtils.build_message(info).send() + start_time = time.time() + pid_count, pic_count = await start_update_image_url( + update_lst[:_num], black_pid, type == "pid" + ) + await MessageUtils.build_message( + f"Pixiv搜图关键词搜图更新完成...\n" + f"累计更新PID {pid_count} 个\n" + f"累计更新图片 {pic_count} 张" + + "\n耗时:{:.2f}秒".format((time.time() - start_time)) + ).send() + logger.info("更新pix关键词", arparma.header_result, session=session) + + +@_check_matcher.handle() +async def _(bot: Bot, arparma: Arparma, session: EventSession): + _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() + x_uid = [] + x_pid = [] + _uid = [int(x[4:]) for x in _pass_keyword if x.startswith("uid:")] + _pid = [int(x[4:]) for x in _pass_keyword if x.startswith("pid:")] + all_images = await Pixiv.query_images(r18=2) + for img in all_images: + if img.pid not in x_pid: + x_pid.append(img.pid) + if img.uid not in x_uid: + x_uid.append(img.uid) + await MessageUtils.build_message( + "从未更新过的UID:" + + ",".join([f"uid:{x}" for x in _uid if x not in x_uid]) + + "\n" + + "从未更新过的PID:" + + ",".join([f"pid:{x}" for x in _pid if x not in x_pid]) + ).send() + if arparma.find("update"): + await MessageUtils.build_message("开始自动自动更新PID....").send() + update_lst = [f"pid:{x}" for x in _uid if x not in x_uid] + black_pid = await PixivKeywordUser.get_black_pid() + start_time = time.time() + pid_count, pic_count = await start_update_image_url( + update_lst, black_pid, False + ) + await MessageUtils.build_message( + f"Pixiv搜图关键词搜图更新完成...\n" + f"累计更新PID {pid_count} 个\n" + f"累计更新图片 {pic_count} 张" + + "\n耗时:{:.2f}秒".format((time.time() - start_time)) + ).send() + logger.info( + f"pix检测更新, 是否更新: {arparma.find('update')}", + arparma.header_result, + session=session, + ) + + +@_omega_matcher.handle() +async def _(): + async def _tasks(line: str, all_pid: list[int], length: int, index: int): + data = line.split("VALUES", maxsplit=1)[-1].strip()[1:-2] + num_list = re.findall(r"(\d+)", data) + pid = int(num_list[1]) + uid = int(num_list[2]) + id_ = 3 + while num_list[id_] not in ["0", "1"]: + id_ += 1 + classified = int(num_list[id_]) + nsfw_tag = int(num_list[id_ + 1]) + width = int(num_list[id_ + 2]) + height = int(num_list[id_ + 3]) + str_list = re.findall(r"'(.*?)',", data) + title = str_list[0] + uname = str_list[1] + tags = str_list[2] + url = str_list[3] + if pid in all_pid: + logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") + return + _, is_create = await OmegaPixivIllusts.get_or_create( + pid=pid, + title=title, + width=width, + height=height, + url=url, + uid=uid, + nsfw_tag=nsfw_tag, + tags=tags, + uname=uname, + classified=classified, + ) + if is_create: + logger.info( + f"成功添加OmegaPixivIllusts图库数据 pid:{pid} 本次预计存储 {length} 张,已更新第 {index} 张" + ) + else: + logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") + + omega_pixiv_illusts = None + for file in os.listdir("."): + if "omega_pixiv_artwork" in file and ".sql" in file: + omega_pixiv_illusts = Path() / file + if omega_pixiv_illusts: + with open(omega_pixiv_illusts, "r", encoding="utf8") as f: + lines = f.readlines() + tasks = [] + length = len([x for x in lines if "INSERT INTO" in x.upper()]) + all_pid = await OmegaPixivIllusts.all().values_list("pid", flat=True) + index = 0 + logger.info("检测到OmegaPixivIllusts数据库,准备开始更新....") + for line in lines: + if "INSERT INTO" in line.upper(): + index += 1 + logger.info(f"line: {line} 加入更新计划") + tasks.append( + asyncio.create_task(_tasks(line, all_pid, length, index)) # type: ignore + ) + await asyncio.gather(*tasks) + omega_pixiv_illusts.unlink() diff --git a/zhenxun/plugins/pixiv_rank_search/__init__.py b/zhenxun/plugins/pixiv_rank_search/__init__.py new file mode 100644 index 00000000..01945cd8 --- /dev/null +++ b/zhenxun/plugins/pixiv_rank_search/__init__.py @@ -0,0 +1,225 @@ +from asyncio.exceptions import TimeoutError + +from httpx import NetworkError +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import is_valid_date + +from .data_source import download_pixiv_imgs, get_pixiv_urls, search_pixiv_urls + +__plugin_meta__ = PluginMetadata( + name="P站排行/搜图", + description="P站排行榜直接冲,P站搜图跟着冲", + usage=""" + P站排行: + 可选参数: + 类型: + 1. 日排行 + 2. 周排行 + 3. 月排行 + 4. 原创排行 + 5. 新人排行 + 6. R18日排行 + 7. R18周排行 + 8. R18受男性欢迎排行 + 9. R18重口排行【慎重!】 + 【使用时选择参数序号即可,R18仅可私聊】 + p站排行 ?[参数] ?[数量] ?[日期] + 示例: + p站排行 [无参数默认为日榜] + p站排行 1 + p站排行 1 5 + p站排行 1 5 2018-4-25 + 【注意空格!!】【在线搜索会较慢】 + --------------------------------- + P站搜图: + 搜图 [关键词] ?[数量] ?[页数=1] ?[r18](不屏蔽R-18) + 示例: + 搜图 樱岛麻衣 + 搜图 樱岛麻衣 5 + 搜图 樱岛麻衣 5 r18 + 搜图 樱岛麻衣#1000users 5 + 【多个关键词用#分割】 + 【默认为 热度排序】 + 【注意空格!!】【在线搜索会较慢】【数量可能不符?可能该页数量不够,也可能被R-18屏蔽】 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + aliases={"P站排行", "搜图"}, + menu_type="来点好康的", + limits=[BaseBlock(result="P站排行榜或搜图正在搜索,请不要重复触发命令...")], + configs=[ + RegisterConfig( + key="TIMEOUT", + value=10, + help="图片下载超时限制", + default_value=10, + type=int, + ), + RegisterConfig( + key="MAX_PAGE_LIMIT", + value=20, + help="作品最大页数限制,超过的作品会被略过", + default_value=20, + type=int, + ), + RegisterConfig( + key="ALLOW_GROUP_R18", + value=False, + help="图允许群聊中使用 r18 参数", + default_value=False, + type=bool, + ), + RegisterConfig( + module="hibiapi", + key="HIBIAPI", + value="https://api.obfs.dev", + help="如果没有自建或其他hibiapi请不要修改", + default_value="https://api.obfs.dev", + ), + RegisterConfig( + module="pixiv", + key="PIXIV_NGINX_URL", + value="i.pixiv.re", + help="Pixiv反向代理", + ), + ], + ).dict(), +) + + +rank_dict = { + "1": "day", + "2": "week", + "3": "month", + "4": "week_original", + "5": "week_rookie", + "6": "day_r18", + "7": "week_r18", + "8": "day_male_r18", + "9": "week_r18g", +} + +_rank_matcher = on_alconna( + Alconna("p站排行", Args["rank_type", int, 1]["num", int, 10]["datetime?", str]), + aliases={"p站排行榜"}, + priority=5, + block=True, + rule=to_me(), +) + +_keyword_matcher = on_alconna( + Alconna( + "搜图", + Args["keyword", str]["num", int, 10]["page", int, 1], + Option("-r", action=store_true, help_text="是否屏蔽r18"), + ), + priority=5, + block=True, + rule=to_me(), +) + + +@_rank_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + rank_type: int, + num: int, + datetime: Match[str], +): + gid = session.id3 or session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + code = 0 + info_list = [] + _datetime = None + if datetime.available: + _datetime = datetime.result + if not is_valid_date(_datetime): + await MessageUtils.build_message("日期不合法,示例: 2018-4-25").finish( + reply_to=True + ) + if rank_type in [6, 7, 8, 9]: + if gid: + await MessageUtils.build_message("羞羞脸!私聊里自己看!").finish( + at_sender=True + ) + info_list, code = await get_pixiv_urls( + rank_dict[str(rank_type)], num, date=_datetime + ) + if code != 200 and info_list: + if isinstance(info_list[0], str): + await MessageUtils.build_message(info_list[0]).finish() + if not info_list: + await MessageUtils.build_message("没有找到啊,等等再试试吧~V").send( + at_sender=True + ) + for title, author, urls in info_list: + try: + images = await download_pixiv_imgs(urls, session.id1) # type: ignore + await MessageUtils.build_message( + [f"title: {title}\nauthor: {author}\n"] + images # type: ignore + ).send() + + except (NetworkError, TimeoutError): + await MessageUtils.build_message("这张图网络直接炸掉了!").send() + logger.info( + f" 查看了P站排行榜 rank_type{rank_type}", arparma.header_result, session=session + ) + + +@_keyword_matcher.handle() +async def _( + bot: Bot, session: EventSession, arparma: Arparma, keyword: str, num: int, page: int +): + gid = session.id3 or session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if gid: + if arparma.find("r") and not Config.get_config( + "pixiv_rank_search", "ALLOW_GROUP_R18" + ): + await MessageUtils.build_message("(脸红#) 你不会害羞的 八嘎!").finish( + at_sender=True + ) + r18 = 0 if arparma.find("r") else 1 + info_list = None + keyword = keyword.replace("#", " ") + info_list, code = await search_pixiv_urls(keyword, num, page, r18) + if code != 200 and isinstance(info_list[0], str): + await MessageUtils.build_message(info_list[0]).finish() + if not info_list: + await MessageUtils.build_message("没有找到啊,等等再试试吧~V").finish( + at_sender=True + ) + for title, author, urls in info_list: + try: + images = await download_pixiv_imgs(urls, session.id1) # type: ignore + await MessageUtils.build_message( + [f"title: {title}\nauthor: {author}\n"] + images # type: ignore + ).send() + + except (NetworkError, TimeoutError): + await MessageUtils.build_message("这张图网络直接炸掉了!").send() + logger.info( + f" 查看了搜索 {keyword} R18:{r18}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/pixiv_rank_search/data_source.py b/zhenxun/plugins/pixiv_rank_search/data_source.py new file mode 100644 index 00000000..761a93f2 --- /dev/null +++ b/zhenxun/plugins/pixiv_rank_search/data_source.py @@ -0,0 +1,171 @@ +from asyncio.exceptions import TimeoutError +from pathlib import Path + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.utils import change_img_md5 + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net/", +} + + +async def get_pixiv_urls( + mode: str, num: int = 10, page: int = 1, date: str | None = None +) -> tuple[list[tuple[str, str, list[str]] | str], int]: + """获取排行榜图片url + + 参数: + mode: 模式类型 + num: 数量. + page: 页数. + date: 日期. + + 返回: + tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态 + """ + + params = {"mode": mode, "page": page} + if date: + params["date"] = date + hibiapi = Config.get_config("hibiapi", "HIBIAPI") + hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi + rank_url = f"{hibiapi}/api/pixiv/rank" + return await parser_data(rank_url, num, params, "rank") + + +async def search_pixiv_urls( + keyword: str, num: int, page: int, r18: int +) -> tuple[list[tuple[str, str, list[str]] | str], int]: + """搜图图片url + + 参数: + keyword: 关键词 + num: 数量 + page: 页数 + r18: 是否r18 + + 返回: + tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态 + """ + params = {"word": keyword, "page": page} + hibiapi = Config.get_config("hibiapi", "HIBIAPI") + hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi + search_url = f"{hibiapi}/api/pixiv/search" + return await parser_data(search_url, num, params, "search", r18) + + +async def parser_data( + url: str, num: int, params: dict, type_: str, r18: int = 0 +) -> tuple[list[tuple[str, str, list[str]] | str], int]: + """解析数据搜索 + + 参数: + url: 访问URL + num: 数量 + params: 请求参数 + type_: 类型,rank或search + r18: 是否r18. + + 返回: + tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态 + """ + info_list = [] + for _ in range(3): + try: + response = await AsyncHttpx.get( + url, + params=params, + timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), + ) + if response.status_code == 200: + data = response.json() + if data.get("illusts"): + data = data["illusts"] + break + except TimeoutError: + pass + except Exception as e: + logger.error(f"P站排行/搜图解析数据发生错误", e=e) + return ["发生了一些些错误..."], 995 + else: + return ["网络不太好?没有该页数?也许过一会就好了..."], 998 + num = num if num < 30 else 30 + _data = [] + for x in data: + if x["page_count"] < Config.get_config("pixiv_rank_search", "MAX_PAGE_LIMIT"): + if type_ == "search" and r18 == 1: + if "R-18" in str(x["tags"]): + continue + _data.append(x) + if len(_data) == num: + break + for x in _data: + title = x["title"] + author = x["user"]["name"] + urls = [] + if x["page_count"] == 1: + urls.append(x["image_urls"]["large"]) + else: + for j in x["meta_pages"]: + urls.append(j["image_urls"]["large"]) + info_list.append((title, author, urls)) + return info_list, 200 + + +async def download_pixiv_imgs( + urls: list[str], user_id: str, forward_msg_index: int | None = None +) -> list[Path]: + """下载图片 + + 参数: + urls: 图片链接 + user_id: 用户id + forward_msg_index: 转发消息中的图片排序. + + 返回: + MessageFactory: 图片 + """ + result_list = [] + index = 0 + for url in urls: + ws_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") + url = url.replace("_webp", "") + if ws_url: + url = url.replace("i.pximg.net", ws_url).replace("i.pixiv.cat", ws_url) + try: + file = ( + TEMP_PATH / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg" + if forward_msg_index is not None + else TEMP_PATH / f"{user_id}_{index}_pixiv.jpg" + ) + file = Path(file) + try: + if await AsyncHttpx.download_file( + url, + file, + timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), + headers=headers, + ): + change_img_md5(file) + image = None + if forward_msg_index is not None: + image = ( + TEMP_PATH + / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg" + ) + else: + image = TEMP_PATH / f"{user_id}_{index}_pixiv.jpg" + if image: + result_list.append(image) + index += 1 + except OSError: + if file.exists(): + file.unlink() + except Exception as e: + logger.error(f"P站排行/搜图下载图片错误", e=e) + return result_list diff --git a/plugins/poke/__init__.py b/zhenxun/plugins/poke/__init__.py old mode 100755 new mode 100644 similarity index 53% rename from plugins/poke/__init__.py rename to zhenxun/plugins/poke/__init__.py index 180935c2..7f46be96 --- a/plugins/poke/__init__.py +++ b/zhenxun/plugins/poke/__init__.py @@ -1,86 +1,86 @@ -import os -import random - -from nonebot import on_notice -from nonebot.adapters.onebot.v11 import PokeNotifyEvent - -from configs.path_config import IMAGE_PATH, RECORD_PATH -from models.ban_user import BanUser -from services.log import logger -from utils.message_builder import image, poke, record -from utils.utils import CountLimiter - -__zx_plugin_name__ = "戳一戳" - -__plugin_usage__ = """ -usage: - 戳一戳随机掉落语音或美图萝莉图 -""".strip() -__plugin_des__ = "戳一戳发送语音美图萝莉图不美哉?" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["戳一戳"], -} - -poke__reply = [ - "lsp你再戳?", - "连个可爱美少女都要戳的肥宅真恶心啊。", - "你再戳!", - "?再戳试试?", - "别戳了别戳了再戳就坏了555", - "我爪巴爪巴,球球别再戳了", - "你戳你🐎呢?!", - "那...那里...那里不能戳...绝对...", - "(。´・ω・)ん?", - "有事恁叫我,别天天一个劲戳戳戳!", - "欸很烦欸!你戳🔨呢", - "?", - "再戳一下试试?", - "???", - "正在关闭对您的所有服务...关闭成功", - "啊呜,太舒服刚刚竟然睡着了。什么事?", - "正在定位您的真实地址...定位成功。轰炸机已起飞", -] - - -_clmt = CountLimiter(3) - -poke_ = on_notice(priority=5, block=False) - - -@poke_.handle() -async def _poke_event(event: PokeNotifyEvent): - if event.self_id == event.target_id: - _clmt.add(event.user_id) - if _clmt.check(event.user_id) or random.random() < 0.3: - rst = "" - if random.random() < 0.15: - await BanUser.ban(event.user_id, 1, 60) - rst = "气死我了!" - await poke_.finish(rst + random.choice(poke__reply), at_sender=True) - rand = random.random() - path = random.choice(["luoli", "meitu"]) - if rand <= 0.3 and len(os.listdir(IMAGE_PATH / "image_management" / path)) > 0: - index = random.randint( - 0, len(os.listdir(IMAGE_PATH / "image_management" / path)) - 1 - ) - result = f"id:{index}" + image( - IMAGE_PATH / "image_management" / path / f"{index}.jpg" - ) - await poke_.send(result) - logger.info(f"USER {event.user_id} 戳了戳我 回复: {result} {result}") - elif 0.3 < rand < 0.6: - voice = random.choice(os.listdir(RECORD_PATH / "dinggong")) - result = record(RECORD_PATH / "dinggong" / voice) - await poke_.send(result) - await poke_.send(voice.split("_")[1]) - logger.info( - f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}' - ) - else: - await poke_.send(poke(event.user_id)) +import os +import random + +from nonebot import on_notice +from nonebot.adapters.onebot.v11 import PokeNotifyEvent +from nonebot.adapters.onebot.v11.message import MessageSegment +from nonebot.plugin import PluginMetadata + +from zhenxun.configs.path_config import IMAGE_PATH, RECORD_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.ban_console import BanConsole +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import CountLimiter + +__plugin_meta__ = PluginMetadata( + name="戳一戳", + description="戳一戳发送语音美图萝莉图不美哉?", + usage=""" + 戳一戳随机掉落语音或美图萝莉图 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").dict(), +) + +REPLY_MESSAGE = [ + "lsp你再戳?", + "连个可爱美少女都要戳的肥宅真恶心啊。", + "你再戳!", + "?再戳试试?", + "别戳了别戳了再戳就坏了555", + "我爪巴爪巴,球球别再戳了", + "你戳你🐎呢?!", + "那...那里...那里不能戳...绝对...", + "(。´・ω・)ん?", + "有事恁叫我,别天天一个劲戳戳戳!", + "欸很烦欸!你戳🔨呢", + "?", + "再戳一下试试?", + "???", + "正在关闭对您的所有服务...关闭成功", + "啊呜,太舒服刚刚竟然睡着了。什么事?", + "正在定位您的真实地址...定位成功。轰炸机已起飞", +] + + +_clmt = CountLimiter(3) + +poke_ = on_notice(priority=5, block=False) + + +@poke_.handle() +async def _(event: PokeNotifyEvent): + uid = str(event.user_id) if event.user_id else None + gid = str(event.group_id) if event.group_id else None + if event.self_id == event.target_id: + _clmt.increase(event.user_id) + if _clmt.check(event.user_id) or random.random() < 0.3: + rst = "" + if random.random() < 0.15: + await BanConsole.ban(uid, gid, 1, 60) + rst = "气死我了!" + await poke_.finish(rst + random.choice(REPLY_MESSAGE), at_sender=True) + rand = random.random() + path = random.choice(["luoli", "meitu"]) + if rand <= 0.3 and len(os.listdir(IMAGE_PATH / "image_management" / path)) > 0: + index = random.randint( + 0, len(os.listdir(IMAGE_PATH / "image_management" / path)) - 1 + ) + await MessageUtils.build_message( + [ + f"id: {index}", + IMAGE_PATH / "image_management" / path / f"{index}.jpg", + ] + ).send() + logger.info(f"USER {event.user_id} 戳了戳我") + elif 0.3 < rand < 0.6: + voice = random.choice(os.listdir(RECORD_PATH / "dinggong")) + result = MessageSegment.record(RECORD_PATH / "dinggong" / voice) + await poke_.send(result) + await poke_.send(voice.split("_")[1]) + logger.info( + f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}', + "戳一戳", + ) + else: + await poke_.send(MessageSegment("poke", {"qq": event.user_id})) diff --git a/zhenxun/plugins/quotations.py b/zhenxun/plugins/quotations.py new file mode 100644 index 00000000..e213ee04 --- /dev/null +++ b/zhenxun/plugins/quotations.py @@ -0,0 +1,32 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="一言二次元语录", + description="二次元语录给你力量", + usage=""" + usage: + 一言二次元语录 + 指令: + 语录/二次元 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1").dict(), +) + +URL = "https://international.v1.hitokoto.cn/?c=a" + +_matcher = on_alconna(Alconna("语录"), aliases={"二次元"}, priority=5, block=True) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + data = (await AsyncHttpx.get(URL, timeout=5)).json() + result = f'{data["hitokoto"]}\t——{data["from"]}' + await MessageUtils.build_message(result).send() + logger.info(f" 发送语录:" + result, arparma.header_result, session=session) diff --git a/zhenxun/plugins/roll.py b/zhenxun/plugins/roll.py new file mode 100644 index 00000000..7c953496 --- /dev/null +++ b/zhenxun/plugins/roll.py @@ -0,0 +1,66 @@ +import asyncio +import random + +from nonebot import on_command +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="roll", + description="犹豫不决吗?那就让我帮你决定吧", + usage=""" + usage: + 随机数字 或 随机选择事件 + 指令: + roll: 随机 0-100 的数字 + roll *[文本]: 随机事件 + 示例:roll 吃饭 睡觉 打游戏 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1").dict(), +) + + +_matcher = on_command("roll", priority=5, block=True) + + +@_matcher.handle() +async def _( + session: EventSession, + message: UniMsg, + user_name: str = UserName(), +): + text = message.extract_plain_text().strip().replace("roll", "", 1).split() + if not text: + await MessageUtils.build_message(f"roll: {random.randint(0, 100)}").finish( + reply_to=True + ) + await MessageUtils.build_message( + random.choice( + [ + "转动命运的齿轮,拨开眼前迷雾...", + f"启动吧,命运的水晶球,为{user_name}指引方向!", + "嗯哼,在此刻转动吧!命运!", + f"在此祈愿,请为{user_name}降下指引...", + ] + ) + ).send() + await asyncio.sleep(1) + random_text = random.choice(text) + await MessageUtils.build_message( + random.choice( + [ + f"让{NICKNAME}看看是什么结果!答案是:‘{random_text}’", + f"根据命运的指引,接下来{user_name} ‘{random_text}’ 会比较好", + f"祈愿被回应了!是 ‘{random_text}’!", + f"结束了,{user_name},命运之轮停在了 ‘{random_text}’!", + ] + ) + ).send(reply_to=True) + logger.info(f"发送roll:{text}", "roll", session=session) diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py new file mode 100644 index 00000000..ee25fdfd --- /dev/null +++ b/zhenxun/plugins/russian/__init__.py @@ -0,0 +1,201 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Arparma +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Match, UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils + +from .command import ( + _accept_matcher, + _rank_matcher, + _record_matcher, + _refuse_matcher, + _russian_matcher, + _settlement_matcher, + _shoot_matcher, +) +from .data_source import Russian, russian_manage +from .model import RussianUser + +__plugin_meta__ = PluginMetadata( + name="俄罗斯轮盘", + description="虽然是运气游戏,但这可是战场啊少年", + usage=""" + 又到了决斗时刻 + 指令: + 装弹 [金额] [子弹数] ?[at]: 开启游戏,装填子弹,可选自定义金额,或邀请决斗对象 + 接受对决: 接受当前存在的对决 + 拒绝对决: 拒绝邀请的对决 + 开枪: 开出未知的一枪 + 结算: 强行结束当前比赛 (仅当一方未开枪超过30秒时可使用) + 我的战绩: 对,你的战绩 + 轮盘胜场排行/轮盘败场排行/轮盘欧洲人排行/轮盘慈善家排行/轮盘最高连胜排行/轮盘最高连败排行: 各种排行榜 + 示例:装弹 100 3 @sdd + * 注:同一时间群内只能有一场对决 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="群内小游戏", + configs=[ + RegisterConfig( + key="MAX_RUSSIAN_BET_GOLD", + value=1000, + help="俄罗斯轮盘最大赌注金额", + default_value=1000, + type=int, + ) + ], + ).dict(), +) + + +@_russian_matcher.handle() +async def _(money: int, num: Match[str], at_user: Match[alcAt]): + _russian_matcher.set_path_arg("money", money) + if num.available: + _russian_matcher.set_path_arg("num", num.result) + if at_user.available: + _russian_matcher.set_path_arg("at_user", at_user.result.target) + + +@_russian_matcher.got_path( + "num", prompt="请输入装填子弹的数量!(最多6颗,输入取消来取消装弹)" +) +async def _( + bot: Bot, + session: EventSession, + message: UniMsg, + arparma: Arparma, + money: int, + num: str, + at_user: Match[alcAt], + uname: str = UserName(), +): + gid = session.id2 + if message.extract_plain_text() == "取消": + await MessageUtils.build_message("已取消装弹...").finish() + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + if money <= 0: + await MessageUtils.build_message("赌注金额必须大于0!").finish(reply_to=True) + if num in ["取消", "算了"]: + await MessageUtils.build_message("已取消装弹...").finish() + if not num.isdigit(): + await MessageUtils.build_message("输入的子弹数必须是数字!").finish( + reply_to=True + ) + b_num = int(num) + if b_num < 0 or b_num > 6: + await MessageUtils.build_message("子弹数量必须在1-6之间!").finish(reply_to=True) + _at_user = at_user.result.target if at_user.available else None + rus = Russian( + at_user=_at_user, player1=(session.id1, uname), money=money, bullet_num=b_num + ) + result = await russian_manage.add_russian(bot, gid, rus) + await result.send() + logger.info( + f"添加俄罗斯轮盘 装弹: {b_num}, 金额: {money}", + arparma.header_result, + session=session, + ) + + +@_accept_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()): + gid = session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + result = await russian_manage.accept(bot, gid, session.id1, uname) + await result.send() + logger.info(f"俄罗斯轮盘接受对决", arparma.header_result, session=session) + + +@_refuse_matcher.handle() +async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): + gid = session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + result = russian_manage.refuse(gid, session.id1, uname) + await result.send() + logger.info(f"俄罗斯轮盘拒绝对决", arparma.header_result, session=session) + + +@_settlement_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + result = await russian_manage.settlement(gid, session.id1, session.platform) + await result.send() + logger.info(f"俄罗斯轮盘结算", arparma.header_result, session=session) + + +@_shoot_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()): + gid = session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + result, settle = await russian_manage.shoot( + bot, gid, session.id1, uname, session.platform + ) + await result.send() + if settle: + await settle.send() + logger.info(f"俄罗斯轮盘开枪", arparma.header_result, session=session) + + +@_record_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + user, _ = await RussianUser.get_or_create(user_id=session.id1, group_id=gid) + await MessageUtils.build_message( + f"俄罗斯轮盘\n" + f"总胜利场次:{user.win_count}\n" + f"当前连胜:{user.winning_streak}\n" + f"最高连胜:{user.max_winning_streak}\n" + f"总失败场次:{user.fail_count}\n" + f"当前连败:{user.losing_streak}\n" + f"最高连败:{user.max_losing_streak}\n" + f"赚取金币:{user.make_money}\n" + f"输掉金币:{user.lose_money}", + ).send(reply_to=True) + logger.info(f"俄罗斯轮盘查看战绩", arparma.header_result, session=session) + + +@_rank_matcher.handle() +async def _(session: EventSession, arparma: Arparma, rank_type: str, num: int): + gid = session.id2 + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + if not gid: + await MessageUtils.build_message("群组id为空...").finish() + if 51 < num or num < 10: + num = 10 + result = await russian_manage.rank(session.id1, gid, rank_type, num) + if isinstance(result, str): + await MessageUtils.build_message(result).finish(reply_to=True) + result.show() + await MessageUtils.build_message(result).send(reply_to=True) + logger.info( + f"查看轮盘排行: {rank_type} 数量: {num}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/russian/command.py b/zhenxun/plugins/russian/command.py new file mode 100644 index 00000000..a20dd2af --- /dev/null +++ b/zhenxun/plugins/russian/command.py @@ -0,0 +1,108 @@ +from nonebot_plugin_alconna import Alconna, Args +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import on_alconna + +from zhenxun.utils.rules import ensure_group + +_russian_matcher = on_alconna( + Alconna( + "俄罗斯轮盘", + Args["money", int]["num?", str]["at_user?", alcAt], + ), + aliases={"装弹", "俄罗斯转盘"}, + rule=ensure_group, + priority=5, + block=True, +) + +_accept_matcher = on_alconna( + Alconna("接受对决"), + aliases={"接受决斗", "接受挑战"}, + rule=ensure_group, + priority=5, + block=True, +) + +_refuse_matcher = on_alconna( + Alconna("拒绝对决"), + aliases={"拒绝决斗", "拒绝挑战"}, + rule=ensure_group, + priority=5, + block=True, +) + +_shoot_matcher = on_alconna( + Alconna("开枪"), + aliases={"咔", "嘭", "嘣"}, + rule=ensure_group, + priority=5, + block=True, +) + +_settlement_matcher = on_alconna( + Alconna("结算"), + rule=ensure_group, + priority=5, + block=True, +) + +_record_matcher = on_alconna( + Alconna("我的战绩"), + rule=ensure_group, + priority=5, + block=True, +) + +_rank_matcher = on_alconna( + Alconna( + "russian-rank", + Args["rank_type", ["win", "lose", "a", "b", "max_win", "max_lose"]][ + "num?", int, 10 + ], + ), + rule=ensure_group, + priority=5, + block=True, +) + +_rank_matcher.shortcut( + r"轮盘胜场排行(?P\d*)", + command="russian-rank", + arguments=["win", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘败场排行(?P\d*)", + command="russian-rank", + arguments=["lose", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘欧洲人排行(?P\d*)", + command="russian-rank", + arguments=["a", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘慈善家排行(?P\d*)", + command="russian-rank", + arguments=["b", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘最高连胜排行(?P\d*)", + command="russian-rank", + arguments=["max_win", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘最高连败排行(?P\d*)", + command="russian-rank", + arguments=["max_lose", "{num}"], + prefix=True, +) diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py new file mode 100644 index 00000000..eb8381cb --- /dev/null +++ b/zhenxun/plugins/russian/data_source.py @@ -0,0 +1,535 @@ +import random +import time +from datetime import datetime, timedelta + +from apscheduler.jobstores.base import JobLookupError +from nonebot.adapters import Bot +from nonebot_plugin_alconna import At, UniMessage +from nonebot_plugin_apscheduler import scheduler +from pydantic import BaseModel + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.enum import GoldHandle +from zhenxun.utils.exception import InsufficientGold +from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType, text2image +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +from .model import RussianUser + +base_config = Config.get("russian") + + +class Russian(BaseModel): + + at_user: str | None + """指定决斗对象""" + player1: tuple[str, str] + """玩家1id, 昵称""" + player2: tuple[str, str] | None = None + """玩家2id, 昵称""" + money: int + """金额""" + bullet_num: int + """子弹数""" + bullet_arr: list[int] = [] + """子弹排列""" + bullet_index: int = 0 + """当前子弹下标""" + next_user: str = "" + """下一个开枪用户""" + time: float = time.time() + """创建时间""" + win_user: str | None = None + """胜利者""" + + +class RussianManage: + + def __init__(self) -> None: + self._data: dict[str, Russian] = {} + + def __check_is_timeout(self, group_id: str) -> bool: + """检查决斗是否超时 + + 参数: + group_id: 群组id + + 返回: + bool: 是否超时 + """ + if russian := self._data.get(group_id): + if russian.time + 30 < time.time(): + return True + return False + + def __random_bullet(self, num: int) -> list[int]: + """随机排列子弹 + + 参数: + num: 子弹数量 + + 返回: + list[int]: 子弹排列数组 + """ + bullet_list = [0, 0, 0, 0, 0, 0, 0] + for i in random.sample([0, 1, 2, 3, 4, 5, 6], num): + bullet_list[i] = 1 + return bullet_list + + def __remove_job(self, group_id: str): + """移除定时任务 + + 参数: + group_id: 群组id + """ + try: + scheduler.remove_job(f"russian_job_{group_id}") + except JobLookupError: + pass + + def __build_job( + self, bot: Bot, group_id: str, is_add: bool = False, platform: str | None = None + ): + """移除定时任务和构建新定时任务 + + 参数: + bot: Bot + group_id: 群组id + is_add: 是否添加新定时任务. + platform: 平台 + """ + self.__remove_job(group_id) + if is_add: + date = datetime.now() + timedelta(seconds=31) + scheduler.add_job( + self.__auto_end_game, + "date", + run_date=date.replace(microsecond=0), + id=f"russian_job_{group_id}", + args=[bot, group_id, platform], + ) + + async def __auto_end_game(self, bot: Bot, group_id: str, platform: str): + """自动结束对决 + + 参数: + bot: Bot + group_id: 群组id + platform: 平台 + """ + result = await self.settlement(group_id, None, platform) + if result: + await PlatformUtils.send_message(bot, None, group_id, result) + + async def add_russian(self, bot: Bot, group_id: str, rus: Russian) -> UniMessage: + """添加决斗 + + 参数: + bot: Bot + group_id: 群组id + rus: Russian + + 返回: + UniMessage: 返回消息 + """ + russian = self._data.get(group_id) + if russian: + if russian.time + 30 < time.time(): + if not russian.player2: + return MessageUtils.build_message( + f"现在是 {russian.player1[1]} 发起的对决, 请接受对决或等待决斗超时..." + ) + else: + return MessageUtils.build_message( + f"{russian.player1[1]} 和 {russian.player2[1]}的对决还未结束!" + ) + return MessageUtils.build_message( + f"现在是 {russian.player1[1]} 发起的对决\n请等待比赛结束后再开始下一轮..." + ) + max_money = base_config.get("MAX_RUSSIAN_BET_GOLD") + if rus.money > max_money: + return MessageUtils.build_message(f"太多了!单次金额不能超过{max_money}!") + user = await UserConsole.get_user(rus.player1[0]) + if user.gold < rus.money: + return MessageUtils.build_message("你没有足够的钱支撑起这场挑战") + rus.bullet_arr = self.__random_bullet(rus.bullet_num) + self._data[group_id] = rus + message_list: list[str | At] = [] + if rus.at_user: + user = await GroupInfoUser.get_or_none( + user_id=rus.at_user, group_id=group_id + ) + message_list = [ + f"{rus.player1[1]} 向", + At(flag="user", target=rus.at_user), + f"发起了决斗!请 {user.user_name if user else rus.at_user} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!", + ] + else: + message_list = [ + "若30秒内无人接受挑战则此次对决作废【首次游玩请at我发送 ’帮助俄罗斯轮盘‘ 来查看命令】" + ] + result = ( + "咔 " * rus.bullet_num + + f"装填完毕\n挑战金额:{rus.money}\n第一枪的概率为:{float(rus.bullet_num) / 7.0 * 100:.2f}%\n" + ) + + message_list.insert(0, result) + self.__build_job(bot, group_id, True) + return MessageUtils.build_message(message_list) # type: ignore + + async def accept( + self, bot: Bot, group_id: str, user_id: str, uname: str + ) -> UniMessage: + """接受对决 + + 参数: + bot: Bot + group_id: 群组id + user_id: 用户id + uname: 用户名称 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if russian.at_user and russian.at_user != user_id: + return MessageUtils.build_message("又不是找你决斗,你接受什么啊!气!") + if russian.player2: + return MessageUtils.build_message( + "当前决斗已被其他玩家接受!请等待下局对决!" + ) + if russian.player1[0] == user_id: + return MessageUtils.build_message("你发起的对决,你接受什么啊!气!") + user = await UserConsole.get_user(user_id) + if user.gold < russian.money: + return MessageUtils.build_message("你没有足够的钱来接受这场挑战...") + russian.player2 = (user_id, uname) + russian.next_user = russian.player1[0] + self.__build_job(bot, group_id, True) + return MessageUtils.build_message( + [ + "决斗已经开始!请", + At(flag="user", target=russian.player1[0]), + "先开枪!", + ] + ) + return MessageUtils.build_message( + "目前没有进行的决斗,请发送 装弹 开启决斗吧!" + ) + + def refuse(self, group_id: str, user_id: str, uname: str) -> UniMessage: + """拒绝决斗 + + 参数: + group_id: 群组id + user_id: 用户id + uname: 用户名称 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if russian.at_user: + if russian.at_user != user_id: + return MessageUtils.build_message( + "又不是找你决斗,你拒绝什么啊!气!" + ) + del self._data[group_id] + self.__remove_job(group_id) + return MessageUtils.build_message( + [ + At(flag="user", target=russian.player1[0]), + f"{uname}拒绝了你的对决!", + ] + ) + return MessageUtils.build_message("当前决斗并没有指定对手,无法拒绝哦!") + return MessageUtils.build_message( + "目前没有进行的决斗,请发送 装弹 开启决斗吧!" + ) + + async def shoot( + self, bot: Bot, group_id: str, user_id: str, uname: str, platform: str + ) -> tuple[UniMessage, UniMessage | None]: + """开枪 + + 参数: + bot: Bot + group_id: 群组id + user_id: 用户id + uname: 用户名称 + platform: 平台 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if not russian.player2: + return ( + MessageUtils.build_message("当前还没有玩家接受对决,无法开枪..."), + None, + ) + if user_id not in [russian.player1[0], russian.player2[0]]: + """非玩家1和玩家2发送开枪""" + return ( + MessageUtils.build_message( + random.choice( + [ + f"不要打扰 {russian.player1[1]} 和 {russian.player2[1]} 的决斗啊!", + f"给我好好做好一个观众!不然{NICKNAME}就要生气了", + f"不要捣乱啊baka{uname}!", + ] + ) + ), + None, + ) + if user_id != russian.next_user: + """相同玩家连续开枪""" + return ( + MessageUtils.build_message( + f"你的左轮不是连发的!该 {russian.player2[1]} 开枪了!" + ), + None, + ) + if russian.bullet_arr[russian.bullet_index] == 1: + """去世""" + result = MessageUtils.build_message( + random.choice( + [ + '"嘭!",你直接去世了', + "眼前一黑,你直接穿越到了异世界...(死亡)", + "终究还是你先走一步...", + ] + ) + ) + settle = await self.settlement(group_id, user_id, platform) + return result, settle + else: + """存活""" + p = ( + (russian.bullet_index + russian.bullet_num + 1) + / len(russian.bullet_arr) + * 100 + ) + result = ( + random.choice( + [ + "呼呼,没有爆裂的声响,你活了下来", + "虽然黑洞洞的枪口很恐怖,但好在没有子弹射出来,你活下来了", + '"咔",你没死,看来运气不错', + ] + ) + + f"\n下一枪中弹的概率: {p:.2f}%, 轮到 " + ) + next_user = ( + russian.player2[0] + if russian.next_user == russian.player1[0] + else russian.player1[0] + ) + russian.next_user = next_user + russian.bullet_index += 1 + self.__build_job(bot, group_id, True) + return ( + MessageUtils.build_message( + [result, At(flag="user", target=next_user), " 了!"] + ), + None, + ) + return ( + MessageUtils.build_message("目前没有进行的决斗,请发送 装弹 开启决斗吧!"), + None, + ) + + async def settlement( + self, group_id: str, user_id: str | None, platform: str | None = None + ) -> UniMessage: + """结算 + + 参数: + group_id: 群组id + user_id: 用户id + platform: 平台 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if not russian.player2: + if self.__check_is_timeout(group_id): + del self._data[group_id] + return MessageUtils.build_message( + "规定时间内还未有人接受决斗,当前决斗过期..." + ) + return MessageUtils.build_message("决斗还未开始,,无法结算哦...") + if user_id and user_id not in [russian.player1[0], russian.player2[0]]: + return MessageUtils.build_message(f"吃瓜群众不要捣乱!黄牌警告!") + if not self.__check_is_timeout(group_id): + return MessageUtils.build_message( + f"{russian.player1[1]} 和 {russian.player2[1]} 比赛并未超时,请继续比赛..." + ) + win_user = None + lose_user = None + if win_user: + russian.next_user = ( + russian.player1[0] + if win_user == russian.player2[0] + else russian.player2[0] + ) + if russian.next_user != russian.player1[0]: + win_user = russian.player1 + lose_user = russian.player2 + else: + win_user = russian.player2 + lose_user = russian.player1 + if win_user and lose_user: + rand = 0 + if russian.money > 10: + rand = random.randint(0, 5) + fee = int(russian.money * float(rand) / 100) + fee = 1 if fee < 1 and rand != 0 else fee + else: + fee = 0 + winner = await RussianUser.add_count(win_user[0], group_id, "win") + loser = await RussianUser.add_count(lose_user[0], group_id, "lose") + await RussianUser.money( + win_user[0], group_id, "win", russian.money - fee + ) + await RussianUser.money(lose_user[0], group_id, "lose", russian.money) + await UserConsole.add_gold( + win_user[0], russian.money - fee, "russian", platform + ) + try: + await UserConsole.reduce_gold( + lose_user[0], + russian.money, + GoldHandle.PLUGIN, + "russian", + platform, + ) + except InsufficientGold: + if u := await UserConsole.get_user(lose_user[0]): + u.gold = 0 + await u.save(update_fields=["gold"]) + result = [ + "这场决斗是 ", + At(flag="user", target=win_user[0]), + " 胜利了!", + ] + image = await text2image( + f"结算:\n" + f"\t胜者:{win_user[1]}\n" + f"\t赢取金币:{russian.money - fee}\n" + f"\t累计胜场:{winner.win_count}\n" + f"\t累计赚取金币:{winner.make_money}\n" + f"-------------------\n" + f"\t败者:{lose_user[1]}\n" + f"\t输掉金币:{russian.money}\n" + f"\t累计败场:{loser.fail_count}\n" + f"\t累计输掉金币:{loser.lose_money}\n" + f"-------------------\n" + f"哼哼,{NICKNAME}从中收取了 {float(rand)}%({fee}金币) 作为手续费!\n" + f"子弹排列:{russian.bullet_arr}", + padding=10, + color="#f9f6f2", + ) + self.__remove_job(group_id) + result.append(image) + del self._data[group_id] + return MessageUtils.build_message(result) + return MessageUtils.build_message("赢家和输家获取错误...") + return MessageUtils.build_message("比赛并没有开始...无法结算...") + + async def __get_x_index(self, users: list[RussianUser], group_id: str): + uid_list = [u.user_id for u in users] + group_user_list = await GroupInfoUser.filter( + user_id__in=uid_list, group_id=group_id + ).all() + group_user = {gu.user_id: gu.user_name for gu in group_user_list} + data = [] + for uid in uid_list: + if uid in group_user: + data.append(group_user[uid]) + else: + data.append(uid) + return data + + async def rank( + self, user_id: str, group_id: str, rank_type: str, num: int + ) -> BuildImage | str: + x_index = [] + data = [] + title = "" + x_name = "" + if rank_type == "win": + users = ( + await RussianUser.filter(group_id=group_id, win_count__not=0) + .order_by("win_count") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.win_count for u in users] + title = "胜场排行" + x_name = "场次" + if rank_type == "lose": + users = ( + await RussianUser.filter(group_id=group_id, fail_count__not=0) + .order_by("fail_count") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.fail_count for u in users] + title = "败场排行" + x_name = "场次" + if rank_type == "a": + users = ( + await RussianUser.filter(group_id=group_id, make_money__not=0) + .order_by("make_money") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.make_money for u in users] + title = "欧洲人排行" + x_name = "金币" + if rank_type == "b": + users = ( + await RussianUser.filter(group_id=group_id, lose_money__not=0) + .order_by("lose_money") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.lose_money for u in users] + title = "慈善家排行" + x_name = "金币" + if rank_type == "max_win": + users = ( + await RussianUser.filter(group_id=group_id, max_winning_streak__not=0) + .order_by("max_winning_streak") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.max_winning_streak for u in users] + title = "最高连胜排行" + x_name = "场次" + if rank_type == "max_lose": + users = ( + await RussianUser.filter(group_id=group_id, max_losing_streak__not=0) + .order_by("max_losing_streak") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.max_losing_streak for u in users] + title = "最高连败排行" + x_name = "场次" + if not data: + return "当前数据为空..." + mat = BuildMat(MatType.BARH) + mat.x_index = x_index + mat.data = data # type: ignore + mat.title = title + mat.x_name = x_name + return await mat.build() + + +russian_manage = RussianManage() diff --git a/plugins/russian/model.py b/zhenxun/plugins/russian/model.py old mode 100755 new mode 100644 similarity index 87% rename from plugins/russian/model.py rename to zhenxun/plugins/russian/model.py index 4875f185..0fab9298 --- a/plugins/russian/model.py +++ b/zhenxun/plugins/russian/model.py @@ -1,6 +1,6 @@ from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class RussianUser(Model): @@ -35,13 +35,12 @@ class RussianUser(Model): @classmethod async def add_count(cls, user_id: str, group_id: str, itype: str): - """ + """添加用户输赢次数 + 说明: - 添加用户输赢次数 - 说明: - :param user_id: qq号 - :param group_id: 群号 - :param itype: 输或赢 'win' or 'lose' + user_id: 用户id + group_id: 群号 + itype: 输或赢 'win' or 'lose' """ user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) if itype == "win": @@ -80,17 +79,17 @@ class RussianUser(Model): "max_losing_streak", ] ) + return user @classmethod - async def money(cls, user_id: str, group_id: str, itype: str, count: int) -> bool: - """ - 说明: - 添加用户输赢金钱 + async def money(cls, user_id: str, group_id: str, itype: str, count: int): + """添加用户输赢金钱 + 参数: - :param user_id: qq号 - :param group_id: 群号 - :param itype: 输或赢 'win' or 'lose' - :param count: 金钱数量 + user_id: 用户id + group_id: 群号 + itype: 输或赢 'win' or 'lose' + count: 金钱数量 """ user, _ = await cls.get_or_create(user_id=str(user_id), group_id=group_id) if itype == "win": diff --git a/zhenxun/plugins/search_anime/__init__.py b/zhenxun/plugins/search_anime/__init__.py new file mode 100644 index 00000000..d12ad03e --- /dev/null +++ b/zhenxun/plugins/search_anime/__init__.py @@ -0,0 +1,68 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from .data_source import from_anime_get_info + +__plugin_meta__ = PluginMetadata( + name="搜番", + description="找不到想看的动漫吗?", + usage=""" + 搜索动漫资源 + 指令: + 搜番 [番剧名称或者关键词] + 示例:搜番 刀剑神域 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + limits=[BaseBlock(result="搜索还未完成,不要重复触发!")], + configs=[ + RegisterConfig( + key="SEARCH_ANIME_MAX_INFO", + value=20, + help="搜索动漫返回的最大数量", + default_value=20, + type=int, + ) + ], + ).dict(), +) + +_matcher = on_alconna(Alconna("搜番", Args["name?", str]), priority=5, block=True) + + +@_matcher.handle() +async def _(name: Match[str]): + if name.available: + _matcher.set_path_arg("name", name.result) + + +@_matcher.got_path("name", prompt="是不是少了番名?") +async def _(session: EventSession, arparma: Arparma, name: str): + gid = session.id3 or session.id2 + await MessageUtils.build_message(f"开始搜番 {name}...").send() + anime_report = await from_anime_get_info( + name, + Config.get_config("search_anime", "SEARCH_ANIME_MAX_INFO"), + ) + if anime_report: + if isinstance(anime_report, str): + await MessageUtils.build_message(anime_report).finish() + await MessageUtils.build_message("\n\n".join(anime_report)).send() + logger.info( + f"搜索番剧 {name} 成功: {anime_report}", + arparma.header_result, + session=session, + ) + else: + logger.info(f"未找到番剧 {name}...") + await MessageUtils.build_message( + f"未找到番剧 {name}(也有可能是超时,再尝试一下?)" + ).send() diff --git a/plugins/search_anime/data_source.py b/zhenxun/plugins/search_anime/data_source.py old mode 100755 new mode 100644 similarity index 65% rename from plugins/search_anime/data_source.py rename to zhenxun/plugins/search_anime/data_source.py index 7adb6836..59d0ac61 --- a/plugins/search_anime/data_source.py +++ b/zhenxun/plugins/search_anime/data_source.py @@ -1,52 +1,53 @@ -from lxml import etree -import feedparser -from urllib import parse -from services.log import logger -from utils.http_utils import AsyncHttpx -from typing import List, Union -import time - - -async def from_anime_get_info(key_word: str, max_: int) -> Union[str, List[str]]: - s_time = time.time() - url = "https://share.dmhy.org/topics/rss/rss.xml?keyword=" + parse.quote(key_word) - try: - repass = await get_repass(url, max_) - except Exception as e: - logger.error(f"发生了一些错误 {type(e)}:{e}") - return "发生了一些错误!" - repass.insert(0, f"搜索 {key_word} 结果(耗时 {int(time.time() - s_time)} 秒):\n") - return repass - - -async def get_repass(url: str, max_: int) -> List[str]: - put_line = [] - text = (await AsyncHttpx.get(url)).text - d = feedparser.parse(text) - max_ = max_ if max_ < len([e.link for e in d.entries]) else len([e.link for e in d.entries]) - url_list = [e.link for e in d.entries][:max_] - for u in url_list: - try: - text = (await AsyncHttpx.get(u)).text - html = etree.HTML(text) - magent = html.xpath('.//a[@id="a_magnet"]/text()')[0] - title = html.xpath(".//h3/text()")[0] - item = html.xpath( - '//div[@class="info resource-info right"]/ul/li' - ) - class_a = ( - item[0] - .xpath("string(.)")[5:] - .strip() - .replace("\xa0", "") - .replace("\t", "") - ) - size = item[3].xpath("string(.)")[5:].strip() - put_line.append( - "【{}】| {}\n【{}】| {}".format(class_a, title, size, magent) - ) - except Exception as e: - logger.error(f"搜番发生错误 {type(e)}:{e}") - return put_line - - +import time +from urllib import parse + +import feedparser +from lxml import etree + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + + +async def from_anime_get_info(key_word: str, max_: int) -> str | list[str]: + s_time = time.time() + url = "https://share.dmhy.org/topics/rss/rss.xml?keyword=" + parse.quote(key_word) + try: + repass = await get_repass(url, max_) + except Exception as e: + logger.error(f"发生了一些错误 {type(e)}", e=e) + return "发生了一些错误!" + repass.insert(0, f"搜索 {key_word} 结果(耗时 {int(time.time() - s_time)} 秒):\n") + return repass + + +async def get_repass(url: str, max_: int) -> list[str]: + put_line = [] + text = (await AsyncHttpx.get(url)).text + d = feedparser.parse(text) + max_ = ( + max_ + if max_ < len([e.link for e in d.entries]) + else len([e.link for e in d.entries]) + ) + url_list = [e.link for e in d.entries][:max_] + for u in url_list: + try: + text = (await AsyncHttpx.get(u)).text + html = etree.HTML(text) # type: ignore + magent = html.xpath('.//a[@id="a_magnet"]/text()')[0] + title = html.xpath(".//h3/text()")[0] + item = html.xpath('//div[@class="info resource-info right"]/ul/li') + class_a = ( + item[0] + .xpath("string(.)")[5:] + .strip() + .replace("\xa0", "") + .replace("\t", "") + ) + size = item[3].xpath("string(.)")[5:].strip() + put_line.append( + "【{}】| {}\n【{}】| {}".format(class_a, title, size, magent) + ) + except Exception as e: + logger.error(f"搜番发生错误", e=e) + return put_line diff --git a/zhenxun/plugins/search_buff_skin_price/__init__.py b/zhenxun/plugins/search_buff_skin_price/__init__.py new file mode 100644 index 00000000..da224aaf --- /dev/null +++ b/zhenxun/plugins/search_buff_skin_price/__init__.py @@ -0,0 +1,104 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from .data_source import get_price, update_buff_cookie + +__plugin_meta__ = PluginMetadata( + name="BUFF查询皮肤", + description="BUFF皮肤底价查询", + usage=""" + 在线实时获取BUFF指定皮肤所有磨损底价 + 指令: + 查询皮肤 [枪械名] [皮肤名称] + 示例:查询皮肤 ak47 二西莫夫 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + limits=[BaseBlock(result="您有皮肤正在搜索,请稍等...")], + configs=[ + RegisterConfig( + key="BUFF_PROXY", + value=None, + help="BUFF代理,有些厂ip可能被屏蔽", + ), + RegisterConfig( + key="COOKIE", + value=None, + help="BUFF的账号cookie", + ), + ], + ).dict(), +) + + +_matcher = on_alconna( + Alconna("查询皮肤", Args["name", str]["skin", str]), + aliases={"皮肤查询"}, + priority=5, + block=True, +) + +_cookie_matcher = on_alconna( + Alconna("设置cookie", Args["cookie", str]), + rule=to_me(), + permission=SUPERUSER, + priority=1, +) + + +@_matcher.handle() +async def _(name: Match[str], skin: Match[str]): + if name.available: + _matcher.set_path_arg("name", name.result) + if skin.available: + _matcher.set_path_arg("skin", skin.result) + + +@_matcher.got_path("name", prompt="要查询什么武器呢?") +@_matcher.got_path("skin", prompt="要查询该武器的什么皮肤呢?") +async def arg_handle( + session: EventSession, + arparma: Arparma, + name: str, + skin: str, +): + if name in ["算了", "取消"] or skin in ["算了", "取消"]: + await MessageUtils.build_message("已取消操作...").finish() + result = "" + if name in ["ak", "ak47"]: + name = "ak-47" + name = name + " | " + skin + status_code = -1 + try: + result, status_code = await get_price(name) + except FileNotFoundError: + await MessageUtils.build_message( + f'请先对{NICKNAME}说"设置cookie"来设置cookie!' + ).send(at_sender=True) + if status_code in [996, 997, 998]: + await MessageUtils.build_message(result).finish() + if result: + logger.info(f"查询皮肤: {name}", arparma.header_result, session=session) + await MessageUtils.build_message(result).finish() + else: + logger.info( + f" 查询皮肤:{name} 没有查询到", arparma.header_result, session=session + ) + await MessageUtils.build_message("没有查询到哦,请检查格式吧").send() + + +@_cookie_matcher.handle() +async def _(session: EventSession, arparma: Arparma, cookie: str): + result = update_buff_cookie(cookie) + await MessageUtils.build_message(result).send(at_sender=True) + logger.info("更新BUFF COOKIE", arparma.header_result, session=session) diff --git a/plugins/search_buff_skin_price/data_source.py b/zhenxun/plugins/search_buff_skin_price/data_source.py old mode 100755 new mode 100644 similarity index 84% rename from plugins/search_buff_skin_price/data_source.py rename to zhenxun/plugins/search_buff_skin_price/data_source.py index 5ab86040..8dbe6a59 --- a/plugins/search_buff_skin_price/data_source.py +++ b/zhenxun/plugins/search_buff_skin_price/data_source.py @@ -1,58 +1,62 @@ -from asyncio.exceptions import TimeoutError -from configs.config import Config -from utils.http_utils import AsyncHttpx -from services.log import logger - - -url = "https://buff.163.com/api/market/goods" - - -async def get_price(d_name: str) -> "str, int": - """ - 查看皮肤价格 - :param d_name: 武器皮肤,如:awp 二西莫夫 - """ - cookie = {"session": Config.get_config("search_buff_skin_price", "COOKIE")} - name_list = [] - price_list = [] - parameter = {"game": "csgo", "page_num": "1", "search": d_name} - try: - response = await AsyncHttpx.get( - url, - proxy=Config.get_config("search_buff_skin_price", "BUFF_PROXY"), - params=parameter, - cookies=cookie, - ) - if response.status_code == 200: - try: - if response.text.find("Login Required") != -1: - return "BUFF登录被重置,请联系管理员重新登入", 996 - data = response.json()["data"] - total_page = data["total_page"] - data = data["items"] - for _ in range(total_page): - for i in range(len(data)): - name = data[i]["name"] - price = data[i]["sell_reference_price"] - name_list.append(name) - price_list.append(price) - except Exception as e: - logger.error(f"BUFF查询皮肤发生错误 {type(e)}:{e}") - return "没有查询到...", 998 - else: - return "访问失败!", response.status_code - except TimeoutError: - return "访问超时! 请重试或稍后再试!", 997 - result = f"皮肤: {d_name}({len(name_list)})\n" - for i in range(len(name_list)): - result += name_list[i] + ": " + price_list[i] + "\n" - return result[:-1], 999 - - -def update_buff_cookie(cookie: str) -> str: - Config.set_config("search_buff_skin_price", "COOKIE", cookie) - return "更新cookie成功" - - -if __name__ == "__main__": - print(get_price("awp 二西莫夫")) +from asyncio.exceptions import TimeoutError + +from zhenxun.configs.config import Config +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +url = "https://buff.163.com/api/market/goods" + + +async def get_price(d_name: str) -> tuple[str, int]: + """查看皮肤价格 + + 参数: + d_name: 武器皮肤,如:awp 二西莫夫 + + 返回: + tuple[str, int]: 查询数据和状态 + """ + cookie = {"session": Config.get_config("search_buff_skin_price", "COOKIE")} + name_list = [] + price_list = [] + parameter = {"game": "csgo", "page_num": "1", "search": d_name} + try: + response = await AsyncHttpx.get( + url, + proxy=Config.get_config("search_buff_skin_price", "BUFF_PROXY"), + params=parameter, + cookies=cookie, + ) + if response.status_code == 200: + try: + if response.text.find("Login Required") != -1: + return "BUFF登录被重置,请联系管理员重新登入", 996 + data = response.json()["data"] + total_page = data["total_page"] + data = data["items"] + for _ in range(total_page): + for i in range(len(data)): + name = data[i]["name"] + price = data[i]["sell_reference_price"] + name_list.append(name) + price_list.append(price) + except Exception as e: + logger.error(f"BUFF查询皮肤发生错误 {type(e)}:{e}") + return "没有查询到...", 998 + else: + return "访问失败!", response.status_code + except TimeoutError: + return "访问超时! 请重试或稍后再试!", 997 + result = f"皮肤: {d_name}({len(name_list)})\n" + for i in range(len(name_list)): + result += name_list[i] + ": " + price_list[i] + "\n" + return result[:-1], 999 + + +def update_buff_cookie(cookie: str) -> str: + Config.set_config("search_buff_skin_price", "COOKIE", cookie) + return "更新cookie成功" + + +if __name__ == "__main__": + print(get_price("awp 二西莫夫")) diff --git a/zhenxun/plugins/search_image/__init__.py b/zhenxun/plugins/search_image/__init__.py new file mode 100644 index 00000000..38e86de0 --- /dev/null +++ b/zhenxun/plugins/search_image/__init__.py @@ -0,0 +1,94 @@ +from pathlib import Path + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma +from nonebot_plugin_alconna import Image as alcImg +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils + +from .saucenao import get_saucenao_image + +__plugin_meta__ = PluginMetadata( + name="识图", + description="以图搜图,看破本源", + usage=""" + 识别图片 [二次元图片] + 指令: + 识图 [图片] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + configs=[ + RegisterConfig( + key="MAX_FIND_IMAGE_COUNT", + value=3, + help="搜索动漫返回的最大数量", + default_value=3, + type=int, + ), + RegisterConfig( + key="API_KEY", + value=None, + help="Saucenao的API_KEY,通过 https://saucenao.com/user.php?page=search-api 注册获取", + ), + ], + ).dict(), +) + + +_matcher = on_alconna( + Alconna("识图", Args["mode?", str]["image?", alcImg]), block=True, priority=5 +) + + +async def get_image_info(mod: str, url: str) -> str | list[str | Path] | None: + if mod == "saucenao": + return await get_saucenao_image(url) + + +@_matcher.handle() +async def _(mode: Match[str], image: Match[alcImg]): + if mode.available: + _matcher.set_path_arg("mode", mode.result) + else: + _matcher.set_path_arg("mode", "saucenao") + if image.available: + _matcher.set_path_arg("image", image.result) + + +@_matcher.got_path("image", prompt="图来!") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + mode: str, + image: alcImg, +): + gid = session.id3 or session.id2 + if not image.url: + await MessageUtils.build_message("图片url为空...").finish() + await MessageUtils.build_message("开始处理图片...").send() + info_list = await get_image_info(mode, image.url) + if isinstance(info_list, str): + await MessageUtils.build_message(info_list).finish(at_sender=True) + if not info_list: + await MessageUtils.build_message("未查询到...").finish() + platform = PlatformUtils.get_platform(bot) + if "qq" == platform and gid: + forward = MessageUtils.template2forward(info_list[1:], bot.self_id) # type: ignore + await bot.send_group_forward_msg( + group_id=int(gid), + messages=forward, # type: ignore + ) + else: + for info in info_list[1:]: + await MessageUtils.build_message(info).send() + logger.info(f" 识图: {image.url}", arparma.header_result, session=session) diff --git a/plugins/search_image/saucenao.py b/zhenxun/plugins/search_image/saucenao.py similarity index 73% rename from plugins/search_image/saucenao.py rename to zhenxun/plugins/search_image/saucenao.py index dd0c395b..eab44fab 100644 --- a/plugins/search_image/saucenao.py +++ b/zhenxun/plugins/search_image/saucenao.py @@ -1,21 +1,28 @@ -from services import logger -from utils.http_utils import AsyncHttpx -from configs.config import Config -from configs.path_config import TEMP_PATH -from utils.message_builder import image -from typing import Union, List import random +from pathlib import Path + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services import logger +from zhenxun.utils.http_utils import AsyncHttpx API_URL_SAUCENAO = "https://saucenao.com/search.php" API_URL_ASCII2D = "https://ascii2d.net/search/url/" API_URL_IQDB = "https://iqdb.org/" -async def get_saucenao_image(url: str) -> Union[str, List[str]]: +async def get_saucenao_image(url: str) -> str | list[str | Path]: + """获取图片源 + + 参数: + url: 图片url + + 返回: + str | list[Image | Text]: 识图数据 + """ api_key = Config.get_config("search_image", "API_KEY") if not api_key: return "Saucenao 缺失API_KEY!" - params = { "output_type": 2, "api_key": api_key, @@ -35,10 +42,8 @@ async def get_saucenao_image(url: str) -> Union[str, List[str]]: ) msg_list = [] index = random.randint(0, 10000) - if await AsyncHttpx.download_file( - url, TEMP_PATH / f"saucenao_search_{index}.jpg" - ): - msg_list.append(image(TEMP_PATH / f"saucenao_search_{index}.jpg")) + if await AsyncHttpx.download_file(url, TEMP_PATH / f"saucenao_search_{index}.jpg"): + msg_list.append(TEMP_PATH / f"saucenao_search_{index}.jpg") for info in data: try: similarity = info["header"]["similarity"] @@ -53,5 +58,5 @@ async def get_saucenao_image(url: str) -> Union[str, List[str]]: tmp += f'source:{info["header"]["thumbnail"]}\n' msg_list.append(tmp[:-1]) except Exception as e: - logger.warning(f"识图获取图片信息发生错误 {type(e)}:{e}") + logger.warning(f"识图获取图片信息发生错误", e=e) return msg_list diff --git a/zhenxun/plugins/send_setu_/__init__.py b/zhenxun/plugins/send_setu_/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/plugins/send_setu_/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/send_setu_/_model.py b/zhenxun/plugins/send_setu_/_model.py similarity index 76% rename from plugins/send_setu_/_model.py rename to zhenxun/plugins/send_setu_/_model.py index ba2920ec..865af7d1 100644 --- a/plugins/send_setu_/_model.py +++ b/zhenxun/plugins/send_setu_/_model.py @@ -1,10 +1,9 @@ -from typing import List, Optional - from tortoise import fields from tortoise.contrib.postgres.functions import Random from tortoise.expressions import Q +from typing_extensions import Self -from services.db_context import Model +from zhenxun.services.db_context import Model class Setu(Model): @@ -19,7 +18,7 @@ class Setu(Model): """作者""" pid = fields.BigIntField() """pid""" - img_hash: str = fields.TextField() + img_hash = fields.TextField() """图片hash""" img_url = fields.CharField(255) """pixiv url链接""" @@ -36,19 +35,21 @@ class Setu(Model): @classmethod async def query_image( cls, - local_id: Optional[int] = None, - tags: Optional[List[str]] = None, + local_id: int | None = None, + tags: list[str] | None = None, r18: bool = False, limit: int = 50, - ): - """ - 说明: - 通过tag查找色图 + ) -> list[Self] | Self | None: + """通过tag查找色图 + 参数: - :param local_id: 本地色图 id - :param tags: tags - :param r18: 是否 r18,0:非r18 1:r18 2:混合 - :param limit: 获取数量 + local_id: 本地色图 id + tags: tags + r18: 是否 r18,0:非r18 1:r18 2:混合 + limit: 获取数量 + + 返回: + list[Self] | Self | None: 色图数据 """ if local_id: return await cls.filter(is_r18=r18, local_id=local_id).first() @@ -65,11 +66,13 @@ class Setu(Model): @classmethod async def delete_image(cls, pid: int, img_url: str) -> int: - """ - 说明: - 删除图片并替换 + """删除图片并替换 + 参数: - :param pid: 图片pid + pid: 图片pid + + 返回: + int: 删除返回的本地id """ print(pid) return_id = -1 diff --git a/zhenxun/plugins/send_setu_/send_setu/__init__.py b/zhenxun/plugins/send_setu_/send_setu/__init__.py new file mode 100644 index 00000000..3dab91f6 --- /dev/null +++ b/zhenxun/plugins/send_setu_/send_setu/__init__.py @@ -0,0 +1,243 @@ +import random +from typing import Tuple + +from nonebot.adapters import Bot +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.withdraw_manage import WithdrawManager + +from ._data_source import SetuManage, base_config + +__plugin_meta__ = PluginMetadata( + name="色图", + description="不要小看涩图啊混蛋!", + usage=""" + 搜索 lolicon 图库,每日色图time... + 多个tag使用#连接 + 指令: + 色图: 随机色图 + 色图 -r: 随机在线r18涩图 + 色图 -id [id]: 本地指定id色图 + 色图 *[tags]: 在线搜索指定tag色图 + 色图 *[tags] -r: 同上, r18色图 + [1-9]张涩图: 本地随机色图连发 + [1-9]张[tags]的涩图: 在线搜索指定tag色图连发 + 示例:色图 萝莉|少女#白丝|黑丝 + 示例:色图 萝莉#猫娘 + 注: + tag至多取前20项,| 为或,萝莉|少女=萝莉或者少女 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="来点好康的", + limits=[PluginCdBlock(result="您冲的太快了,请稍后再冲.")], + configs=[ + RegisterConfig( + key="WITHDRAW_SETU_MESSAGE", + value=(0, 1), + help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + default_value=(0, 1), + type=Tuple[int, int], + ), + RegisterConfig( + key="ONLY_USE_LOCAL_SETU", + value=False, + help="仅仅使用本地色图,不在线搜索", + default_value=False, + type=bool, + ), + RegisterConfig( + key="INITIAL_SETU_PROBABILITY", + value=0.7, + help="初始色图概率,总概率 = 初始色图概率 + 好感度", + default_value=0.7, + type=float, + ), + RegisterConfig( + key="DOWNLOAD_SETU", + value=True, + help="是否存储下载的色图,使用本地色图可以加快图片发送速度", + default_value=True, + type=float, + ), + RegisterConfig( + key="TIMEOUT", + value=10, + help="色图下载超时限制(秒)", + default_value=10, + type=int, + ), + RegisterConfig( + key="SHOW_INFO", + value=True, + help="是否显示色图的基本信息,如PID等", + default_value=True, + type=bool, + ), + RegisterConfig( + key="ALLOW_GROUP_R18", + value=False, + help="在群聊中启用R18权限", + default_value=False, + type=bool, + ), + RegisterConfig( + key="MAX_ONCE_NUM2FORWARD", + value=None, + help="单次发送的图片数量达到指定值时转发为合并消息", + default_value=None, + type=int, + ), + RegisterConfig( + key="MAX_ONCE_NUM", + value=10, + help="单次发送图片数量限制", + default_value=10, + type=int, + ), + RegisterConfig( + module="pixiv", + key="PIXIV_NGINX_URL", + value="i.pixiv.re", + help="Pixiv反向代理", + default_value="i.pixiv.re", + ), + ], + ).dict(), +) + + +@run_postprocessor +async def _( + matcher: Matcher, + exception: Exception | None, + session: EventSession, +): + if matcher.plugin_name == "send_setu": + # 添加数据至数据库 + try: + await SetuManage.save_to_database() + logger.info("色图数据自动存储数据库成功...") + except Exception: + pass + + +_matcher = on_alconna( + Alconna( + "色图", + Args["tags?", str], + Option("-n", Args["num", int, 1], help_text="数量"), + Option("-id", Args["local_id", int], help_text="本地id"), + Option("-r", action=store_true, help_text="r18"), + ), + aliases={"涩图", "不够色", "来一发", "再来点"}, + priority=5, + block=True, +) + +_matcher.shortcut( + r".*?(?P\d*)[份|发|张|个|次|点](?P.*)[瑟|色|涩]图.*?", + command="色图", + arguments=["{tags}", "-n", "{num}"], + prefix=True, +) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + num: Match[int], + tags: Match[str], + local_id: Match[int], +): + _tags = tags.result.split("#") if tags.available else None + if _tags and NICKNAME in _tags: + await MessageUtils.build_message( + "咳咳咳,虽然我很可爱,但是我木有自己的色图~~~有的话记得发我一份呀" + ).finish() + if not session.id1: + await MessageUtils.build_message("用户id为空...").finish() + gid = session.id3 or session.id2 + user_console = await UserConsole.get_user(session.id1, session.platform) + user, _ = await SignUser.get_or_create( + user_id=session.id1, + defaults={"user_console": user_console, "platform": session.platform}, + ) + if session.id1 not in bot.config.superusers: + """超级用户跳过罗翔""" + if result := SetuManage.get_luo(float(user.impression)): + await result.finish() + is_r18 = arparma.find("r") + _num = num.result if num.available else 1 + if is_r18 and gid: + """群聊中禁止查看r18""" + if not base_config.get("ALLOW_GROUP_R18"): + await MessageUtils.build_message( + random.choice( + [ + "这种不好意思的东西怎么可能给这么多人看啦", + "羞羞脸!给我滚出克私聊!", + "变态变态变态变态大变态!", + ] + ) + ).finish() + if local_id.available: + """指定id""" + result = await SetuManage.get_setu(local_id=local_id.result) + if isinstance(result, str): + await MessageUtils.build_message(result).finish(reply_to=True) + await result[0].finish() + result_list = await SetuManage.get_setu(tags=_tags, num=_num, is_r18=is_r18) + if isinstance(result_list, str): + await MessageUtils.build_message(result_list).finish(reply_to=True) + max_once_num2forward = base_config.get("MAX_ONCE_NUM2FORWARD") + platform = PlatformUtils.get_platform(bot) + if ( + "qq" == platform + and gid + and max_once_num2forward + and len(result_list) >= max_once_num2forward + ): + logger.debug("使用合并转发转发色图数据", arparma.header_result, session=session) + forward = MessageUtils.template2forward(result_list, bot.self_id) # type: ignore + await bot.send_group_forward_msg( + group_id=int(gid), + messages=forward, # type: ignore + ) + else: + for result in result_list: + logger.info(f"发送色图 {result}", arparma.header_result, session=session) + receipt = await result.send() + if receipt: + message_id = receipt.msg_ids[0]["message_id"] + await WithdrawManager.withdraw_message( + bot, + message_id, + base_config.get("WITHDRAW_SETU_MESSAGE"), + session, + ) + logger.info( + f"调用发送 {num}张 色图 tags: {_tags}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/send_setu_/send_setu/_data_source.py b/zhenxun/plugins/send_setu_/send_setu/_data_source.py new file mode 100644 index 00000000..6bac3d22 --- /dev/null +++ b/zhenxun/plugins/send_setu_/send_setu/_data_source.py @@ -0,0 +1,360 @@ +import os +import random +from pathlib import Path + +from asyncpg import UniqueViolationError +from nonebot_plugin_alconna import UniMessage + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import compressed_image +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.utils import change_img_md5, change_pixiv_image_links + +from .._model import Setu + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net", +} + +base_config = Config.get("send_setu") + + +class SetuManage: + + URL = "https://api.lolicon.app/setu/v2" + save_data = [] + + @classmethod + async def get_setu( + cls, + *, + local_id: int | None = None, + num: int = 10, + tags: list[str] | None = None, + is_r18: bool = False, + ) -> list[UniMessage] | str: + """获取色图 + + 参数: + local_id: 指定图片id + num: 数量 + tags: 标签 + is_r18: 是否r18 + + 返回: + list[MessageFactory] | str: 色图数据列表或消息 + + """ + result_list = [] + if local_id: + """本地id""" + data_list = await cls.get_setu_list(local_id=local_id) + if isinstance(data_list, str): + return data_list + file = await cls.get_image(data_list[0]) + if isinstance(file, str): + return file + return [cls.init_image_message(file, data_list[0])] + if base_config.get("ONLY_USE_LOCAL_SETU"): + """仅使用本地色图""" + flag = False + data_list = await cls.get_setu_list(tags=tags, is_r18=is_r18) + if isinstance(data_list, str): + return data_list + cls.save_data = data_list + if num > len(data_list): + num = len(data_list) + flag = True + setu_list = random.sample(data_list, num) + for setu in setu_list: + base_path = None + if setu.is_r18: + base_path = IMAGE_PATH / "_r18" + else: + base_path = IMAGE_PATH / "_setu" + file_path = base_path / f"{setu.local_id}.jpg" + if not file_path.exists(): + return f"本地色图Id: {setu.local_id} 不存在..." + result_list.append(cls.init_image_message(file_path, setu)) + if flag: + result_list.append( + MessageUtils.build_message("坏了,已经没图了,被榨干了!") + ) + return result_list + data_list = await cls.search_lolicon(tags, num, is_r18) + if isinstance(data_list, str): + """搜索失败, 从本地数据库中搜索""" + data_list = await cls.get_setu_list(tags=tags, is_r18=is_r18) + if isinstance(data_list, str): + return data_list + if not data_list: + return "没找到符合条件的色图..." + cls.save_data = data_list + flag = False + if num > len(data_list): + num = len(data_list) + flag = True + for setu in data_list: + file = await cls.get_image(setu) + if isinstance(file, str): + result_list.append(MessageUtils.build_message(file)) + continue + result_list.append(cls.init_image_message(file, setu)) + if not result_list: + return "没找到符合条件的色图..." + if flag: + result_list.append( + MessageUtils.build_message("坏了,已经没图了,被榨干了!") + ) + return result_list + + @classmethod + def init_image_message(cls, file: Path, setu: Setu) -> UniMessage: + """初始化图片发送消息 + + 参数: + file: 图片路径 + setu: Setu + + 返回: + UniMessage: 发送消息内容 + """ + data_list = [] + if base_config.get("SHOW_INFO"): + data_list.append( + f"id:{setu.local_id or ''}\n" + f"title:{setu.title}\n" + f"author:{setu.author}\n" + f"PID:{setu.pid}\n" + ) + data_list.append(file) + return MessageUtils.build_message(data_list) + + @classmethod + async def get_setu_list( + cls, + *, + local_id: int | None = None, + tags: list[str] | None = None, + is_r18: bool = False, + ) -> list[Setu] | str: + """获取数据库中的色图数据 + + 参数: + local_id: 色图本地id. + tags: 标签. + is_r18: 是否r18. + + 返回: + list[Setu] | str: 色图数据列表或消息 + """ + image_list: list[Setu] = [] + if local_id: + image_count = await Setu.filter(is_r18=is_r18).count() - 1 + if local_id < 0 or local_id > image_count: + return f"超过当前上下限!({image_count})" + image_list = [await Setu.query_image(local_id, r18=is_r18)] # type: ignore + elif tags: + image_list = await Setu.query_image(tags=tags, r18=is_r18) # type: ignore + else: + image_list = await Setu.query_image(r18=is_r18) # type: ignore + if not image_list: + return "没找到符合条件的色图..." + return image_list + + @classmethod + def get_luo(cls, impression: float) -> UniMessage | None: + """罗翔 + + 参数: + impression: 好感度 + + 返回: + MessageFactory | None: 返回数据 + """ + if initial_setu_probability := base_config.get("INITIAL_SETU_PROBABILITY"): + probability = float(impression) + initial_setu_probability * 100 + if probability < random.randint(1, 101): + return MessageUtils.build_message( + [ + "我为什么要给你发这个?", + IMAGE_PATH + / "luoxiang" + / random.choice(os.listdir(IMAGE_PATH / "luoxiang")), + f"\n(快向{NICKNAME}签到提升好感度吧!)", + ] + ) + return None + + @classmethod + async def get_image(cls, setu: Setu) -> str | Path: + """下载图片 + + 参数: + setu: Setu + + 返回: + str | Path: 图片路径或返回消息 + """ + url = change_pixiv_image_links(setu.img_url) + index = setu.local_id if setu.local_id else random.randint(1, 100000) + file_name = f"{index}_temp_setu.jpg" + base_path = TEMP_PATH + if setu.local_id: + """本地图片存在直接返回""" + file_name = f"{index}.jpg" + if setu.is_r18: + base_path = IMAGE_PATH / "_r18" + else: + base_path = IMAGE_PATH / "_setu" + local_file = base_path / file_name + if local_file.exists(): + return local_file + file = base_path / file_name + download_success = False + for i in range(3): + logger.debug(f"尝试在线下载第 {i+1} 次", "色图") + try: + if await AsyncHttpx.download_file( + url, + file, + timeout=base_config.get("TIMEOUT"), + ): + download_success = True + if setu.local_id is not None: + if ( + os.path.getsize(base_path / f"{index}.jpg") + > 1024 * 1024 * 1.5 + ): + compressed_image( + base_path / f"{index}.jpg", + ) + change_img_md5(file) + logger.info(f"下载 lolicon 图片 {url} 成功, id:{index}") + break + except TimeoutError as e: + logger.error(f"下载图片超时", "色图", e=e) + except Exception as e: + logger.error(f"下载图片错误", "色图", e=e) + return file if download_success else "图片被小怪兽恰掉啦..!QAQ" + + @classmethod + async def search_lolicon( + cls, tags: list[str] | None, num: int, is_r18: bool + ) -> list[Setu] | str: + """搜索lolicon色图 + + 参数: + tags: 标签 + num: 数量 + is_r18: 是否r18 + + 返回: + list[Setu] | str: 色图数据或返回消息 + """ + params = { + "r18": 1 if is_r18 else 0, # 添加r18参数 0为否,1为是,2为混合 + "tag": tags, # 若指定tag + "num": 20, # 一次返回的结果数量 + "size": ["original"], + } + for count in range(3): + logger.debug(f"尝试获取图片URL第 {count+1} 次", "色图") + try: + response = await AsyncHttpx.get( + cls.URL, timeout=base_config.get("TIMEOUT"), params=params + ) + if response.status_code == 200: + data = response.json() + if not data["error"]: + data = data["data"] + result_list = cls.__handle_data(data) + num = num if num < len(data) else len(data) + random_list = random.sample(result_list, num) + if not random_list: + return "没找到符合条件的色图..." + return random_list + else: + return "没找到符合条件的色图..." + except TimeoutError as e: + logger.error(f"获取图片URL超时", "色图", e=e) + except Exception as e: + logger.error(f"访问页面错误", "色图", e=e) + return "我网线被人拔了..QAQ" + + @classmethod + def __handle_data(cls, data: dict) -> list[Setu]: + """lolicon数据处理 + + 参数: + data: lolicon数据 + + 返回: + list[Setu]: 整理的数据 + """ + result_list = [] + for i in range(len(data)): + img_url = data[i]["urls"]["original"] + img_url = change_pixiv_image_links(img_url) + title = data[i]["title"] + author = data[i]["author"] + pid = data[i]["pid"] + tags = [] + for j in range(len(data[i]["tags"])): + tags.append(data[i]["tags"][j]) + # if command != "色图r": + # if "R-18" in tags: + # tags.remove("R-18") + setu = Setu( + title=title, + author=author, + pid=pid, + img_url=img_url, + tags=",".join(tags), + is_r18="R-18" in tags, + ) + result_list.append(setu) + return result_list + + @classmethod + async def save_to_database(cls): + """存储色图数据到数据库 + + 参数: + data_list: 色图数据列表 + """ + set_list = [] + exists_list = [] + for data in cls.save_data: + if f"{data.pid}:{data.img_url}" not in exists_list: + exists_list.append(f"{data.pid}:{data.img_url}") + set_list.append(data) + if set_list: + create_list = [] + _cnt = 0 + _r18_cnt = 0 + for setu in set_list: + try: + if not await Setu.exists(pid=setu.pid, img_url=setu.img_url): + idx = await Setu.filter(is_r18=setu.is_r18).count() + setu.local_id = idx + (_r18_cnt if setu.is_r18 else _cnt) + setu.img_hash = "" + if setu.is_r18: + _r18_cnt += 1 + else: + _cnt += 1 + create_list.append(setu) + except UniqueViolationError: + pass + cls.save_data = [] + if create_list: + try: + await Setu.bulk_create(create_list, 10) + logger.debug(f"成功保存 {len(create_list)} 条色图数据") + except Exception as e: + logger.error("存储色图数据错误...", e=e) diff --git a/zhenxun/plugins/send_setu_/update_setu/__init__.py b/zhenxun/plugins/send_setu_/update_setu/__init__.py new file mode 100644 index 00000000..2b5b6ae9 --- /dev/null +++ b/zhenxun/plugins/send_setu_/update_setu/__init__.py @@ -0,0 +1,59 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from .data_source import update_setu_img + +__plugin_meta__ = PluginMetadata( + name="更新色图", + description="更新数据库内存在的色图", + usage=""" + 更新数据库内存在的色图 + 指令: + 更新色图 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + limits=[BaseBlock(result="色图正在更新...")], + ).dict(), +) + +_matcher = on_alconna( + Alconna("更新色图"), rule=to_me(), permission=SUPERUSER, priority=1, block=True +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + if Config.get_config("send_setu", "DOWNLOAD_SETU"): + await MessageUtils.build_message("开始更新色图...").send(reply_to=True) + result = await update_setu_img(True) + if result: + await MessageUtils.build_message(result).send() + logger.info("更新色图", arparma.header_result, session=session) + else: + await MessageUtils.build_message("更新色图配置未开启...").send() + + +# 更新色图 +@scheduler.scheduled_job( + "cron", + hour=4, + minute=30, +) +async def _(): + if Config.get_config("send_setu", "DOWNLOAD_SETU"): + result = await update_setu_img() + if result: + logger.info(result, "自动更新色图") diff --git a/plugins/send_setu_/update_setu/data_source.py b/zhenxun/plugins/send_setu_/update_setu/data_source.py old mode 100755 new mode 100644 similarity index 81% rename from plugins/send_setu_/update_setu/data_source.py rename to zhenxun/plugins/send_setu_/update_setu/data_source.py index 52d548b3..07d217d6 --- a/plugins/send_setu_/update_setu/data_source.py +++ b/zhenxun/plugins/send_setu_/update_setu/data_source.py @@ -1,178 +1,187 @@ -import os -import shutil -from datetime import datetime - -import nonebot -import ujson as json -from asyncpg.exceptions import UniqueViolationError -from nonebot import Driver -from PIL import UnidentifiedImageError - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH, TEXT_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import compressed_image, get_img_hash -from utils.utils import change_pixiv_image_links, get_bot - -from .._model import Setu - -driver: Driver = nonebot.get_driver() - -_path = IMAGE_PATH - - -# 替换旧色图数据,修复local_id一直是50的问题 -@driver.on_startup -async def update_old_setu_data(): - path = TEXT_PATH - setu_data_file = path / "setu_data.json" - r18_data_file = path / "r18_setu_data.json" - if setu_data_file.exists() or r18_data_file.exists(): - index = 0 - r18_index = 0 - count = 0 - fail_count = 0 - for file in [setu_data_file, r18_data_file]: - if file.exists(): - data = json.load(open(file, "r", encoding="utf8")) - for x in data: - if file == setu_data_file: - idx = index - if "R-18" in data[x]["tags"]: - data[x]["tags"].remove("R-18") - else: - idx = r18_index - img_url = ( - data[x]["img_url"].replace("i.pixiv.cat", "i.pximg.net") - if "i.pixiv.cat" in data[x]["img_url"] - else data[x]["img_url"] - ) - # idx = r18_index if 'R-18' in data[x]["tags"] else index - try: - if not await Setu.exists(pid=data[x]["pid"], url=img_url): - await Setu.create( - local_id=idx, - title=data[x]["title"], - author=data[x]["author"], - pid=data[x]["pid"], - img_hash=data[x]["img_hash"], - img_url=img_url, - is_r18="R-18" in data[x]["tags"], - tags=",".join(data[x]["tags"]), - ) - count += 1 - if "R-18" in data[x]["tags"]: - r18_index += 1 - else: - index += 1 - logger.info(f'添加旧色图数据成功 PID:{data[x]["pid"]} index:{idx}....') - except UniqueViolationError: - fail_count += 1 - logger.info( - f'添加旧色图数据失败,色图重复 PID:{data[x]["pid"]} index:{idx}....' - ) - file.unlink() - setu_url_path = path / "setu_url.json" - setu_r18_url_path = path / "setu_r18_url.json" - if setu_url_path.exists(): - setu_url_path.unlink() - if setu_r18_url_path.exists(): - setu_r18_url_path.unlink() - logger.info(f"更新旧色图数据完成,成功更新数据:{count} 条,累计失败:{fail_count} 条") - - -# 删除色图rar文件夹 -shutil.rmtree(IMAGE_PATH / "setu_rar", ignore_errors=True) -shutil.rmtree(IMAGE_PATH / "r18_rar", ignore_errors=True) -shutil.rmtree(IMAGE_PATH / "rar", ignore_errors=True) - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -async def update_setu_img(flag: bool = False): - """ - 更新色图 - :param flag: 是否手动更新 - """ - image_list = await Setu.all().order_by("local_id") - image_list.reverse() - _success = 0 - error_info = [] - error_type = [] - count = 0 - for image in image_list: - count += 1 - path = _path / "_r18" if image.is_r18 else _path / "_setu" - local_image = path / f"{image.local_id}.jpg" - path.mkdir(exist_ok=True, parents=True) - TEMP_PATH.mkdir(exist_ok=True, parents=True) - if not local_image.exists() or not image.img_hash: - temp_file = TEMP_PATH / f"{image.local_id}.jpg" - if temp_file.exists(): - temp_file.unlink() - url_ = change_pixiv_image_links(image.img_url) - try: - if not await AsyncHttpx.download_file( - url_, TEMP_PATH / f"{image.local_id}.jpg" - ): - continue - _success += 1 - try: - if ( - os.path.getsize( - TEMP_PATH / f"{image.local_id}.jpg", - ) - > 1024 * 1024 * 1.5 - ): - compressed_image( - TEMP_PATH / f"{image.local_id}.jpg", - path / f"{image.local_id}.jpg", - ) - else: - logger.info( - f"不需要压缩,移动图片{TEMP_PATH}/{image.local_id}.jpg " - f"--> /{path}/{image.local_id}.jpg" - ) - os.rename( - TEMP_PATH / f"{image.local_id}.jpg", - path / f"{image.local_id}.jpg", - ) - except FileNotFoundError: - logger.warning(f"文件 {image.local_id}.jpg 不存在,跳过...") - continue - img_hash = str(get_img_hash(f"{path}/{image.local_id}.jpg")) - image.img_hash = img_hash - await image.save(update_fields=["img_hash"]) - # await Setu.update_setu_data(image.pid, img_hash=img_hash) - except UnidentifiedImageError: - # 图片已删除 - unlink = False - with open(local_image, "r") as f: - if "404 Not Found" in f.read(): - unlink = True - if unlink: - local_image.unlink() - max_num = await Setu.delete_image(image.pid, image.img_url) - if (path / f"{max_num}.jpg").exists(): - os.rename(path / f"{max_num}.jpg", local_image) - logger.warning(f"更新色图 PID:{image.pid} 404,已删除并替换") - except Exception as e: - _success -= 1 - logger.error(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}") - if type(e) not in error_type: - error_type.append(type(e)) - error_info.append(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}") - else: - logger.info(f"更新色图 {image.local_id}.jpg 已存在") - if _success or error_info or flag: - if bot := get_bot(): - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f'{str(datetime.now()).split(".")[0]} 更新 色图 完成,本地存在 {count} 张,实际更新 {_success} 张,' - f"以下为更新时未知错误:\n" + "\n".join(error_info), - ) +import os +import shutil +from datetime import datetime + +import nonebot +import ujson as json +from asyncpg.exceptions import UniqueViolationError +from nonebot.drivers import Driver +from PIL import UnidentifiedImageError + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH, TEXT_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import compressed_image +from zhenxun.utils.utils import change_pixiv_image_links + +from .._model import Setu + +driver: Driver = nonebot.get_driver() + +_path = IMAGE_PATH + + +# 替换旧色图数据,修复local_id一直是50的问题 +@driver.on_startup +async def update_old_setu_data(): + path = TEXT_PATH + setu_data_file = path / "setu_data.json" + r18_data_file = path / "r18_setu_data.json" + if setu_data_file.exists() or r18_data_file.exists(): + index = 0 + r18_index = 0 + count = 0 + fail_count = 0 + for file in [setu_data_file, r18_data_file]: + if file.exists(): + data = json.load(open(file, "r", encoding="utf8")) + for x in data: + if file == setu_data_file: + idx = index + if "R-18" in data[x]["tags"]: + data[x]["tags"].remove("R-18") + else: + idx = r18_index + img_url = ( + data[x]["img_url"].replace("i.pixiv.cat", "i.pximg.net") + if "i.pixiv.cat" in data[x]["img_url"] + else data[x]["img_url"] + ) + # idx = r18_index if 'R-18' in data[x]["tags"] else index + try: + if not await Setu.exists(pid=data[x]["pid"], url=img_url): + await Setu.create( + local_id=idx, + title=data[x]["title"], + author=data[x]["author"], + pid=data[x]["pid"], + img_hash=data[x]["img_hash"], + img_url=img_url, + is_r18="R-18" in data[x]["tags"], + tags=",".join(data[x]["tags"]), + ) + count += 1 + if "R-18" in data[x]["tags"]: + r18_index += 1 + else: + index += 1 + logger.info( + f'添加旧色图数据成功 PID:{data[x]["pid"]} index:{idx}....' + ) + except UniqueViolationError: + fail_count += 1 + logger.info( + f'添加旧色图数据失败,色图重复 PID:{data[x]["pid"]} index:{idx}....' + ) + file.unlink() + setu_url_path = path / "setu_url.json" + setu_r18_url_path = path / "setu_r18_url.json" + if setu_url_path.exists(): + setu_url_path.unlink() + if setu_r18_url_path.exists(): + setu_r18_url_path.unlink() + logger.info( + f"更新旧色图数据完成,成功更新数据:{count} 条,累计失败:{fail_count} 条" + ) + + +# 删除色图rar文件夹 +shutil.rmtree(IMAGE_PATH / "setu_rar", ignore_errors=True) +shutil.rmtree(IMAGE_PATH / "r18_rar", ignore_errors=True) +shutil.rmtree(IMAGE_PATH / "rar", ignore_errors=True) + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net", +} + + +async def update_setu_img(flag: bool = False) -> str | None: + """更新色图 + + 参数: + flag: 是否手动更新. + + 返回: + str | None: 更新信息 + """ + image_list = await Setu.all().order_by("local_id") + image_list.reverse() + _success = 0 + error_info = [] + error_type = [] + count = 0 + for image in image_list: + count += 1 + path = _path / "_r18" if image.is_r18 else _path / "_setu" + local_image = path / f"{image.local_id}.jpg" + path.mkdir(exist_ok=True, parents=True) + TEMP_PATH.mkdir(exist_ok=True, parents=True) + if not local_image.exists() or not image.img_hash: + temp_file = TEMP_PATH / f"{image.local_id}.jpg" + if temp_file.exists(): + temp_file.unlink() + url_ = change_pixiv_image_links(image.img_url) + try: + if not await AsyncHttpx.download_file( + url_, TEMP_PATH / f"{image.local_id}.jpg" + ): + continue + _success += 1 + try: + if ( + os.path.getsize( + TEMP_PATH / f"{image.local_id}.jpg", + ) + > 1024 * 1024 * 1.5 + ): + compressed_image( + TEMP_PATH / f"{image.local_id}.jpg", + path / f"{image.local_id}.jpg", + ) + else: + logger.info( + f"不需要压缩,移动图片{TEMP_PATH}/{image.local_id}.jpg " + f"--> /{path}/{image.local_id}.jpg" + ) + os.rename( + TEMP_PATH / f"{image.local_id}.jpg", + path / f"{image.local_id}.jpg", + ) + except FileNotFoundError: + logger.warning(f"文件 {image.local_id}.jpg 不存在,跳过...") + continue + # img_hash = str(get_img_hash(f"{path}/{image.local_id}.jpg")) + image.img_hash = "" + await image.save(update_fields=["img_hash"]) + # await Setu.update_setu_data(image.pid, img_hash=img_hash) + except UnidentifiedImageError: + # 图片已删除 + unlink = False + with open(local_image, "r") as f: + if "404 Not Found" in f.read(): + unlink = True + if unlink: + local_image.unlink() + max_num = await Setu.delete_image(image.pid, image.img_url) + if (path / f"{max_num}.jpg").exists(): + os.rename(path / f"{max_num}.jpg", local_image) + logger.warning(f"更新色图 PID:{image.pid} 404,已删除并替换") + except Exception as e: + _success -= 1 + logger.error(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}") + if type(e) not in error_type: + error_type.append(type(e)) + error_info.append( + f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}" + ) + else: + logger.info(f"更新色图 {image.local_id}.jpg 已存在") + if _success or error_info or flag: + return ( + f'{str(datetime.now()).split(".")[0]} 更新 色图 完成,本地存在 {count} 张,实际更新 {_success} 张,以下为更新时未知错误:\n' + + "\n".join(error_info), + ) + return None diff --git a/zhenxun/plugins/send_voice/__init__.py b/zhenxun/plugins/send_voice/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/plugins/send_voice/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/send_voice/dinggong.py b/zhenxun/plugins/send_voice/dinggong.py new file mode 100644 index 00000000..a01129ca --- /dev/null +++ b/zhenxun/plugins/send_voice/dinggong.py @@ -0,0 +1,51 @@ +import os +import random + +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, UniMessage, Voice, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import RECORD_PATH +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +__plugin_meta__ = PluginMetadata( + name="钉宫骂我", + description="请狠狠的骂我一次!", + usage=""" + 多骂我一点,球球了 + 指令: + 骂老子 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + limits=[PluginCdBlock(cd=3, result="就...就算求我骂你也得慢慢来...")], + ).dict(), +) + +_matcher = on_alconna(Alconna("ma-wo"), rule=to_me(), priority=5, block=True) + +_matcher.shortcut( + r".*?骂.*?我.*?", + command="ma-wo", + arguments=[], + prefix=True, +) + +path = RECORD_PATH / "dinggong" + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + if not path.exists(): + await MessageUtils.build_message("钉宫语音文件夹不存在...").finish() + files = os.listdir(path) + if not files: + await MessageUtils.build_message("钉宫语音文件夹为空...").finish() + voice = random.choice(files) + await UniMessage([Voice(path=path / voice)]).send() + await MessageUtils.build_message(voice.split("_")[1]).send() + logger.info(f"发送钉宫骂人: {voice}", arparma.header_result, session=session) diff --git a/plugins/statistics/__init__.py b/zhenxun/plugins/statistics/__init__.py old mode 100755 new mode 100644 similarity index 94% rename from plugins/statistics/__init__.py rename to zhenxun/plugins/statistics/__init__.py index 4ed43619..5cf30279 --- a/plugins/statistics/__init__.py +++ b/zhenxun/plugins/statistics/__init__.py @@ -1,127 +1,132 @@ -from configs.path_config import DATA_PATH -import nonebot -import os -try: - import ujson as json -except ModuleNotFoundError: - import json - -nonebot.load_plugins("plugins/statistics") - -old_file1 = DATA_PATH / "_prefix_count.json" -old_file2 = DATA_PATH / "_prefix_user_count.json" -new_path = DATA_PATH / "statistics" -new_path.mkdir(parents=True, exist_ok=True) -if old_file1.exists(): - os.rename(old_file1, new_path / "_prefix_count.json") -if old_file2.exists(): - os.rename(old_file2, new_path / "_prefix_user_count.json") - - -# 修改旧数据 - -statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" -statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" - -for file in [statistics_group_file, statistics_user_file]: - if file.exists(): - with open(file, "r", encoding="utf8") as f: - data = json.load(f) - if not (statistics_group_file.parent / f"{file}.bak").exists(): - with open(f"{file}.bak", "w", encoding="utf8") as wf: - json.dump(data, wf, ensure_ascii=False, indent=4) - for x in ["total_statistics", "day_statistics"]: - for key in data[x].keys(): - num = 0 - if data[x][key].get("ai") is not None: - if data[x][key].get("Ai") is not None: - data[x][key]["Ai"] += data[x][key]["ai"] - else: - data[x][key]["Ai"] = data[x][key]["ai"] - del data[x][key]["ai"] - if data[x][key].get("抽卡") is not None: - if data[x][key].get("游戏抽卡") is not None: - data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] - else: - data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] - del data[x][key]["抽卡"] - if data[x][key].get("我的道具") is not None: - num += data[x][key]["我的道具"] - del data[x][key]["我的道具"] - if data[x][key].get("使用道具") is not None: - num += data[x][key]["使用道具"] - del data[x][key]["使用道具"] - if data[x][key].get("我的金币") is not None: - num += data[x][key]["我的金币"] - del data[x][key]["我的金币"] - if data[x][key].get("购买") is not None: - num += data[x][key]["购买"] - del data[x][key]["购买"] - if data[x][key].get("商店") is not None: - data[x][key]["商店"] += num - else: - data[x][key]["商店"] = num - for x in ["week_statistics", "month_statistics"]: - for key in data[x].keys(): - if key == "total": - if data[x][key].get("ai") is not None: - if data[x][key].get("Ai") is not None: - data[x][key]["Ai"] += data[x][key]["ai"] - else: - data[x][key]["Ai"] = data[x][key]["ai"] - del data[x][key]["ai"] - if data[x][key].get("抽卡") is not None: - if data[x][key].get("游戏抽卡") is not None: - data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] - else: - data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] - del data[x][key]["抽卡"] - if data[x][key].get("我的道具") is not None: - num += data[x][key]["我的道具"] - del data[x][key]["我的道具"] - if data[x][key].get("使用道具") is not None: - num += data[x][key]["使用道具"] - del data[x][key]["使用道具"] - if data[x][key].get("我的金币") is not None: - num += data[x][key]["我的金币"] - del data[x][key]["我的金币"] - if data[x][key].get("购买") is not None: - num += data[x][key]["购买"] - del data[x][key]["购买"] - if data[x][key].get("商店") is not None: - data[x][key]["商店"] += num - else: - data[x][key]["商店"] = num - else: - for day in data[x][key].keys(): - num = 0 - if data[x][key][day].get("ai") is not None: - if data[x][key][day].get("Ai") is not None: - data[x][key][day]["Ai"] += data[x][key][day]["ai"] - else: - data[x][key][day]["Ai"] = data[x][key][day]["ai"] - del data[x][key][day]["ai"] - if data[x][key][day].get("抽卡") is not None: - if data[x][key][day].get("游戏抽卡") is not None: - data[x][key][day]["游戏抽卡"] += data[x][key][day]["抽卡"] - else: - data[x][key][day]["游戏抽卡"] = data[x][key][day]["抽卡"] - del data[x][key][day]["抽卡"] - if data[x][key][day].get("我的道具") is not None: - num += data[x][key][day]["我的道具"] - del data[x][key][day]["我的道具"] - if data[x][key][day].get("使用道具") is not None: - num += data[x][key][day]["使用道具"] - del data[x][key][day]["使用道具"] - if data[x][key][day].get("我的金币") is not None: - num += data[x][key][day]["我的金币"] - del data[x][key][day]["我的金币"] - if data[x][key][day].get("购买") is not None: - num += data[x][key][day]["购买"] - del data[x][key][day]["购买"] - if data[x][key][day].get("商店") is not None: - data[x][key][day]["商店"] += num - else: - data[x][key][day]["商店"] = num - with open(file, "w", encoding="utf8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) +import os +from pathlib import Path + +import nonebot +import ujson as json + +from zhenxun.configs.path_config import DATA_PATH + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) + +old_file1 = DATA_PATH / "_prefix_count.json" +old_file2 = DATA_PATH / "_prefix_user_count.json" +new_path = DATA_PATH / "statistics" +new_path.mkdir(parents=True, exist_ok=True) +if old_file1.exists(): + os.rename(old_file1, new_path / "_prefix_count.json") +if old_file2.exists(): + os.rename(old_file2, new_path / "_prefix_user_count.json") + + +# 修改旧数据 + +statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" +statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" + +for file in [statistics_group_file, statistics_user_file]: + if file.exists(): + with open(file, "r", encoding="utf8") as f: + data = json.load(f) + if not (statistics_group_file.parent / f"{file}.bak").exists(): + with open(f"{file}.bak", "w", encoding="utf8") as wf: + json.dump(data, wf, ensure_ascii=False, indent=4) + for x in ["total_statistics", "day_statistics"]: + for key in data[x].keys(): + num = 0 + if data[x][key].get("ai") is not None: + if data[x][key].get("Ai") is not None: + data[x][key]["Ai"] += data[x][key]["ai"] + else: + data[x][key]["Ai"] = data[x][key]["ai"] + del data[x][key]["ai"] + if data[x][key].get("抽卡") is not None: + if data[x][key].get("游戏抽卡") is not None: + data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] + else: + data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] + del data[x][key]["抽卡"] + if data[x][key].get("我的道具") is not None: + num += data[x][key]["我的道具"] + del data[x][key]["我的道具"] + if data[x][key].get("使用道具") is not None: + num += data[x][key]["使用道具"] + del data[x][key]["使用道具"] + if data[x][key].get("我的金币") is not None: + num += data[x][key]["我的金币"] + del data[x][key]["我的金币"] + if data[x][key].get("购买") is not None: + num += data[x][key]["购买"] + del data[x][key]["购买"] + if data[x][key].get("商店") is not None: + data[x][key]["商店"] += num + else: + data[x][key]["商店"] = num + for x in ["week_statistics", "month_statistics"]: + for key in data[x].keys(): + num = 0 + if key == "total": + if data[x][key].get("ai") is not None: + if data[x][key].get("Ai") is not None: + data[x][key]["Ai"] += data[x][key]["ai"] + else: + data[x][key]["Ai"] = data[x][key]["ai"] + del data[x][key]["ai"] + if data[x][key].get("抽卡") is not None: + if data[x][key].get("游戏抽卡") is not None: + data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] + else: + data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] + del data[x][key]["抽卡"] + if data[x][key].get("我的道具") is not None: + num += data[x][key]["我的道具"] + del data[x][key]["我的道具"] + if data[x][key].get("使用道具") is not None: + num += data[x][key]["使用道具"] + del data[x][key]["使用道具"] + if data[x][key].get("我的金币") is not None: + num += data[x][key]["我的金币"] + del data[x][key]["我的金币"] + if data[x][key].get("购买") is not None: + num += data[x][key]["购买"] + del data[x][key]["购买"] + if data[x][key].get("商店") is not None: + data[x][key]["商店"] += num + else: + data[x][key]["商店"] = num + else: + for day in data[x][key].keys(): + num = 0 + if data[x][key][day].get("ai") is not None: + if data[x][key][day].get("Ai") is not None: + data[x][key][day]["Ai"] += data[x][key][day]["ai"] + else: + data[x][key][day]["Ai"] = data[x][key][day]["ai"] + del data[x][key][day]["ai"] + if data[x][key][day].get("抽卡") is not None: + if data[x][key][day].get("游戏抽卡") is not None: + data[x][key][day]["游戏抽卡"] += data[x][key][day][ + "抽卡" + ] + else: + data[x][key][day]["游戏抽卡"] = data[x][key][day][ + "抽卡" + ] + del data[x][key][day]["抽卡"] + if data[x][key][day].get("我的道具") is not None: + num += data[x][key][day]["我的道具"] + del data[x][key][day]["我的道具"] + if data[x][key][day].get("使用道具") is not None: + num += data[x][key][day]["使用道具"] + del data[x][key][day]["使用道具"] + if data[x][key][day].get("我的金币") is not None: + num += data[x][key][day]["我的金币"] + del data[x][key][day]["我的金币"] + if data[x][key][day].get("购买") is not None: + num += data[x][key][day]["购买"] + del data[x][key][day]["购买"] + if data[x][key][day].get("商店") is not None: + data[x][key][day]["商店"] += num + else: + data[x][key][day]["商店"] = num + with open(file, "w", encoding="utf8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) diff --git a/zhenxun/plugins/statistics/_data_source.py b/zhenxun/plugins/statistics/_data_source.py new file mode 100644 index 00000000..e83707b1 --- /dev/null +++ b/zhenxun/plugins/statistics/_data_source.py @@ -0,0 +1,130 @@ +from datetime import datetime, timedelta + +from tortoise.functions import Count + +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType + + +class StatisticsManage: + + @classmethod + async def get_statistics( + cls, + plugin_name: str | None, + is_global: bool, + search_type: str | None, + user_id: str | None = None, + group_id: str | None = None, + ): + day = None + day_type = "" + if search_type == "day": + day = 1 + day_type = "日" + if search_type == "week": + day = 7 + day_type = "周" + if search_type == "month": + day = 30 + day_type = "月" + if day_type: + day_type += f"({day}天)" + title = "" + if user_id: + """查用户""" + query = GroupInfoUser.filter(user_id=user_id) + if group_id: + query = query.filter(group_id=group_id) + user = await query.first() + title = f"{user.user_name if user else user_id} {day_type}功能调用统计" + elif group_id: + """查群组""" + group = await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ) + title = f"{group.group_name if group else group_id} {day_type}功能调用统计" + else: + title = "功能调用统计" + if is_global and not user_id: + title = "全局 " + title + return await cls.get_global_statistics(plugin_name, day, title) + if user_id: + return await cls.get_my_statistics(user_id, group_id, day, title) + if group_id: + return await cls.get_group_statistics(group_id, day, title) + return None + + @classmethod + async def get_global_statistics( + cls, plugin_name: str | None, day: int | None, title: str + ) -> BuildImage | str: + query = Statistics + if plugin_name: + query = query.filter(plugin_name=plugin_name) + if day: + time = datetime.now() - timedelta(days=day) + query = query.filter(create_time__gte=time) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .values_list("plugin_name", "count") + ) + if not data_list: + return "统计数据为空..." + return await cls.__build_image(data_list, title) + + @classmethod + async def get_my_statistics( + cls, user_id: str, group_id: str | None, day: int | None, title: str + ): + query = Statistics.filter(user_id=user_id) + if group_id: + query = query.filter(group_id=group_id) + if day: + time = datetime.now() - timedelta(days=day) + query = query.filter(create_time__gte=time) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .values_list("plugin_name", "count") + ) + if not data_list: + return "统计数据为空..." + return await cls.__build_image(data_list, title) + + @classmethod + async def get_group_statistics(cls, group_id: str, day: int | None, title: str): + query = Statistics.filter(group_id=group_id) + if day: + time = datetime.now() - timedelta(days=day) + query = query.filter(create_time__gte=time) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .values_list("plugin_name", "count") + ) + if not data_list: + return "统计数据为空..." + return await cls.__build_image(data_list, title) + + @classmethod + async def __build_image(cls, data_list: list[tuple[str, int]], title: str): + mat = BuildMat(MatType.BARH) + module2count = {x[0]: x[1] for x in data_list} + plugin_info = await PluginInfo.filter( + module__in=module2count.keys(), plugin_type=PluginType.NORMAL + ).all() + x_index = [] + data = [] + for plugin in plugin_info: + x_index.append(plugin.name) + data.append(module2count.get(plugin.module, 0)) + mat.x_index = x_index + mat.data = data + mat.title = title + return await mat.build() diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py new file mode 100644 index 00000000..fc070e0c --- /dev/null +++ b/zhenxun/plugins/statistics/statistics_handle.py @@ -0,0 +1,162 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils + +from ._data_source import StatisticsManage + +__plugin_meta__ = PluginMetadata( + name="功能调用统计", + description="功能调用统计可视化", + usage=""" + usage: + 功能调用统计可视化 + 指令: + 功能调用统计 + 日功能调用统计 + 周功能调用统计 + 月功能调用统计 + 我的功能调用统计 : 当前群我的统计 + 我的功能调用统计 -g: 我的全局统计 + 我的日功能调用统计 + 我的周功能调用统计 + 我的月功能调用统计 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.NORMAL, + menu_type="数据统计", + aliases={"功能调用统计"}, + superuser_help=""" + "全局功能调用统计", + "全局日功能调用统计", + "全局周功能调用统计", + "全局月功能调用统计", + """.strip(), + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "功能调用统计", + Args["name?", str], + Option("-g|--global", action=store_true, help_text="全局统计"), + Option("-my", action=store_true, help_text="我的"), + Option("-t|--type", Args["search_type", ["day", "week", "month"]]), + ), + priority=5, + block=True, +) + +_matcher.shortcut( + "日功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "day"], + prefix=True, +) + +_matcher.shortcut( + "周功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "week"], + prefix=True, +) + +_matcher.shortcut( + "月功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "month"], + prefix=True, +) + +_matcher.shortcut( + "全局功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-g"], + prefix=True, +) + +_matcher.shortcut( + "全局日功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "day", "-g"], + prefix=True, +) + +_matcher.shortcut( + "全局周功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "week", "-g"], + prefix=True, +) + +_matcher.shortcut( + "全局月功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "month", "-g"], + prefix=True, +) + +_matcher.shortcut( + "我的功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-my"], + prefix=True, +) + +_matcher.shortcut( + "我的日功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "day", "-my"], + prefix=True, +) + +_matcher.shortcut( + "我的周功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "week", "-my"], + prefix=True, +) + +_matcher.shortcut( + "我的月功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "month", "-my"], + prefix=True, +) + + +@_matcher.handle() +async def _( + session: EventSession, arparma: Arparma, name: Match[str], search_type: Match[str] +): + plugin_name = name.result if name.available else None + st = search_type.result if search_type.available else None + gid = session.id3 or session.id2 + uid = session.id1 if (arparma.find("my") or not gid) else None + is_global = arparma.find("global") + if uid and is_global: + """个人全局""" + gid = None + if result := await StatisticsManage.get_statistics( + plugin_name, arparma.find("global"), st, uid, gid + ): + if isinstance(result, str): + await MessageUtils.build_message(result).finish(reply_to=True) + else: + await MessageUtils.build_message(result).send() + else: + await MessageUtils.build_message("获取数据失败...").send() diff --git a/zhenxun/plugins/statistics/statistics_hook.py b/zhenxun/plugins/statistics/statistics_hook.py new file mode 100644 index 00000000..cb1f4b1f --- /dev/null +++ b/zhenxun/plugins/statistics/statistics_hook.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from nonebot.adapters import Bot +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor +from nonebot.plugin import PluginMetadata +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="功能调用统计", + description="功能调用统计", + usage="""""".strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN + ).dict(), +) + + +@run_postprocessor +async def _( + matcher: Matcher, exception: Exception | None, bot: Bot, session: EventSession +): + if session.id1: + plugin = await PluginInfo.get_or_none(module=matcher.plugin_name) + plugin_type = plugin.plugin_type if plugin else None + if plugin_type == PluginType.NORMAL and matcher.plugin_name not in [ + "update_info", + "statistics_handle", + ]: + await Statistics.create( + user_id=session.id1, + group_id=session.id3 or session.id2, + plugin_name=matcher.plugin_name, + create_time=datetime.now(), + ) diff --git a/zhenxun/plugins/translate/__init__.py b/zhenxun/plugins/translate/__init__.py new file mode 100644 index 00000000..372a5774 --- /dev/null +++ b/zhenxun/plugins/translate/__init__.py @@ -0,0 +1,91 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.depends import CheckConfig +from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils + +from .data_source import language, translate_message + +__plugin_meta__ = PluginMetadata( + name="翻译", + description="出国旅游好助手", + usage=""" + 指令: + 翻译语种: (查看soruce与to可用值,代码与中文都可) + 示例: + 翻译 你好: 将中文翻译为英文 + 翻译 Hello: 将英文翻译为中文 + + 翻译 你好 -to 希腊语: 将"你好"翻译为希腊语 + 翻译 你好: 允许form和to使用中文 + 翻译 你好 -form:中文 to:日语 你好: 指定原语种并将"你好"翻译为日文 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + configs=[ + RegisterConfig(key="APPID", value=None, help="百度翻译APPID"), + RegisterConfig(key="SECRET_KEY", value=None, help="百度翻译秘钥"), + ], + ).dict(), +) + +_matcher = on_alconna( + Alconna( + "翻译", + Args["text", str], + Option("-s|--source", Args["source_text", str, "auto"]), + Option("-t|--to", Args["to_text", str, "auto"]), + ), + priority=5, + block=True, +) + +_language_matcher = on_alconna(Alconna("翻译语种"), priority=5, block=True) + + +@_language_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + s = "" + column_list = ["语种", "代码"] + data_list = [] + for key, value in language.items(): + data_list.append([key, value]) + image = await ImageTemplate.table_page("翻译语种", "", column_list, data_list) + await MessageUtils.build_message(image).send() + logger.info(f"查看翻译语种", arparma.header_result, session=session) + + +@_matcher.handle( + parameterless=[ + CheckConfig(config="APPID"), + CheckConfig(config="SECRET_KEY"), + ] +) +async def _( + session: EventSession, + arparma: Arparma, + text: str, + source_text: Match[str], + to_text: Match[str], +): + source = source_text.result if source_text.available else "auto" + to = to_text.result if to_text.available else "auto" + values = language.values() + keys = language.keys() + if source not in values and source not in keys: + await MessageUtils.build_message("源语种不支持...").finish() + if to not in values and to not in keys: + await MessageUtils.build_message("目标语种不支持...").finish() + result = await translate_message(text, source, to) + await MessageUtils.build_message(result).send(reply_to=True) + logger.info( + f"source: {source}, to: {to}, 翻译: {text}", + arparma.header_result, + session=session, + ) diff --git a/plugins/translate/data_source.py b/zhenxun/plugins/translate/data_source.py old mode 100755 new mode 100644 similarity index 57% rename from plugins/translate/data_source.py rename to zhenxun/plugins/translate/data_source.py index 517938e2..a7a3018d --- a/plugins/translate/data_source.py +++ b/zhenxun/plugins/translate/data_source.py @@ -1,119 +1,83 @@ -import time -from hashlib import md5 -from typing import Any, Tuple - -from nonebot.internal.matcher import Matcher -from nonebot.internal.params import Depends -from nonebot.params import RegexGroup -from nonebot.typing import T_State - -from configs.config import Config -from utils.http_utils import AsyncHttpx - -URL = "http://api.fanyi.baidu.com/api/trans/vip/translate" - - -language = { - "自动": "auto", - "粤语": "yue", - "韩语": "kor", - "泰语": "th", - "葡萄牙语": "pt", - "希腊语": "el", - "保加利亚语": "bul", - "芬兰语": "fin", - "斯洛文尼亚语": "slo", - "繁体中文": "cht", - "中文": "zh", - "文言文": "wyw", - "法语": "fra", - "阿拉伯语": "ara", - "德语": "de", - "荷兰语": "nl", - "爱沙尼亚语": "est", - "捷克语": "cs", - "瑞典语": "swe", - "越南语": "vie", - "英语": "en", - "日语": "jp", - "西班牙语": "spa", - "俄语": "ru", - "意大利语": "it", - "波兰语": "pl", - "丹麦语": "dan", - "罗马尼亚语": "rom", - "匈牙利语": "hu", -} - - -def CheckParam(): - """ - 检查翻译内容是否在language中 - """ - - async def dependency( - matcher: Matcher, - state: T_State, - reg_group: Tuple[Any, ...] = RegexGroup(), - ): - form, to, _ = reg_group - values = language.values() - if form: - form = form.split(":")[-1] - if form not in language and form not in values: - await matcher.finish("FORM选择的语种不存在") - state["form"] = form - else: - state["form"] = "auto" - if to: - to = to.split(":")[-1] - if to not in language and to not in values: - await matcher.finish("TO选择的语种不存在") - state["to"] = to - else: - state["to"] = "auto" - - return Depends(dependency) - - -async def translate_msg(word: str, form: str, to: str) -> str: - """翻译 - - Args: - word (str): 翻译文字 - form (str): 源语言 - to (str): 目标语言 - - Returns: - str: 翻译后的文字 - """ - if form in language: - form = language[form] - if to in language: - to = language[to] - salt = str(time.time()) - app_id = Config.get_config("translate", "APPID") - secret_key = Config.get_config("translate", "SECRET_KEY") - sign = app_id + word + salt + secret_key # type: ignore - md5_ = md5() - md5_.update(sign.encode("utf-8")) - sign = md5_.hexdigest() - params = { - "q": word, - "from": form, - "to": to, - "appid": app_id, - "salt": salt, - "sign": sign, - } - url = URL + "?" - for key, value in params.items(): - url += f"{key}={value}&" - url = url[:-1] - resp = await AsyncHttpx.get(url) - data = resp.json() - if data.get("error_code"): - return data.get("error_msg") - if trans_result := data.get("trans_result"): - return trans_result[0]["dst"] - return "没有找到翻译捏" +import time +from hashlib import md5 + +from zhenxun.configs.config import Config +from zhenxun.utils.http_utils import AsyncHttpx + +URL = "http://api.fanyi.baidu.com/api/trans/vip/translate" + + +language = { + "自动": "auto", + "粤语": "yue", + "韩语": "kor", + "泰语": "th", + "葡萄牙语": "pt", + "希腊语": "el", + "保加利亚语": "bul", + "芬兰语": "fin", + "斯洛文尼亚语": "slo", + "繁体中文": "cht", + "中文": "zh", + "文言文": "wyw", + "法语": "fra", + "阿拉伯语": "ara", + "德语": "de", + "荷兰语": "nl", + "爱沙尼亚语": "est", + "捷克语": "cs", + "瑞典语": "swe", + "越南语": "vie", + "英语": "en", + "日语": "jp", + "西班牙语": "spa", + "俄语": "ru", + "意大利语": "it", + "波兰语": "pl", + "丹麦语": "dan", + "罗马尼亚语": "rom", + "匈牙利语": "hu", +} + + +async def translate_message(word: str, form: str, to: str) -> str: + """翻译 + + 参数: + word (str): 翻译文字 + form (str): 源语言 + to (str): 目标语言 + + 返回: + str: 翻译后的文字 + """ + if form in language: + form = language[form] + if to in language: + to = language[to] + salt = str(time.time()) + app_id = Config.get_config("translate", "APPID") + secret_key = Config.get_config("translate", "SECRET_KEY") + sign = app_id + word + salt + secret_key # type: ignore + md5_ = md5() + md5_.update(sign.encode("utf-8")) + sign = md5_.hexdigest() + params = { + "q": word, + "from": form, + "to": to, + "appid": app_id, + "salt": salt, + "sign": sign, + } + url = URL + "?" + for key, value in params.items(): + url += f"{key}={value}&" + url = url[:-1] + resp = await AsyncHttpx.get(url) + data = resp.json() + if data.get("error_code"): + return data.get("error_msg") + if trans_result := data.get("trans_result"): + return trans_result[0]["dst"] + return "没有找到翻译捏..." diff --git a/zhenxun/plugins/wbtop/__init__.py b/zhenxun/plugins/wbtop/__init__.py new file mode 100644 index 00000000..fce1b995 --- /dev/null +++ b/zhenxun/plugins/wbtop/__init__.py @@ -0,0 +1,56 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncPlaywright +from zhenxun.utils.message import MessageUtils + +from .data_source import get_hot_image + +__plugin_meta__ = PluginMetadata( + name="微博热搜", + description="刚买完瓜,在吃瓜现场", + usage=""" + 指令: + 微博热搜:发送实时热搜 + 微博热搜 [id]:截图该热搜页面 + 示例:微博热搜 5 + """.strip(), + extra=PluginExtraData( + author="HibiKier & yajiwa", + version="0.1", + ).dict(), +) + + +_matcher = on_alconna(Alconna("微博热搜", Args["idx?", int]), priority=5, block=True) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma, idx: Match[int]): + result, data_list = await get_hot_image() + if isinstance(result, str): + await MessageUtils.build_message(result).finish(reply_to=True) + if idx.available: + _idx = idx.result + url = data_list[_idx - 1]["url"] + file = IMAGE_PATH / "temp" / f"wbtop_{session.id1}.png" + img = await AsyncPlaywright.screenshot( + url, + file, + "#pl_feed_main", + wait_time=12, + ) + if img: + await MessageUtils.build_message(file).send() + logger.info( + f"查询微博热搜 Id: {_idx}", arparma.header_result, session=session + ) + else: + await MessageUtils.build_message("获取图片失败...").send() + else: + await MessageUtils.build_message(result).send() + logger.info(f"查询微博热搜", arparma.header_result, session=session) diff --git a/zhenxun/plugins/wbtop/data_source.py b/zhenxun/plugins/wbtop/data_source.py new file mode 100644 index 00000000..e9c20627 --- /dev/null +++ b/zhenxun/plugins/wbtop/data_source.py @@ -0,0 +1,63 @@ +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage + +URL = "https://weibo.com/ajax/side/hotSearch" + + +async def get_data() -> list | str: + """获取数据 + + 返回: + list | str: 数据或消息 + """ + data_list = [] + for _ in range(3): + try: + response = await AsyncHttpx.get(URL, timeout=20) + if response.status_code == 200: + data_json = response.json()["data"]["realtime"] + for item in data_json: + if "is_ad" in item: + """广告跳过""" + continue + data = { + "hot_word": item["note"], + "hot_word_num": str(item["num"]), + "url": "https://s.weibo.com/weibo?q=%23" + item["word"] + "%23", + } + data_list.append(data) + if not data: + return "没有搜索到..." + return data_list + except Exception as e: + logger.error("获取微博热搜错误", e=e) + return "获取失败,请十分钟后再试..." + + +async def get_hot_image() -> tuple[BuildImage | str, list]: + """构造图片 + + 返回: + BuildImage | str: 热搜图片 + """ + data = await get_data() + if isinstance(data, str): + return data, [] + bk = BuildImage(700, 32 * 50 + 280, color="#797979") + wbtop_bk = BuildImage(700, 280, background=f"{IMAGE_PATH}/other/webtop.png") + await bk.paste(wbtop_bk) + text_bk = BuildImage(700, 32 * 50, color="#797979") + image_list = [] + for i, _data in enumerate(data): + title = f"{i + 1}. {_data['hot_word']}" + hot = str(_data["hot_word_num"]) + img = BuildImage(700, 30, font_size=20) + _, h = img.getsize(title) + await img.text((10, int((30 - h) / 2)), title) + await img.text((580, int((30 - h) / 2)), hot) + image_list.append(img) + text_bk = await text_bk.auto_paste(image_list, 1, 2, 0) + await bk.paste(text_bk, (0, 280)) + return bk, data diff --git a/plugins/web_ui/__init__.py b/zhenxun/plugins/web_ui/__init__.py similarity index 83% rename from plugins/web_ui/__init__.py rename to zhenxun/plugins/web_ui/__init__.py index 00bcd2b5..37684372 100644 --- a/plugins/web_ui/__init__.py +++ b/zhenxun/plugins/web_ui/__init__.py @@ -8,9 +8,8 @@ from nonebot.matcher import Matcher from nonebot.message import run_preprocessor from nonebot.typing import T_State -from configs.config import Config as gConfig -from services.log import logger, logger_ -from utils.manager import plugins2settings_manager +from zhenxun.configs.config import Config as gConfig +from zhenxun.services.log import logger, logger_ from .api.logs import router as ws_log_routes from .api.logs.log_manager import LOG_STORAGE @@ -25,9 +24,11 @@ from .auth import router as auth_router driver = nonebot.get_driver() -gConfig.add_plugin_config("web-ui", "username", "admin", name="web-ui", help_="前端管理用户名") +gConfig.add_plugin_config("web-ui", "username", "admin", help="前端管理用户名") -gConfig.add_plugin_config("web-ui", "password", None, name="web-ui", help_="前端管理密码") +gConfig.add_plugin_config("web-ui", "password", None, help="前端管理密码") + +gConfig.set_name("web-ui", "web-ui") BaseApiRouter = APIRouter(prefix="/zhenxun/api") @@ -51,13 +52,14 @@ WsApiRouter.include_router(chat_routes) @driver.on_startup def _(): try: + async def log_sink(message: str): - loop = None + loop = None if not loop: try: loop = asyncio.get_running_loop() except Exception as e: - logger.warning('Web Ui log_sink', e=e) + logger.warning("Web Ui log_sink", e=e) if not loop: loop = asyncio.new_event_loop() loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) diff --git a/plugins/web_ui/api/__init__.py b/zhenxun/plugins/web_ui/api/__init__.py similarity index 100% rename from plugins/web_ui/api/__init__.py rename to zhenxun/plugins/web_ui/api/__init__.py diff --git a/plugins/web_ui/api/logs/__init__.py b/zhenxun/plugins/web_ui/api/logs/__init__.py similarity index 100% rename from plugins/web_ui/api/logs/__init__.py rename to zhenxun/plugins/web_ui/api/logs/__init__.py diff --git a/plugins/web_ui/api/logs/log_manager.py b/zhenxun/plugins/web_ui/api/logs/log_manager.py similarity index 78% rename from plugins/web_ui/api/logs/log_manager.py rename to zhenxun/plugins/web_ui/api/logs/log_manager.py index f375313d..71992c91 100644 --- a/plugins/web_ui/api/logs/log_manager.py +++ b/zhenxun/plugins/web_ui/api/logs/log_manager.py @@ -1,7 +1,5 @@ import asyncio -import re -from typing import Awaitable, Callable, Dict, Generic, List, Set, TypeVar -from urllib.parse import urlparse +from typing import Awaitable, Callable, Generic, TypeVar PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" @@ -10,15 +8,14 @@ LogListener = Callable[[_T], Awaitable[None]] class LogStorage(Generic[_T]): - """ 日志存储 """ def __init__(self, rotation: float = 5 * 60): self.count, self.rotation = 0, rotation - self.logs: Dict[int, str] = {} - self.listeners: Set[LogListener[str]] = set() + self.logs: dict[int, str] = {} + self.listeners: set[LogListener[str]] = set() async def add(self, log: str): seq = self.count = self.count + 1 @@ -36,4 +33,3 @@ class LogStorage(Generic[_T]): LOG_STORAGE: LogStorage[str] = LogStorage[str]() - diff --git a/plugins/web_ui/api/logs/logs.py b/zhenxun/plugins/web_ui/api/logs/logs.py similarity index 93% rename from plugins/web_ui/api/logs/logs.py rename to zhenxun/plugins/web_ui/api/logs/logs.py index e6abebf3..01c78096 100644 --- a/plugins/web_ui/api/logs/logs.py +++ b/zhenxun/plugins/web_ui/api/logs/logs.py @@ -1,5 +1,3 @@ -from typing import List - from fastapi import APIRouter, WebSocket from loguru import logger from nonebot.utils import escape_tag @@ -10,7 +8,7 @@ from .log_manager import LOG_STORAGE router = APIRouter() -@router.get("/logs", response_model=List[str]) +@router.get("/logs", response_model=list[str]) async def system_logs_history(reverse: bool = False): """历史日志 diff --git a/plugins/web_ui/api/tabs/__init__.py b/zhenxun/plugins/web_ui/api/tabs/__init__.py similarity index 100% rename from plugins/web_ui/api/tabs/__init__.py rename to zhenxun/plugins/web_ui/api/tabs/__init__.py diff --git a/plugins/web_ui/api/tabs/database/__init__.py b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py similarity index 62% rename from plugins/web_ui/api/tabs/database/__init__.py rename to zhenxun/plugins/web_ui/api/tabs/database/__init__.py index f047d711..2fd77085 100644 --- a/plugins/web_ui/api/tabs/database/__init__.py +++ b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py @@ -1,18 +1,13 @@ -from os import name -from typing import Optional - import nonebot from fastapi import APIRouter, Request from nonebot.drivers import Driver from tortoise import Tortoise from tortoise.exceptions import OperationalError -from configs.config import NICKNAME -from services.db_context import TestSQL -from utils.utils import get_matchers +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.services.db_context import TestSQL from ....base_model import BaseResultModel, QueryModel, Result -from ....config import QueryDateType from ....utils import authentication from .models.model import SqlModel, SqlText from .models.sql_log import SqlLog @@ -38,38 +33,55 @@ FROM information_schema.columns WHERE table_name = '{}'; """ + @driver.on_startup async def _(): - for matcher in get_matchers(True): - if _plugin := matcher.plugin: - try: - _module = _plugin.module - except AttributeError: - pass + for plugin in nonebot.get_loaded_plugins(): + module = plugin.name + sql_list = [] + if plugin.metadata and plugin.metadata.extra: + sql_list = plugin.metadata.extra.get("sql_list") + if module in SQL_DICT: + raise ValueError(f"{module} 常用SQL module 重复") + if sql_list: + SqlModel( + name="", + module=module, + sql_list=sql_list, + ) + SQL_DICT[module] = SqlModel + if SQL_DICT: + result = await PluginInfo.filter(module__in=SQL_DICT.keys()).values_list( + "module", "name" + ) + module2name = {r[0]: r[1] for r in result} + for s in SQL_DICT: + module = SQL_DICT[s].module + if module in module2name: + SQL_DICT[s].name = module2name[module] else: - plugin_name = matcher.plugin_name - if plugin_name in SQL_DICT: - raise ValueError(f"{plugin_name} 常用SQL plugin_name 重复") - SqlModel( - name=getattr(_module, "__plugin_name__", None) or plugin_name or "", - plugin_name=plugin_name or "", - sql_list=getattr(_module, "sql_list", []), - ) - SQL_DICT[plugin_name] = SqlModel + SQL_DICT[s].name = module -@router.get("/get_table_list", dependencies=[authentication()], description="获取数据库表") -async def _() -> Result: + +@router.get( + "/get_table_list", dependencies=[authentication()], description="获取数据库表" +) +async def _() -> Result: db = Tortoise.get_connection("default") query = await db.execute_query_dict(SELECT_TABLE_SQL) return Result.ok(query) -@router.get("/get_table_column", dependencies=[authentication()], description="获取表字段") -async def _(table_name: str) -> Result: + +@router.get( + "/get_table_column", dependencies=[authentication()], description="获取表字段" +) +async def _(table_name: str) -> Result: db = Tortoise.get_connection("default") print(SELECT_TABLE_COLUMN_SQL.format(table_name)) query = await db.execute_query_dict(SELECT_TABLE_COLUMN_SQL.format(table_name)) return Result.ok(query) + @router.post("/exec_sql", dependencies=[authentication()], description="执行sql") async def _(sql: SqlText, request: Request) -> Result: ip = request.client.host if request.client else "unknown" @@ -91,14 +103,19 @@ async def _(sql: SqlText, request: Request) -> Result: @router.post("/get_sql_log", dependencies=[authentication()], description="sql日志列表") async def _(query: QueryModel) -> Result: total = await SqlLog.all().count() - if (total % query.size): + if total % query.size: total += 1 - data = await SqlLog.all().order_by("-id").offset((query.index - 1) * query.size).limit(query.size) + data = ( + await SqlLog.all() + .order_by("-id") + .offset((query.index - 1) * query.size) + .limit(query.size) + ) return Result.ok(BaseResultModel(total=total, data=data)) @router.get("/get_common_sql", dependencies=[authentication()], description="常用sql") -async def _(plugin_name: Optional[str] = None) -> Result: +async def _(plugin_name: str | None = None) -> Result: if plugin_name: return Result.ok(SQL_DICT.get(plugin_name)) return Result.ok(str(SQL_DICT)) diff --git a/plugins/web_ui/api/tabs/database/models/model.py b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py similarity index 69% rename from plugins/web_ui/api/tabs/database/models/model.py rename to zhenxun/plugins/web_ui/api/tabs/database/models/model.py index 37c682a6..e18e4cfb 100644 --- a/plugins/web_ui/api/tabs/database/models/model.py +++ b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py @@ -1,8 +1,6 @@ -from typing import List - from pydantic import BaseModel -from utils.models import CommonSql +from zhenxun.utils.plugin_models.base import CommonSql class SqlText(BaseModel): @@ -20,7 +18,7 @@ class SqlModel(BaseModel): name: str """插件中文名称""" - plugin_name: str + module: str """插件名称""" - sql_list: List[CommonSql] + sql_list: list[CommonSql] """插件列表""" diff --git a/plugins/web_ui/api/tabs/database/models/sql_log.py b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py similarity index 65% rename from plugins/web_ui/api/tabs/database/models/sql_log.py rename to zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py index d58ddd34..691f1b5a 100644 --- a/plugins/web_ui/api/tabs/database/models/sql_log.py +++ b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py @@ -1,8 +1,6 @@ -from typing import Optional, Union - from tortoise import fields -from services.db_context import Model +from zhenxun.services.db_context import Model class SqlLog(Model): @@ -26,15 +24,14 @@ class SqlLog(Model): @classmethod async def add( - cls, ip: str, sql: str, result: Optional[str] = None, is_suc: bool = True + cls, ip: str, sql: str, result: str | None = None, is_suc: bool = True ): - """ - 说明: - 获取用户在群内的等级 + """获取用户在群内的等级 + 参数: - :param ip: ip - :param sql: sql - :param result: 返回结果 - :param is_suc: 是否成功 + ip: ip + sql: sql + result: 返回结果 + is_suc: 是否成功 """ await cls.create(ip=ip, sql=sql, result=result, is_suc=is_suc) diff --git a/plugins/web_ui/api/tabs/main/__init__.py b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py similarity index 75% rename from plugins/web_ui/api/tabs/main/__init__.py rename to zhenxun/plugins/web_ui/api/tabs/main/__init__.py index ac892e65..ed8bb576 100644 --- a/plugins/web_ui/api/tabs/main/__init__.py +++ b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py @@ -2,7 +2,6 @@ import asyncio import time from datetime import datetime, timedelta from pathlib import Path -from typing import List, Optional import nonebot from fastapi import APIRouter, WebSocket @@ -10,12 +9,12 @@ from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState from tortoise.functions import Count from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK -from configs.config import NICKNAME -from models.chat_history import ChatHistory -from models.group_info import GroupInfo -from models.statistics import Statistics -from services.log import logger -from utils.manager import plugin_data_manager, plugins2settings_manager, plugins_manager +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_info import GroupInfo +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils from ....base_model import Result from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType @@ -29,19 +28,17 @@ ws_router = APIRouter() router = APIRouter(prefix="/main") - @router.get("/get_base_info", dependencies=[authentication()], description="基础信息") -async def _(bot_id: Optional[str] = None) -> Result: - """ - 获取Bot基础信息 +async def _(bot_id: str | None = None) -> Result: + """获取Bot基础信息 - Args: + 参数: bot_id (Optional[str], optional): bot_id. Defaults to None. - Returns: + 返回: Result: 获取指定bot信息与bot列表 """ - bot_list: List[BaseInfo] = [] + bot_list: list[BaseInfo] = [] if bots := nonebot.get_bots(): select_bot: BaseInfo for key, bot in bots.items(): @@ -74,9 +71,9 @@ async def _(bot_id: Optional[str] = None) -> Result: for bot in bot_list: bot.bot = None # type: ignore # 插件加载数量 - select_bot.plugin_count = len(plugins2settings_manager) - pm_data = plugins_manager.get_data() - select_bot.fail_plugin_count = len([pd for pd in pm_data if pm_data[pd].error]) + select_bot.plugin_count = await PluginInfo.all().count() + fail_count = await PluginInfo.filter(load_status=False).count() + select_bot.fail_plugin_count = fail_count select_bot.success_plugin_count = ( select_bot.plugin_count - select_bot.fail_plugin_count ) @@ -84,13 +81,18 @@ async def _(bot_id: Optional[str] = None) -> Result: select_bot.connect_time = bot_live.get(select_bot.self_id) or 0 if select_bot.connect_time: connect_date = datetime.fromtimestamp(select_bot.connect_time) - select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S") + connect_date_str = connect_date.strftime("%Y-%m-%d %H:%M:%S") + select_bot.connect_date = datetime.strptime( + connect_date_str, "%Y-%m-%d %H:%M:%S" + ) version_file = Path() / "__version__" if version_file.exists(): if text := version_file.open().read(): if ver := text.replace("__version__: ", "").strip(): select_bot.version = ver - day_call = await Statistics.filter(create_time__gte=now - timedelta(hours=now.hour)).count() + day_call = await Statistics.filter( + create_time__gte=now - timedelta(hours=now.hour) + ).count() select_bot.day_call = day_call return Result.ok(bot_list, "拿到信息啦!") return Result.warning_("无Bot连接...") @@ -125,8 +127,10 @@ async def _(bot_id: str) -> Result: ) -@router.get("/get_ch_count", dependencies=[authentication()], description="获取接收消息数量") -async def _(bot_id: str, query_type: Optional[QueryDateType] = None) -> Result: +@router.get( + "/get_ch_count", dependencies=[authentication()], description="获取接收消息数量" +) +async def _(bot_id: str, query_type: QueryDateType | None = None) -> Result: if bots := nonebot.get_bots(): if not query_type: return Result.ok(await ChatHistory.filter(bot_id=bot_id).count()) @@ -158,27 +162,36 @@ async def _(bot_id: str, query_type: Optional[QueryDateType] = None) -> Result: return Result.warning_("无Bot连接...") -@router.get("get_fg_count", dependencies=[authentication()], description="好友/群组数量") +@router.get( + "get_fg_count", dependencies=[authentication()], description="好友/群组数量" +) async def _(bot_id: str) -> Result: if bots := nonebot.get_bots(): if bot_id not in bots: return Result.warning_("指定Bot未连接...") bot = bots[bot_id] - data = { - "friend_count": len(await bot.get_friend_list()), - "group_count": len(await bot.get_group_list()), - } - return Result.ok(data) + platform = PlatformUtils.get_platform(bot) + if platform == "qq": + data = { + "friend_count": len(await bot.get_friend_list()), + "group_count": len(await bot.get_group_list()), + } + return Result.ok(data) + return Result.warning_("暂不支持该平台...") return Result.warning_("无Bot连接...") -@router.get("/get_run_time", dependencies=[authentication()], description="获取nb运行时间") +@router.get( + "/get_run_time", dependencies=[authentication()], description="获取nb运行时间" +) async def _() -> Result: return Result.ok(int(time.time() - run_time)) -@router.get("/get_active_group", dependencies=[authentication()], description="获取活跃群聊") -async def _(date_type: Optional[QueryDateType] = None) -> Result: +@router.get( + "/get_active_group", dependencies=[authentication()], description="获取活跃群聊" +) +async def _(date_type: QueryDateType | None = None) -> Result: query = ChatHistory now = datetime.now() if date_type == QueryDateType.DAY: @@ -190,14 +203,19 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result: if date_type == QueryDateType.YEAR: query = ChatHistory.filter(create_time__gte=now - timedelta(days=365)) data_list = ( - await query.annotate(count=Count("id")).filter(group_id__not_isnull=True) - .group_by("group_id").order_by("-count").limit(5) + await query.annotate(count=Count("id")) + .filter(group_id__not_isnull=True) + .group_by("group_id") + .order_by("-count") + .limit(5) .values_list("group_id", "count") ) active_group_list = [] id2name = {} if data_list: - if info_list := await GroupInfo.filter(group_id__in=[x[0] for x in data_list]).all(): + if info_list := await GroupInfo.filter( + group_id__in=[x[0] for x in data_list] + ).all(): for group_info in info_list: id2name[group_info.group_id] = group_info.group_name for data in data_list: @@ -217,8 +235,10 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result: return Result.ok(active_group_list) -@router.get("/get_hot_plugin", dependencies=[authentication()], description="获取热门插件") -async def _(date_type: Optional[QueryDateType] = None) -> Result: +@router.get( + "/get_hot_plugin", dependencies=[authentication()], description="获取热门插件" +) +async def _(date_type: QueryDateType | None = None) -> Result: query = Statistics now = datetime.now() if date_type == QueryDateType.DAY: @@ -231,14 +251,18 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result: query = Statistics.filter(create_time__gte=now - timedelta(days=365)) data_list = ( await query.annotate(count=Count("id")) - .group_by("plugin_name").order_by("-count").limit(5) + .group_by("plugin_name") + .order_by("-count") + .limit(5) .values_list("plugin_name", "count") ) hot_plugin_list = [] + module_list = [x[0] for x in data_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} for data in data_list: - name = data[0] - if plugin_data := plugin_data_manager.get(data[0]): - name = plugin_data.name + module = data[0] + name = module2name.get(module) or module hot_plugin_list.append( HotPlugin( module=data[0], @@ -253,7 +277,7 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result: @ws_router.websocket("/system_status") -async def system_logs_realtime(websocket: WebSocket, sleep: Optional[int] = 5): +async def system_logs_realtime(websocket: WebSocket, sleep: int = 5): await websocket.accept() logger.debug("ws system_status is connect") try: diff --git a/plugins/web_ui/api/tabs/main/data_source.py b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py similarity index 84% rename from plugins/web_ui/api/tabs/main/data_source.py rename to zhenxun/plugins/web_ui/api/tabs/main/data_source.py index 42a3df42..ca445016 100644 --- a/plugins/web_ui/api/tabs/main/data_source.py +++ b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py @@ -1,9 +1,8 @@ import time -from typing import Optional import nonebot -from nonebot import Driver from nonebot.adapters.onebot.v11 import Bot +from nonebot.drivers import Driver driver: Driver = nonebot.get_driver() @@ -15,7 +14,7 @@ class BotLive: def add(self, bot_id: str): self._data[bot_id] = time.time() - def get(self, bot_id: str) -> Optional[int]: + def get(self, bot_id: str) -> int | None: return self._data.get(bot_id) def remove(self, bot_id: str): diff --git a/plugins/web_ui/api/tabs/main/model.py b/zhenxun/plugins/web_ui/api/tabs/main/model.py similarity index 89% rename from plugins/web_ui/api/tabs/main/model.py rename to zhenxun/plugins/web_ui/api/tabs/main/model.py index 7dd770c6..c9d76706 100644 --- a/plugins/web_ui/api/tabs/main/model.py +++ b/zhenxun/plugins/web_ui/api/tabs/main/model.py @@ -1,7 +1,6 @@ from datetime import datetime -from typing import Optional, Union -from nonebot.adapters.onebot.v11 import Bot +from nonebot.adapters import Bot from nonebot.config import Config from pydantic import BaseModel @@ -37,7 +36,7 @@ class BaseInfo(BaseModel): """今日 累计接收消息""" connect_time: int = 0 """连接时间""" - connect_date: Optional[datetime] = None + connect_date: datetime | None = None """连接日期""" plugin_count: int = 0 @@ -50,13 +49,12 @@ class BaseInfo(BaseModel): is_select: bool = False """当前选择""" - config: Optional[Config] = None + config: Config | None = None """nb配置""" day_call: int = 0 """今日调用插件次数""" version: str = "unknown" """真寻版本""" - class Config: arbitrary_types_allowed = True @@ -84,7 +82,7 @@ class ActiveGroup(BaseModel): 活跃群聊数据 """ - group_id: Union[str, int] + group_id: str """群组id""" name: str """群组名称""" diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py new file mode 100644 index 00000000..1067cfb8 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py @@ -0,0 +1,546 @@ +import re +from typing import Literal + +import nonebot +from fastapi import APIRouter +from nonebot.adapters.onebot.v11 import ActionFailed +from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState +from tortoise.functions import Count + +from zhenxun.configs.config import NICKNAME +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import RequestHandleType, RequestType +from zhenxun.utils.exception import NotFoundError +from zhenxun.utils.platform import PlatformUtils + +from ....base_model import Result +from ....config import AVA_URL, GROUP_AVA_URL +from ....utils import authentication +from ...logs.log_manager import LOG_STORAGE +from .model import ( + DeleteFriend, + Friend, + FriendRequestResult, + GroupDetail, + GroupRequestResult, + GroupResult, + HandleRequest, + LeaveGroup, + Message, + MessageItem, + Plugin, + ReqResult, + SendMessage, + Task, + UpdateGroup, + UserDetail, +) + +ws_router = APIRouter() +router = APIRouter(prefix="/manage") + +SUB_PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" + +GROUP_PATTERN = r".*?Message (-?\d*) from (\d*)@\[群:(\d*)] '(.*)'" + +PRIVATE_PATTERN = r".*?Message (-?\d*) from (\d*) '(.*)'" + +AT_PATTERN = r"\[CQ:at,qq=(.*)\]" + +IMAGE_PATTERN = r"\[image:file=.*,url=(.*);.*?\]" + + +@router.get( + "/get_group_list", dependencies=[authentication()], description="获取群组列表" +) +async def _(bot_id: str) -> Result: + """ + 获取群信息 + """ + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + group_list_result = [] + try: + group_info = {} + group_list = await bots[bot_id].get_group_list() + for g in group_list: + gid = g["group_id"] + g["ava_url"] = GROUP_AVA_URL.format(gid, gid) + group_list_result.append(GroupResult(**g)) + except Exception as e: + logger.error("调用API错误", "/get_group_list", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(group_list_result, "拿到了新鲜出炉的数据!") + return Result.warning_("无Bot连接...") + + +@router.post( + "/update_group", dependencies=[authentication()], description="修改群组信息" +) +async def _(group: UpdateGroup) -> Result: + try: + group_id = group.group_id + if db_group := await GroupConsole.get_group(group_id): + task_list = await TaskInfo.all().values_list("module", flat=True) + db_group.level = group.level + db_group.status = group.status + if group.close_plugins: + db_group.block_plugin = ",".join(group.close_plugins) + "," + if group.task: + block_task = [] + for t in task_list: + if t not in group.task: + block_task.append(t) + if block_task: + db_group.block_task = ",".join(block_task) + "," + await db_group.save( + update_fields=["level", "status", "block_plugin", "block_task"] + ) + except Exception as e: + logger.error("调用API错误", "/get_group", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(info="已完成记录!") + + +@router.get( + "/get_friend_list", dependencies=[authentication()], description="获取好友列表" +) +async def _(bot_id: str) -> Result: + """ + 获取群信息 + """ + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + try: + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台暂不支持该功能...") + friend_list = await bots[bot_id].get_friend_list() + for f in friend_list: + f["ava_url"] = AVA_URL.format(f["user_id"]) + return Result.ok( + [Friend(**f) for f in friend_list if str(f["user_id"]) != bot_id], + "拿到了新鲜出炉的数据!", + ) + except Exception as e: + logger.error("调用API错误", "/get_group_list", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_request_count", dependencies=[authentication()], description="获取请求数量" +) +async def _() -> Result: + f_count = await FgRequest.filter(request_type=RequestType.FRIEND).count() + g_count = await FgRequest.filter(request_type=RequestType.GROUP).count() + data = { + "friend_count": f_count, + "group_count": g_count, + } + return Result.ok(data, f"{NICKNAME}带来了最新的数据!") + + +@router.get( + "/get_request_list", dependencies=[authentication()], description="获取请求列表" +) +async def _() -> Result: + try: + req_result = ReqResult() + data_list = await FgRequest.filter(handle_type__isnull=True).all() + for req in data_list: + if req.request_type == RequestType.FRIEND: + req_result.friend.append( + FriendRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=AVA_URL.format(req.user_id), + type=str(req.request_type).lower(), + ) + ) + else: + req_result.group.append( + GroupRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id), + type=str(req.request_type).lower(), + invite_group=req.group_id, + group_name=None, + ) + ) + req_result.friend.reverse() + req_result.group.reverse() + except Exception as e: + logger.error("调用API错误", "/get_request", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(req_result, f"{NICKNAME}带来了最新的数据!") + + +@router.delete( + "/clear_request", dependencies=[authentication()], description="清空请求列表" +) +async def _(request_type: Literal["private", "group"]) -> Result: + await FgRequest.filter(handle_type__not_isnull=True).update( + handle_type=RequestHandleType.IGNORE + ) + return Result.ok(info="成功清除了数据!") + + +@router.post("/refuse_request", dependencies=[authentication()], description="拒绝请求") +async def _(parma: HandleRequest) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = parma.bot_id + if bot_id not in nonebot.get_bots(): + return Result.warning_("指定Bot未连接...") + try: + await FgRequest.refused(bots[bot_id], parma.id) + except ActionFailed as e: + await FgRequest.expire(parma.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + except NotFoundError: + return Result.warning_("未找到此Id请求...") + return Result.ok(info="成功处理了请求!") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/refuse_request", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/delete_request", dependencies=[authentication()], description="忽略请求") +async def _(parma: HandleRequest) -> Result: + await FgRequest.ignore(parma.id) + return Result.ok(info="成功处理了请求!") + + +@router.post( + "/approve_request", dependencies=[authentication()], description="同意请求" +) +async def _(parma: HandleRequest) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = parma.bot_id + if bot_id not in nonebot.get_bots(): + return Result.warning_("指定Bot未连接...") + if req := await FgRequest.get_or_none(id=parma.id): + if group := await GroupConsole.get_group(group_id=req.group_id): + group.group_flag = 1 + await group.save(update_fields=["group_flag"]) + else: + group_info = await bots[bot_id].get_group_info( + group_id=req.group_id + ) + await GroupConsole.update_or_create( + group_id=str(group_info["group_id"]), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": group_info["max_member_count"], + "member_count": group_info["member_count"], + "group_flag": 1, + }, + ) + else: + return Result.warning_("未找到此Id请求...") + try: + await FgRequest.approve(bots[bot_id], parma.id) + return Result.ok(info="成功处理了请求!") + except ActionFailed as e: + await FgRequest.expire(parma.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/approve_request", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/leave_group", dependencies=[authentication()], description="退群") +async def _(param: LeaveGroup) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = param.bot_id + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台不支持退群操作...") + group_list = await bots[bot_id].get_group_list() + if param.group_id not in [str(g["group_id"]) for g in group_list]: + return Result.warning_("Bot未在该群聊中...") + await bots[bot_id].set_group_leave(group_id=param.group_id) + return Result.ok(info="成功处理了请求!") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/leave_group", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/delete_friend", dependencies=[authentication()], description="删除好友") +async def _(param: DeleteFriend) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = param.bot_id + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台不支持删除好友操作...") + friend_list = await bots[bot_id].get_friend_list() + if param.user_id not in [str(g["user_id"]) for g in friend_list]: + return Result.warning_("Bot未有其好友...") + await bots[bot_id].delete_friend(user_id=param.user_id) + return Result.ok(info="成功处理了请求!") + return Result.warning_("Bot未连接...") + except Exception as e: + logger.error("调用API错误", "/delete_friend", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.get( + "/get_friend_detail", dependencies=[authentication()], description="获取好友详情" +) +async def _(bot_id: str, user_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id in bots: + if fd := [ + x + for x in await bots[bot_id].get_friend_list() + if str(x["user_id"]) == user_id + ]: + like_plugin_list = ( + await Statistics.filter(user_id=user_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + module_list = [x[0] for x in like_plugin_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + user = fd[0] + user_detail = UserDetail( + user_id=user_id, + ava_url=AVA_URL.format(user_id), + nickname=user["nickname"], + remark=user["remark"], + is_ban=await BanConsole.is_ban(user_id), + chat_count=await ChatHistory.filter(user_id=user_id).count(), + call_count=await Statistics.filter(user_id=user_id).count(), + like_plugin=like_plugin, + ) + return Result.ok(user_detail) + else: + return Result.warning_("未添加指定好友...") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_group_detail", dependencies=[authentication()], description="获取群组详情" +) +async def _(bot_id: str, group_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id in bots: + group = await GroupConsole.get_or_none(group_id=group_id) + if not group: + return Result.warning_("指定群组未被收录...") + like_plugin_list = ( + await Statistics.filter(group_id=group_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + plugins = await PluginInfo.all() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + close_plugins = [] + if group.block_plugin: + for module in group.block_plugin.split(","): + module_ = module.replace(":super", "") + is_super_block = module.endswith(":super") + plugin = Plugin( + module=module_, + plugin_name=module, + is_super_block=is_super_block, + ) + plugin.plugin_name = module2name.get(module) or module + close_plugins.append(plugin) + all_task = await TaskInfo.annotate().values_list("module", "name") + task_module2name = {x[0]: x[1] for x in all_task} + task_list = [] + if group.block_task: + split_task = group.block_task.split(",") + for task in all_task: + task_list.append( + Task( + name=task[0], + zh_name=task_module2name.get(task[0]) or task[0], + status=task[0] not in split_task, + ) + ) + else: + for task in all_task: + task_list.append( + Task( + name=task[0], + zh_name=task_module2name.get(task[0]) or task[0], + status=True, + ) + ) + group_detail = GroupDetail( + group_id=group_id, + ava_url=GROUP_AVA_URL.format(group_id, group_id), + name=group.group_name, + member_count=group.member_count, + max_member_count=group.max_member_count, + chat_count=await ChatHistory.filter(group_id=group_id).count(), + call_count=await Statistics.filter(group_id=group_id).count(), + like_plugin=like_plugin, + level=group.level, + status=group.status, + close_plugins=close_plugins, + task=task_list, + ) + return Result.ok(group_detail) + else: + return Result.warning_("未添加指定群组...") + return Result.warning_("无Bot连接...") + + +@router.post( + "/send_message", dependencies=[authentication()], description="获取群组详情" +) +async def _(param: SendMessage) -> Result: + if bots := nonebot.get_bots(): + if param.bot_id in bots: + platform = PlatformUtils.get_platform(bots[param.bot_id]) + if platform != "qq": + return Result.warning_("暂不支持该平台...") + try: + if param.user_id: + await bots[param.bot_id].send_private_msg( + user_id=str(param.user_id), message=param.message + ) + else: + await bots[param.bot_id].send_group_msg( + group_id=str(param.group_id), message=param.message + ) + except Exception as e: + return Result.fail(str(e)) + return Result.ok("发送成功!") + return Result.warning_("指定Bot未连接...") + return Result.warning_("无Bot连接...") + + +MSG_LIST = [] + +ID2NAME = {} + + +async def message_handle( + sub_log: str, type: Literal["private", "group"] +) -> Message | None: + global MSG_LIST, ID2NAME + pattern = PRIVATE_PATTERN if type == "private" else GROUP_PATTERN + msg_id = None + uid = None + gid = None + msg = None + img_list = re.findall(IMAGE_PATTERN, sub_log) + if r := re.search(pattern, sub_log): + if type == "private": + msg_id = r.group(1) + uid = r.group(2) + msg = r.group(3) + if uid not in ID2NAME: + if user := await FriendUser.get_or_none(user_id=uid): + ID2NAME[uid] = user.user_name or user.nickname + else: + msg_id = r.group(1) + uid = r.group(2) + gid = r.group(3) + msg = r.group(4) + if gid not in ID2NAME: + if user := await GroupInfoUser.get_or_none(user_id=uid, group_id=gid): + ID2NAME[uid] = user.user_name or user.nickname + if at_list := re.findall(AT_PATTERN, msg): + user_list = await GroupInfoUser.filter( + user_id__in=at_list, group_id=gid + ).all() + id2name = {u.user_id: (u.user_name or u.nickname) for u in user_list} + for qq in at_list: + msg = re.sub(rf"\[CQ:at,qq={qq}\]", f"@{id2name[qq] or ''}", msg) + if msg_id in MSG_LIST: + return + MSG_LIST.append(msg_id) + messages = [] + if msg and uid: + rep = re.split(r"\[CQ:image.*\]", msg) + if img_list: + for i in range(len(rep)): + messages.append(MessageItem(type="text", msg=rep[i])) + if i < len(img_list): + messages.append(MessageItem(type="img", msg=img_list[i])) + else: + messages = [MessageItem(type="text", msg=x) for x in rep] + return Message( + object_id=uid if type == "private" else gid, # type: ignore + user_id=uid, + group_id=gid, + message=messages, + name=ID2NAME.get(uid) or "", + ava_url=AVA_URL.format(uid), + ) + return None + + +@ws_router.websocket("/chat") +async def _(websocket: WebSocket): + await websocket.accept() + + async def log_listener(log: str): + global MSG_LIST, ID2NAME + sub_log = re.sub(SUB_PATTERN, "", log) + if "message.private.friend" in log: + if message := await message_handle(sub_log, "private"): + await websocket.send_json(message.dict()) + else: + if r := re.search(GROUP_PATTERN, sub_log): + if message := await message_handle(sub_log, "group"): + await websocket.send_json(message.dict()) + if len(MSG_LIST) > 30: + MSG_LIST = MSG_LIST[-1:] + + LOG_STORAGE.listeners.add(log_listener) + try: + while websocket.client_state == WebSocketState.CONNECTED: + recv = await websocket.receive() + except WebSocketDisconnect: + pass + finally: + LOG_STORAGE.listeners.remove(log_listener) + return diff --git a/plugins/web_ui/api/tabs/manage/model.py b/zhenxun/plugins/web_ui/api/tabs/manage/model.py similarity index 77% rename from plugins/web_ui/api/tabs/manage/model.py rename to zhenxun/plugins/web_ui/api/tabs/manage/model.py index ab8b83f1..8df5a6cd 100644 --- a/plugins/web_ui/api/tabs/manage/model.py +++ b/zhenxun/plugins/web_ui/api/tabs/manage/model.py @@ -1,7 +1,5 @@ -from typing import Dict, List, Literal, Optional, Union +from typing import Literal -from matplotlib.dates import FR -from nonebot.adapters.onebot.v11 import Bot from pydantic import BaseModel @@ -10,7 +8,7 @@ class Group(BaseModel): 群组信息 """ - group_id: Union[str, int] + group_id: str """群组id""" group_name: str """群组名称""" @@ -32,11 +30,12 @@ class Task(BaseModel): status: bool """状态""" + class Plugin(BaseModel): """ 插件 """ - + module: str """模块名""" plugin_name: str @@ -50,7 +49,7 @@ class GroupResult(BaseModel): 群组返回数据 """ - group_id: Union[str, int] + group_id: str """群组id""" group_name: str """群组名称""" @@ -63,7 +62,7 @@ class Friend(BaseModel): 好友数据 """ - user_id: Union[str, int] + user_id: str """用户id""" nickname: str = "" """昵称""" @@ -72,6 +71,7 @@ class Friend(BaseModel): ava_url: str = "" """头像url""" + class UpdateGroup(BaseModel): """ 更新群组信息 @@ -83,9 +83,9 @@ class UpdateGroup(BaseModel): """状态""" level: int """群权限""" - task: List[str] + task: list[str] """被动状态""" - close_plugins: List[str] + close_plugins: list[str] """关闭插件""" @@ -94,25 +94,17 @@ class FriendRequestResult(BaseModel): 好友/群组请求管理 """ - bot_id: Union[str, int] + bot_id: str """bot_id""" - oid: str + oid: int """排序""" - id: int + id: str """id""" flag: str """flag""" - nickname: Optional[str] + nickname: str | None """昵称""" - level: Optional[int] - """等级""" - sex: Optional[str] - """性别""" - age: Optional[int] - """年龄""" - from_: Optional[str] - """来自""" - comment: Optional[str] + comment: str | None """备注信息""" ava_url: str """头像""" @@ -125,9 +117,9 @@ class GroupRequestResult(FriendRequestResult): 群聊邀请请求 """ - invite_group: Union[int, str] + invite_group: str """邀请群聊""" - group_name: Optional[str] + group_name: str | None """群聊名称""" @@ -136,12 +128,10 @@ class HandleRequest(BaseModel): 操作请求接收数据 """ - bot_id: Optional[str] = None + bot_id: str | None = None """bot_id""" - flag: str - """flag""" - request_type: Literal["private", "group"] - """类型""" + id: int + """数据id""" class LeaveGroup(BaseModel): @@ -165,14 +155,15 @@ class DeleteFriend(BaseModel): user_id: str """用户id""" + class ReqResult(BaseModel): """ 好友/群组请求列表 """ - friend: List[FriendRequestResult] = [] + friend: list[FriendRequestResult] = [] """好友请求列表""" - group: List[GroupRequestResult] = [] + group: list[GroupRequestResult] = [] """群组请求列表""" @@ -195,7 +186,7 @@ class UserDetail(BaseModel): """发言次数""" call_count: int """功能调用次数""" - like_plugin: Dict[str, int] + like_plugin: dict[str, int] """最喜爱的功能""" @@ -218,17 +209,18 @@ class GroupDetail(BaseModel): """发言次数""" call_count: int """功能调用次数""" - like_plugin: Dict[str, int] + like_plugin: dict[str, int] """最喜爱的功能""" level: int """群权限""" status: bool """状态(睡眠)""" - close_plugins: List[Plugin] + close_plugins: list[Plugin] """关闭的插件""" - task: List[Task] + task: list[Task] """被动列表""" + class MessageItem(BaseModel): type: str @@ -236,6 +228,7 @@ class MessageItem(BaseModel): msg: str """内容""" + class Message(BaseModel): """ 消息 @@ -245,9 +238,9 @@ class Message(BaseModel): """主体id user_id 或 group_id""" user_id: str """用户id""" - group_id: Optional[str] = None + group_id: str | None = None """群组id""" - message: List[MessageItem] + message: list[MessageItem] """消息""" name: str """用户名称""" @@ -255,16 +248,16 @@ class Message(BaseModel): """用户头像""" - class SendMessage(BaseModel): """ 发送消息 """ + bot_id: str """bot id""" - user_id: Optional[str] = None + user_id: str | None = None """用户id""" - group_id: Optional[str] = None + group_id: str | None = None """群组id""" message: str """消息""" diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py new file mode 100644 index 00000000..b2a90577 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -0,0 +1,190 @@ +import re + +import cattrs +from fastapi import APIRouter, Query + +from zhenxun.configs.config import Config +from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, PluginType + +from ....base_model import Result +from ....utils import authentication +from .model import ( + PluginConfig, + PluginCount, + PluginDetail, + PluginInfo, + PluginSwitch, + UpdatePlugin, +) + +router = APIRouter(prefix="/plugin") + + +@router.get( + "/get_plugin_list", dependencies=[authentication()], deprecated="获取插件列表" # type: ignore +) +async def _( + plugin_type: list[PluginType] = Query(None), menu_type: str | None = None +) -> Result: + try: + plugin_list: list[PluginInfo] = [] + query = DbPluginInfo + if plugin_type: + query = query.filter(plugin_type__in=plugin_type, load_status=True) + if menu_type: + query = query.filter(menu_type=menu_type) + plugins = await query.all() + for plugin in plugins: + plugin_info = PluginInfo( + module=plugin.module, + plugin_name=plugin.name, + default_status=plugin.default_status, + limit_superuser=plugin.limit_superuser, + cost_gold=plugin.cost_gold, + menu_type=plugin.menu_type, + version=plugin.version or "0", + level=plugin.level, + status=plugin.status, + author=plugin.author, + ) + plugin_list.append(plugin_info) + except Exception as e: + logger.error("调用API错误", "/get_plugins", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(plugin_list, "拿到了新鲜出炉的数据!") + + +@router.get( + "/get_plugin_count", dependencies=[authentication()], deprecated="获取插件数量" # type: ignore +) +async def _() -> Result: + plugin_count = PluginCount() + plugin_count.normal = await DbPluginInfo.filter( + plugin_type=PluginType.NORMAL, load_status=True + ).count() + plugin_count.admin = await DbPluginInfo.filter( + plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN], load_status=True + ).count() + plugin_count.superuser = await DbPluginInfo.filter( + plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN], + load_status=True, + ).count() + plugin_count.other = await DbPluginInfo.filter( + plugin_type=PluginType.HIDDEN, load_status=True + ).count() + return Result.ok(plugin_count) + + +@router.post( + "/update_plugin", dependencies=[authentication()], description="更新插件参数" +) +async def _(plugin: UpdatePlugin) -> Result: + try: + db_plugin = await DbPluginInfo.get_or_none( + module=plugin.module, load_status=True + ) + if not db_plugin: + return Result.fail("插件不存在...") + db_plugin.default_status = plugin.default_status + db_plugin.limit_superuser = plugin.limit_superuser + db_plugin.cost_gold = plugin.cost_gold + db_plugin.level = plugin.level + db_plugin.menu_type = plugin.menu_type + db_plugin.block_type = plugin.block_type + if plugin.block_type == BlockType.ALL: + db_plugin.status = False + else: + db_plugin.status = True + await db_plugin.save() + # 配置项 + if plugin.configs and (configs := Config.get(plugin.module)): + for key in plugin.configs: + if c := configs.configs.get(key): + value = plugin.configs[key] + if c.type and value is not None: + value = cattrs.structure(value, c.type) + Config.set_config(plugin.module, key, value) + except Exception as e: + logger.error("调用API错误", "/update_plugins", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(info="已经帮你写好啦!") + + +@router.post("/change_switch", dependencies=[authentication()], description="开关插件") +async def _(param: PluginSwitch) -> Result: + db_plugin = await DbPluginInfo.get_or_none(module=param.module, load_status=True) + if not db_plugin: + return Result.fail("插件不存在...") + if not param.status: + db_plugin.block_type = BlockType.ALL + db_plugin.status = False + else: + db_plugin.block_type = None + db_plugin.status = True + await db_plugin.save() + return Result.ok(info="成功改变了开关状态!") + + +@router.get( + "/get_plugin_menu_type", dependencies=[authentication()], description="获取插件类型" +) +async def _() -> Result: + menu_type_list = [] + result = await DbPluginInfo.annotate().values_list("menu_type", flat=True) + for r in result: + if r not in menu_type_list and r: + menu_type_list.append(r) + return Result.ok(menu_type_list) + + +@router.get("/get_plugin", dependencies=[authentication()], description="获取插件详情") +async def _(module: str) -> Result: + db_plugin = await DbPluginInfo.get_or_none(module=module, load_status=True) + if not db_plugin: + return Result.fail("插件不存在...") + config_list = [] + if config := Config.get(module): + for cfg in config.configs: + type_str = "" + type_inner = None + x = str(config.configs[cfg].type) + r = re.search(r"", str(config.configs[cfg].type)) + if r: + type_str = r.group(1) + else: + r = re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)) + if r: + type_str = r.group(1) + if type_str: + type_str = type_str.lower() + type_inner = r.group(2) + if type_inner: + type_inner = [x.strip() for x in type_inner.split(",")] + config_list.append( + PluginConfig( + module=module, + key=cfg, + value=config.configs[cfg].value, + help=config.configs[cfg].help, + default_value=config.configs[cfg].default_value, + type=type_str, + type_inner=type_inner, # type: ignore + ) + ) + plugin_info = PluginDetail( + module=module, + plugin_name=db_plugin.name, + default_status=db_plugin.default_status, + limit_superuser=db_plugin.limit_superuser, + cost_gold=db_plugin.cost_gold, + menu_type=db_plugin.menu_type, + version=db_plugin.version or "0", + level=db_plugin.level, + status=db_plugin.status, + author=db_plugin.author, + config_list=config_list, + block_type=db_plugin.block_type, + ) + return Result.ok(plugin_info) diff --git a/plugins/web_ui/api/tabs/plugin_manage/model.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py similarity index 63% rename from plugins/web_ui/api/tabs/plugin_manage/model.py rename to zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py index 31c1ae9a..e2952038 100644 --- a/plugins/web_ui/api/tabs/plugin_manage/model.py +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py @@ -1,10 +1,8 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any from pydantic import BaseModel -from utils.manager.models import Plugin as PluginManager -from utils.manager.models import PluginBlock, PluginCd, PluginCount, PluginSetting -from utils.typing import BLOCK_TYPE +from zhenxun.utils.enum import BlockType class PluginSwitch(BaseModel): @@ -48,9 +46,9 @@ class UpdatePlugin(BaseModel): """插件菜单类型""" level: int """插件所需群权限""" - block_type: Optional[BLOCK_TYPE] = None + block_type: BlockType | None = None """禁用类型""" - configs: Optional[Dict[str, Any]] = None + configs: dict[str, Any] | None = None """配置项""" @@ -71,15 +69,15 @@ class PluginInfo(BaseModel): """花费金币""" menu_type: str """插件菜单类型""" - version: Union[int, str, float] + version: str """插件版本""" level: int """群权限""" status: bool """当前状态""" - author: Optional[str] = None + author: str | None = None """作者""" - block_type: BLOCK_TYPE = None + block_type: BlockType | None = None """禁用类型""" @@ -94,37 +92,16 @@ class PluginConfig(BaseModel): """键""" value: Any """值""" - help: Optional[str] = None + help: str | None = None """帮助""" default_value: Any """默认值""" - type: Optional[Any] = None + type: Any = None """值类型""" - type_inner: Optional[List[str]] = None + type_inner: list[str] | None = None """List Tuple等内部类型检验""" - -class Plugin(BaseModel): - """ - 插件 - """ - - module: str - """模块名称""" - plugin_settings: Optional[PluginSetting] - """settings""" - plugin_manager: Optional[PluginManager] - """manager""" - plugin_config: Optional[Dict[str, PluginConfig]] - """配置项""" - cd_limit: Optional[PluginCd] - """cd限制""" - block_limit: Optional[PluginBlock] - """阻断限制""" - count_limit: Optional[PluginCount] - """次数限制""" - class PluginCount(BaseModel): """ 插件数量 @@ -145,4 +122,4 @@ class PluginDetail(PluginInfo): 插件详情 """ - config_list: List[PluginConfig] \ No newline at end of file + config_list: list[PluginConfig] diff --git a/plugins/web_ui/api/tabs/system/__init__.py b/zhenxun/plugins/web_ui/api/tabs/system/__init__.py similarity index 100% rename from plugins/web_ui/api/tabs/system/__init__.py rename to zhenxun/plugins/web_ui/api/tabs/system/__init__.py diff --git a/plugins/web_ui/api/tabs/system/model.py b/zhenxun/plugins/web_ui/api/tabs/system/model.py similarity index 100% rename from plugins/web_ui/api/tabs/system/model.py rename to zhenxun/plugins/web_ui/api/tabs/system/model.py diff --git a/plugins/web_ui/auth/__init__.py b/zhenxun/plugins/web_ui/auth/__init__.py similarity index 78% rename from plugins/web_ui/auth/__init__.py rename to zhenxun/plugins/web_ui/auth/__init__.py index d927977f..d5a4ead7 100644 --- a/plugins/web_ui/auth/__init__.py +++ b/zhenxun/plugins/web_ui/auth/__init__.py @@ -5,7 +5,7 @@ import nonebot from fastapi import APIRouter, Depends from fastapi.security import OAuth2PasswordRequestForm -from configs.config import Config +from zhenxun.configs.config import Config from ..base_model import Result from ..utils import ( @@ -28,10 +28,14 @@ async def login_get_token(form_data: OAuth2PasswordRequestForm = Depends()): password = Config.get_config("web-ui", "password") if not username or not password: return Result.fail("你滴配置文件里用户名密码配置项为空", 998) - if username != form_data.username or password != form_data.password: + if username != form_data.username or str(password) != form_data.password: return Result.fail("真笨, 账号密码都能记错!", 999) + user = get_user(form_data.username) + if not user: + return Result.fail("用户不存在...", 997) access_token = create_token( - user=get_user(form_data.username), expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + user=user, + expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), ) token_data["token"].append(access_token) if len(token_data["token"]) > 3: diff --git a/plugins/web_ui/base_model.py b/zhenxun/plugins/web_ui/base_model.py similarity index 81% rename from plugins/web_ui/base_model.py rename to zhenxun/plugins/web_ui/base_model.py index ee7a93e4..67bb280f 100644 --- a/plugins/web_ui/base_model.py +++ b/zhenxun/plugins/web_ui/base_model.py @@ -1,13 +1,8 @@ from datetime import datetime -from logging import warning -from typing import Any, Dict, Generic, List, Optional, TypeVar, Union +from typing import Any, Generic, Optional, TypeVar -from nonebot.adapters.onebot.v11 import Bot from pydantic import BaseModel, validator - -from configs.utils import Config -from utils.manager.models import Plugin as PluginManager -from utils.manager.models import PluginBlock, PluginCd, PluginCount, PluginSetting +from typing_extensions import Self T = TypeVar("T") @@ -39,15 +34,15 @@ class Result(BaseModel): """返回数据""" @classmethod - def warning_(cls, info: str, code: int = 200) -> "Result": + def warning_(cls, info: str, code: int = 200) -> Self: return cls(suc=True, warning=info, code=code) @classmethod - def fail(cls, info: str = "异常错误", code: int = 500) -> "Result": + def fail(cls, info: str = "异常错误", code: int = 500) -> Self: return cls(suc=False, info=info, code=code) @classmethod - def ok(cls, data: Any = None, info: str = "操作成功", code: int = 200) -> "Result": + def ok(cls, data: Any = None, info: str = "操作成功", code: int = 200) -> Self: return cls(suc=True, info=info, code=code, data=data) @@ -74,7 +69,7 @@ class QueryModel(BaseModel, Generic[T]): if size < 1: raise ValueError("每页数量小于1...") return size - + class BaseResultModel(BaseModel): """ @@ -98,8 +93,6 @@ class SystemStatus(BaseModel): check_time: datetime - - class SystemFolderSize(BaseModel): """ 资源文件占比 @@ -113,5 +106,3 @@ class SystemFolderSize(BaseModel): """完整路径""" is_dir: bool """是否为文件夹""" - - diff --git a/zhenxun/plugins/web_ui/config.py b/zhenxun/plugins/web_ui/config.py new file mode 100644 index 00000000..0f16949a --- /dev/null +++ b/zhenxun/plugins/web_ui/config.py @@ -0,0 +1,36 @@ +import nonebot +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from strenum import StrEnum + +app = nonebot.get_app() + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160" + +GROUP_AVA_URL = "http://p.qlogo.cn/gh/{}/{}/640/" + + +class QueryDateType(StrEnum): + """ + 查询日期类型 + """ + + DAY = "day" + """日""" + WEEK = "week" + """周""" + MONTH = "month" + """月""" + YEAR = "year" + """年""" diff --git a/plugins/web_ui/utils.py b/zhenxun/plugins/web_ui/utils.py similarity index 60% rename from plugins/web_ui/utils.py rename to zhenxun/plugins/web_ui/utils.py index eba64b63..f39f36ac 100644 --- a/plugins/web_ui/utils.py +++ b/zhenxun/plugins/web_ui/utils.py @@ -1,7 +1,6 @@ import os from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, Optional, Union import psutil import ujson as json @@ -10,16 +9,8 @@ from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from nonebot.utils import run_sync -from configs.config import Config -from configs.path_config import ( - DATA_PATH, - FONT_PATH, - IMAGE_PATH, - LOG_PATH, - RECORD_PATH, - TEMP_PATH, - TEXT_PATH, -) +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH from .base_model import SystemFolderSize, SystemStatus, User @@ -39,7 +30,7 @@ if token_file.exists(): pass -def get_user(uname: str) -> Optional[User]: +def get_user(uname: str) -> User | None: """获取账号密码 参数: @@ -54,7 +45,7 @@ def get_user(uname: str) -> Optional[User]: return User(username=username, password=password) -def create_token(user: User, expires_delta: Optional[timedelta] = None): +def create_token(user: User, expires_delta: timedelta | None = None): """创建token 参数: @@ -72,11 +63,11 @@ def create_token(user: User, expires_delta: Optional[timedelta] = None): def authentication(): """权限验证 - 异常: JWTError: JWTError HTTPException: HTTPException """ + # if token not in token_data["token"]: def inner(token: str = Depends(oauth2_scheme)): try: @@ -86,17 +77,18 @@ def authentication(): if user is None: raise JWTError except JWTError: - raise HTTPException(status_code=400, detail="登录验证失败或已失效, 踢出房间!") + raise HTTPException( + status_code=400, detail="登录验证失败或已失效, 踢出房间!" + ) return Depends(inner) def _get_dir_size(dir_path: Path) -> float: - """ - 说明: - 获取文件夹大小 + """获取文件夹大小 + 参数: - :param dir_path: 文件夹路径 + dir_path: 文件夹路径 """ size = 0 for root, dirs, files in os.walk(dir_path): @@ -106,10 +98,7 @@ def _get_dir_size(dir_path: Path) -> float: @run_sync def get_system_status() -> SystemStatus: - """ - 说明: - 获取系统信息等 - """ + """获取系统信息等""" cpu = psutil.cpu_percent() memory = psutil.virtual_memory().percent disk = psutil.disk_usage("/").percent @@ -123,12 +112,9 @@ def get_system_status() -> SystemStatus: @run_sync def get_system_disk( - full_path: Optional[str], -) -> List[SystemFolderSize]: - """ - 说明: - 获取资源文件大小等 - """ + full_path: str | None, +) -> list[SystemFolderSize]: + """获取资源文件大小等""" base_path = Path(full_path) if full_path else Path() other_size = 0 data_list = [] @@ -136,35 +122,15 @@ def get_system_disk( f = base_path / file if f.is_dir(): size = _get_dir_size(f) / 1024 / 1024 - data_list.append(SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True)) + data_list.append( + SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True) + ) else: other_size += f.stat().st_size / 1024 / 1024 if other_size: - data_list.append(SystemFolderSize(name='other_file', size=other_size, full_path=full_path, is_dir=False)) + data_list.append( + SystemFolderSize( + name="other_file", size=other_size, full_path=full_path, is_dir=False + ) + ) return data_list - # else: - # if type_ == "image": - # dir_path = IMAGE_PATH - # elif type_ == "font": - # dir_path = FONT_PATH - # elif type_ == "text": - # dir_path = TEXT_PATH - # elif type_ == "record": - # dir_path = RECORD_PATH - # elif type_ == "data": - # dir_path = DATA_PATH - # elif type_ == "temp": - # dir_path = TEMP_PATH - # else: - # dir_path = LOG_PATH - # dir_map = {} - # other_file_size = 0 - # for file in os.listdir(dir_path): - # file = Path(dir_path / file) - # if file.is_dir(): - # dir_map[file.name] = _get_dir_size(file) / 1024 / 1024 - # else: - # other_file_size += os.path.getsize(file) / 1024 / 1024 - # dir_map["其他文件"] = other_file_size - # dir_map["check_time"] = datetime.now().replace(microsecond=0) - # return dir_map diff --git a/zhenxun/plugins/what_anime/__init__.py b/zhenxun/plugins/what_anime/__init__.py new file mode 100644 index 00000000..d3b91876 --- /dev/null +++ b/zhenxun/plugins/what_anime/__init__.py @@ -0,0 +1,60 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma +from nonebot_plugin_alconna import Image as alcImg +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from .data_source import get_anime + +__plugin_meta__ = PluginMetadata( + name="识番", + description="以图识番", + usage=""" + usage: + api.trace.moe 以图识番 + 指令: + 识番 [图片] + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", menu_type="一些工具" + ).dict(), +) + + +_matcher = on_alconna(Alconna("识番", Args["image?", alcImg]), block=True, priority=5) + + +@_matcher.handle() +async def _(image: Match[alcImg]): + if image.available: + _matcher.set_path_arg("image", image.result) + + +@_matcher.got_path("image", prompt="图来!") +async def _( + session: EventSession, + arparma: Arparma, + image: alcImg, +): + if not image.url: + await MessageUtils.build_message("图片url为空...").finish() + await MessageUtils.build_message("开始识别...").send() + anime_data_report = await get_anime(image.url) + if anime_data_report: + await MessageUtils.build_message(anime_data_report).send(reply_to=True) + logger.info( + f" 识番 {image.url} --> {anime_data_report}", + arparma.header_result, + session=session, + ) + else: + logger.info( + f"识番 {image.url} 未找到...", arparma.header_result, session=session + ) + await MessageUtils.build_message(f"没有寻找到该番剧,果咩..").send( + reply_to=True + ) diff --git a/plugins/what_anime/data_source.py b/zhenxun/plugins/what_anime/data_source.py old mode 100755 new mode 100644 similarity index 86% rename from plugins/what_anime/data_source.py rename to zhenxun/plugins/what_anime/data_source.py index 87fc071a..15801f62 --- a/plugins/what_anime/data_source.py +++ b/zhenxun/plugins/what_anime/data_source.py @@ -1,47 +1,47 @@ -import time -from services.log import logger -from utils.langconv import * -from utils.http_utils import AsyncHttpx - - -async def get_anime(anime: str) -> str: - s_time = time.time() - url = "https://api.trace.moe/search?anilistInfo&url={}".format(anime) - logger.debug("[info]Now starting get the {}".format(url)) - try: - anime_json = (await AsyncHttpx.get(url)).json() - if not anime_json["error"]: - if anime_json == "Error reading imagenull": - return "图像源错误,注意必须是静态图片哦" - repass = "" - # 拿到动漫 中文名 - for anime in anime_json["result"][:5]: - synonyms = anime["anilist"]["synonyms"] - for x in synonyms: - _count_ch = 0 - for word in x: - if "\u4e00" <= word <= "\u9fff": - _count_ch += 1 - if _count_ch > 3: - anime_name = x - break - else: - anime_name = anime["anilist"]["title"]["native"] - episode = anime["episode"] - from_ = int(anime["from"]) - m, s = divmod(from_, 60) - similarity = anime["similarity"] - putline = "[ {} ][{}][{}:{}] 相似度:{:.2%}".format( - Converter("zh-hans").convert(anime_name), - episode if episode else "?", - m, - s, - similarity, - ) - repass += putline + "\n" - return f"耗时 {int(time.time() - s_time)} 秒\n" + repass[:-1] - else: - return f'访问错误 error:{anime_json["error"]}' - except Exception as e: - logger.error(f"识番发生错误 {type(e)}:{e}") - return "发生了奇怪的错误,那就没办法了,再试一次?" +import time + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + + +async def get_anime(anime: str) -> str: + s_time = time.time() + url = "https://api.trace.moe/search?anilistInfo&url={}".format(anime) + logger.debug("[info]Now starting get the {}".format(url)) + try: + anime_json = (await AsyncHttpx.get(url)).json() + if not anime_json["error"]: + if anime_json == "Error reading imagenull": + return "图像源错误,注意必须是静态图片哦" + repass = "" + # 拿到动漫 中文名 + for anime in anime_json["result"][:5]: + synonyms = anime["anilist"]["synonyms"] + for x in synonyms: + _count_ch = 0 + for word in x: + if "\u4e00" <= word <= "\u9fff": + _count_ch += 1 + if _count_ch > 3: + anime_name = x + break + else: + anime_name = anime["anilist"]["title"]["native"] + episode = anime["episode"] + from_ = int(anime["from"]) + m, s = divmod(from_, 60) + similarity = anime["similarity"] + putline = "[ {} ][{}][{}:{}] 相似度:{:.2%}".format( + anime_name, + episode if episode else "?", + m, + s, + similarity, + ) + repass += putline + "\n" + return f"耗时 {int(time.time() - s_time)} 秒\n" + repass[:-1] + else: + return f'访问错误 error:{anime_json["error"]}' + except Exception as e: + logger.error(f"识番发生错误", e=e) + return "发生了奇怪的错误,那就没办法了,再试一次?" diff --git a/zhenxun/plugins/word_bank/__init__.py b/zhenxun/plugins/word_bank/__init__.py new file mode 100644 index 00000000..c3ef6546 --- /dev/null +++ b/zhenxun/plugins/word_bank/__init__.py @@ -0,0 +1,18 @@ +from pathlib import Path + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "word_bank", + "WORD_BANK_LEVEL", + 5, + help="设置增删词库的权限等级", + default_value=5, + type=int, +) +Config.set_name("word_bank", "词库问答") + + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/word_bank/_config.py b/zhenxun/plugins/word_bank/_config.py similarity index 64% rename from plugins/word_bank/_config.py rename to zhenxun/plugins/word_bank/_config.py index d1f6ab67..3d074c18 100644 --- a/plugins/word_bank/_config.py +++ b/zhenxun/plugins/word_bank/_config.py @@ -1,4 +1,7 @@ +from zhenxun.configs.path_config import DATA_PATH +data_dir = DATA_PATH / "word_bank" +data_dir.mkdir(parents=True, exist_ok=True) scope2int = { "全局": 0, @@ -19,5 +22,3 @@ int2type = { 2: "正则", 3: "图片", } - - diff --git a/zhenxun/plugins/word_bank/_data_source.py b/zhenxun/plugins/word_bank/_data_source.py new file mode 100644 index 00000000..c9a53a1b --- /dev/null +++ b/zhenxun/plugins/word_bank/_data_source.py @@ -0,0 +1,287 @@ +from nonebot_plugin_alconna import At +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Image +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMessage, UniMsg + +from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils + +from ._model import WordBank + + +def get_img_and_at_list(message: UniMsg) -> tuple[list[str], list[str]]: + """获取图片和at数据 + + 参数: + message: UniMsg + + 返回: + tuple[list[str], list[str]]: 图片列表,at列表 + """ + img_list, at_list = [], [] + for msg in message: + if isinstance(msg, alcImage): + img_list.append(msg.url) + elif isinstance(msg, alcAt): + at_list.append(msg.target) + return img_list, at_list + + +def get_problem(message: UniMsg) -> str: + """获取问题内容 + + 参数: + message: UniMsg + + 返回: + str: 问题文本 + """ + problem = "" + a, b = True, True + for msg in message: + if isinstance(msg, alcText) or isinstance(msg, str): + msg = str(msg) + if "问" in str(msg) and a: + a = False + split_text = msg.split("问") + if len(split_text) > 1: + problem += "问".join(split_text[1:]) + if b: + if "答" in problem: + b = False + problem = problem.split("答")[0] + elif "答" in msg and b: + b = False + # problem += "答".join(msg.split("答")[:-1]) + problem += msg.split("答")[0] + if not a and not b: + break + if isinstance(msg, alcAt): + problem += f"[at:{msg.target}]" + return problem + + +def get_answer(message: UniMsg) -> UniMessage | None: + """获取at时回答 + + 参数: + message: UniMsg + + 返回: + str: 回答内容 + """ + temp_message = None + answer = "" + index = 0 + for msg in message: + index += 1 + if isinstance(msg, alcText) or isinstance(msg, str): + msg = str(msg) + if "答" in msg: + answer += "答".join(msg.split("答")[1:]) + break + if answer: + temp_message = message[index:] + temp_message.insert(0, alcText(answer)) + return temp_message + + +class WordBankManage: + + @classmethod + async def update_word( + cls, + replace: str, + problem: str = "", + index: int | None = None, + group_id: str | None = None, + word_scope: int = 1, + ) -> tuple[str, str]: + """修改群词条 + + 参数: + params: 参数 + group_id: 群号 + word_scope: 词条范围 + + 返回: + tuple[str, str]: 处理消息,替换的旧词条 + """ + return await cls.__word_handle( + problem, group_id, "update", index, None, word_scope, replace + ) + + @classmethod + async def delete_word( + cls, + problem: str, + index: int | None = None, + aid: int | None = None, + group_id: str | None = None, + word_scope: int = 1, + ) -> tuple[str, str]: + """删除群词条 + + 参数: + params: 参数 + index: 指定下标 + aid: 指定回答下标 + group_id: 群号 + word_scope: 词条范围 + + 返回: + tuple[str, str]: 处理消息,空 + """ + return await cls.__word_handle( + problem, group_id, "delete", index, aid, word_scope + ) + + @classmethod + async def __word_handle( + cls, + problem: str, + group_id: str | None, + handle_type: str, + index: int | None = None, + aid: int | None = None, + word_scope: int = 0, + replace_problem: str = "", + ) -> tuple[str, str]: + """词条操作 + + 参数: + problem: 参数 + group_id: 群号 + handle_type: 类型 + index: 指定回答下标 + aid: 指定回答下标 + word_scope: 词条范围 + replace_problem: 替换问题内容 + + 返回: + tuple[str, str]: 处理消息,替换的旧词条 + """ + if index is not None: + problem, code = await cls.__get_problem_str(index, group_id, word_scope) + if code != 200: + return problem, "" + if handle_type == "delete": + if index: + problem, _problem_list = await WordBank.get_problem_all_answer( + problem, None, group_id, word_scope + ) + if not _problem_list: + return problem, "" + if await WordBank.delete_group_problem(problem, group_id, aid, word_scope): # type: ignore + return "删除词条成功!", "" + return "词条不存在", "" + if handle_type == "update": + old_problem = await WordBank.update_group_problem( + problem, replace_problem, group_id, word_scope=word_scope + ) + return f"修改词条成功!\n{old_problem} -> {replace_problem}", old_problem + return "类型错误", "" + + @classmethod + async def __get_problem_str( + cls, idx: int, group_id: str | None = None, word_scope: int = 1 + ) -> tuple[str, int]: + """通过id获取问题字符串 + + 参数: + idx: 下标 + group_id: 群号 + word_scope: 获取类型 + """ + if word_scope in [0, 2]: + all_problem = await WordBank.get_problem_by_scope(word_scope) + elif group_id: + all_problem = await WordBank.get_group_all_problem(group_id) + else: + raise Exception("词条类型与群组id不能为空") + if idx < 0 or idx >= len(all_problem): + return "问题下标id必须在范围内", 999 + return all_problem[idx][0], 200 + + @classmethod + async def show_word( + cls, + problem: str | None, + index: int | None = None, + group_id: str | None = None, + word_scope: int | None = 1, + ) -> UniMessage: + """获取群词条 + + 参数: + problem: 问题 + group_id: 群组id + word_scope: 词条范围 + index: 指定回答下标 + """ + if problem or index != None: + msg_list = [] + problem, _problem_list = await WordBank.get_problem_all_answer( + problem, # type: ignore + index, + group_id if group_id is None else None, + word_scope, + ) + if not _problem_list: + return MessageUtils.build_message(problem) + for msg in _problem_list: + _text = str(msg) + if isinstance(msg, At): + _text = f"[at:{msg.target}]" + elif isinstance(msg, Image): + _text = msg.url or msg.path + elif isinstance(msg, list): + _text = [] + for m in msg: + __text = str(m) + if isinstance(m, At): + __text = f"[at:{m.target}]" + elif isinstance(m, Image): + # TODO: 显示词条回答图片 + # __text = (m.data["image"], 30, 30) + __text = "[图片]" + _text.append(__text) + msg_list.append("".join(str(_text))) + column_name = ["序号", "回答内容"] + data_list = [] + for index, msg in enumerate(msg_list): + data_list.append([index, msg]) + template_image = await ImageTemplate.table_page( + f"词条 {problem} 的回答", None, column_name, data_list + ) + return MessageUtils.build_message(template_image) + else: + result = [] + if group_id: + _problem_list = await WordBank.get_group_all_problem(group_id) + elif word_scope is not None: + _problem_list = await WordBank.get_problem_by_scope(word_scope) + else: + raise Exception("群组id和词条范围不能都为空") + global_problem_list = await WordBank.get_problem_by_scope(0) + if not _problem_list and not global_problem_list: + return MessageUtils.build_message("未收录任何词条...") + column_name = ["序号", "关键词", "匹配类型", "收录用户"] + data_list = [list(s) for s in _problem_list] + for i in range(len(data_list)): + data_list[i].insert(0, i) + group_image = await ImageTemplate.table_page( + "群组内词条" if group_id else "私聊词条", None, column_name, data_list + ) + result.append(group_image) + if global_problem_list: + data_list = [list(s) for s in global_problem_list] + for i in range(len(data_list)): + data_list[i].insert(0, i) + global_image = await ImageTemplate.table_page( + "全局词条", None, column_name, data_list + ) + result.append(global_image) + return MessageUtils.build_message(result) diff --git a/plugins/word_bank/_model.py b/zhenxun/plugins/word_bank/_model.py similarity index 52% rename from plugins/word_bank/_model.py rename to zhenxun/plugins/word_bank/_model.py index cfd1c647..abf03779 100644 --- a/plugins/word_bank/_model.py +++ b/zhenxun/plugins/word_bank/_model.py @@ -3,24 +3,21 @@ import re import time import uuid from datetime import datetime -from typing import Any, List, Optional, Tuple, Union +from typing import Any -from nonebot.adapters.onebot.v11 import ( - GroupMessageEvent, - Message, - MessageEvent, - MessageSegment, -) -from nonebot.internal.adapter.template import MessageTemplate +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMessage from tortoise import Tortoise, fields from tortoise.expressions import Q +from typing_extensions import Self -from configs.path_config import DATA_PATH -from services.db_context import Model -from utils.http_utils import AsyncHttpx -from utils.image_utils import get_img_hash -from utils.message_builder import at, face, image -from utils.utils import get_message_img +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.services.db_context import Model +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import get_img_hash +from zhenxun.utils.message import MessageUtils from ._config import int2type @@ -55,6 +52,10 @@ class WordBank(Model): """创建时间""" update_time = fields.DatetimeField(auto_now_add=True) """更新时间""" + platform = fields.CharField(255, default="qq") + """平台""" + author = fields.CharField(255, null=True, default="") + """收录人""" class Meta: table = "word_bank2" @@ -63,23 +64,22 @@ class WordBank(Model): @classmethod async def exists( cls, - user_id: Optional[str], - group_id: Optional[str], + user_id: str | None, + group_id: str | None, problem: str, - answer: Optional[str], - word_scope: Optional[int] = None, - word_type: Optional[int] = None, + answer: str | None, + word_scope: int | None = None, + word_type: int | None = None, ) -> bool: - """ - 说明: - 检测问题是否存在 + """检测问题是否存在 + 参数: - :param user_id: 用户id - :param group_id: 群号 - :param problem: 问题 - :param answer: 回答 - :param word_scope: 词条范围 - :param word_type: 词条类型 + user_id: 用户id + group_id: 群号 + problem: 问题 + answer: 回答 + word_scope: 词条范围 + word_type: 词条类型 """ query = cls.filter(problem=problem) if user_id: @@ -92,45 +92,49 @@ class WordBank(Model): query = query.filter(word_type=word_type) if word_scope is not None: query = query.filter(word_scope=word_scope) - return bool(await query.first()) + return await query.exists() @classmethod async def add_problem_answer( cls, user_id: str, - group_id: Optional[str], + group_id: str | None, word_scope: int, word_type: int, - problem: Union[str, Message], - answer: Union[str, Message], - to_me_nickname: Optional[str] = None, + problem: str, + answer: list[str | alcText | alcAt | alcImage], + to_me_nickname: str | None = None, + platform: str = "", + author: str = "", ): - """ - 说明: - 添加或新增一个问答 + """添加或新增一个问答 + 参数: - :param user_id: 用户id - :param group_id: 群号 - :param word_scope: 词条范围, - :param word_type: 词条类型, - :param problem: 问题 - :param answer: 回答 - :param to_me_nickname: at真寻名称 + user_id: 用户id + group_id: 群号 + word_scope: 词条范围, + word_type: 词条类型, + problem: 问题, 为图片时是URl + answer: 回答 + to_me_nickname: at真寻名称 + platform: 所属平台 + author: 收录人id """ # 对图片做额外处理 image_path = None if word_type == 3: - url = get_message_img(problem)[0] _file = ( path / "problem" / f"{group_id}" / f"{user_id}_{int(time.time())}.jpg" ) _file.parent.mkdir(exist_ok=True, parents=True) - await AsyncHttpx.download_file(url, _file) - problem = str(get_img_hash(_file)) + await AsyncHttpx.download_file(problem, _file) + problem = get_img_hash(_file) image_path = f"problem/{group_id}/{user_id}_{int(time.time())}.jpg" - answer, _list = await cls._answer2format(answer, user_id, group_id) + new_answer, placeholder_list = await cls._answer2format( + answer, user_id, group_id + ) if not await cls.exists( - user_id, group_id, str(problem), answer, word_scope, word_type + user_id, group_id, problem, new_answer, word_scope, word_type ): await cls.create( user_id=user_id, @@ -139,44 +143,49 @@ class WordBank(Model): word_type=word_type, status=True, problem=str(problem).strip(), - answer=answer, + answer=new_answer, image_path=image_path, - placeholder=",".join(_list), + placeholder=",".join(placeholder_list), create_time=datetime.now().replace(microsecond=0), update_time=datetime.now().replace(microsecond=0), to_me=to_me_nickname, + platform=platform, + author=author, ) @classmethod async def _answer2format( - cls, answer: Union[str, Message], user_id: str, group_id: Optional[str] - ) -> Tuple[str, List[Any]]: - """ - 说明: - 将CQ码转化为占位符 + cls, + answer: list[str | alcText | alcAt | alcImage], + user_id: str, + group_id: str | None, + ) -> tuple[str, list[Any]]: + """将特殊字段转化为占位符,图片,at等 + 参数: - :param answer: 回答内容 - :param user_id: 用户id - :param group_id: 群号 + answer: 回答内容 + user_id: 用户id + group_id: 群号 + + 返回: + tuple[str, list[Any]]: 替换后的文本回答内容,占位符 """ - if isinstance(answer, str): - return answer, [] - _list = [] + placeholder_list = [] text = "" index = 0 for seg in answer: placeholder = uuid.uuid1() if isinstance(seg, str): text += seg - elif seg.type == "text": - text += seg.data["text"] - elif seg.type == "face": + elif isinstance(seg, alcText): + text += seg.text + elif seg.type == "face": # TODO: face貌似无用... text += f"[face:placeholder_{placeholder}]" - _list.append(seg.data["id"]) - elif seg.type == "at": + placeholder_list.append(seg.data["id"]) + elif isinstance(seg, alcAt): text += f"[at:placeholder_{placeholder}]" - _list.append(seg.data["qq"]) - else: + placeholder_list.append(seg.target) + elif isinstance(seg, alcImage) and seg.url: text += f"[image:placeholder_{placeholder}]" index += 1 _file = ( @@ -186,30 +195,30 @@ class WordBank(Model): / f"{user_id}_{placeholder}.jpg" ) _file.parent.mkdir(exist_ok=True, parents=True) - await AsyncHttpx.download_file(seg.data["url"], _file) - _list.append( + await AsyncHttpx.download_file(seg.url, _file) + placeholder_list.append( f"answer/{group_id or user_id}/{user_id}_{placeholder}.jpg" ) - return text, _list + return text, placeholder_list @classmethod async def _format2answer( cls, problem: str, - answer: Union[str, Message], + answer: str, user_id: int, group_id: int, - query: Optional["WordBank"] = None, - ) -> Union[str, Message]: - """ - 说明: - 将占位符转换为CQ码 + query: Self | None = None, + ) -> UniMessage: + """将占位符转换为实际内容 + 参数: - :param problem: 问题内容 - :param answer: 回答内容 - :param user_id: 用户id - :param group_id: 群号 + problem: 问题内容 + answer: 回答内容 + user_id: 用户id + group_id: 群组id """ + result_list = [] if not query: query = await cls.get_or_none( problem=problem, @@ -218,44 +227,45 @@ class WordBank(Model): answer=answer, ) if not answer: - answer = query.answer # type: ignore + answer = str(query.answer) # type: ignore if query and query.placeholder: - type_list = re.findall(rf"\[(.*?):placeholder_.*?]", str(answer)) - temp_answer = re.sub(rf"\[(.*?):placeholder_.*?]", "{}", str(answer)) - seg_list = [] - for t, p in zip(type_list, query.placeholder.split(",")): - if t == "image": - seg_list.append(image(path / p)) - elif t == "face": - seg_list.append(face(int(p))) - elif t == "at": - seg_list.append(at(p)) - return MessageTemplate(temp_answer, Message).format(*seg_list) # type: ignore - return answer + type_list = re.findall(rf"\[(.*?):placeholder_.*?]", answer) + answer_split = re.split(rf"\[.*:placeholder_.*?]", answer) + placeholder_split = query.placeholder.split(",") + for index, ans in enumerate(answer_split): + result_list.append(ans) + if index < len(type_list): + t = type_list[index] + p = placeholder_split[index] + if t == "image": + result_list.append(path / p) + elif t == "at": + result_list.append(alcAt(flag="user", target=p)) + return MessageUtils.build_message(result_list) + return MessageUtils.build_message(answer) @classmethod async def check_problem( cls, - event: MessageEvent, + group_id: str | None, problem: str, - word_scope: Optional[int] = None, - word_type: Optional[int] = None, - ) -> Optional[Any]: - """ - 说明: - 检测是否包含该问题并获取所有回答 + word_scope: int | None = None, + word_type: int | None = None, + ) -> Any: + """检测是否包含该问题并获取所有回答 + 参数: - :param event: event - :param problem: 问题内容 - :param word_scope: 词条范围 - :param word_type: 词条类型 + group_id: 群组id + problem: 问题内容 + word_scope: 词条范围 + word_type: 词条类型 """ query = cls - if isinstance(event, GroupMessageEvent): + if group_id: if word_scope: query = query.filter(word_scope=word_scope) else: - query = query.filter(Q(group_id=event.group_id) | Q(word_scope=0)) + query = query.filter(Q(group_id=group_id) | Q(word_scope=0)) else: query = query.filter(Q(word_scope=2) | Q(word_scope=0)) if word_type: @@ -283,24 +293,23 @@ class WordBank(Model): @classmethod async def get_answer( cls, - event: MessageEvent, + group_id: str | None, problem: str, - word_scope: Optional[int] = None, - word_type: Optional[int] = None, - ) -> Optional[Union[str, Message]]: - """ - 说明: - 根据问题内容获取随机回答 + word_scope: int | None = None, + word_type: int | None = None, + ) -> UniMessage | None: + """根据问题内容获取随机回答 + 参数: - :param event: event - :param problem: 问题内容 - :param word_scope: 词条范围 - :param word_type: 词条类型 + user_id: 用户id + group_id: 群组id + problem: 问题内容 + word_scope: 词条范围 + word_type: 词条类型 """ - data_list = await cls.check_problem(event, problem, word_scope, word_type) + data_list = await cls.check_problem(group_id, problem, word_scope, word_type) if data_list: random_answer = random.choice(data_list) - temp_answer = random_answer.answer if random_answer.word_type == 2: r = re.search(random_answer.problem, problem) has_placeholder = re.search(rf"\$(\d)", random_answer.answer) @@ -316,64 +325,93 @@ class WordBank(Model): random_answer, ) if random_answer.placeholder - else random_answer.answer + else MessageUtils.build_message(random_answer.answer) ) @classmethod async def get_problem_all_answer( cls, problem: str, - index: Optional[int] = None, - group_id: Optional[str] = None, - word_scope: Optional[int] = 0, - ) -> List[Union[str, Message]]: - """ - 说明: - 获取指定问题所有回答 + index: int | None = None, + group_id: str | None = None, + word_scope: int | None = 0, + ) -> tuple[str, list[UniMessage]]: + """获取指定问题所有回答 + 参数: - :param problem: 问题 - :param index: 下标 - :param group_id: 群号 - :param word_scope: 词条范围 + problem: 问题 + index: 下标 + group_id: 群号 + word_scope: 词条范围 + + 返回: + tuple[str, list[UniMessage]]: 问题和所有回答 """ if index is not None: + # TODO: group_by和order_by不能同时使用 if group_id: - problem_ = (await cls.filter(group_id=group_id).all())[index] + _problem = ( + await cls.filter(group_id=group_id).order_by("create_time") + # .group_by("problem") + .values_list("problem", flat=True) + ) else: - problem_ = (await cls.filter(word_scope=(word_scope or 0)).all())[index] - problem = problem_.problem - answer = cls.filter(problem=problem) + _problem = ( + await cls.filter(word_scope=(word_scope or 0)).order_by( + "create_time" + ) + # .group_by("problem") + .values_list("problem", flat=True) + ) + # if index is None and problem not in _problem: + # return "词条不存在...", [] + sort_problem = [] + for p in _problem: + if p not in sort_problem: + sort_problem.append(p) + if index > len(sort_problem) - 1: + return "下标错误,必须小于问题数量...", [] + problem = sort_problem[index] # type: ignore + f = cls.filter(problem=problem, word_scope=(word_scope or 0)) if group_id: - answer = answer.filter(group_id=group_id) - return [await cls._format2answer("", "", 0, 0, x) for x in (await answer.all())] + f = f.filter(group_id=group_id) + answer_list = await f.all() + if not answer_list: + return "词条不存在...", [] + return problem, [await cls._format2answer("", "", 0, 0, a) for a in answer_list] @classmethod async def delete_group_problem( cls, problem: str, - group_id: Optional[str], - index: Optional[int] = None, + group_id: str | None, + index: int | None = None, word_scope: int = 1, ): - """ - 说明: - 删除指定问题全部或指定回答 + """删除指定问题全部或指定回答 + 参数: - :param problem: 问题文本 - :param group_id: 群号 - :param index: 回答下标 - :param word_scope: 词条范围 + problem: 问题文本 + group_id: 群号 + index: 回答下标 + word_scope: 词条范围 """ if await cls.exists(None, group_id, problem, None, word_scope): if index is not None: if group_id: - query = await cls.filter(group_id=group_id, problem=problem).all() + query = await cls.filter( + group_id=group_id, problem=problem, word_scope=word_scope + ).all() else: - query = await cls.filter(word_scope=0, problem=problem).all() + query = await cls.filter( + word_scope=word_scope, problem=problem + ).all() await query[index].delete() else: if group_id: - await WordBank.filter(group_id=group_id, problem=problem).delete() + await WordBank.filter( + group_id=group_id, problem=problem, word_scope=word_scope + ).delete() else: await WordBank.filter( word_scope=word_scope, problem=problem @@ -386,27 +424,31 @@ class WordBank(Model): cls, problem: str, replace_str: str, - group_id: Optional[str], - index: Optional[int] = None, + group_id: str | None, + index: int | None = None, word_scope: int = 1, - ): - """ - 说明: - 修改词条问题 + ) -> str: + """修改词条问题 + 参数: - :param problem: 问题 - :param replace_str: 替换问题 - :param group_id: 群号 - :param index: 下标 - :param word_scope: 词条范围 + problem: 问题 + replace_str: 替换问题 + group_id: 群号 + index: 问题下标 + word_scope: 词条范围 + + 返回: + str: 修改前的问题 """ if index is not None: if group_id: query = await cls.filter(group_id=group_id, problem=problem).all() else: query = await cls.filter(word_scope=word_scope, problem=problem).all() + tmp = query[index].problem query[index].problem = replace_str await query[index].save(update_fields=["problem"]) + return tmp else: if group_id: await cls.filter(group_id=group_id, problem=problem).update( @@ -416,85 +458,80 @@ class WordBank(Model): await cls.filter(word_scope=word_scope, problem=problem).update( problem=replace_str ) + return problem @classmethod - async def get_group_all_problem( - cls, group_id: str - ) -> List[Tuple[Any, Union[MessageSegment, str]]]: - """ - 说明: - 获取群聊所有词条 + async def get_group_all_problem(cls, group_id: str) -> list[tuple[Any | str]]: + """获取群聊所有词条 + 参数: - :param group_id: 群号 + group_id: 群号 """ return cls._handle_problem( - await cls.filter(group_id=group_id).all() # type: ignore + await cls.filter(group_id=group_id).order_by("create_time").all() # type: ignore ) @classmethod async def get_problem_by_scope(cls, word_scope: int): - """ - 说明: - 通过词条范围获取词条 + """通过词条范围获取词条 + 参数: - :param word_scope: 词条范围 + word_scope: 词条范围 """ return cls._handle_problem( - await cls.filter(word_scope=word_scope).all() # type: ignore + await cls.filter(word_scope=word_scope).order_by("create_time").all() # type: ignore ) @classmethod async def get_problem_by_type(cls, word_type: int): - """ - 说明: - 通过词条类型获取词条 + """通过词条类型获取词条 + 参数: - :param word_type: 词条类型 + word_type: 词条类型 """ return cls._handle_problem( - await cls.filter(word_type=word_type).all() # type: ignore + await cls.filter(word_type=word_type).order_by("create_time").all() # type: ignore ) @classmethod - def _handle_problem(cls, msg_list: List["WordBank"]): - """ - 说明: - 格式化处理问题 + def _handle_problem(cls, problem_list: list["WordBank"]): + """格式化处理问题 + 参数: - :param msg_list: 消息列表 + msg_list: 消息列表 """ _tmp = [] - problem_list = [] - for q in msg_list: + result_list = [] + for q in problem_list: if q.problem not in _tmp: + # TODO: 获取收录人名称 problem = ( - q.problem, - image(path / q.image_path) - if q.image_path - else f"[{int2type[q.word_type]}] " + q.problem, + (path / q.image_path, 30, 30) if q.image_path else q.problem, + int2type[q.word_type], + # q.author, + "-", ) - problem_list.append(problem) + result_list.append(problem) _tmp.append(q.problem) - return problem_list + return result_list @classmethod async def _move( cls, user_id: str, - group_id: Optional[str], - problem: Union[str, Message], - answer: Union[str, Message], + group_id: str | None, + problem: str, + answer: str, placeholder: str, ): - """ - 说明: - 旧词条图片移动方法 + """旧词条图片移动方法 + 参数: - :param user_id: 用户id - :param group_id: 群号 - :param problem: 问题 - :param answer: 回答 - :param placeholder: 占位符 + user_id: 用户id + group_id: 群号 + problem: 问题 + answer: 回答 + placeholder: 占位符 """ word_scope = 0 word_type = 0 @@ -525,4 +562,6 @@ class WordBank(Model): "ALTER TABLE word_bank2 RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id "ALTER TABLE word_bank2 ALTER COLUMN user_id TYPE character varying(255);", "ALTER TABLE word_bank2 ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE word_bank2 ADD platform varchar(255) DEFAULT 'qq';", + "ALTER TABLE word_bank2 ADD author varchar(255) DEFAULT '';", ] diff --git a/zhenxun/plugins/word_bank/_rule.py b/zhenxun/plugins/word_bank/_rule.py new file mode 100644 index 00000000..3ce032ca --- /dev/null +++ b/zhenxun/plugins/word_bank/_rule.py @@ -0,0 +1,58 @@ +from io import BytesIO + +import imagehash +from nonebot.adapters import Bot, Event +from nonebot.typing import T_State +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession +from PIL import Image + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +from ._data_source import get_img_and_at_list +from ._model import WordBank + + +async def check( + bot: Bot, + event: Event, + message: UniMsg, + session: EventSession, + state: T_State, +) -> bool: + text = message.extract_plain_text().strip() + img_list, at_list = get_img_and_at_list(message) + problem = text + if not text and len(img_list) == 1: + try: + r = await AsyncHttpx.get(img_list[0]) + problem = str(imagehash.average_hash(Image.open(BytesIO(r.content)))) + except Exception as e: + logger.warning(f"获取图片失败", "词条检测", session=session, e=e) + if at_list: + temp = "" + # TODO: 支持更多消息类型 + for msg in message: + if isinstance(msg, alcAt): + temp += f"[at:{msg.target}]" + elif isinstance(msg, alcText): + temp += msg.text + problem = temp + if event.is_tome() and bot.config.nickname: + if isinstance(message[0], alcAt) and message[0].target == bot.self_id: + problem = f"[at:{bot.self_id}]" + problem + else: + if problem and bot.config.nickname: + nickname = [ + nk for nk in bot.config.nickname if str(message).startswith(nk) + ] + problem = nickname[0] + problem if nickname else problem + if problem and ( + await WordBank.check_problem(session.id3 or session.id2, problem) is not None + ): + state["problem"] = problem # type: ignore + return True + return False diff --git a/zhenxun/plugins/word_bank/command.py b/zhenxun/plugins/word_bank/command.py new file mode 100644 index 00000000..64049d30 --- /dev/null +++ b/zhenxun/plugins/word_bank/command.py @@ -0,0 +1,47 @@ +from nonebot import on_regex +from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna, store_true + +from zhenxun.utils.rules import admin_check, ensure_group + +_add_matcher = on_regex( + r"^(全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*)", + priority=5, + block=True, + rule=admin_check("word_bank", "WORD_BANK_LEVEL"), +) + + +_del_matcher = on_alconna( + Alconna( + "删除词条", + Args["problem?", str], + Option("--all", action=store_true, help_text="所有词条"), + Option("--id", Args["index", int], help_text="下标id"), + Option("--aid", Args["answer_id", int], help_text="回答下标id"), + ), + priority=5, + block=True, +) + + +_update_matcher = on_alconna( + Alconna( + "修改词条", + Args["replace", str]["problem?", str], + Option("--id", Args["index", int], help_text="词条id"), + Option("--all", action=store_true, help_text="全局词条"), + ) +) + +_show_matcher = on_alconna( + Alconna( + "显示词条", + Args["problem?", str], + Option("-g|--group", Args["gid", str], help_text="群组id"), + Option("--id", Args["index", int], help_text="词条id"), + Option("--all", action=store_true, help_text="全局词条"), + ), + aliases={"查看词条"}, + priority=5, + block=True, +) diff --git a/zhenxun/plugins/word_bank/message_handle.py b/zhenxun/plugins/word_bank/message_handle.py new file mode 100644 index 00000000..a9bf624a --- /dev/null +++ b/zhenxun/plugins/word_bank/message_handle.py @@ -0,0 +1,31 @@ +from nonebot import on_message +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_State +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services import logger +from zhenxun.utils.enum import PluginType + +from ._model import WordBank +from ._rule import check + +__plugin_meta__ = PluginMetadata( + name="词库问答回复操作", + description="", + usage="""""", + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN + ).dict(), +) + +_matcher = on_message(priority=6, block=True, rule=check) + + +@_matcher.handle() +async def _(session: EventSession, state: T_State): + if problem := state.get("problem"): + gid = session.id3 or session.id2 + if result := await WordBank.get_answer(gid, problem): + await result.send() + logger.info(f" 触发词条 {problem}", "词条检测", session=session) diff --git a/zhenxun/plugins/word_bank/word_handle.py b/zhenxun/plugins/word_bank/word_handle.py new file mode 100644 index 00000000..0f70a44a --- /dev/null +++ b/zhenxun/plugins/word_bank/word_handle.py @@ -0,0 +1,325 @@ +import re +from typing import Any + +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import unescape +from nonebot.exception import FinishedException +from nonebot.internal.params import Arg, ArgStr +from nonebot.params import RegexGroup +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_State +from nonebot_plugin_alconna import AlconnaQuery, Arparma +from nonebot_plugin_alconna import Image +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Match, Query, UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils + +from ._config import scope2int, type2int +from ._data_source import WordBankManage, get_answer, get_img_and_at_list, get_problem +from ._model import WordBank +from .command import _add_matcher, _del_matcher, _show_matcher, _update_matcher + +base_config = Config.get("word_bank") + + +__plugin_meta__ = PluginMetadata( + name="词库问答", + description="自定义词条内容随机回复", + usage=""" + usage: + 对指定问题的随机回答,对相同问题可以设置多个不同回答 + 删除词条后每个词条的id可能会变化,请查看后再删除 + 更推荐使用id方式删除 + 问题回答支持的类型:at, image + 查看词条命令:群聊时为 群词条+全局词条,私聊时为 私聊词条+全局词条 + 添加词条正则:添加词条(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*) + 正则问可以通过$1类推()捕获的组 + 指令: + 添加词条 ?[模糊|正则|图片]问...答...:添加问答词条,可重复添加相同问题的不同回答 + 示例: + 添加词条问你好答你也好 + 添加图片词条问答看看涩图 + 删除词条 ?[问题] ?[序号] ?[回答序号]:删除指定词条指定或全部回答 + 示例: + 删除词条 谁是萝莉 : 删除文字是 谁是萝莉 的词条 + 删除词条 --id 2 : 删除序号为2的词条 + 删除词条 谁是萝莉 --aid 2 : 删除 谁是萝莉 词条的第2个回答 + 删除词条 --id 2 --aid 2 : 删除序号为2词条的第2个回答 + 修改词条 [替换文字] ?[旧词条文字] ?[序号]:修改词条问题 + 示例: + 修改词条 谁是萝莉 谁是萝莉啊? : 将词条 谁是萝莉 修改为 谁是萝莉啊? + 修改词条 谁是萝莉 --id 2 : 将序号为2的词条修改为 谁是萝莉 + 查看词条 ?[问题] ?[序号]:查看全部词条或对应词条回答 + 示例: + 查看词条: + (在群组中使用时): 查看当前群组词条和全局词条 + (在私聊中使用时): 查看当前私聊词条和全局词条 + 查看词条 谁是萝莉 : 查看词条 谁是萝莉 的全部回答 + 查看词条 --id 2 : 查看词条序号为2的全部回答 + 查看词条 谁是萝莉 --all: 查看全局词条 谁是萝莉 的全部回答 + 查看词条 --id 2 --all: 查看全局词条序号为2的全部回答 + """.strip(), + extra=PluginExtraData( + author="HibiKier & yajiwa", + version="0.1", + superuser_help=""" + 在私聊中超级用户额外设置 + 指令: + (全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*):添加问答词条,可重复添加相同问题的不同回答 + 全局添加词条 + 私聊添加词条 + (私聊情况下)删除词条: 删除私聊词条 + (私聊情况下)修改词条: 修改私聊词条 + 通过添加参数 --all才指定全局词条 + 示例: + 删除词条 --id 2 --all: 删除全局词条中序号为2的词条 + 用法与普通用法相同 + """, + admin_level=base_config.get("WORD_BANK_LEVEL"), + ).dict(), +) + + +@_add_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + state: T_State, + message: UniMsg, + reg_group: tuple[Any, ...] = RegexGroup(), +): + img_list, at_list = get_img_and_at_list(message) + user_id = session.id1 + group_id = session.id3 or session.id2 + if not group_id and user_id not in bot.config.superusers: + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) + word_scope, word_type, problem, answer = reg_group + if not word_scope and not group_id: + word_scope = "私聊" + if ( + word_scope + and word_scope in ["全局", "私聊"] + and user_id not in bot.config.superusers + ): + await MessageUtils.build_message("权限不足,无法添加该范围词条...").finish( + reply_to=True + ) + if (not problem or not problem.strip()) and word_type != "图片": + await MessageUtils.build_message("词条问题不能为空!").finish(reply_to=True) + if (not answer or not answer.strip()) and not len(img_list) and not len(at_list): + await MessageUtils.build_message("词条回答不能为空!").finish(reply_to=True) + if word_type != "图片": + state["problem_image"] = "YES" + temp_problem = message.copy() + # answer = message.copy() + # 对at问题对额外处理 + # if at_list: + answer = get_answer(message.copy()) + # text = str(message.pop(0)).split("答", maxsplit=1)[-1].strip() + # temp_problem.insert(0, alcMessageUtils.build_message(text)) + state["word_scope"] = word_scope + state["word_type"] = word_type + state["problem"] = get_problem(temp_problem) + state["answer"] = answer + logger.info( + f"添加词条 范围: {word_scope} 类型: {word_type} 问题: {problem} 回答: {answer}", + "添加词条", + session=session, + ) + + +@_add_matcher.got("problem_image", prompt="请发送该回答设置的问题图片") +async def _( + bot: Bot, + session: EventSession, + message: UniMsg, + word_scope: str | None = ArgStr("word_scope"), + word_type: str | None = ArgStr("word_type"), + problem: str | None = ArgStr("problem"), + answer: Any = Arg("answer"), +): + if not session.id1: + await MessageUtils.build_message("用户id不存在...").finish() + user_id = session.id1 + group_id = session.id3 or session.id2 + try: + if word_type == "图片": + problem = [m for m in message if isinstance(m, alcImage)][0].url + elif word_type == "正则" and problem: + problem = unescape(problem) + try: + re.compile(problem) + except re.error: + await MessageUtils.build_message( + f"添加词条失败,正则表达式 {problem} 非法!" + ).finish(reply_to=True) + # if str(event.user_id) in bot.config.superusers and isinstance(event, PrivateMessageEvent): + # word_scope = "私聊" + nickname = None + if problem and bot.config.nickname: + nickname = [nk for nk in bot.config.nickname if problem.startswith(nk)] + if not problem: + await MessageUtils.build_message("获取问题失败...").finish(reply_to=True) + await WordBank.add_problem_answer( + user_id, + ( + group_id + if group_id and (not word_scope or word_scope == "私聊") + else "0" + ), + scope2int[word_scope] if word_scope else 1, + type2int[word_type] if word_type else 0, + problem, + answer, + nickname[0] if nickname else None, + session.platform, + session.id1, + ) + except Exception as e: + if isinstance(e, FinishedException): + await _add_matcher.finish() + logger.error( + f"添加词条 {problem} 错误...", + "添加词条", + session=session, + e=e, + ) + await MessageUtils.build_message( + f"添加词条 {problem if word_type != '图片' else '图片'} 发生错误!" + ).finish(reply_to=True) + if word_type == "图片": + result = MessageUtils.build_message( + ["添加词条 ", Image(url=problem), " 成功!"] + ) + else: + result = MessageUtils.build_message(f"添加词条 {problem} 成功!") + await result.send() + logger.info( + f"添加词条 {problem} 成功!", + "添加词条", + session=session, + ) + + +@_del_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + problem: Match[str], + index: Match[int], + answer_id: Match[int], + arparma: Arparma, + all: Query[bool] = AlconnaQuery("all.value", False), +): + if not problem.available and not index.available: + await MessageUtils.build_message( + "此命令之后需要跟随指定词条或id,通过“显示词条“查看" + ).finish(reply_to=True) + word_scope = 1 if session.id3 or session.id2 else 2 + if all.result: + word_scope = 0 + if gid := session.id3 or session.id2: + result, _ = await WordBankManage.delete_word( + problem.result, + index.result if index.available else None, + answer_id.result if answer_id.available else None, + gid, + word_scope, + ) + else: + if session.id1 not in bot.config.superusers: + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) + result, _ = await WordBankManage.delete_word( + problem.result, + index.result if index.available else None, + answer_id.result if answer_id.available else None, + None, + word_scope, + ) + await MessageUtils.build_message(result).send(reply_to=True) + logger.info(f"删除词条: {problem.result}", arparma.header_result, session=session) + + +@_update_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + replace: str, + problem: Match[str], + index: Match[int], + arparma: Arparma, + all: Query[bool] = AlconnaQuery("all.value", False), +): + if not problem.available and not index.available: + await MessageUtils.build_message( + "此命令之后需要跟随指定词条或id,通过“显示词条“查看" + ).finish(reply_to=True) + word_scope = 1 if session.id3 or session.id2 else 2 + if all.result: + word_scope = 0 + if gid := session.id3 or session.id2: + result, old_problem = await WordBankManage.update_word( + replace, + problem.result if problem.available else "", + index.result if index.available else None, + gid, + word_scope, + ) + else: + if session.id1 not in bot.config.superusers: + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) + result, old_problem = await WordBankManage.update_word( + replace, + problem.result if problem.available else "", + index.result if index.available else None, + session.id3 or session.id2, + word_scope, + ) + await MessageUtils.build_message(result).send(reply_to=True) + logger.info( + f"更新词条词条: {old_problem} -> {replace}", + arparma.header_result, + session=session, + ) + + +@_show_matcher.handle() +async def _( + session: EventSession, + problem: Match[str], + index: Match[int], + gid: Match[str], + arparma: Arparma, + all: Query[bool] = AlconnaQuery("all.value", False), +): + word_scope = 1 if session.id3 or session.id2 else 2 + if all.result: + word_scope = 0 + group_id = session.id3 or session.id2 + if gid.available: + group_id = gid.result + if problem.available: + if index.available: + if index.result < 0 or index.result > len( + await WordBank.get_problem_by_scope(2) + ): + await MessageUtils.build_message("id必须在范围内...").finish( + reply_to=True + ) + result = await WordBankManage.show_word( + problem.result, + index.result if index.available else None, + group_id, + word_scope, + ) + else: + result = await WordBankManage.show_word( + None, index.result if index.available else None, group_id, word_scope + ) + await result.send() + logger.info(f"查看词条回答: {problem}", arparma.header_result, session=session) diff --git a/services/__init__.py b/zhenxun/services/__init__.py old mode 100755 new mode 100644 similarity index 95% rename from services/__init__.py rename to zhenxun/services/__init__.py index 8bbd77c0..aae2c093 --- a/services/__init__.py +++ b/zhenxun/services/__init__.py @@ -1,2 +1,2 @@ -from .db_context import * -from .log import * +from .db_context import * +from .log import * diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py new file mode 100644 index 00000000..2bf9c109 --- /dev/null +++ b/zhenxun/services/db_context.py @@ -0,0 +1,118 @@ +import ujson as json +from nonebot.utils import is_coroutine_callable +from tortoise import Tortoise, fields +from tortoise.connection import connections +from tortoise.models import Model as Model_ + +from zhenxun.configs.config import ( + address, + bind, + database, + password, + port, + sql_name, + user, +) +from zhenxun.configs.path_config import DATA_PATH + +from .log import logger + +MODELS: list[str] = [] + +SCRIPT_METHOD = [] + +DATABASE_SETTING_FILE = DATA_PATH / "database.json" + + +class Model(Model_): + """ + 自动添加模块 + + Args: + Model_ (_type_): _description_ + """ + + def __init_subclass__(cls, **kwargs): + MODELS.append(cls.__module__) + + if func := getattr(cls, "_run_script", None): + SCRIPT_METHOD.append((cls.__module__, func)) + + +class TestSQL(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + + class Meta: + abstract = True + table = "test_sql" + table_description = "执行SQL命令,不记录任何数据" + + +async def init(): + if DATABASE_SETTING_FILE.exists(): + with open(DATABASE_SETTING_FILE, "r", encoding="utf-8") as f: + setting_data = json.load(f) + else: + i_bind = bind + if not i_bind and any([user, password, address, port, database]): + i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" + setting_data = { + "bind": i_bind, + "sql_name": sql_name, + "user": user, + "password": password, + "address": address, + "port": port, + "database": database, + } + with open(DATABASE_SETTING_FILE, "w", encoding="utf-8") as f: + json.dump(setting_data, f, ensure_ascii=False, indent=4) + i_bind = setting_data.get("bind") + _sql_name = setting_data.get("sql_name") + _user = setting_data.get("user") + _password = setting_data.get("password") + _address = setting_data.get("address") + _port = setting_data.get("port") + _database = setting_data.get("database") + if not i_bind and not any([_user, _password, _address, _port, _database]): + raise ValueError("\n数据库配置未填写...") + if not i_bind: + i_bind = f"{_sql_name}://{_user}:{_password}@{_address}:{_port}/{_database}" + try: + await Tortoise.init( + db_url=i_bind, modules={"models": MODELS}, timezone="Asia/Shanghai" + ) + if SCRIPT_METHOD: + db = Tortoise.get_connection("default") + logger.debug( + f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个..." + ) + sql_list = [] + for module, func in SCRIPT_METHOD: + try: + if is_coroutine_callable(func): + sql = await func() + else: + sql = func() + if sql: + sql_list += sql + except Exception as e: + logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e) + for sql in sql_list: + logger.debug(f"执行SQL: {sql}") + try: + await db.execute_query_dict(sql) + # await TestSQL.raw(sql) + except Exception as e: + logger.debug(f"执行SQL: {sql} 错误...", e=e) + if sql_list: + logger.debug("SCRIPT_METHOD方法执行完毕!") + await Tortoise.generate_schemas() + logger.info(f"Database loaded successfully!") + except Exception as e: + raise Exception(f"数据库连接错误... e:{e}") + + +async def disconnect(): + await connections.close_all() diff --git a/zhenxun/services/log.py b/zhenxun/services/log.py new file mode 100644 index 00000000..a2b5e07a --- /dev/null +++ b/zhenxun/services/log.py @@ -0,0 +1,340 @@ +from datetime import datetime, timedelta +from typing import Any, Dict, overload + +from nonebot import require + +require("nonebot_plugin_session") +from loguru import logger as logger_ +from nonebot.log import default_filter, default_format +from nonebot_plugin_session import Session + +from zhenxun.configs.path_config import LOG_PATH + +logger_.add( + LOG_PATH / f"{datetime.now().date()}.log", + level="INFO", + rotation="00:00", + format=default_format, + filter=default_filter, + retention=timedelta(days=30), +) + +logger_.add( + LOG_PATH / f"error_{datetime.now().date()}.log", + level="ERROR", + rotation="00:00", + format=default_format, + filter=default_filter, + retention=timedelta(days=30), +) + + +class logger: + TEMPLATE_A = "Adapter[{}] {}" + TEMPLATE_B = "Adapter[{}] [{}]: {}" + TEMPLATE_C = "Adapter[{}] 用户[{}] 触发 [{}]: {}" + TEMPLATE_D = "Adapter[{}] 群聊[{}] 用户[{}] 触发 [{}]: {}" + TEMPLATE_E = "Adapter[{}] 群聊[{}] 用户[{}] 触发 [{}] [Target]({}): {}" + + TEMPLATE_ADAPTER = "Adapter[{}] " + TEMPLATE_USER = "用户[{}] " + TEMPLATE_GROUP = "群聊[{}] " + TEMPLATE_COMMAND = "CMD[{}] " + TEMPLATE_PLATFORM = "平台[{}] " + TEMPLATE_TARGET = "[Target]([{}]) " + + SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]" + + WARNING_TEMPLATE = "[{}]: {}" + + ERROR_TEMPLATE = "[{}]: {}" + + @overload + @classmethod + def info( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + ): ... + + @overload + @classmethod + def info( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + target: Any = None, + platform: str | None = None, + ): ... + + @classmethod + def info( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3: + group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target, platform + ) + try: + logger_.opt(colors=True).info(template) + except Exception as e: + logger_.info(template) + + @classmethod + def success( + cls, + info: str, + command: str, + param: Dict[str, Any] | None = None, + result: str = "", + ): + param_str = "" + if param: + param_str = ",".join([f"{k}:{v}" for k, v in param.items()]) + logger_.opt(colors=True).success( + cls.SUCCESS_TEMPLATE.format(command, info, param_str, result) + ) + + @overload + @classmethod + def warning( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): ... + + @overload + @classmethod + def warning( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): ... + + @classmethod + def warning( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3: + group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target, platform + ) + if e: + template += f" || 错误{type(e)}: {e}" + try: + logger_.opt(colors=True).warning(template) + except Exception as e: + logger_.warning(template) + + @overload + @classmethod + def error( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): ... + + @overload + @classmethod + def error( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): ... + + @classmethod + def error( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3: + group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target, platform + ) + if e: + template += f" || 错误 {type(e)}: {e}" + try: + logger_.opt(colors=True).error(template) + except Exception as e: + logger_.error(template) + + @overload + @classmethod + def debug( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): ... + + @overload + @classmethod + def debug( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): ... + + @classmethod + def debug( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + e: Exception | None = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3: + group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target, platform + ) + if e: + template += f" || 错误 {type(e)}: {e}" + try: + logger_.opt(colors=True).debug(template) + except Exception as e: + logger_.debug(template) + + @classmethod + def __parser_template( + cls, + info: str, + command: str | None = None, + user_id: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + platform: str | None = None, + ) -> str: + arg_list = [] + template = "" + if adapter is not None: + template += cls.TEMPLATE_ADAPTER + arg_list.append(adapter) + if platform is not None: + template += cls.TEMPLATE_PLATFORM + arg_list.append(platform) + if group_id is not None: + template += cls.TEMPLATE_GROUP + arg_list.append(group_id) + if user_id is not None: + template += cls.TEMPLATE_USER + arg_list.append(user_id) + if command is not None: + template += cls.TEMPLATE_COMMAND + arg_list.append(command) + if target is not None: + template += cls.TEMPLATE_TARGET + arg_list.append(target) + arg_list.append(info) + template += "{}" + return template.format(*arg_list) diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py new file mode 100644 index 00000000..33666f7e --- /dev/null +++ b/zhenxun/utils/_build_image.py @@ -0,0 +1,720 @@ +import base64 +import math +import uuid +from io import BytesIO +from pathlib import Path +from typing import Literal, Tuple, TypeAlias, overload + +from nonebot.utils import run_sync +from PIL import Image, ImageDraw, ImageFilter, ImageFont +from PIL.Image import Image as tImage +from PIL.ImageFont import FreeTypeFont +from typing_extensions import Self + +from zhenxun.configs.path_config import FONT_PATH + +ModeType = Literal[ + "1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr" +] +"""图片类型""" + +ColorAlias: TypeAlias = str | Tuple[int, int, int] | Tuple[int, int, int, int] | None + +CenterType = Literal["center", "height", "width"] +""" +粘贴居中 + +center: 水平垂直居中 + +height: 垂直居中 + +width: 水平居中 +""" + + +class BuildImage: + """ + 快捷生成图片与操作图片的工具类 + """ + + def __init__( + self, + width: int = 0, + height: int = 0, + color: ColorAlias = (255, 255, 255), + mode: ModeType = "RGBA", + font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf", + font_size: int = 20, + background: str | BytesIO | Path | None = None, + ) -> None: + self.uid = uuid.uuid1() + self.width = width + self.height = height + self.color = color + self.font = ( + self.load_font(font, font_size) + if not isinstance(font, FreeTypeFont) + else font + ) + if background: + self.markImg = Image.open(background) + if width and height: + self.markImg = self.markImg.resize((width, height), Image.LANCZOS) + else: + self.width = self.markImg.width + self.height = self.markImg.height + else: + if not width or not height: + raise ValueError("长度和宽度不能为空...") + self.markImg = Image.new(mode, (width, height), color) # type: ignore + self.draw = ImageDraw.Draw(self.markImg) + + @property + def size(self) -> Tuple[int, int]: + return self.markImg.size + + @classmethod + def open(cls, path: str | Path) -> Self: + """打开图片 + + 参数: + path: 图片路径 + + 返回: + Self: BuildImage + """ + return cls(background=path) + + @classmethod + async def build_text_image( + cls, + text: str, + font: str | FreeTypeFont | Path = "HYWenHei-85W.ttf", + size: int = 10, + font_color: str | Tuple[int, int, int] = (0, 0, 0), + color: ColorAlias = None, + padding: int | Tuple[int, int, int, int] | None = None, + ) -> Self: + """构建文本图片 + + 参数: + text: 文本 + font: 字体路径 + size: 字体大小 + font_color: 字体颜色. + color: 背景颜色 + padding: 外边距 + + 返回: + Self: Self + """ + if not text.strip(): + return cls(1, 1) + _font = None + if isinstance(font, FreeTypeFont): + _font = font + elif isinstance(font, (str, Path)): + _font = cls.load_font(font, size) + width, height = cls.get_text_size(text, _font) + if isinstance(padding, int): + width += padding * 2 + height += padding * 2 + elif isinstance(padding, tuple): + width += padding[1] + padding[3] + height += padding[0] + padding[2] + markImg = cls(width, height, color, font=_font) + await markImg.text( + (0, 0), text, fill=font_color, font=_font, center_type="center" + ) + return markImg + + @classmethod + async def auto_paste( + cls, + img_list: list[Self | tImage], + row: int, + space: int = 10, + padding: int = 50, + color: ColorAlias = (255, 255, 255), + background: str | BytesIO | Path | None = None, + ) -> Self: + """自动贴图 + + 参数: + img_list: 图片列表 + row: 一行图片的数量 + space: 图片之间的间距. + padding: 外边距. + color: 图片背景颜色. + background: 图片背景图片. + + 返回: + Self: Self + """ + if not img_list: + raise ValueError("贴图类别为空...") + width, height = img_list[0].size + background_width = width * row + space * (row - 1) + padding * 2 + row_count = math.ceil(len(img_list) / row) + if row_count == 1: + background_width = ( + sum([img.width for img in img_list]) + space * (row - 1) + padding * 2 + ) + background_height = height * row_count + space * (row_count - 1) + padding * 2 + background_image = cls( + background_width, background_height, color=color, background=background + ) + _cur_width, _cur_height = padding, padding + for img in img_list: + await background_image.paste(img, (_cur_width, _cur_height)) + _cur_width += space + img.width + if _cur_width + padding >= background_image.width: + _cur_height += space + img.height + _cur_width = padding + return background_image + + @classmethod + def load_font( + cls, font: str | Path = "HYWenHei-85W.ttf", font_size: int = 10 + ) -> FreeTypeFont: + """加载字体 + + 参数: + font: 字体名称 + font_size: 字体大小 + + 返回: + FreeTypeFont: 字体 + """ + path = FONT_PATH / font if type(font) == str else font + return ImageFont.truetype(str(path), font_size) + + @overload + @classmethod + def get_text_size( + cls, text: str, font: FreeTypeFont | None = None + ) -> Tuple[int, int]: ... + + @overload + @classmethod + def get_text_size( + cls, text: str, font: str | None = None, font_size: int = 10 + ) -> Tuple[int, int]: ... + + @classmethod + def get_text_size( + cls, + text: str, + font: str | FreeTypeFont | None = "HYWenHei-85W.ttf", + font_size: int = 10, + ) -> Tuple[int, int]: + """获取该字体下文本需要的长宽 + + 参数: + text: 文本内容 + font: 字体名称或FreeTypeFont + font_size: 字体大小 + + 返回: + Tuple[int, int]: 长宽 + """ + _font = font + if font and type(font) == str: + _font = cls.load_font(font, font_size) + temp_image = Image.new("RGB", (1, 1), (255, 255, 255)) + draw = ImageDraw.Draw(temp_image) + text_box = draw.textbbox((0, 0), str(text), font=_font) # type: ignore + text_width = text_box[2] - text_box[0] + text_height = text_box[3] - text_box[1] + return text_width, text_height + 10 + # return _font.getsize(str(text)) # type: ignore + + def getsize(self, msg: str) -> Tuple[int, int]: + """ + 获取文字在该图片 font_size 下所需要的空间 + + 参数: + msg: 文本 + + 返回: + Tuple[int, int]: 长宽 + """ + temp_image = Image.new("RGB", (1, 1), (255, 255, 255)) + draw = ImageDraw.Draw(temp_image) + text_box = draw.textbbox((0, 0), str(msg), font=self.font) + text_width = text_box[2] - text_box[0] + text_height = text_box[3] - text_box[1] + return text_width, text_height + 10 + # return self.font.getsize(msg) # type: ignore + + def __center_xy( + self, + pos: Tuple[int, int], + width: int, + height: int, + center_type: CenterType | None, + ) -> Tuple[int, int]: + """ + 根据居中类型定位xy + + 参数: + pos: 定位 + image: image + center_type: 居中类型 + + 返回: + Tuple[int, int]: 定位 + """ + # _width, _height = pos + if self.width and self.height: + if center_type == "center": + width = int((self.width - width) / 2) + height = int((self.height - height) / 2) + elif center_type == "width": + width = int((self.width - width) / 2) + height = pos[1] + elif center_type == "height": + width = pos[0] + height = int((self.height - height) / 2) + return width, height + + @run_sync + def paste( + self, + image: Self | tImage, + pos: Tuple[int, int] = (0, 0), + center_type: CenterType | None = None, + ) -> Self: + """贴图 + + 参数: + image: BuildImage 或 Image + pos: 定位. + center_type: 居中. + + 返回: + BuildImage: Self + + 异常: + ValueError: 居中类型错误 + """ + if center_type and center_type not in ["center", "height", "width"]: + raise ValueError("center_type must be 'center', 'width' or 'height'") + width, height = 0, 0 + _image = image + if isinstance(image, BuildImage): + _image = image.markImg + if _image.width and _image.height and center_type: + pos = self.__center_xy(pos, _image.width, _image.height, center_type) + try: + self.markImg.paste(_image, pos, _image) # type: ignore + except ValueError: + self.markImg.paste(_image, pos) # type: ignore + return self + + @run_sync + def point( + self, pos: Tuple[int, int], fill: Tuple[int, int, int] | None = None + ) -> Self: + """ + 绘制多个或单独的像素 + + 参数: + pos: 坐标 + fill: 填充颜色. + + 返回: + BuildImage: Self + """ + self.draw.point(pos, fill=fill) + return self + + @run_sync + def ellipse( + self, + pos: Tuple[int, int, int, int], + fill: Tuple[int, int, int] | None = None, + outline: Tuple[int, int, int] | None = None, + width: int = 1, + ) -> Self: + """ + 绘制圆 + + 参数: + pos: 坐标范围 + fill: 填充颜色. + outline: 描线颜色. + width: 描线宽度. + + 返回: + BuildImage: Self + """ + self.draw.ellipse(pos, fill, outline, width) + return self + + @run_sync + def text( + self, + pos: Tuple[int, int], + text: str, + fill: str | Tuple[int, int, int] = (0, 0, 0), + center_type: CenterType | None = None, + font: FreeTypeFont | str | Path | None = None, + font_size: int = 10, + ) -> Self: + """ + 在图片上添加文字 + + 参数: + pos: 文字位置 + text: 文字内容 + fill: 文字颜色. + center_type: 居中类型. + font: 字体. + font_size: 字体大小. + + 返回: + BuildImage: Self + + 异常: + ValueError: 居中类型错误 + """ + text = str(text) + if center_type and center_type not in ["center", "height", "width"]: + raise ValueError("center_type must be 'center', 'width' or 'height'") + max_length_text = "" + sentence = text.split("\n") + for x in sentence: + max_length_text = x if len(x) > len(max_length_text) else max_length_text + if font: + if not isinstance(font, FreeTypeFont): + font = self.load_font(font, font_size) + else: + font = self.font + if center_type: + ttf_w, ttf_h = self.getsize(max_length_text) # type: ignore + # ttf_h = ttf_h * len(sentence) + pos = self.__center_xy(pos, ttf_w, ttf_h, center_type) + self.draw.text(pos, text, fill=fill, font=font) + return self + + @run_sync + def save(self, path: str | Path): + """ + 保存图片 + + 参数: + path: 图片路径 + """ + self.markImg.save(path) # type: ignore + + def show(self): + """ + 说明: + 显示图片 + """ + self.markImg.show() + + @run_sync + def resize(self, ratio: float = 0, width: int = 0, height: int = 0) -> Self: + """ + 压缩图片 + + 参数: + ratio: 压缩倍率. + width: 压缩图片宽度至 width. + height: 压缩图片高度至 height. + + 返回: + BuildImage: Self + + 异常: + ValueError: 缺少参数 + """ + if not width and not height and not ratio: + raise ValueError("缺少参数...") + if self.width and self.height: + if not width and not height and ratio: + width = int(self.width * ratio) + height = int(self.height * ratio) + self.markImg = self.markImg.resize((width, height), Image.LANCZOS) # type: ignore + self.width, self.height = self.markImg.size + self.draw = ImageDraw.Draw(self.markImg) + return self + + @run_sync + def crop(self, box: Tuple[int, int, int, int]) -> Self: + """ + 裁剪图片 + + 参数: + box: 左上角坐标,右下角坐标 (left, upper, right, lower) + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.crop(box) + self.width, self.height = self.markImg.size + self.draw = ImageDraw.Draw(self.markImg) + return self + + @run_sync + def transparent(self, alpha_ratio: float = 1, n: int = 0) -> Self: + """ + 图片透明化 + + 参数: + alpha_ratio: 透明化程度. + n: 透明化大小内边距. + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.convert("RGBA") + x, y = self.markImg.size + for i in range(n, x - n): + for k in range(n, y - n): + color = self.markImg.getpixel((i, k)) + color = color[:-1] + (int(100 * alpha_ratio),) + self.markImg.putpixel((i, k), color) + self.draw = ImageDraw.Draw(self.markImg) + return self + + def pic2bs4(self) -> str: + """BuildImage 转 base64 + + 返回: + str: base64 + """ + buf = BytesIO() + self.markImg.save(buf, format="PNG") + base64_str = base64.b64encode(buf.getvalue()).decode() + return "base64://" + base64_str + + def pic2bytes(self) -> bytes: + """获取bytes + + 返回: + bytes: bytes + """ + buf = BytesIO() + self.markImg.save(buf, format="PNG") + return buf.getvalue() + + def convert(self, type_: ModeType) -> Self: + """ + 修改图片类型 + + 参数: + type_: ModeType + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.convert(type_) + return self + + @run_sync + def rectangle( + self, + xy: Tuple[int, int, int, int], + fill: Tuple[int, int, int] | None = None, + outline: str | None = None, + width: int = 1, + ) -> Self: + """ + 画框 + + 参数: + xy: 坐标 + fill: 填充颜色. + outline: 轮廓颜色. + width: 线宽. + + 返回: + BuildImage: Self + """ + self.draw.rectangle(xy, fill, outline, width) + return self + + @run_sync + def polygon( + self, + xy: list[Tuple[int, int]], + fill: Tuple[int, int, int] = (0, 0, 0), + outline: int = 1, + ) -> Self: + """ + 画多边形 + + 参数: + xy: 坐标 + fill: 颜色. + outline: 线宽. + + 返回: + BuildImage: Self + """ + self.draw.polygon(xy, fill, outline) + return self + + @run_sync + def line( + self, + xy: Tuple[int, int, int, int], + fill: Tuple[int, int, int] | str = "#D8DEE4", + width: int = 1, + ) -> Self: + """ + 画线 + + 参数: + xy: 坐标 + fill: 填充. + width: 线宽. + + 返回: + BuildImage: Self + """ + self.draw.line(xy, fill, width) + return self + + @run_sync + def circle(self) -> Self: + """ + 图像变圆 + + 返回: + BuildImage: Self + """ + self.markImg.convert("RGBA") + size = self.markImg.size + r2 = min(size[0], size[1]) + if size[0] != size[1]: + self.markImg = self.markImg.resize((r2, r2), Image.LANCZOS) # type: ignore + width = 1 + antialias = 4 + ellipse_box = [0, 0, r2 - 2, r2 - 2] + mask = Image.new( + size=[int(dim * antialias) for dim in self.markImg.size], # type: ignore + mode="L", + color="black", + ) + draw = ImageDraw.Draw(mask) + for offset, fill in (width / -2.0, "black"), (width / 2.0, "white"): + left, top = [(value + offset) * antialias for value in ellipse_box[:2]] + right, bottom = [(value - offset) * antialias for value in ellipse_box[2:]] + draw.ellipse([left, top, right, bottom], fill=fill) + mask = mask.resize(self.markImg.size, Image.LANCZOS) + try: + self.markImg.putalpha(mask) + except ValueError: + pass + return self + + @run_sync + def circle_corner( + self, + radii: int = 30, + point_list: list[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], + ) -> Self: + """ + 矩形四角变圆 + + 参数: + radii: 半径. + point_list: 需要变化的角. + + 返回: + BuildImage: Self + """ + # 画圆(用于分离4个角) + img = self.markImg.convert("RGBA") + alpha = img.split()[-1] + circle = Image.new("L", (radii * 2, radii * 2), 0) + draw = ImageDraw.Draw(circle) + draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 + w, h = img.size + if "lt" in point_list: + alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) + if "rt" in point_list: + alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) + if "lb" in point_list: + alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) + if "rb" in point_list: + alpha.paste( + circle.crop((radii, radii, radii * 2, radii * 2)), + (w - radii, h - radii), + ) + img.putalpha(alpha) + self.markImg = img + self.draw = ImageDraw.Draw(self.markImg) + return self + + @run_sync + def rotate(self, angle: int, expand: bool = False) -> Self: + """ + 旋转图片 + + 参数: + angle: 角度 + expand: 放大图片适应角度. + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.rotate(angle, expand=expand) + return self + + @run_sync + def transpose(self, angle: Literal[0, 1, 2, 3, 4, 5, 6]) -> Self: + """ + 旋转图片(包括边框) + + 参数: + angle: 角度 + + 返回: + BuildImage: Self + """ + self.markImg.transpose(angle) + return self + + @run_sync + def filter(self, filter_: str, aud: int | None = None) -> Self: + """ + 图片变化 + + 参数: + filter_: 变化效果 + aud: 利率. + + 返回: + BuildImage: Self + """ + _type = None + if filter_ == "GaussianBlur": # 高斯模糊 + _type = ImageFilter.GaussianBlur + elif filter_ == "EDGE_ENHANCE": # 锐化效果 + _type = ImageFilter.EDGE_ENHANCE + elif filter_ == "BLUR": # 模糊效果 + _type = ImageFilter.BLUR + elif filter_ == "CONTOUR": # 铅笔滤镜 + _type = ImageFilter.CONTOUR + elif filter_ == "FIND_EDGES": # 边缘检测 + _type = ImageFilter.FIND_EDGES + if _type: + if aud: + self.markImg = self.markImg.filter(_type(aud)) # type: ignore + else: + self.markImg = self.markImg.filter(_type) + self.draw = ImageDraw.Draw(self.markImg) + return self + + def tobytes(self) -> bytes: + """转换为bytes + + 返回: + bytes: bytes + """ + return self.markImg.tobytes() diff --git a/zhenxun/utils/_build_mat.py b/zhenxun/utils/_build_mat.py new file mode 100644 index 00000000..6f30d301 --- /dev/null +++ b/zhenxun/utils/_build_mat.py @@ -0,0 +1,567 @@ +import random +from io import BytesIO +from pathlib import Path +from re import S + +from pydantic import BaseModel +from strenum import StrEnum + +from ._build_image import BuildImage + + +class MatType(StrEnum): + + LINE = "LINE" + """折线图""" + BAR = "BAR" + """柱状图""" + BARH = "BARH" + """横向柱状图""" + + +class BuildMatData(BaseModel): + + mat_type: MatType + """类型""" + data: list[int | float] = [] + """数据""" + x_name: str | None = None + """X轴坐标名称""" + y_name: str | None = None + """Y轴坐标名称""" + x_index: list[str] = [] + """显示轴坐标值""" + y_index: list[int | float] = [] + """数据轴坐标值""" + space: tuple[int, int] = (20, 20) + """坐标值间隔(X, Y)""" + rotate: tuple[int, int] = (0, 0) + """坐标值旋转(X, Y)""" + title: str | None = None + """标题""" + font: str = "msyh.ttf" + """字体""" + font_size: int = 15 + """字体大小""" + display_num: bool = True + """是否在点与柱状图顶部显示数值""" + is_grid: bool = False + """是否添加栅格""" + background_color: tuple[int, int, int] | str = (255, 255, 255) + """背景颜色""" + background: Path | bytes | None = None + """背景图片""" + bar_color: list[str] = ["*"] + """柱状图柱子颜色, 多个时随机, 使用 * 时七色随机""" + padding: tuple[int, int] = (50, 50) + """图表上下左右边距""" + + +class BuildMat: + """ + 针对 折线图/柱状图,基于 BuildImage 编写的 非常难用的 自定义画图工具 + 目前仅支持 正整数 + """ + + class InitGraph(BaseModel): + + mark_image: BuildImage + """BuildImage""" + x_height: int + """横坐标开始高度""" + y_width: int + """纵坐标开始宽度""" + x_point: list[int] + """横坐标坐标""" + y_point: list[int] + """纵坐标坐标""" + graph_height: int + """坐标轴高度""" + + class Config: + arbitrary_types_allowed = True + + def __init__(self, mat_type: MatType) -> None: + self.line_length = 760 + self._x_padding = 0 + self._y_padding = 0 + self.build_data = BuildMatData(mat_type=mat_type) + + @property + def x_name(self) -> str | None: + return self.build_data.x_name + + @x_name.setter + def x_name(self, data: str) -> str | None: + self.build_data.x_name = data + + @property + def y_name(self) -> str | None: + return self.build_data.y_name + + @y_name.setter + def y_name(self, data: str) -> str | None: + self.build_data.y_name = data + + @property + def data(self) -> list[int | float]: + return self.build_data.data + + @data.setter + def data(self, data: list[int | float]): + self._check_value(data, self.build_data.y_index) + self.build_data.data = data + + @property + def x_index(self) -> list[str]: + return self.build_data.x_index + + @x_index.setter + def x_index(self, data: list[str]): + self.build_data.x_index = data + + @property + def y_index(self) -> list[int | float]: + return self.build_data.y_index + + @y_index.setter + def y_index(self, data: list[int | float]): + # self._check_value(self.build_data.data, data) + data.sort() + self.build_data.y_index = data + + @property + def space(self) -> tuple[int, int]: + return self.build_data.space + + @space.setter + def space(self, data: tuple[int, int]): + self.build_data.space = data + + @property + def rotate(self) -> tuple[int, int]: + return self.build_data.rotate + + @rotate.setter + def rotate(self, data: tuple[int, int]): + self.build_data.rotate = data + + @property + def title(self) -> str | None: + return self.build_data.title + + @title.setter + def title(self, data: str): + self.build_data.title = data + + @property + def font(self) -> str: + return self.build_data.font + + @font.setter + def font(self, data: str): + self.build_data.font = data + + # @property + # def font_size(self) -> int: + # return self.build_data.font_size + + # @font_size.setter + # def font_size(self, data: int): + # self.build_data.font_size = data + + @property + def display_num(self) -> bool: + return self.build_data.display_num + + @display_num.setter + def display_num(self, data: bool): + self.build_data.display_num = data + + @property + def is_grid(self) -> bool: + return self.build_data.is_grid + + @is_grid.setter + def is_grid(self, data: bool): + self.build_data.is_grid = data + + @property + def background_color(self) -> tuple[int, int, int] | str: + return self.build_data.background_color + + @background_color.setter + def background_color(self, data: tuple[int, int, int] | str): + self.build_data.background_color = data + + @property + def background(self) -> Path | bytes | None: + return self.build_data.background + + @background.setter + def background(self, data: Path | bytes): + self.build_data.background = data + + @property + def bar_color(self) -> list[str]: + return self.build_data.bar_color + + @bar_color.setter + def bar_color(self, data: list[str]): + self.build_data.bar_color = data + + def _check_value( + self, + y: list[int | float], + y_index: list[int | float] | None = None, + x_index: list[int | float] | None = None, + ): + """检查值合法性 + + 参数: + y: 坐标值 + y_index: y轴坐标值 + x_index: x轴坐标值 + """ + if y_index: + _value = x_index if self.build_data.mat_type == "barh" else y_index + if not isinstance(y[0], str): + __y = [float(t_y) for t_y in y] + _y_index = [float(t_y) for t_y in y_index] + if max(__y) > max(_y_index): + raise ValueError("坐标点的值必须小于y轴坐标的最大值...") + i = -9999999999 + for _y in _y_index: + if _y > i: + i = _y + else: + raise ValueError("y轴坐标值必须有序...") + + async def build(self) -> BuildImage: + """构造图片""" + A = BuildImage(1, 1) + bar_color = self.build_data.bar_color + if "*" in bar_color: + bar_color = [ + "#FF0000", + "#FF7F00", + "#FFFF00", + "#00FF00", + "#00FFFF", + "#0000FF", + "#8B00FF", + ] + init_graph = await self._init_graph() + mark_image = None + if self.build_data.mat_type == MatType.LINE: + mark_image = await self._build_line_graph(init_graph, bar_color) + if self.build_data.mat_type == MatType.BAR: + mark_image = await self._build_bar_graph(init_graph, bar_color) + if self.build_data.mat_type == MatType.BARH: + mark_image = await self._build_barh_graph(init_graph, bar_color) + if mark_image: + padding_width, padding_height = self.build_data.padding + width = mark_image.width + padding_width + height = mark_image.height + padding_height * 2 + if self.build_data.background: + if isinstance(self.build_data.background, bytes): + A = BuildImage( + width, height, background=BytesIO(self.build_data.background) + ) + elif isinstance(self.build_data.background, Path): + A = BuildImage(width, height, background=self.build_data.background) + else: + A = BuildImage(width, height, self.build_data.background_color) + if A: + await A.paste(mark_image, (10, padding_height)) + if self.build_data.title: + font = BuildImage.load_font( + self.build_data.font, self.build_data.font_size + 7 + ) + title_width, title_height = BuildImage.get_text_size( + self.build_data.title, font + ) + pos = ( + int(A.width / 2 - title_width / 2), + int(padding_height / 2 - title_height / 2), + ) + await A.text(pos, self.build_data.title) + if self.build_data.x_name: + font = BuildImage.load_font( + self.build_data.font, self.build_data.font_size + 4 + ) + title_width, title_height = BuildImage.get_text_size( + self.build_data.x_name, font # type: ignore + ) + pos = ( + A.width - title_width - 20, + A.height - int(padding_height / 2 + title_height), + ) + await A.text(pos, self.build_data.x_name) + return A + + async def _init_graph(self) -> InitGraph: + """构造初始化图表 + + 返回: + InitGraph: InitGraph + """ + padding_width = 0 + padding_height = 0 + font = BuildImage.load_font(self.build_data.font, self.build_data.font_size) + x_width_list = [] + y_height_list = [] + for x in self.build_data.x_index: + text_size = BuildImage.get_text_size(x, font) + if text_size[1] > padding_height: + padding_height = text_size[1] + x_width_list.append(text_size) + if not self.build_data.y_index: + """没有指定y_index时,使用data自动生成""" + max_num = max(self.build_data.data) + if max_num < 5: + max_num = 5 + s = int(max_num / 5) + _y_index = [max_num] + for _n in range(4): + max_num -= s + _y_index.append(max_num) + _y_index.sort() + # if len(_y_index) > 1: + # if _y_index[0] == _y_index[-1]: + # _tmp = ["_" for _ in range(len(_y_index) - 1)] + # _tmp.append(str(_y_index[0])) + # _y_index = _tmp + self.build_data.y_index = _y_index # type: ignore + for item in self.build_data.y_index: + text_size = BuildImage.get_text_size(str(item), font) + if text_size[0] > padding_width: + padding_width = text_size[0] + y_height_list.append(text_size) + if self.build_data.mat_type == MatType.BARH: + _tmp = x_width_list + x_width_list = y_height_list + y_height_list = _tmp + old_space = self.build_data.space + width = padding_width * 2 + self.build_data.space[0] * 2 + 20 + height = ( + sum([h[1] + self.build_data.space[1] for h in y_height_list]) + + self.build_data.space[1] * 2 + + 30 + ) + _x_index = self.build_data.x_index + _y_index = self.build_data.y_index + _barh_max_text_width = 0 + if self.build_data.mat_type == MatType.BARH: + """XY轴下标互换""" + _tmp = _y_index + _y_index = _x_index + _x_index = _tmp + """额外增加字体宽度""" + for s in self.build_data.x_index: + s_w, s_h = BuildImage.get_text_size(s, font) + if s_w > _barh_max_text_width: + _barh_max_text_width = s_w + width += _barh_max_text_width + width += self.build_data.space[0] * 2 - old_space[0] * 2 + """X轴重新等均分配""" + x_length = width - padding_width * 2 - _barh_max_text_width + x_space = int((x_length - 20) / (len(_x_index) + 1)) + if x_space < 50: + """加大间距更加美观""" + x_space = 50 + self.build_data.space = (x_space, self.build_data.space[1]) + width += self.build_data.space[0] * (len(_x_index) - 1) + else: + """非横向柱状图时加字体宽度""" + width += sum([w[0] + self.build_data.space[0] for w in x_width_list]) + + A = BuildImage( + width + 5, + (height + 10), + # color=(255, 255, 255), + color=(255, 255, 255, 0), + ) + padding_height += 5 + """高""" + await A.line( + ( + padding_width + 5 + _barh_max_text_width, + padding_height, + padding_width + 5 + _barh_max_text_width, + height - padding_height, + ), + width=2, + ) + """长""" + await A.line( + ( + padding_width + 5 + _barh_max_text_width, + height - padding_height, + width - padding_width + 5, + height - padding_height, + ), + width=2, + ) + x_cur_width = ( + padding_width + _barh_max_text_width + self.build_data.space[0] + 5 + ) + if self.build_data.mat_type != MatType.BARH: + """添加字体宽度""" + x_cur_width += x_width_list[0][0] + x_cur_height = height - y_height_list[0][1] - 5 + # await A.point((x_cur_width, x_cur_height), (0, 0, 0)) + x_point = [] + for i, _x in enumerate(_x_index): + """X轴数值""" + grid_height = x_cur_height + if self.build_data.is_grid: + grid_height = padding_height + await A.line( + ( + x_cur_width, + x_cur_height - 1, + x_cur_width, + grid_height - 5, + ) + ) + x_point.append(x_cur_width - 1) + mid_point = x_cur_width - int(x_width_list[i][0] / 2) + await A.text((mid_point, x_cur_height), str(_x), font=font) + x_cur_width += self.build_data.space[0] + if self.build_data.mat_type != MatType.BARH: + """添加字体宽度""" + x_cur_width += x_width_list[i][0] + y_cur_width = padding_width + _barh_max_text_width + y_cur_height = height - self.build_data.padding[1] - 9 + start_height = y_cur_height + # await A.point((y_cur_width, y_cur_height), (0, 0, 0)) + y_point = [] + for i, _y in enumerate(_y_index): + """Y轴数值""" + grid_width = y_cur_width + if self.build_data.is_grid: + grid_width = width - padding_width + 5 + y_point.append(y_cur_height) + await A.line((y_cur_width + 5, y_cur_height, grid_width + 11, y_cur_height)) + text_width = BuildImage.get_text_size(str(_y), font)[0] + await A.text( + ( + y_cur_width - text_width, + y_cur_height - int(y_height_list[i][1] / 2) - 3, + ), + str(_y), + font=font, + ) + y_cur_height -= y_height_list[i][1] + self.build_data.space[1] + graph_height = 0 + if self.build_data.mat_type == MatType.BARH: + graph_height = ( + x_cur_width + - self.build_data.space[0] + - _barh_max_text_width + - padding_width + - 5 + ) + else: + graph_height = start_height - y_cur_height + 7 + return self.InitGraph( + mark_image=A, + x_height=height - y_height_list[0][1] - 5, + y_width=padding_width + 5 + _barh_max_text_width, + graph_height=graph_height, + x_point=x_point, + y_point=y_point, + ) + + async def _build_line_graph( + self, init_graph: InitGraph, bar_color: list[str] + ) -> BuildImage: + """构建折线图 + + 参数: + init_graph: InitGraph + bar_color: 颜色列表 + + 返回: + BuildImage: 折线图 + """ + font = BuildImage.load_font(self.build_data.font, self.build_data.font_size) + mark_image = init_graph.mark_image + x_height = init_graph.x_height + graph_height = init_graph.graph_height + random_color = random.choice(bar_color) + _black_point = BuildImage(11, 11, color=random_color) + await _black_point.circle() + max_num = max(self.y_index) + point_list = [] + for x_p, y in zip(init_graph.x_point, self.build_data.data): + """折线图标点""" + y_height = int(y / max_num * graph_height) + await mark_image.paste(_black_point, (x_p - 3, x_height - y_height)) + point_list.append((x_p + 1, x_height - y_height + 1)) + for i in range(len(point_list) - 1): + """画线""" + a_x, a_y = point_list[i] + b_x, b_y = point_list[i + 1] + await mark_image.line((a_x, a_y, b_x, b_y), random_color) + if self.build_data.display_num: + """显示数值""" + value = self.build_data.data[i] + text_size = BuildImage.get_text_size(str(value), font) + await mark_image.text( + (a_x - int(text_size[0] / 2), a_y - text_size[1] - 5), + str(value), + font=font, + ) + """最后一个数值显示""" + value = self.build_data.data[-1] + text_size = BuildImage.get_text_size(str(value), font) + await mark_image.text( + ( + point_list[-1][0] - int(text_size[0] / 2), + point_list[-1][1] - text_size[1] - 5, + ), + str(value), + font=font, + ) + return mark_image + + async def _build_bar_graph(self, init_graph: InitGraph, bar_color: list[str]): + """构建折线图 + + 参数: + init_graph: InitGraph + bar_color: 颜色列表 + + 返回: + BuildImage: 折线图 + """ + pass + + async def _build_barh_graph(self, init_graph: InitGraph, bar_color: list[str]): + """构建折线图 + + 参数: + init_graph: InitGraph + bar_color: 颜色列表 + + 返回: + BuildImage: 横向柱状图 + """ + font = BuildImage.load_font(self.build_data.font, self.build_data.font_size) + mark_image = init_graph.mark_image + y_width = init_graph.y_width + graph_height = init_graph.graph_height + random_color = random.choice(bar_color) + max_num = max(self.y_index) + for y_p, y in zip(init_graph.y_point, self.build_data.data): + bar_width = int(y / max_num * graph_height) or 1 + bar = BuildImage(bar_width, 18, random_color) + await mark_image.paste(bar, (y_width + 1, y_p - 9)) + if self.build_data.display_num: + """显示数值""" + await mark_image.text( + (y_width + bar_width + 5, y_p - 12), str(y), font=font + ) + return mark_image diff --git a/zhenxun/utils/_image_template.py b/zhenxun/utils/_image_template.py new file mode 100644 index 00000000..399c054c --- /dev/null +++ b/zhenxun/utils/_image_template.py @@ -0,0 +1,282 @@ +import random +from io import BytesIO +from pathlib import Path +from typing import Any, Callable, Dict + +from fastapi import background +from PIL.ImageFont import FreeTypeFont +from pydantic import BaseModel + +from ._build_image import BuildImage + + +class RowStyle(BaseModel): + + font: FreeTypeFont | str | Path | None = "HYWenHei-85W.ttf" + """字体""" + font_size: int = 20 + """字体大小""" + font_color: str | tuple[int, int, int] = (0, 0, 0) + """字体颜色""" + + class Config: + arbitrary_types_allowed = True + + +class ImageTemplate: + + color_list = ["#C2CEFE", "#FFA94C", "#3FE6A0", "#D1D4F5"] + + @classmethod + async def hl_page( + cls, + head_text: str, + items: Dict[str, str], + row_space: int = 10, + padding: int = 30, + ) -> BuildImage: + """列文档 (如插件帮助) + + 参数: + head_text: 头标签文本 + items: 列内容 + row_space: 列间距. + padding: 间距. + + 返回: + BuildImage: 图片 + """ + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + width, height = BuildImage.get_text_size(head_text, font) + for title, item in items.items(): + title_width, title_height = await cls.__get_text_size(title, font) + it_width, it_height = await cls.__get_text_size(item, font) + width = max([width, title_width, it_width]) + height += title_height + it_height + width = max([width + padding * 2 + 100, 300]) + height = max([height + padding * 2 + 150, 100]) + A = BuildImage(width + padding * 2, height + padding * 2, color="#FAF9FE") + top_head = BuildImage(width, 100, color="#FFFFFF", font_size=40) + await top_head.line((0, 1, width, 1), "#C2CEFE", 2) + await top_head.text((15, 20), head_text, "#9FA3B2", "center") + await top_head.circle_corner() + await A.paste(top_head, (0, 20), "width") + _min_width = top_head.width - 60 + cur_h = top_head.height + 35 + row_space * len(items) + for title, item in items.items(): + title_width, title_height = BuildImage.get_text_size(title, font) + title_background = BuildImage( + title_width + 6, title_height + 10, font=font, color="#C1CDFF" + ) + await title_background.text((3, 5), title) + await title_background.circle_corner(5) + _text_width, _text_height = await cls.__get_text_size(item, font) + _width = max([title_background.width, _text_width, _min_width]) + text_image = await cls.__build_text_image( + item, _width, _text_height, font, color="#FDFCFA" + ) + B = BuildImage(_width + 20, title_height + text_image.height + 40) + await B.paste(title_background, (10, 10)) + await B.paste(text_image, (10, 20 + title_background.height)) + await B.line((0, 0, 0, B.height), random.choice(cls.color_list)) + await A.paste(B, (0, cur_h), "width") + cur_h += B.height + row_space + return A + + @classmethod + async def table_page( + cls, + head_text: str, + tip_text: str | None, + column_name: list[str], + data_list: list[list[str | tuple[Path | BuildImage, int, int]]], + row_space: int = 35, + column_space: int = 30, + padding: int = 5, + text_style: Callable[[str, str], RowStyle] | None = None, + ) -> BuildImage: + """表格页 + + 参数: + head_text: 标题文本. + tip_text: 标题注释. + column_name: 表头列表. + data_list: 数据列表. + row_space: 行间距. + column_space: 列间距. + padding: 文本内间距. + text_style: 文本样式. + + 返回: + BuildImage: 表格图片 + """ + font = BuildImage.load_font(font_size=50) + min_width, _ = BuildImage.get_text_size(head_text, font) + table = await cls.table( + column_name, + data_list, + row_space, + column_space, + padding, + text_style, + ) + await table.circle_corner() + table_bk = BuildImage( + max(table.width, min_width) + 100, table.height + 50, "#EAEDF2" + ) + await table_bk.paste(table, center_type="center") + height = table_bk.height + 200 + background = BuildImage(table_bk.width, height, (255, 255, 255), font_size=50) + await background.paste(table_bk, (0, 200)) + await background.text((0, 50), head_text, "#334762", center_type="width") + if tip_text: + text_image = await BuildImage.build_text_image(tip_text, size=22) + await background.paste(text_image, (0, 110), center_type="width") + return background + + @classmethod + async def table( + cls, + column_name: list[str], + data_list: list[list[str | tuple[Path | BuildImage, int, int]]], + row_space: int = 25, + column_space: int = 10, + padding: int = 5, + text_style: Callable[[str, str], RowStyle] | None = None, + ) -> BuildImage: + """表格 + + 参数: + column_name: 表头列表 + data_list: 数据列表 + row_space: 行间距. + column_space: 列间距. + padding: 文本内间距. + text_style: 文本样式. + min_width: 最低宽度 + + 返回: + BuildImage: 表格图片 + """ + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + column_data = [] + for i in range(len(column_name)): + c = [] + for l in data_list: + if len(l) > i: + c.append(l[i]) + else: + c.append("") + column_data.append(c) + build_data_list = [] + _, base_h = BuildImage.get_text_size("A", font) + for i, column_list in enumerate(column_data): + name_width, _ = BuildImage.get_text_size(column_name[i], font) + _temp = {"width": name_width, "data": column_list} + for s in column_list: + if isinstance(s, tuple): + w = s[1] + else: + w, _ = BuildImage.get_text_size(s, font) + if w > _temp["width"]: + _temp["width"] = w + build_data_list.append(_temp) + column_image_list = [] + for i, data in enumerate(build_data_list): + width = data["width"] + padding * 2 + height = (base_h + row_space) * (len(data["data"]) + 1) + padding * 2 + background = BuildImage(width, height, (255, 255, 255)) + column_name_image = await BuildImage.build_text_image( + column_name[i], font, 12, "#C8CCCF" + ) + await background.paste(column_name_image, (0, 20), center_type="width") + cur_h = column_name_image.height + row_space + 20 + for item in data["data"]: + style = RowStyle(font=font) + if text_style: + style = text_style(column_name[i], item) + if isinstance(item, tuple): + """图片""" + data, width, height = item + if isinstance(data, Path): + image_ = BuildImage(width, height, background=data) + elif isinstance(data, bytes): + image_ = BuildImage(width, height, background=BytesIO(data)) + elif isinstance(data, BuildImage): + image_ = data + await background.paste(image_, (padding, cur_h)) + else: + await background.text( + (padding, cur_h), + item if item is not None else "", + style.font_color, + font=style.font, + font_size=style.font_size, + ) + cur_h += base_h + row_space + column_image_list.append(background) + # height = max([bk.height for bk in column_image_list]) + # width = sum([bk.width for bk in column_image_list]) + return await BuildImage.auto_paste( + column_image_list, len(column_image_list), column_space + ) + + @classmethod + async def __build_text_image( + cls, + text: str, + width: int, + height: int, + font: FreeTypeFont, + font_color: str | tuple[int, int, int] = (0, 0, 0), + color: str | tuple[int, int, int] = (255, 255, 255), + ) -> BuildImage: + """文本转图片 + + 参数: + text: 文本 + width: 宽度 + height: 长度 + font: 字体 + font_color: 文本颜色 + color: 背景颜色 + + 返回: + BuildImage: 文本转图片 + """ + _, h = BuildImage.get_text_size("A", font) + A = BuildImage(width, height, color=color) + cur_h = 0 + for s in text.split("\n"): + text_image = await BuildImage.build_text_image( + s, font, font_color=font_color + ) + await A.paste(text_image, (0, cur_h)) + cur_h += h + return A + + @classmethod + async def __get_text_size( + cls, + text: str, + font: FreeTypeFont, + ) -> tuple[int, int]: + """获取文本所占大小 + + 参数: + text: 文本 + font: 字体 + + 返回: + tuple[int, int]: 宽, 高 + """ + width = 0 + height = 0 + _, h = BuildImage.get_text_size("A", font) + image_list = [] + for s in text.split("\n"): + s = s.strip() or "A" + w, _ = BuildImage.get_text_size(s, font) + width = width if width > w else w + height += h + return width, height diff --git a/utils/browser.py b/zhenxun/utils/browser.py old mode 100755 new mode 100644 similarity index 81% rename from utils/browser.py rename to zhenxun/utils/browser.py index 66530008..c7d89727 --- a/utils/browser.py +++ b/zhenxun/utils/browser.py @@ -1,48 +1,45 @@ -import asyncio -from typing import Optional - -from nonebot import get_driver -from playwright.async_api import Browser, Playwright, async_playwright - -from services.log import logger - -driver = get_driver() - -_playwright: Optional[Playwright] = None -_browser: Optional[Browser] = None - - -@driver.on_startup -async def start_browser(): - global _playwright - global _browser - _playwright = await async_playwright().start() - _browser = await _playwright.chromium.launch() - - -@driver.on_shutdown -async def shutdown_browser(): - if _browser: - await _browser.close() - if _playwright: - await _playwright.stop() # type: ignore - - -def get_browser() -> Browser: - if not _browser: - raise RuntimeError("playwright is not initalized") - return _browser - - -def install(): - """自动安装、更新 Chromium""" - logger.info("正在检查 Chromium 更新") - import sys - - from playwright.__main__ import main - - sys.argv = ["", "install", "chromium"] - try: - main() - except SystemExit: - pass +from nonebot import get_driver +from playwright.async_api import Browser, Playwright, async_playwright + +from zhenxun.services.log import logger + +driver = get_driver() + +_playwright: Playwright | None = None +_browser: Browser | None = None + + +@driver.on_startup +async def start_browser(): + global _playwright + global _browser + _playwright = await async_playwright().start() + _browser = await _playwright.chromium.launch() + + +@driver.on_shutdown +async def shutdown_browser(): + if _browser: + await _browser.close() + if _playwright: + await _playwright.stop() # type: ignore + + +def get_browser() -> Browser: + if not _browser: + raise RuntimeError("playwright is not initalized") + return _browser + + +def install(): + """自动安装、更新 Chromium""" + logger.info("正在检查 Chromium 更新") + import sys + + from playwright.__main__ import main + + sys.argv = ["", "install", "chromium"] + try: + main() + except SystemExit: + pass diff --git a/zhenxun/utils/decorator/shop.py b/zhenxun/utils/decorator/shop.py new file mode 100644 index 00000000..3e46db6a --- /dev/null +++ b/zhenxun/utils/decorator/shop.py @@ -0,0 +1,290 @@ +from typing import Any, Callable, Dict + +from nonebot.adapters.onebot.v11 import Message, MessageSegment +from nonebot.plugin import require +from pydantic import BaseModel + +from zhenxun.models.goods_info import GoodsInfo + + +class Goods(BaseModel): + + before_handle: list[Callable] = [] + after_handle: list[Callable] = [] + price: int + des: str = "" + discount: float + limit_time: int + daily_limit: int + icon: str | None = None + is_passive: bool + func: Callable + kwargs: Dict[str, str] = {} + send_success_msg: bool + max_num_limit: int + + +class ShopRegister(dict): + + def __init__(self, *args, **kwargs): + super(ShopRegister, self).__init__(*args, **kwargs) + self._data: Dict[str, Goods] = {} + self._flag = True + + def before_handle(self, name: str | tuple[str, ...], load_status: bool = True): + """使用前检查方法 + + 参数: + name: 道具名称 + load_status: 加载状态 + """ + + def register_before_handle(name_list: tuple[str, ...], func: Callable): + if load_status: + for name_ in name_list: + if goods := self._data.get(name_): + self._data[name_].before_handle.append(func) + + _name = (name,) if isinstance(name, str) else name + return lambda func: register_before_handle(_name, func) + + def after_handle(self, name: str | tuple[str, ...], load_status: bool = True): + """使用后执行方法 + + 参数: + name: 道具名称 + load_status: 加载状态 + """ + + def register_after_handle(name_list: tuple[str, ...], func: Callable): + if load_status: + for name_ in name_list: + if goods := self._data.get(name_): + self._data[name_].after_handle.append(func) + + _name = (name,) if isinstance(name, str) else name + return lambda func: register_after_handle(_name, func) + + def register( + self, + name: tuple[str, ...], + price: tuple[float, ...], + des: tuple[str, ...], + discount: tuple[float, ...], + limit_time: tuple[int, ...], + load_status: tuple[bool, ...], + daily_limit: tuple[int, ...], + is_passive: tuple[bool, ...], + icon: tuple[str, ...], + send_success_msg: tuple[bool, ...], + max_num_limit: tuple[int, ...], + **kwargs, + ): + """注册商品 + + 参数: + name: 商品名称 + price: 价格 + des: 简介 + discount: 折扣 + limit_time: 售卖限时时间 + load_status: 是否加载 + daily_limit: 每日限购 + is_passive: 是否被动道具 + icon: 图标 + send_success_msg: 成功时发送消息 + max_num_limit: 单次最大使用次数 + """ + + def add_register_item(func: Callable): + if name in self._data.keys(): + raise ValueError("该商品已注册,请替换其他名称!") + for n, p, d, dd, l, s, dl, pa, i, ssm, mnl in zip( + name, + price, + des, + discount, + limit_time, + load_status, + daily_limit, + is_passive, + icon, + send_success_msg, + max_num_limit, + ): + if s: + _temp_kwargs = {} + for key, value in kwargs.items(): + if key.startswith(f"{n}_"): + _temp_kwargs[key.split("_", maxsplit=1)[-1]] = value + else: + _temp_kwargs[key] = value + goods = self._data.get(n) or Goods( + price=p, + des=d, + discount=dd, + limit_time=l, + daily_limit=dl, + is_passive=pa, + func=func, + send_success_msg=ssm, + max_num_limit=mnl, + ) + goods.price = p + goods.des = d + goods.discount = dd + goods.limit_time = l + goods.daily_limit = dl + goods.icon = i + goods.is_passive = pa + goods.func = func + goods.kwargs = _temp_kwargs + goods.send_success_msg = ssm + goods.max_num_limit = mnl + self._data[n] = goods + return func + + return lambda func: add_register_item(func) + + async def load_register(self): + require("shop") + from zhenxun.builtin_plugins.shop._data_source import ShopManage + + # 统一进行注册 + if self._flag: + # 只进行一次注册 + self._flag = False + for name in self._data.keys(): + if goods := self._data.get(name): + uuid = await GoodsInfo.add_goods( + name, + goods.price, + goods.des, + goods.discount, + goods.limit_time, + goods.daily_limit, + goods.is_passive, + goods.icon, + ) + if uuid: + await ShopManage.register_use( + name, + uuid, + goods.func, + goods.send_success_msg, + goods.max_num_limit, + goods.before_handle, + goods.after_handle, + **self._data[name].kwargs, + ) + + def __call__( + self, + name: str | tuple[str, ...], + price: float | tuple[float, ...], + des: str | tuple[str, ...], + discount: float | tuple[float, ...] = 1, + limit_time: int | tuple[int, ...] = 0, + load_status: bool | tuple[bool, ...] = True, + daily_limit: int | tuple[int, ...] = 0, + is_passive: bool | tuple[bool, ...] = False, + icon: str | tuple[str, ...] = "", + send_success_msg: bool | tuple[bool, ...] = True, + max_num_limit: int | tuple[int, ...] = 1, + **kwargs, + ): + """注册商品 + + 参数: + name: 商品名称 + price: 价格 + des: 简介 + discount: 折扣 + limit_time: 售卖限时时间 + load_status: 是否加载 + daily_limit: 每日限购 + is_passive: 是否被动道具 + icon: 图标 + send_success_msg: 成功时发送消息 + max_num_limit: 单次最大使用次数 + """ + _tuple_list = [] + _current_len = -1 + for x in [name, price, des, discount, limit_time, load_status]: + if isinstance(x, tuple): + if _current_len == -1: + _current_len = len(x) + if _current_len != len(x): + raise ValueError( + f"注册商品 {name} 中 name,price,des,discount,limit_time,load_status,daily_limit 数量不符!" + ) + _current_len = _current_len if _current_len > -1 else 1 + _name = self.__get(name, _current_len) + _price = self.__get(price, _current_len) + _discount = self.__get(discount, _current_len) + _limit_time = self.__get(limit_time, _current_len) + _des = self.__get(des, _current_len) + _load_status = self.__get(load_status, _current_len) + _daily_limit = self.__get(daily_limit, _current_len) + _is_passive = self.__get(is_passive, _current_len) + _icon = self.__get(icon, _current_len) + _send_success_msg = self.__get(send_success_msg, _current_len) + _max_num_limit = self.__get(max_num_limit, _current_len) + return self.register( + _name, + _price, + _des, + _discount, + _limit_time, + _load_status, + _daily_limit, + _is_passive, + _icon, + _send_success_msg, + _max_num_limit, + **kwargs, + ) + + def __get(self, value, _current_len): + return ( + value + if isinstance(value, tuple) + else tuple([value for _ in range(_current_len)]) + ) + + def __setitem__(self, key, value): + self._data[key] = value + + def __getitem__(self, key): + return self._data[key] + + def __contains__(self, key): + return key in self._data + + def __str__(self): + return str(self._data) + + def keys(self): + return self._data.keys() + + def values(self): + return self._data.values() + + def items(self): + return self._data.items() + + +class NotMeetUseConditionsException(Exception): + """ + 不满足条件异常类 + """ + + def __init__(self, info: str | MessageSegment | Message | None): + super().__init__(self) + self._info = info + + def get_info(self): + return self._info + + +shop_register = ShopRegister() diff --git a/zhenxun/utils/depends/__init__.py b/zhenxun/utils/depends/__init__.py new file mode 100644 index 00000000..ca6b1ba4 --- /dev/null +++ b/zhenxun/utils/depends/__init__.py @@ -0,0 +1,108 @@ +from typing import Any + +from nonebot.internal.params import Depends +from nonebot.matcher import Matcher +from nonebot.params import Command +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.config import Config +from zhenxun.utils.message import MessageUtils + + +def CheckUg(check_user: bool = True, check_group: bool = True): + """检测群组id和用户id是否存在 + + 参数: + check_user: 检查用户id. + check_group: 检查群组id. + """ + + async def dependency(session: EventSession): + if check_user: + user_id = session.id1 + if not user_id: + await MessageUtils.build_message("用户id为空").finish() + if check_group: + group_id = session.id3 or session.id2 + if not group_id: + await MessageUtils.build_message("群组id为空").finish() + + return Depends(dependency) + + +def OneCommand(): + """ + 获取单个命令Command + """ + + async def dependency( + cmd: tuple[str, ...] = Command(), + ): + return cmd[0] if cmd else None + + return Depends(dependency) + + +def UserName(): + """ + 用户名称 + """ + + async def dependency(user_info: UserInfo = EventUserInfo()): + return ( + user_info.user_displayname or user_info.user_remark or user_info.user_name + ) or "" + + return Depends(dependency) + + +def GetConfig( + module: str | None = None, + config: str = "", + default_value: Any = None, + prompt: str | None = None, +): + """获取配置项 + + 参数: + module: 模块名,为空时默认使用当前插件模块名 + config: 配置项名称 + default_value: 默认值 + prompt: 为空时提示 + """ + + async def dependency(matcher: Matcher): + module_ = module or matcher.plugin_name + if module_: + value = Config.get_config(module_, config, default_value) + if value is None and prompt: + # await matcher.finish(prompt or f"配置项 {config} 未填写!") + await matcher.finish(prompt) + return value + + return Depends(dependency) + + +def CheckConfig( + module: str | None = None, + config: str | list[str] = "", + prompt: str | None = None, +): + """检测配置项在配置文件中是否填写 + + 参数: + module: 模块名,为空时默认使用当前插件模块名 + config: 需要检查的配置项名称 + prompt: 为空时提示 + """ + + async def dependency(matcher: Matcher): + module_ = module or matcher.plugin_name + if module_: + config_list = [config] if isinstance(config, str) else config + for c in config_list: + if Config.get_config(module_, c) is None: + await matcher.finish(prompt or f"配置项 {c} 未填写!") + + return Depends(dependency) diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py new file mode 100644 index 00000000..bf6c5daa --- /dev/null +++ b/zhenxun/utils/enum.py @@ -0,0 +1,101 @@ +from strenum import StrEnum + + +class GoldHandle(StrEnum): + """ + 金币处理 + """ + + BUY = "BUY" + """购买""" + GET = "GET" + """获取""" + PLUGIN = "PLUGIN" + """插件花费""" + + +class PropHandle(StrEnum): + """ + 道具处理 + """ + + BUY = "BUY" + """购买""" + USE = "USE" + """使用""" + + +class PluginType(StrEnum): + """ + 插件类型 + """ + + SUPERUSER = "SUPERUSER" + ADMIN = "ADMIN" + SUPER_AND_ADMIN = "ADMIN_SUPER" + NORMAL = "NORMAL" + HIDDEN = "HIDDEN" + + +class BlockType(StrEnum): + """ + 禁用状态 + """ + + PRIVATE = "PRIVATE" + GROUP = "GROUP" + ALL = "ALL" + + +class PluginLimitType(StrEnum): + """ + 插件限制类型 + """ + + CD = "CD" + COUNT = "COUNT" + BLOCK = "BLOCK" + + +class LimitCheckType(StrEnum): + """ + 插件限制类型 + """ + + PRIVATE = "PRIVATE" + GROUP = "GROUP" + ALL = "ALL" + + +class LimitWatchType(StrEnum): + """ + 插件限制监听对象 + """ + + USER = "USER" + GROUP = "GROUP" + ALL = "ALL" + + +class RequestType(StrEnum): + """ + 请求类型 + """ + + FRIEND = "FRIEND" + GROUP = "GROUP" + + +class RequestHandleType(StrEnum): + """ + 请求处理类型 + """ + + APPROVE = "APPROVE" + """同意""" + REFUSED = "REFUSED" + """拒绝""" + IGNORE = "IGNORE" + """忽略""" + EXPIRE = "EXPIRE" + """过期或失效""" diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py new file mode 100644 index 00000000..dbade4e4 --- /dev/null +++ b/zhenxun/utils/exception.py @@ -0,0 +1,46 @@ +class NotFoundError(Exception): + """ + 未发现 + """ + + pass + + +class GroupInfoNotFound(Exception): + """ + 群组未找到 + """ + + pass + + +class EmptyError(Exception): + """ + 空错误 + """ + + pass + + +class UserAndGroupIsNone(Exception): + """ + 用户和群组为空 + """ + + pass + + +class InsufficientGold(Exception): + """ + 金币不足 + """ + + pass + + +class NotFindSuperuser(Exception): + """ + 未找到超级用户 + """ + + pass diff --git a/utils/http_utils.py b/zhenxun/utils/http_utils.py similarity index 63% rename from utils/http_utils.py rename to zhenxun/utils/http_utils.py index ad542dcc..b751f9cd 100644 --- a/utils/http_utils.py +++ b/zhenxun/utils/http_utils.py @@ -2,27 +2,28 @@ import asyncio from asyncio.exceptions import TimeoutError from contextlib import asynccontextmanager from pathlib import Path -from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union +from typing import Any, AsyncGenerator, Dict, Literal import aiofiles import httpx import rich from httpx import ConnectTimeout, Response -from nonebot.adapters.onebot.v11 import MessageSegment -from playwright.async_api import BrowserContext, Page +from nonebot import require +from nonebot_plugin_alconna import UniMessage +from playwright.async_api import Page from retrying import retry -from services.log import logger -from utils.user_agent import get_user_agent, get_user_agent_str +from zhenxun.configs.config import SYSTEM_PROXY +from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.user_agent import get_user_agent from .browser import get_browser -from .message_builder import image -from .utils import get_local_proxy class AsyncHttpx: - proxy = {"http://": get_local_proxy(), "https://": get_local_proxy()} + proxy = {"http://": SYSTEM_PROXY, "https://": SYSTEM_PROXY} @classmethod @retry(stop_max_attempt_number=3) @@ -30,32 +31,31 @@ class AsyncHttpx: cls, url: str, *, - params: Optional[Dict[str, Any]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, + params: Dict[str, Any] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, verify: bool = True, use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, + proxy: Dict[str, str] | None = None, + timeout: int = 30, **kwargs, ) -> Response: - """ - 说明: - Get + """Get + 参数: - :param url: url - :param params: params - :param headers: 请求头 - :param cookies: cookies - :param verify: verify - :param use_proxy: 使用默认代理 - :param proxy: 指定代理 - :param timeout: 超时时间 + url: url + params: params + headers: 请求头 + cookies: cookies + verify: verify + use_proxy: 使用默认代理 + proxy: 指定代理 + timeout: 超时时间 """ if not headers: headers = get_user_agent() - proxy_ = proxy if proxy else cls.proxy if use_proxy else None - async with httpx.AsyncClient(proxies=proxy_, verify=verify) as client: + _proxy = proxy if proxy else cls.proxy if use_proxy else None + async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore return await client.get( url, params=params, @@ -70,39 +70,39 @@ class AsyncHttpx: cls, url: str, *, - data: Optional[Dict[str, str]] = None, + data: Dict[str, Any] | None = None, content: Any = None, files: Any = None, verify: bool = True, use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - json: Optional[Dict[str, Any]] = None, - params: Optional[Dict[str, str]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, + proxy: Dict[str, str] | None = None, + json: Dict[str, Any] | None = None, + params: Dict[str, str] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, + timeout: int = 30, **kwargs, ) -> Response: """ 说明: Post 参数: - :param url: url - :param data: data - :param content: content - :param files: files - :param use_proxy: 是否默认代理 - :param proxy: 指定代理 - :param json: json - :param params: params - :param headers: 请求头 - :param cookies: cookies - :param timeout: 超时时间 + url: url + data: data + content: content + files: files + use_proxy: 是否默认代理 + proxy: 指定代理 + json: json + params: params + headers: 请求头 + cookies: cookies + timeout: 超时时间 """ if not headers: headers = get_user_agent() - proxy_ = proxy if proxy else cls.proxy if use_proxy else None - async with httpx.AsyncClient(proxies=proxy_, verify=verify) as client: + _proxy = proxy if proxy else cls.proxy if use_proxy else None + async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore return await client.post( url, content=content, @@ -120,32 +120,31 @@ class AsyncHttpx: async def download_file( cls, url: str, - path: Union[str, Path], + path: str | Path, *, - params: Optional[Dict[str, str]] = None, + params: Dict[str, str] | None = None, verify: bool = True, use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, + proxy: Dict[str, str] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, + timeout: int = 30, stream: bool = False, **kwargs, ) -> bool: - """ - 说明: - 下载文件 + """下载文件 + 参数: - :param url: url - :param path: 存储路径 - :param params: params - :param verify: verify - :param use_proxy: 使用代理 - :param proxy: 指定代理 - :param headers: 请求头 - :param cookies: cookies - :param timeout: 超时时间 - :param stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件) + url: url + path: 存储路径 + params: params + verify: verify + use_proxy: 使用代理 + proxy: 指定代理 + headers: 请求头 + cookies: cookies + timeout: 超时时间 + stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件) """ if isinstance(path, str): path = Path(path) @@ -175,10 +174,10 @@ class AsyncHttpx: else: if not headers: headers = get_user_agent() - proxy_ = proxy if proxy else cls.proxy if use_proxy else None + _proxy = proxy if proxy else cls.proxy if use_proxy else None try: async with httpx.AsyncClient( - proxies=proxy_, verify=verify + proxies=_proxy, verify=verify # type: ignore ) as client: async with client.stream( "GET", @@ -194,12 +193,12 @@ class AsyncHttpx: ) async with aiofiles.open(path, "wb") as wf: total = int(response.headers["Content-Length"]) - with rich.progress.Progress( - rich.progress.TextColumn(path.name), - "[progress.percentage]{task.percentage:>3.0f}%", - rich.progress.BarColumn(bar_width=None), - rich.progress.DownloadColumn(), - rich.progress.TransferSpeedColumn(), + with rich.progress.Progress( # type: ignore + rich.progress.TextColumn(path.name), # type: ignore + "[progress.percentage]{task.percentage:>3.0f}%", # type: ignore + rich.progress.BarColumn(bar_width=None), # type: ignore + rich.progress.DownloadColumn(), # type: ignore + rich.progress.TransferSpeedColumn(), # type: ignore ) as progress: download_task = progress.add_task( "Download", total=total @@ -211,7 +210,9 @@ class AsyncHttpx: download_task, completed=response.num_bytes_downloaded, ) - logger.info(f"下载 {url} 成功.. Path:{path.absolute()}") + logger.info( + f"下载 {url} 成功.. Path:{path.absolute()}" + ) return True except (TimeoutError, ConnectTimeout): pass @@ -224,31 +225,30 @@ class AsyncHttpx: @classmethod async def gather_download_file( cls, - url_list: List[str], - path_list: List[Union[str, Path]], + url_list: list[str], + path_list: list[str | Path], *, - limit_async_number: Optional[int] = None, - params: Optional[Dict[str, str]] = None, + limit_async_number: int | None = None, + params: Dict[str, str] | None = None, use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, + proxy: Dict[str, str] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, + timeout: int = 30, **kwargs, - ) -> List[bool]: - """ - 说明: - 分组同时下载文件 + ) -> list[bool]: + """分组同时下载文件 + 参数: - :param url_list: url列表 - :param path_list: 存储路径列表 - :param limit_async_number: 限制同时请求数量 - :param params: params - :param use_proxy: 使用代理 - :param proxy: 指定代理 - :param headers: 请求头 - :param cookies: cookies - :param timeout: 超时时间 + url_list: url列表 + path_list: 存储路径列表 + limit_async_number: 限制同时请求数量 + params: params + use_proxy: 使用代理 + proxy: 指定代理 + headers: 请求头 + cookies: cookies + timeout: 超时时间 """ if n := len(url_list) != len(path_list): raise UrlPathNumberNotEqual( @@ -300,11 +300,10 @@ class AsyncPlaywright: @classmethod @asynccontextmanager async def new_page(cls, **kwargs) -> AsyncGenerator[Page, None]: - """ - 说明: - 获取一个新页面 + """获取一个新页面 + 参数: - :param user_agent: 请求头 + user_agent: 请求头 """ browser = get_browser() ctx = await browser.new_context(**kwargs) @@ -319,31 +318,30 @@ class AsyncPlaywright: async def screenshot( cls, url: str, - path: Union[Path, str], - element: Union[str, List[str]], + path: Path | str, + element: str | list[str], *, - wait_time: Optional[int] = None, - viewport_size: Optional[Dict[str, int]] = None, - wait_until: Optional[ - Literal["domcontentloaded", "load", "networkidle"] - ] = "networkidle", - timeout: Optional[float] = None, - type_: Optional[Literal["jpeg", "png"]] = None, - user_agent: Optional[str] = None, + wait_time: int | None = None, + viewport_size: Dict[str, int] | None = None, + wait_until: ( + Literal["domcontentloaded", "load", "networkidle"] | None + ) = "networkidle", + timeout: float | None = None, + type_: Literal["jpeg", "png"] | None = None, + user_agent: str | None = None, **kwargs, - ) -> Optional[MessageSegment]: - """ - 说明: - 截图,该方法仅用于简单快捷截图,复杂截图请操作 page + ) -> UniMessage | None: + """截图,该方法仅用于简单快捷截图,复杂截图请操作 page + 参数: - :param url: 网址 - :param path: 存储路径 - :param element: 元素选择 - :param wait_time: 等待截取超时时间 - :param viewport_size: 窗口大小 - :param wait_until: 等待类型 - :param timeout: 超时限制 - :param type_: 保存类型 + url: 网址 + path: 存储路径 + element: 元素选择 + wait_time: 等待截取超时时间 + viewport_size: 窗口大小 + wait_until: 等待类型 + timeout: 超时限制 + type_: 保存类型 """ if viewport_size is None: viewport_size = dict(width=2560, height=1080) @@ -367,7 +365,7 @@ class AsyncPlaywright: card = await card.wait_for_selector(e, timeout=wait_time) if card: await card.screenshot(path=path, timeout=timeout, type=type_) - return image(path) + return MessageUtils.build_message(path) return None diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py new file mode 100644 index 00000000..c193a565 --- /dev/null +++ b/zhenxun/utils/image_utils.py @@ -0,0 +1,422 @@ +import os +import random +import re +from io import BytesIO +from pathlib import Path +from typing import Awaitable, Callable + +import cv2 +import imagehash +from imagehash import ImageHash +from nonebot.utils import is_coroutine_callable +from PIL import Image + +from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +from ._build_image import BuildImage, ColorAlias +from ._build_mat import BuildMat, MatType +from ._image_template import ImageTemplate, RowStyle + +# TODO: text2image 长度错误 + + +async def text2image( + text: str, + auto_parse: bool = True, + font_size: int = 20, + color: str | tuple[int, int, int] = (255, 255, 255), + font: str = "HYWenHei-85W.ttf", + font_color: str | tuple[int, int, int] = (0, 0, 0), + padding: int | tuple[int, int, int, int] = 0, + _add_height: float = 0, +) -> BuildImage: + """解析文本并转为图片 + 使用标签 + + 可选配置项 + font: str -> 特殊文本字体 + fs / font_size: int -> 特殊文本大小 + fc / font_color: Union[str, Tuple[int, int, int]] -> 特殊文本颜色 + 示例 + 在不在,HibiKi小姐, + 你最近还好吗,我非常想你,这段时间我非常不好过, + 抽卡抽不到金色,这让我很痛苦 + 参数: + text: 文本 + auto_parse: 是否自动解析,否则原样发送 + font_size: 普通字体大小 + color: 背景颜色 + font: 普通字体 + font_color: 普通字体颜色 + padding: 文本外边距,元组类型时为 (上,左,下,右) + _add_height: 由于get_size无法返回正确的高度,采用手动方式额外添加高度 + """ + if not text: + raise ValueError("文本转图片 text 不能为空...") + pw = ph = top_padding = left_padding = 0 + if padding: + if isinstance(padding, int): + pw = padding * 2 + ph = padding * 2 + top_padding = left_padding = padding + elif isinstance(padding, tuple): + pw = padding[0] + padding[2] + ph = padding[1] + padding[3] + top_padding = padding[0] + left_padding = padding[1] + _font = BuildImage.load_font(font, font_size) + if auto_parse and re.search(r"(.*)", text): + _data = [] + new_text = "" + placeholder_index = 0 + for s in text.split(""): + r = re.search(r"(.*)", s) + if r: + start, end = r.span() + if start != 0 and (t := s[:start]): + new_text += t + _data.append( + [ + (start, end), + f"[placeholder_{placeholder_index}]", + r.group(1).strip(), + r.group(2), + ] + ) + new_text += f"[placeholder_{placeholder_index}]" + placeholder_index += 1 + new_text += text.split("")[-1] + image_list = [] + current_placeholder_index = 0 + # 切分换行,每行为单张图片 + for s in new_text.split("\n"): + _tmp_text = s + img_width = 0 + img_height = BuildImage.get_text_size("正", _font)[1] + _tmp_index = current_placeholder_index + for _ in range(s.count("[placeholder_")): + placeholder = _data[_tmp_index] + if "font_size" in placeholder[2]: + r = re.search(r"font_size=['\"]?(\d+)", placeholder[2]) + if r: + w, h = BuildImage.get_text_size( + placeholder[3], font, int(r.group(1)) + ) + img_height = img_height if img_height > h else h + img_width += w + else: + img_width += BuildImage.get_text_size(placeholder[3], _font)[0] + _tmp_text = _tmp_text.replace(f"[placeholder_{_tmp_index}]", "") + _tmp_index += 1 + img_width += BuildImage.get_text_size(_tmp_text, _font)[0] + # 开始画图 + A = BuildImage( + img_width, img_height, color=color, font=font, font_size=font_size + ) + basic_font_h = A.getsize("正")[1] + current_width = 0 + # 遍历占位符 + for _ in range(s.count("[placeholder_")): + if not s.startswith(f"[placeholder_{current_placeholder_index}]"): + slice_ = s.split(f"[placeholder_{current_placeholder_index}]") + await A.text( + (current_width, A.height - basic_font_h - 1), + slice_[0], + font_color, + ) + current_width += A.getsize(slice_[0])[0] + placeholder = _data[current_placeholder_index] + # 解析配置 + _font = font + _font_size = font_size + _font_color = font_color + for e in placeholder[2].split(): + if e.startswith("font="): + _font = e.split("=")[-1] + if e.startswith("font_size=") or e.startswith("fs="): + _font_size = int(e.split("=")[-1]) + if _font_size > 1000: + _font_size = 1000 + if _font_size < 1: + _font_size = 1 + if e.startswith("font_color") or e.startswith("fc="): + _font_color = e.split("=")[-1] + text_img = await BuildImage.build_text_image( + placeholder[3], font=_font, size=_font_size, font_color=_font_color + ) + _img_h = ( + int(A.height / 2 - text_img.height / 2) + if new_text == "[placeholder_0]" + else A.height - text_img.height + ) + await A.paste(text_img, (current_width, _img_h - 1)) + current_width += text_img.width + s = s[ + s.index(f"[placeholder_{current_placeholder_index}]") + + len(f"[placeholder_{current_placeholder_index}]") : + ] + current_placeholder_index += 1 + if s: + slice_ = s.split(f"[placeholder_{current_placeholder_index}]") + await A.text((current_width, A.height - basic_font_h), slice_[0]) + current_width += A.getsize(slice_[0])[0] + await A.crop((0, 0, current_width, A.height)) + # A.show() + image_list.append(A) + height = 0 + width = 0 + for img in image_list: + height += img.h + width = width if width > img.w else img.w + width += pw + height += ph + A = BuildImage(width + left_padding, height + top_padding, color=color) + current_height = top_padding + for img in image_list: + await A.paste(img, (left_padding, current_height)) + current_height += img.h + else: + width = 0 + height = 0 + _, h = BuildImage.get_text_size("正", _font) + line_height = int(font_size / 3) + image_list = [] + for s in text.split("\n"): + w, _ = BuildImage.get_text_size(s.strip() or "正", _font) + height += h + line_height + width = width if width > w else w + image_list.append( + await BuildImage.build_text_image( + s.strip(), font, font_size, font_color + ) + ) + width += pw + height += ph + A = BuildImage( + width + left_padding, + height + top_padding + 2, + color=color, + ) + cur_h = ph + for img in image_list: + await A.paste(img, (pw, cur_h)) + cur_h += img.height + line_height + return A + + +def group_image(image_list: list[BuildImage]) -> tuple[list[list[BuildImage]], int]: + """ + 说明: + 根据图片大小进行分组 + 参数: + image_list: 排序图片列表 + """ + image_list.sort(key=lambda x: x.height, reverse=True) + max_image = max(image_list, key=lambda x: x.height) + + image_list.remove(max_image) + max_h = max_image.height + total_w = 0 + + # 图片分组 + image_group = [[max_image]] + is_use = [] + surplus_list = image_list[:] + + for image in image_list: + if image.uid not in is_use: + group = [image] + is_use.append(image.uid) + curr_h = image.height + while True: + surplus_list = [x for x in surplus_list if x.uid not in is_use] + for tmp in surplus_list: + temp_h = curr_h + tmp.height + 10 + if temp_h < max_h or abs(max_h - temp_h) < 100: + curr_h += tmp.height + 15 + is_use.append(tmp.uid) + group.append(tmp) + break + else: + break + total_w += max([x.width for x in group]) + 15 + image_group.append(group) + while surplus_list: + surplus_list = [x for x in surplus_list if x.uid not in is_use] + if not surplus_list: + break + surplus_list.sort(key=lambda x: x.height, reverse=True) + for img in surplus_list: + if img.uid not in is_use: + _w = 0 + index = -1 + for i, ig in enumerate(image_group): + if s := sum([x.height for x in ig]) > _w: + _w = s + index = i + if index != -1: + image_group[index].append(img) + is_use.append(img.uid) + + max_h = 0 + max_w = 0 + for ig in image_group: + if (_h := sum([x.height + 15 for x in ig])) > max_h: + max_h = _h + max_w += max([x.width for x in ig]) + 30 + is_use.clear() + while abs(max_h - max_w) > 200 and len(image_group) - 1 >= len(image_group[-1]): + for img in image_group[-1]: + _min_h = 999999 + _min_index = -1 + for i, ig in enumerate(image_group): + # if i not in is_use and (_h := sum([x.h for x in ig]) + img.h) > _min_h: + if (_h := sum([x.height for x in ig]) + img.height) < _min_h: + _min_h = _h + _min_index = i + is_use.append(_min_index) + image_group[_min_index].append(img) + max_w -= max([x.width for x in image_group[-1]]) - 30 + image_group.pop(-1) + max_h = max([sum([x.height + 15 for x in ig]) for ig in image_group]) + return image_group, max(max_h + 250, max_w + 70) + + +async def build_sort_image( + image_group: list[list[BuildImage]], + h: int | None = None, + padding_top: int = 200, + color: ColorAlias = ( + 255, + 255, + 255, + ), + background_path: Path | None = None, + background_handle: Callable[[BuildImage], Awaitable] | None = None, +) -> BuildImage: + """ + 说明: + 对group_image的图片进行组装 + 参数: + image_group: 分组图片列表 + h: max(宽,高),一般为group_image的返回值,有值时,图片必定为正方形 + padding_top: 图像列表与最顶层间距 + color: 背景颜色 + background_path: 背景图片文件夹路径(随机) + background_handle: 背景图额外操作 + """ + bk_file = None + if background_path: + random_bk = os.listdir(background_path) + if random_bk: + bk_file = random.choice(random_bk) + image_w = 0 + image_h = 0 + if not h: + for ig in image_group: + _w = max([x.width + 30 for x in ig]) + image_w += _w + 30 + _h = sum([x.height + 10 for x in ig]) + if _h > image_h: + image_h = _h + image_h += padding_top + else: + image_w = h + image_h = h + A = BuildImage( + image_w, + image_h, + font_size=24, + font="CJGaoDeGuo.otf", + color=color, + background=(background_path / bk_file) if background_path and bk_file else None, + ) + if background_handle: + if is_coroutine_callable(background_handle): + await background_handle(A) + else: + background_handle(A) + curr_w = 50 + for ig in image_group: + curr_h = padding_top - 20 + for img in ig: + await A.paste(img, (curr_w, curr_h)) + curr_h += img.height + 10 + curr_w += max([x.width for x in ig]) + 30 + return A + + +def compressed_image( + in_file: str | Path, + out_file: str | Path | None = None, + ratio: float = 0.9, +): + """压缩图片 + + 参数: + in_file: 被压缩的文件路径 + out_file: 压缩后输出的文件路径 + ratio: 压缩率,宽高 * 压缩率 + """ + in_file = IMAGE_PATH / in_file if isinstance(in_file, str) else in_file + if out_file: + out_file = IMAGE_PATH / out_file if isinstance(out_file, str) else out_file + else: + out_file = in_file + h, w, d = cv2.imread(str(in_file.absolute())).shape + img = cv2.resize( + cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio)) + ) + cv2.imwrite(str(out_file.absolute()), img) + + +def get_img_hash(image_file: str | Path) -> str: + """获取图片的hash值 + + 参数: + image_file: 图片文件路径 + + 返回: + str: 哈希值 + """ + hash_value = "" + try: + with open(image_file, "rb") as fp: + hash_value = imagehash.average_hash(Image.open(fp)) + except Exception as e: + logger.warning(f"获取图片Hash出错", "禁言检测", e=e) + return str(hash_value) + + +async def get_download_image_hash(url: str, mark: str) -> str: + """下载图片获取哈希值 + + 参数: + url: 图片url + mark: 随机标志符 + + 返回: + str: 哈希值 + """ + try: + if await AsyncHttpx.download_file( + url, TEMP_PATH / f"compare_download_{mark}_img.jpg" + ): + img_hash = get_img_hash(TEMP_PATH / f"compare_download_{mark}_img.jpg") + return str(img_hash) + except Exception as e: + logger.warning(f"下载读取图片Hash出错", e=e) + return "" + + +def pic2bytes(image) -> bytes: + """获取bytes + + 返回: + bytes: bytes + """ + buf = BytesIO() + image.save(buf, format="PNG") + return buf.getvalue() diff --git a/zhenxun/utils/message.py b/zhenxun/utils/message.py new file mode 100644 index 00000000..54e7f908 --- /dev/null +++ b/zhenxun/utils/message.py @@ -0,0 +1,136 @@ +from io import BytesIO +from pathlib import Path + +import nonebot +from nonebot.adapters.onebot.v11 import Message, MessageSegment +from nonebot_plugin_alconna import At, Image, Text, UniMessage + +from zhenxun.configs.config import NICKNAME +from zhenxun.services.log import logger +from zhenxun.utils._build_image import BuildImage + +driver = nonebot.get_driver() + +MESSAGE_TYPE = ( + str | int | float | Path | bytes | BytesIO | BuildImage | At | Image | Text +) + + +class MessageUtils: + + @classmethod + def __build_message(cls, msg_list: list[MESSAGE_TYPE]) -> list[Text | Image]: + """构造消息 + + 参数: + msg_list: 消息列表 + + 返回: + list[Text | Text]: 构造完成的消息列表 + """ + is_bytes = False + try: + is_bytes = driver.config.image_to_bytes in ["True", "true"] + except AttributeError: + pass + message_list = [] + for msg in msg_list: + if isinstance(msg, (Image, Text, At)): + message_list.append(msg) + elif isinstance(msg, (str, int, float)): + message_list.append(Text(str(msg))) + elif isinstance(msg, Path): + if msg.exists(): + if is_bytes: + image = BuildImage.open(msg) + message_list.append(Image(raw=image.pic2bytes())) + else: + message_list.append(Image(path=msg)) + else: + logger.warning(f"图片路径不存在: {msg}") + elif isinstance(msg, bytes): + message_list.append(Image(raw=msg)) + elif isinstance(msg, BytesIO): + message_list.append(Image(raw=msg)) + elif isinstance(msg, BuildImage): + message_list.append(Image(raw=msg.pic2bytes())) + return message_list + + @classmethod + def build_message( + cls, msg_list: MESSAGE_TYPE | list[MESSAGE_TYPE | list[MESSAGE_TYPE]] + ) -> UniMessage: + """构造消息 + + 参数: + msg_list: 消息列表 + + 返回: + UniMessage: 构造完成的消息列表 + """ + message_list = [] + if not isinstance(msg_list, list): + msg_list = [msg_list] + for m in msg_list: + _data = m if isinstance(m, list) else [m] + message_list += cls.__build_message(_data) # type: ignore + return UniMessage(message_list) + + @classmethod + def custom_forward_msg( + cls, + msg_list: list[str | Message], + uin: str, + name: str = f"这里是{NICKNAME}", + ) -> list[dict]: + """生成自定义合并消息 + + 参数: + msg_list: 消息列表 + uin: 发送者 QQ + name: 自定义名称 + + 返回: + list[dict]: 转发消息 + """ + mes_list = [] + for _message in msg_list: + data = { + "type": "node", + "data": { + "name": name, + "uin": f"{uin}", + "content": _message, + }, + } + mes_list.append(data) + return mes_list + + @classmethod + def template2forward(cls, msg_list: list[UniMessage], uni: str) -> list[dict]: + """模板转转发消息 + + 参数: + msg_list: 消息列表 + uni: 发送者qq + + 返回: + list[dict]: 转发消息 + """ + forward_data = [] + for r_list in msg_list: + s = "" + if isinstance(r_list, (UniMessage, list)): + for r in r_list: + if isinstance(r, Text): + s += str(r) + elif isinstance(r, Image): + if v := r.url or r.path: + s += MessageSegment.image(v) + elif isinstance(r_list, Image): + if v := r_list.url or r_list.path: + s = MessageSegment.image(v) + else: + s = str(r_list) + forward_data.append(s) + return cls.custom_forward_msg(forward_data, uni) diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py new file mode 100644 index 00000000..07213280 --- /dev/null +++ b/zhenxun/utils/platform.py @@ -0,0 +1,631 @@ +import random +from typing import Awaitable, Callable, Literal, Set + +import httpx +import nonebot +from nonebot.adapters import Bot +from nonebot.adapters.discord import Bot as DiscordBot +from nonebot.adapters.dodo import Bot as DodoBot +from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot.utils import is_coroutine_callable +from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage +from pydantic import BaseModel + +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.exception import NotFindSuperuser +from zhenxun.utils.message import MessageUtils + + +class UserData(BaseModel): + + name: str + """昵称""" + card: str | None = None + """名片/备注""" + user_id: str + """用户id""" + group_id: str | None = None + """群组id""" + role: str | None = None + """角色""" + avatar_url: str | None = None + """头像url""" + join_time: int | None = None + """加入时间""" + + +class PlatformUtils: + + @classmethod + async def ban_user(cls, bot: Bot, user_id: str, group_id: str, duration: int): + """禁言 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id + duration: 禁言时长(分钟) + """ + if isinstance(bot, v11Bot): + await bot.set_group_ban( + group_id=int(group_id), + user_id=int(user_id), + duration=duration * 60, + ) + + @classmethod + async def send_superuser( + cls, + bot: Bot, + message: UniMessage, + superuser_id: str | None = None, + ) -> Receipt | None: + """发送消息给超级用户 + + 参数: + bot: Bot + message: 消息 + superuser_id: 指定超级用户id. + + 异常: + NotFindSuperuser: 未找到超级用户id + + 返回: + Receipt | None: Receipt + """ + if not superuser_id: + platform = cls.get_platform(bot) + platform_superusers = bot.config.PLATFORM_SUPERUSERS.get(platform) or [] + if not platform_superusers: + raise NotFindSuperuser() + superuser_id = random.choice(platform_superusers) + return await cls.send_message(bot, superuser_id, None, message) + + @classmethod + async def get_group_member_list(cls, bot: Bot, group_id: str) -> list[UserData]: + """获取群组/频道成员列表 + + 参数: + bot: Bot + group_id: 群组/频道id + + 返回: + list[UserData]: 用户数据列表 + """ + if isinstance(bot, v11Bot): + if member_list := await bot.get_group_member_list(group_id=int(group_id)): + return [ + UserData( + name=user["nickname"], + card=user["card"], + user_id=user["user_id"], + group_id=user["group_id"], + role=user["role"], + join_time=user["join_time"], + ) + for user in member_list + ] + if isinstance(bot, v12Bot): + if member_list := await bot.get_group_member_list(group_id=group_id): + return [ + UserData( + name=user["user_name"], + card=user["user_displayname"], + user_id=user["user_id"], + group_id=group_id, + ) + for user in member_list + ] + if isinstance(bot, DodoBot): + if result_data := await bot.get_member_list( + island_source_id=group_id, page_size=100, max_id=0 + ): + max_id = result_data.max_id + result_list = result_data.list + data_list = [] + while max_id == 100: + result_data = await bot.get_member_list( + island_source_id=group_id, page_size=100, max_id=0 + ) + result_list += result_data.list + max_id = result_data.max_id + for user in result_list: + data_list.append( + UserData( + name=user.nick_name, + card=user.personal_nick_name, + avatar_url=user.avatar_url, + user_id=user.dodo_source_id, + group_id=user.island_source_id, + join_time=int(user.join_time.timestamp()), + ) + ) + return data_list + if isinstance(bot, KaiheilaBot): + if result_data := await bot.guild_userList(guild_id=group_id): + if result_data.users: + data_list = [] + for user in result_data.users: + second = None + if user.joined_at: + second = int(user.joined_at / 1000) + data_list.append( + UserData( + name=user.nickname or "", + avatar_url=user.avatar, + user_id=user.id_, # type: ignore + group_id=group_id, + join_time=second, + ) + ) + return data_list + if isinstance(bot, DiscordBot): + # TODO: discord获取用户 + pass + return [] + + @classmethod + async def get_user( + cls, bot: Bot, user_id: str, group_id: str | None = None + ) -> UserData | None: + """获取用户信息 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组/频道id. + + 返回: + UserData | None: 用户数据 + """ + if isinstance(bot, v11Bot): + if group_id: + if user := await bot.get_group_member_info( + group_id=int(group_id), user_id=int(user_id) + ): + return UserData( + name=user["nickname"], + card=user["card"], + user_id=user["user_id"], + group_id=user["group_id"], + role=user["role"], + join_time=user["join_time"], + ) + else: + if friend_list := await bot.get_friend_list(): + for f in friend_list: + if f["user_id"] == int(user_id): + return UserData( + name=f["nickname"], + card=f["remark"], + user_id=f["user_id"], + ) + if isinstance(bot, v12Bot): + if group_id: + if user := await bot.get_group_member_info( + group_id=group_id, user_id=user_id + ): + return UserData( + name=user["user_name"], + card=user["user_displayname"], + user_id=user["user_id"], + group_id=group_id, + ) + else: + if friend_list := await bot.get_friend_list(): + for f in friend_list: + if f["user_id"] == int(user_id): + return UserData( + name=f["user_name"], + card=f["user_remark"], + user_id=f["user_id"], + ) + if isinstance(bot, DodoBot): + if group_id: + if user := await bot.get_member_info( + island_source_id=group_id, dodo_source_id=user_id + ): + return UserData( + name=user.nick_name, + card=user.personal_nick_name, + avatar_url=user.avatar_url, + user_id=user.dodo_source_id, + group_id=user.island_source_id, + join_time=int(user.join_time.timestamp()), + ) + else: + # TODO: DoDo个人数据 + pass + if isinstance(bot, KaiheilaBot): + if group_id: + if user := await bot.user_view(guild_id=group_id, user_id=user_id): + second = None + if user.joined_at: + second = int(user.joined_at / 1000) + return UserData( + name=user.nickname or "", + avatar_url=user.avatar, + user_id=user_id, + group_id=group_id, + join_time=second, + ) + else: + # TODO: kaiheila用户详情 + pass + if isinstance(bot, DiscordBot): + # TODO: discord获取用户 + pass + return None + + @classmethod + async def get_user_avatar(cls, user_id: str, platform: str) -> bytes | None: + """快捷获取用户头像 + + 参数: + user_id: 用户id + platform: 平台 + """ + if platform == "qq": + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=160" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error( + "获取用户头像错误", + "Util", + target=user_id, + platform=platform, + ) + else: + pass + return None + + @classmethod + async def get_group_avatar(cls, gid: str, platform: str) -> bytes | None: + """快捷获取用群头像 + + 参数: + gid: 群组id + platform: 平台 + """ + if platform == "qq": + url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error( + "获取群头像错误", "Util", target=gid, platform=platform + ) + else: + pass + return None + + @classmethod + async def send_message( + cls, + bot: Bot, + user_id: str | None, + group_id: str | None, + message: str | UniMessage, + ) -> Receipt | None: + """发送消息 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id或频道id + message: 消息文本 + + 返回: + Receipt | None: 是否发送成功 + """ + if target := cls.get_target(bot, user_id, group_id): + send_message = ( + MessageUtils.build_message(message) + if isinstance(message, str) + else message + ) + return await send_message.send(target=target, bot=bot) + return None + + @classmethod + async def update_group(cls, bot: Bot) -> int: + """更新群组信息 + + 参数: + bot: Bot + + 返回: + int: 更新个数 + """ + create_list = [] + group_list, platform = await cls.get_group_list(bot) + if group_list: + exists_group_list = await GroupConsole.all().values_list( + "group_id", "channel_id" + ) + for group in group_list: + group.platform = platform + if (group.group_id, group.channel_id) not in exists_group_list: + create_list.append(group) + logger.debug( + "群聊信息更新成功", + "更新群信息", + target=f"{group.group_id}:{group.channel_id}", + ) + if create_list: + await GroupConsole.bulk_create(create_list, 10) + return len(create_list) + + @classmethod + def get_platform(cls, bot: Bot) -> str | None: + """获取平台 + + 参数: + bot: Bot + + 返回: + str | None: 平台 + """ + if isinstance(bot, (v11Bot, v12Bot)): + return "qq" + # if isinstance(bot, DodoBot): + # return "dodo" + # if isinstance(bot, KaiheilaBot): + # return "kaiheila" + # if isinstance(bot, DiscordBot): + # return "discord" + return None + + @classmethod + async def get_group_list(cls, bot: Bot) -> tuple[list[GroupConsole], str]: + """获取群组列表 + + 参数: + bot: Bot + + 返回: + tuple[list[GroupConsole], str]: 群组列表, 平台 + """ + if isinstance(bot, v11Bot): + group_list = await bot.get_group_list() + return [ + GroupConsole( + group_id=str(g["group_id"]), + group_name=g["group_name"], + max_member_count=g["max_member_count"], + member_count=g["member_count"], + ) + for g in group_list + ], "qq" + if isinstance(bot, v12Bot): + group_list = await bot.get_group_list() + return [ + GroupConsole( + group_id=g.group_id, # type: ignore + user_name=g.group_name, # type: ignore + ) + for g in group_list + ], "qq" + if isinstance(bot, DodoBot): + island_list = await bot.get_island_list() + source_id_list = [ + (g.island_source_id, g.island_name) + for g in island_list + if g.island_source_id + ] + group_list = [] + for id, name in source_id_list: + channel_list = await bot.get_channel_list(island_source_id=id) + group_list.append(GroupConsole(group_id=id, group_name=name)) + group_list += [ + GroupConsole( + group_id=id, group_name=c.channel_name, channel_id=c.channel_id + ) + for c in channel_list + ] + return group_list, "dodo" + if isinstance(bot, KaiheilaBot): + group_list = [] + guilds = await bot.guild_list() + if guilds.guilds: + for guild_id, name in [(g.id_, g.name) for g in guilds.guilds if g.id_]: + view = await bot.guild_view(guild_id=guild_id) + group_list.append(GroupConsole(group_id=guild_id, group_name=name)) + if view.channels: + group_list += [ + GroupConsole( + group_id=guild_id, group_name=c.name, channel_id=c.id_ + ) + for c in view.channels + if c.type != 0 + ] + return group_list, "kaiheila" + if isinstance(bot, DiscordBot): + # TODO: discord群组列表 + pass + return [], "" + + @classmethod + async def update_friend(cls, bot: Bot) -> int: + """更新好友信息 + + 参数: + bot: Bot + + 返回: + int: 更新个数 + """ + create_list = [] + friend_list, platform = await cls.get_friend_list(bot) + if friend_list: + user_id_list = await FriendUser.all().values_list("user_id", flat=True) + for friend in friend_list: + friend.platform = platform + if friend.user_id not in user_id_list: + create_list.append(friend) + if create_list: + await FriendUser.bulk_create(create_list, 10) + return len(create_list) + + @classmethod + async def get_friend_list(cls, bot: Bot) -> tuple[list[FriendUser], str]: + """获取好友列表 + + 参数: + bot: Bot + + 返回: + list[FriendUser]: 好友列表 + """ + if isinstance(bot, v11Bot): + friend_list = await bot.get_friend_list() + return [ + FriendUser(user_id=str(f["user_id"]), user_name=f["nickname"]) + for f in friend_list + ], "qq" + if isinstance(bot, v12Bot): + friend_list = await bot.get_friend_list() + return [ + FriendUser( + user_id=f.user_id, # type: ignore + user_name=f.user_displayname or f.user_remark or f.user_name, # type: ignore + ) + for f in friend_list + ], "qq" + if isinstance(bot, DodoBot): + # TODO: dodo好友列表 + pass + if isinstance(bot, KaiheilaBot): + # TODO: kaiheila好友列表 + pass + if isinstance(bot, DiscordBot): + # TODO: discord好友列表 + pass + return [], "" + + @classmethod + def get_target( + cls, + bot: Bot, + user_id: str | None = None, + group_id: str | None = None, + channel_id: str | None = None, + ): + """获取发生Target + + 参数: + bot: Bot + user_id: 用户id + group_id: 频道id或群组id + channel_id: 频道id + + 返回: + target: 对应平台Target + """ + target = None + if isinstance(bot, (v11Bot, v12Bot)): + if group_id: + target = Target(group_id) + elif user_id: + target = Target(user_id, private=True) + elif isinstance(bot, (DodoBot, KaiheilaBot)): + if group_id and channel_id: + target = Target(channel_id, parent_id=group_id, channel=True) + elif user_id: + target = Target(user_id, private=True) + return target + + +async def broadcast_group( + message: str | UniMessage, + bot: Bot | list[Bot] | None = None, + bot_id: str | Set[str] | None = None, + ignore_group: Set[int] | None = None, + check_func: Callable[[str], Awaitable] | None = None, + log_cmd: str | None = None, + platform: Literal["qq", "dodo", "kaiheila"] | None = None, +): + """获取所有Bot或指定Bot对象广播群聊 + + 参数: + message: 广播消息内容 + bot: 指定bot对象. + bot_id: 指定bot id. + ignore_group: 忽略群聊列表. + check_func: 发送前对群聊检测方法,判断是否发送. + log_cmd: 日志标记. + platform: 指定平台 + """ + if platform and platform not in ["qq", "dodo", "kaiheila"]: + raise ValueError("指定平台不支持") + if not message: + raise ValueError("群聊广播消息不能为空") + bot_dict = nonebot.get_bots() + bot_list: list[Bot] = [] + if bot: + if isinstance(bot, list): + bot_list = bot + else: + bot_list.append(bot) + elif bot_id: + _bot_id_list = bot_id + if isinstance(bot_id, str): + _bot_id_list = [bot_id] + for id_ in _bot_id_list: + if bot_id in bot_dict: + bot_list.append(bot_dict[bot_id]) + else: + logger.warning(f"Bot:{id_} 对象未连接或不存在") + else: + bot_list = list(bot_dict.values()) + _used_group = [] + for _bot in bot_list: + try: + if platform and platform != PlatformUtils.get_platform(_bot): + continue + group_list, _ = await PlatformUtils.get_group_list(_bot) + if group_list: + for group in group_list: + key = f"{group.group_id}:{group.channel_id}" + try: + if ( + ignore_group + and ( + group.group_id in ignore_group + or group.channel_id in ignore_group + ) + ) or key in _used_group: + continue + is_run = False + if check_func: + if is_coroutine_callable(check_func): + is_run = await check_func(group.group_id) + else: + is_run = check_func(group.group_id) + if not is_run: + continue + target = PlatformUtils.get_target( + _bot, None, group.group_id, group.channel_id + ) + if target: + _used_group.append(key) + message_list = message + await MessageUtils.build_message(message_list).send( + target, _bot + ) + logger.debug("发送成功", log_cmd, target=key) + else: + logger.warning("target为空", log_cmd, target=key) + except Exception as e: + logger.error("发送失败", log_cmd, target=key, e=e) + except Exception as e: + logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e) diff --git a/zhenxun/utils/plugin_models/base.py b/zhenxun/utils/plugin_models/base.py new file mode 100644 index 00000000..a50f6b4a --- /dev/null +++ b/zhenxun/utils/plugin_models/base.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class CommonSql(BaseModel): + + sql: str + """sql语句""" + remark: str + """备注""" diff --git a/zhenxun/utils/rules.py b/zhenxun/utils/rules.py new file mode 100644 index 00000000..f3381a48 --- /dev/null +++ b/zhenxun/utils/rules.py @@ -0,0 +1,61 @@ +from nonebot.adapters import Bot, Event +from nonebot.internal.rule import Rule +from nonebot.permission import SUPERUSER +from nonebot_plugin_session import EventSession, SessionLevel + +from zhenxun.configs.config import Config +from zhenxun.models.level_user import LevelUser + + +def admin_check(a: int | str, key: str | None = None) -> Rule: + """ + 管理员权限等级检查 + + 参数: + a: 权限等级或 配置项 module + key: 配置项 key. + + 返回: + Rule: Rule + """ + + async def _rule(bot: Bot, event: Event, session: EventSession) -> bool: + if await SUPERUSER(bot, event): + return True + if session.id1 and session.id2: + level = a + if type(a) == str and key: + level = Config.get_config(a, key) + if level is not None: + return bool( + await LevelUser.check_level(session.id1, session.id2, int(level)) + ) + return False + + return Rule(_rule) + + +def ensure_group(session: EventSession) -> bool: + """ + 是否在群聊中 + + 参数: + session: session + + 返回: + bool: bool + """ + return session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3] + + +def ensure_private(session: EventSession) -> bool: + """ + 是否在私聊中 + + 参数: + session: session + + 返回: + bool: bool + """ + return not session.id3 and not session.id2 diff --git a/plugins/my_info/data_source.py b/zhenxun/utils/typing.py similarity index 50% rename from plugins/my_info/data_source.py rename to zhenxun/utils/typing.py index 6fb66a5e..b28b04f6 100644 --- a/plugins/my_info/data_source.py +++ b/zhenxun/utils/typing.py @@ -1,6 +1,3 @@ - - - diff --git a/utils/user_agent.py b/zhenxun/utils/user_agent.py old mode 100755 new mode 100644 similarity index 100% rename from utils/user_agent.py rename to zhenxun/utils/user_agent.py diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py new file mode 100644 index 00000000..7ea698a9 --- /dev/null +++ b/zhenxun/utils/utils.py @@ -0,0 +1,232 @@ +import os +import time +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any + +import httpx +import pypinyin +import pytz + +from zhenxun.configs.config import Config +from zhenxun.services.log import logger + + +class ResourceDirManager: + """ + 临时文件管理器 + """ + + temp_path = [] + + @classmethod + def __tree_append(cls, path: Path): + """递归添加文件夹 + + 参数: + path: 文件夹路径 + """ + for f in os.listdir(path): + file = path / f + if file.is_dir(): + if file not in cls.temp_path: + cls.temp_path.append(file) + logger.debug(f"添加临时文件夹: {path}") + cls.__tree_append(file) + + @classmethod + def add_temp_dir(cls, path: str | Path, tree: bool = False): + """添加临时清理文件夹,这些文件夹会被自动清理 + + 参数: + path: 文件夹路径 + tree: 是否递归添加文件夹 + """ + if isinstance(path, str): + path = Path(path) + if path not in cls.temp_path: + cls.temp_path.append(path) + logger.debug(f"添加临时文件夹: {path}") + if tree: + cls.__tree_append(path) + + +class CountLimiter: + """ + 每日调用命令次数限制 + """ + + tz = pytz.timezone("Asia/Shanghai") + + def __init__(self, max_num): + self.today = -1 + self.count = defaultdict(int) + self.max = max_num + + def check(self, key) -> bool: + day = datetime.now(self.tz).day + if day != self.today: + self.today = day + self.count.clear() + return bool(self.count[key] < self.max) + + def get_num(self, key): + return self.count[key] + + def increase(self, key, num=1): + self.count[key] += num + + def reset(self, key): + self.count[key] = 0 + + +class UserBlockLimiter: + """ + 检测用户是否正在调用命令 + """ + + def __init__(self): + self.flag_data = defaultdict(bool) + self.time = time.time() + + def set_true(self, key: Any): + self.time = time.time() + self.flag_data[key] = True + + def set_false(self, key: Any): + self.flag_data[key] = False + + def check(self, key: Any) -> bool: + if time.time() - self.time > 30: + self.set_false(key) + return not self.flag_data[key] + + +class FreqLimiter: + """ + 命令冷却,检测用户是否处于冷却状态 + """ + + def __init__(self, default_cd_seconds: int): + self.next_time = defaultdict(float) + self.default_cd = default_cd_seconds + + def check(self, key: Any) -> bool: + return time.time() >= self.next_time[key] + + def start_cd(self, key: Any, cd_time: int = 0): + self.next_time[key] = time.time() + ( + cd_time if cd_time > 0 else self.default_cd + ) + + def left_time(self, key: Any) -> float: + return self.next_time[key] - time.time() + + +def cn2py(word: str) -> str: + """将字符串转化为拼音 + + 参数: + word: 文本 + """ + temp = "" + for i in pypinyin.pinyin(word, style=pypinyin.NORMAL): + temp += "".join(i) + return temp + + +async def get_user_avatar(uid: int | str) -> bytes | None: + """快捷获取用户头像 + + 参数: + uid: 用户id + """ + url = f"http://q1.qlogo.cn/g?b=qq&nk={uid}&s=160" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error("获取用户头像错误", "Util", target=uid) + return None + + +async def get_group_avatar(gid: int | str) -> bytes | None: + """快捷获取用群头像 + + 参数: + gid: 群号 + """ + url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error("获取群头像错误", "Util", target=gid) + return None + + +def change_pixiv_image_links( + url: str, size: str | None = None, nginx_url: str | None = None +) -> str: + """根据配置改变图片大小和反代链接 + + 参数: + url: 图片原图链接 + size: 模式 + nginx_url: 反代 + + 返回: + str: url + """ + if size == "master": + img_sp = url.rsplit(".", maxsplit=1) + url = img_sp[0] + img_type = img_sp[1] + url = url.replace("original", "master") + f"_master1200.{img_type}" + if not nginx_url: + nginx_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") + if nginx_url: + url = ( + url.replace("i.pximg.net", nginx_url) + .replace("i.pixiv.cat", nginx_url) + .replace("_webp", "") + ) + return url + + +def change_img_md5(path_file: str | Path) -> bool: + """改变图片MD5 + + 参数: + path_file: 图片路径 + + 返还: + bool: 是否修改成功 + """ + try: + with open(path_file, "a") as f: + f.write(str(int(time.time() * 1000))) + return True + except Exception as e: + logger.warning(f"改变图片MD5错误 Path:{path_file}", e=e) + return False + + +def is_valid_date(date_text: str, separator: str = "-") -> bool: + """日期是否合法 + + 参数: + date_text: 日期 + separator: 分隔符 + + 返回: + bool: 日期是否合法 + """ + try: + datetime.strptime(date_text, f"%Y{separator}%m{separator}%d") + return True + except ValueError: + return False diff --git a/zhenxun/utils/withdraw_manage.py b/zhenxun/utils/withdraw_manage.py new file mode 100644 index 00000000..d88b394f --- /dev/null +++ b/zhenxun/utils/withdraw_manage.py @@ -0,0 +1,112 @@ +import asyncio + +from nonebot.adapters import Bot + +# from nonebot.adapters.discord import Bot as DiscordBot +# from nonebot.adapters.dodo import Bot as DodoBot +# from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot_plugin_session import EventSession +from ruamel.yaml.comments import CommentedSeq + +from zhenxun.services.log import logger + + +class WithdrawManager: + + _data = {} + _index = 0 + + @classmethod + def check(cls, session: EventSession, withdraw_time: tuple[int, int]) -> bool: + """配置项检查 + + 参数: + session: Session + withdraw_time: 配置项数据, (0, 1) + + 返回: + bool: 是否允许撤回 + """ + if withdraw_time[0] and withdraw_time[0] > 0: + if withdraw_time[1] == 2: + return True + if withdraw_time[1] == 1 and (session.id2 or session.id3): + return True + if withdraw_time[1] == 0 and not (session.id2 or session.id3): + return True + return False + + @classmethod + def append(cls, bot: Bot, message_id: str | int, time: int): + """添加消息撤回 + + 参数: + bot: Bot + message_id: 消息Id + time: 延迟时间 + """ + cls._data[cls._index] = ( + bot, + message_id, + time, + ) + cls._index += 1 + + @classmethod + def remove(cls, index: int): + """移除 + + 参数: + index: index + """ + if index in cls._data: + del cls._data[index] + + @classmethod + async def withdraw_message( + cls, + bot: Bot, + message_id: str | int, + time: int | tuple[int, int] | None = None, + session: EventSession | None = None, + ): + """消息撤回 + + 参数: + bot: Bot + message_id: 消息Id + time: 延迟时间 + """ + if time: + gid = None + _time = 1 + if isinstance(time, (tuple, CommentedSeq)): + if time[0] == 0: + return + if session: + gid = session.id3 or session.id2 + if not gid and int(time[1]) not in [0, 2]: + return + if gid and int(time[1]) not in [1, 2]: + return + _time = time[0] + else: + _time = time + logger.debug( + f"将在 {_time}秒 内撤回消息ID: {message_id}", "WithdrawManager" + ) + await asyncio.sleep(_time) + if isinstance(bot, v11Bot): + logger.debug(f"v11Bot 撤回消息ID: {message_id}", "WithdrawManager") + await bot.delete_msg(message_id=int(message_id)) + elif isinstance(bot, v12Bot): + logger.debug(f"v12Bot 撤回消息ID: {message_id}", "WithdrawManager") + await bot.delete_message(message_id=str(message_id)) + # elif isinstance(bot, KaiheilaBot): + # pass + # elif isinstance(bot, DodoBot): + # pass + # elif isinstance(bot, DiscordBot): + # pass