From fab5be70b354b7bca0aa026379f336c6ca36207f Mon Sep 17 00:00:00 2001 From: snowy Date: Sun, 24 Mar 2024 09:43:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8F=92=E4=BB=B6=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {src => liteyuki}/liteyuki_main/__init__.py | 10 +- liteyuki/liteyuki_main/loader.py | 22 ++ {src => liteyuki}/liteyuki_main/webdash.py | 4 +- .../plugins/liteyuki_markdowntest.py | 4 +- .../plugins/liteyuki_minigame}/__init__.py | 0 .../plugins/liteyuki_npm}/__init__.py | 0 .../plugins/liteyuki_npm}/common.py | 25 ++- .../plugins/liteyuki_npm}/helper.py | 0 .../plugins/liteyuki_npm}/installer.py | 16 +- .../plugins/liteyuki_npm}/manager.py | 94 +++++--- .../plugins/liteyuki_npm}/permission.py | 2 +- .../plugins/liteyuki_user}/__init__.py | 0 .../plugins/liteyuki_user}/input_handle.py | 0 .../plugins/liteyuki_user}/profile_manager.py | 12 +- .../plugins/liteyuki_weather}/__init__.py | 0 .../plugins/liteyuki_weather}/data_model.py | 0 {src => liteyuki}/resources/fonts/bold.ttf | Bin {src => liteyuki}/resources/fonts/heavy.ttf | Bin {src => liteyuki}/resources/fonts/normal.ttf | Bin {src => liteyuki}/resources/lang/en.lang | 9 +- {src => liteyuki}/resources/lang/ja.lang | 7 + {src => liteyuki}/resources/lang/zh-CN.lang | 11 +- {src => liteyuki}/resources/lang/zh-HK.lang | 10 +- .../resources/lang/zh-Kawaii.lang | 6 + {src => liteyuki}/resources/lang/zh-WY.lang | 8 +- {src => liteyuki}/resources/metadata.yml | 0 .../resources/unsorted/plugins.json | 0 liteyuki/utils/__init__.py | 37 ++++ {src => liteyuki}/utils/config.py | 2 +- {src => liteyuki}/utils/data.py | 10 +- liteyuki/utils/data_manager.py | 45 ++++ {src => liteyuki}/utils/language.py | 13 +- {src => liteyuki}/utils/log.py | 39 +--- {src => liteyuki}/utils/ly_typing.py | 0 {src => liteyuki}/utils/message.py | 0 liteyuki/utils/orm.py | 207 ++++++++++++++++++ {src => liteyuki}/utils/permission.py | 2 +- {src => liteyuki}/utils/resource.py | 7 +- {src => liteyuki}/utils/tools.py | 9 + main.py | 10 +- requirements.txt | 7 +- src/liteyuki_main/loader.py | 18 -- src/plugins/liteyuki_plugin_eventpush.py | 122 ----------- src/utils/__init__.py | 1 - src/utils/data_manager.py | 35 --- 45 files changed, 501 insertions(+), 303 deletions(-) rename {src => liteyuki}/liteyuki_main/__init__.py (74%) create mode 100644 liteyuki/liteyuki_main/loader.py rename {src => liteyuki}/liteyuki_main/webdash.py (96%) rename {src => liteyuki}/plugins/liteyuki_markdowntest.py (93%) rename {src/plugins/liteyuki_plugin_minigame => liteyuki/plugins/liteyuki_minigame}/__init__.py (100%) rename {src/plugins/liteyuki_plugin_npm => liteyuki/plugins/liteyuki_npm}/__init__.py (100%) rename {src/plugins/liteyuki_plugin_npm => liteyuki/plugins/liteyuki_npm}/common.py (74%) rename {src/plugins/liteyuki_plugin_npm => liteyuki/plugins/liteyuki_npm}/helper.py (100%) rename {src/plugins/liteyuki_plugin_npm => liteyuki/plugins/liteyuki_npm}/installer.py (97%) rename {src/plugins/liteyuki_plugin_npm => liteyuki/plugins/liteyuki_npm}/manager.py (59%) rename {src/plugins/liteyuki_plugin_npm => liteyuki/plugins/liteyuki_npm}/permission.py (80%) rename {src/plugins/liteyuki_plugin_user => liteyuki/plugins/liteyuki_user}/__init__.py (100%) rename {src/plugins/liteyuki_plugin_user => liteyuki/plugins/liteyuki_user}/input_handle.py (100%) rename {src/plugins/liteyuki_plugin_user => liteyuki/plugins/liteyuki_user}/profile_manager.py (92%) rename {src/plugins/liteyuki_plugin_weather => liteyuki/plugins/liteyuki_weather}/__init__.py (100%) rename {src/plugins/liteyuki_plugin_weather => liteyuki/plugins/liteyuki_weather}/data_model.py (100%) rename {src => liteyuki}/resources/fonts/bold.ttf (100%) rename {src => liteyuki}/resources/fonts/heavy.ttf (100%) rename {src => liteyuki}/resources/fonts/normal.ttf (100%) rename {src => liteyuki}/resources/lang/en.lang (93%) rename {src => liteyuki}/resources/lang/ja.lang (95%) rename {src => liteyuki}/resources/lang/zh-CN.lang (91%) rename {src => liteyuki}/resources/lang/zh-HK.lang (94%) rename {src => liteyuki}/resources/lang/zh-Kawaii.lang (96%) rename {src => liteyuki}/resources/lang/zh-WY.lang (95%) rename {src => liteyuki}/resources/metadata.yml (100%) rename {src => liteyuki}/resources/unsorted/plugins.json (100%) create mode 100644 liteyuki/utils/__init__.py rename {src => liteyuki}/utils/config.py (98%) rename {src => liteyuki}/utils/data.py (98%) create mode 100644 liteyuki/utils/data_manager.py rename {src => liteyuki}/utils/language.py (92%) rename {src => liteyuki}/utils/log.py (60%) rename {src => liteyuki}/utils/ly_typing.py (100%) rename {src => liteyuki}/utils/message.py (100%) create mode 100644 liteyuki/utils/orm.py rename {src => liteyuki}/utils/permission.py (58%) rename {src => liteyuki}/utils/resource.py (86%) rename {src => liteyuki}/utils/tools.py (88%) delete mode 100644 src/liteyuki_main/loader.py delete mode 100644 src/plugins/liteyuki_plugin_eventpush.py delete mode 100644 src/utils/__init__.py delete mode 100644 src/utils/data_manager.py diff --git a/src/liteyuki_main/__init__.py b/liteyuki/liteyuki_main/__init__.py similarity index 74% rename from src/liteyuki_main/__init__.py rename to liteyuki/liteyuki_main/__init__.py index 7e6fe820..7e5006d8 100644 --- a/src/liteyuki_main/__init__.py +++ b/liteyuki/liteyuki_main/__init__.py @@ -1,10 +1,10 @@ import nonebot from nonebot.plugin import PluginMetadata -from src.utils.language import get_system_lang -from src.utils.data_manager import * +from liteyuki.utils.language import get_default_lang +from liteyuki.utils.data_manager import * from .loader import * from .webdash import * -from src.utils.config import config +from liteyuki.utils.config import config __author__ = "snowykami" __plugin_meta__ = PluginMetadata( @@ -20,6 +20,6 @@ __plugin_meta__ = PluginMetadata( auto_migrate() # 自动迁移数据库 -sys_lang = get_system_lang() +sys_lang = get_default_lang() nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))) -nonebot.logger.info(sys_lang.get("main.enable_webdash", URL=f"http://127.0.0.1:{config.get('port', 8080)}")) +nonebot.logger.info(sys_lang.get("main.enable_webdash", URL=f"http://127.0.0.1:{config.get('port', 20216)}")) diff --git a/liteyuki/liteyuki_main/loader.py b/liteyuki/liteyuki_main/loader.py new file mode 100644 index 00000000..9c2a3af3 --- /dev/null +++ b/liteyuki/liteyuki_main/loader.py @@ -0,0 +1,22 @@ +import os + +import nonebot.plugin + +from liteyuki.utils.data_manager import InstalledPlugin, plugin_db +from liteyuki.utils.resource import load_resource_from_dir +from liteyuki.utils.tools import check_for_package + +THIS_PLUGIN_NAME = os.path.basename(os.path.dirname(__file__)) +RESOURCE_PATH = "liteyuki/resources" +load_resource_from_dir(RESOURCE_PATH) + +nonebot.plugin.load_plugins("liteyuki/plugins") +nonebot.plugin.load_plugins("plugins") + +installed_plugins = plugin_db.all(InstalledPlugin) +if installed_plugins: + for installed_plugin in plugin_db.all(InstalledPlugin): + if not check_for_package(installed_plugin.module_name): + nonebot.logger.error(f"{installed_plugin.module_name} not installed, but in loading database. please run `npm fixup` in chat to reinstall it.") + else: + nonebot.load_plugin(installed_plugin.module_name) \ No newline at end of file diff --git a/src/liteyuki_main/webdash.py b/liteyuki/liteyuki_main/webdash.py similarity index 96% rename from src/liteyuki_main/webdash.py rename to liteyuki/liteyuki_main/webdash.py index 6833e563..ad0e1963 100644 --- a/src/liteyuki_main/webdash.py +++ b/liteyuki/liteyuki_main/webdash.py @@ -3,8 +3,8 @@ import psutil from dash import Dash, Input, Output, dcc, html from starlette.middleware.wsgi import WSGIMiddleware -from src.utils.language import Language -from src.utils.tools import convert_size +from liteyuki.utils.language import Language +from liteyuki.utils.tools import convert_size app = nonebot.get_app() diff --git a/src/plugins/liteyuki_markdowntest.py b/liteyuki/plugins/liteyuki_markdowntest.py similarity index 93% rename from src/plugins/liteyuki_markdowntest.py rename to liteyuki/plugins/liteyuki_markdowntest.py index 831f48e0..b1a013c7 100644 --- a/src/plugins/liteyuki_markdowntest.py +++ b/liteyuki/plugins/liteyuki_markdowntest.py @@ -6,8 +6,8 @@ from nonebot.params import CommandArg from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata -from src.utils.message import send_markdown -from src.utils.ly_typing import T_Message, T_Bot, v11, T_MessageEvent +from liteyuki.utils.message import send_markdown +from liteyuki.utils.ly_typing import T_Message, T_Bot, v11, T_MessageEvent md_test = on_command("mdts", aliases={"会话md"}, permission=SUPERUSER) md_group = on_command("mdg", aliases={"群md"}, permission=SUPERUSER) diff --git a/src/plugins/liteyuki_plugin_minigame/__init__.py b/liteyuki/plugins/liteyuki_minigame/__init__.py similarity index 100% rename from src/plugins/liteyuki_plugin_minigame/__init__.py rename to liteyuki/plugins/liteyuki_minigame/__init__.py diff --git a/src/plugins/liteyuki_plugin_npm/__init__.py b/liteyuki/plugins/liteyuki_npm/__init__.py similarity index 100% rename from src/plugins/liteyuki_plugin_npm/__init__.py rename to liteyuki/plugins/liteyuki_npm/__init__.py diff --git a/src/plugins/liteyuki_plugin_npm/common.py b/liteyuki/plugins/liteyuki_npm/common.py similarity index 74% rename from src/plugins/liteyuki_plugin_npm/common.py rename to liteyuki/plugins/liteyuki_npm/common.py index 67fb5e83..ab5a2680 100644 --- a/src/plugins/liteyuki_plugin_npm/common.py +++ b/liteyuki/plugins/liteyuki_npm/common.py @@ -4,9 +4,9 @@ from typing import Optional import aiofiles import nonebot.plugin -from src.utils.data import Database, LiteModel -from src.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db -from src.utils.ly_typing import T_MessageEvent +from liteyuki.utils.data import Database, LiteModel +from liteyuki.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db +from liteyuki.utils.ly_typing import T_MessageEvent LNPM_COMMAND_START = "lnpm" @@ -58,16 +58,17 @@ def get_plugin_default_enable(plugin_module_name: str) -> bool: Returns: bool: 插件默认状态 """ - return (nonebot.plugin.get_plugin(plugin_module_name).metadata.extra.get('default_enable', True) - if nonebot.plugin.get_plugin(plugin_module_name) and nonebot.plugin.get_plugin(plugin_module_name).metadata else True) \ - if nonebot.plugin.get_plugin(plugin_module_name) else False + plug = nonebot.plugin.get_plugin_by_module_name(plugin_module_name) + return (plug.metadata.extra.get('default_enable', True) + if plug.metadata else True) if plug else True -def get_plugin_current_enable(event: T_MessageEvent, plugin_module_name: str) -> bool: +def get_plugin_session_enable(event: T_MessageEvent, plugin_module_name: str) -> bool: """ - 获取插件当前启用状态 + 获取插件当前会话启用状态 Args: + event: 会话事件 plugin_module_name (str): 插件模块名 Returns: @@ -88,6 +89,10 @@ def get_plugin_current_enable(event: T_MessageEvent, plugin_module_name: str) -> return plugin_module_name in session.enabled_plugins +def get_plugin_global_enable(plugin_module_name: str) -> bool: + return True + + def get_plugin_can_be_toggle(plugin_module_name: str) -> bool: """ 获取插件是否可以被启用/停用 @@ -98,5 +103,5 @@ def get_plugin_can_be_toggle(plugin_module_name: str) -> bool: Returns: bool: 插件是否可以被启用/停用 """ - return nonebot.plugin.get_plugin(plugin_module_name).metadata.extra.get('toggleable', True) \ - if nonebot.plugin.get_plugin(plugin_module_name) and nonebot.plugin.get_plugin(plugin_module_name).metadata else True + plug = nonebot.plugin.get_plugin_by_module_name(plugin_module_name) + return plug.metadata.extra.get('toggleable', True) if plug and plug.metadata else True diff --git a/src/plugins/liteyuki_plugin_npm/helper.py b/liteyuki/plugins/liteyuki_npm/helper.py similarity index 100% rename from src/plugins/liteyuki_plugin_npm/helper.py rename to liteyuki/plugins/liteyuki_npm/helper.py diff --git a/src/plugins/liteyuki_plugin_npm/installer.py b/liteyuki/plugins/liteyuki_npm/installer.py similarity index 97% rename from src/plugins/liteyuki_plugin_npm/installer.py rename to liteyuki/plugins/liteyuki_npm/installer.py index faa661bf..40b9f673 100644 --- a/src/plugins/liteyuki_plugin_npm/installer.py +++ b/liteyuki/plugins/liteyuki_npm/installer.py @@ -9,9 +9,9 @@ from arclet.alconna import Arparma, MultiVar from nonebot.permission import SUPERUSER from nonebot_plugin_alconna import Alconna, Args, Subcommand, on_alconna -from src.utils.language import get_user_lang -from src.utils.ly_typing import T_Bot -from src.utils.message import Markdown as md, send_markdown +from liteyuki.utils.language import get_user_lang +from liteyuki.utils.ly_typing import T_Bot +from liteyuki.utils.message import Markdown as md, send_markdown from .common import * npm_alc = on_alconna( @@ -45,9 +45,6 @@ npm_alc = on_alconna( ) - - - @npm_alc.handle() async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): ulang = get_user_lang(str(event.user_id)) @@ -103,7 +100,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): if r_load: if found_in_db_plugin is None: - plugin_db.save(installed_plugin) + plugin_db.upsert(installed_plugin) info = ulang.get('npm.install_success', NAME=store_plugin.name).replace('_', r'\\_') # markdown转义 await send_markdown( f"{info}\n\n" @@ -192,9 +189,6 @@ async def npm_search(keywords: list[str]) -> list[StorePlugin]: return results - - - def npm_install(plugin_module_name) -> tuple[bool, str]: """ Args: @@ -209,8 +203,8 @@ def npm_install(plugin_module_name) -> tuple[bool, str]: sys.stderr = buffer mirrors = [ - "https://pypi.mirrors.cqupt.edu.cn/simple", # 重庆邮电大学 "https://pypi.tuna.tsinghua.edu.cn/simple", # 清华大学 + "https://pypi.mirrors.cqupt.edu.cn/simple", # 重庆邮电大学 "https://pypi.liteyuki.icu/simple", # 轻雪镜像 "https://pypi.org/simple", # 官方源 ] diff --git a/src/plugins/liteyuki_plugin_npm/manager.py b/liteyuki/plugins/liteyuki_npm/manager.py similarity index 59% rename from src/plugins/liteyuki_plugin_npm/manager.py rename to liteyuki/plugins/liteyuki_npm/manager.py index 28cf06e7..7abb33ba 100644 --- a/src/plugins/liteyuki_plugin_npm/manager.py +++ b/liteyuki/plugins/liteyuki_npm/manager.py @@ -8,23 +8,35 @@ from nonebot.message import run_preprocessor from nonebot.permission import SUPERUSER from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma -from src.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db -from src.utils.message import Markdown as md, send_markdown -from src.utils.permission import GROUP_ADMIN, GROUP_OWNER -from src.utils.ly_typing import T_Bot, T_MessageEvent -from src.utils.language import get_user_lang -from .common import get_plugin_can_be_toggle, get_plugin_current_enable, get_plugin_default_enable +from liteyuki.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db +from liteyuki.utils.message import Markdown as md, send_markdown +from liteyuki.utils.permission import GROUP_ADMIN, GROUP_OWNER +from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent +from liteyuki.utils.language import get_user_lang +from .common import get_plugin_can_be_toggle, get_plugin_global_enable, get_plugin_session_enable, get_plugin_default_enable from .installer import get_store_plugin, npm_update -list_plugins = on_command("list-plugin", aliases={"列出插件", "插件列表"}, priority=0) -# toggle_plugin = on_command("enable-plugin", aliases={"启用插件", "停用插件", "disable-plugin"}, priority=0) +list_plugins = on_alconna( + Alconna( + ['list-plugins', "插件列表", "列出插件"], + ) +) + toggle_plugin = on_alconna( Alconna( ['enable-plugin', 'disable-plugin'], - Args['plugin_name', str]['global', bool, False], + Args['plugin_name', str], ) ) +global_toggle = on_alconna( + Alconna( + ['toggle-global'], + Args['plugin_name', str], + ), + permission=SUPERUSER +) + @list_plugins.handle() async def _(event: T_MessageEvent, bot: T_Bot): @@ -35,39 +47,59 @@ async def _(event: T_MessageEvent, bot: T_Bot): for plugin in nonebot.get_loaded_plugins(): # 检查是否有 metadata 属性 # 添加帮助按钮 - btn_usage = md.button(lang.get('npm.usage'), f'help {plugin.name}', False) + btn_usage = md.button(lang.get('npm.usage'), f'help {plugin.module_name}', False) store_plugin = await get_store_plugin(plugin.module_name) + session_enable = get_plugin_session_enable(event, plugin.module_name) + default_enable = get_plugin_default_enable(plugin.module_name) + print(session_enable, default_enable, plugin.module_name) + if store_plugin: btn_homepage = md.link(lang.get('npm.homepage'), store_plugin.homepage) - elif plugin.metadata and plugin.metadata.extra.get('liteyuki'): - btn_homepage = md.link(lang.get('npm.homepage'), "https://github.com/snowykami/LiteyukiBot") + show_name = store_plugin.name + show_desc = store_plugin.desc + elif plugin.metadata: + if plugin.metadata.extra.get('liteyuki'): + btn_homepage = md.link(lang.get('npm.homepage'), "https://github.com/snowykami/LiteyukiBot") + else: + btn_homepage = lang.get('npm.homepage') + show_name = plugin.metadata.name + show_desc = plugin.metadata.description else: btn_homepage = lang.get('npm.homepage') + show_name = plugin.name + show_desc = lang.get('npm.no_description') if plugin.metadata: - reply += (f"\n**{md.escape(plugin.metadata.name)}**\n" - f"\n > {plugin.metadata.description}") + reply += (f"\n**{md.escape(show_name)}**\n" + f"\n > {md.escape(show_desc)}") else: - reply += (f"**{md.escape(plugin.name)}**\n" - f"\n > {lang.get('npm.no_description')}") + reply += (f"**{md.escape(show_name)}**\n" + f"\n > {md.escape(show_desc)}") reply += f"\n > {btn_usage} {btn_homepage}" if await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event): # 添加启用/停用插件按钮 - btn_toggle = lang.get('npm.disable') if plugin.metadata and not plugin.metadata.extra.get('toggleable') \ - else md.button(lang.get('npm.disable'), f'enable-plugin {plugin.module_name}') + cmd_toggle = f"{'disable' if session_enable else 'enable'}-plugin {plugin.module_name}" + text_toggle = lang.get('npm.disable' if session_enable else 'npm.enable') + can_be_toggle = get_plugin_can_be_toggle(plugin.module_name) + btn_toggle = text_toggle if not can_be_toggle else md.button(text_toggle, cmd_toggle) + reply += f" {btn_toggle}" if await SUPERUSER(bot, event): plugin_in_database = plugin_db.first(InstalledPlugin, 'module_name = ?', plugin.module_name) - # 添加移除插件 + # 添加移除插件和全局切换按钮 + global_enable = get_plugin_global_enable(plugin.module_name) btn_uninstall = ( md.button(lang.get('npm.uninstall'), f'npm uninstall {plugin.module_name}')) if plugin_in_database else lang.get( 'npm.uninstall') - btn_toggle_global = lang.get('npm.disable') if plugin.metadata and not plugin.metadata.extra.get('toggleable') \ - else md.button(lang.get('npm.disable_global'), f'disable-plugin {plugin.module_name} true') + + btn_toggle_global_text = lang.get('npm.disable_global' if global_enable else 'npm.enable_global') + cmd_toggle_global = f'npm toggle-global {plugin.module_name}' + btn_toggle_global = btn_toggle_global_text if not can_be_toggle else md.button(btn_toggle_global_text, cmd_toggle_global) + reply += f" {btn_uninstall} {btn_toggle_global}" reply += "\n\n***\n" @@ -83,9 +115,10 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): plugin_module_name = result.args.get("plugin_name") toggle = result.header_result == 'enable-plugin' # 判断是启用还是停用 - current_enable = get_plugin_current_enable(event, plugin_module_name) # 获取插件当前状态 + current_enable = get_plugin_session_enable(event, plugin_module_name) # 获取插件当前状态 default_enable = get_plugin_default_enable(plugin_module_name) # 获取插件默认状态 + can_be_toggled = get_plugin_can_be_toggle(plugin_module_name) # 获取插件是否可以被启用/停用 if not can_be_toggled: @@ -102,10 +135,6 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): session = group_db.first(GroupChat, "group_id = ?", event.group_id, default=GroupChat(group_id=event.group_id)) else: raise FinishedException(ulang.get("Permission Denied")) - # 启用 已停用的默认启用插件 将其从停用列表移除 - # 启用 已停用的默认停用插件 将其放到启用列表 - # 停用 已启用的默认启用插件 将其放到停用列表 - # 停用 已启用的默认停用插件 将其从启用列表移除 try: if toggle: if default_enable: @@ -126,14 +155,21 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): ERROR=str(e)) ) + await toggle_plugin.finish( + ulang.get( + "npm.toggle_success", + NAME=plugin_module_name, + STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")) + ) + if event.message_type == "private": - user_db.save(session) + user_db.upsert(session) else: - group_db.save(session) + group_db.upsert(session) @run_preprocessor async def _(event: T_MessageEvent, matcher: Matcher): plugin = matcher.plugin # TODO 插件启用/停用检查hook - nonebot.logger.info(f"Plugin: {plugin.module_name}") + nonebot.logger.info(f"Plugin Callapi: {plugin.module_name}") diff --git a/src/plugins/liteyuki_plugin_npm/permission.py b/liteyuki/plugins/liteyuki_npm/permission.py similarity index 80% rename from src/plugins/liteyuki_plugin_npm/permission.py rename to liteyuki/plugins/liteyuki_npm/permission.py index d2533fbf..ee346f48 100644 --- a/src/plugins/liteyuki_plugin_npm/permission.py +++ b/liteyuki/plugins/liteyuki_npm/permission.py @@ -1,5 +1,5 @@ # 插件权限管理器,对api调用进行hook限制,防止插件滥用api -from src.utils.data import LiteModel +from liteyuki.utils.data import LiteModel class PermissionAllow(LiteModel): diff --git a/src/plugins/liteyuki_plugin_user/__init__.py b/liteyuki/plugins/liteyuki_user/__init__.py similarity index 100% rename from src/plugins/liteyuki_plugin_user/__init__.py rename to liteyuki/plugins/liteyuki_user/__init__.py diff --git a/src/plugins/liteyuki_plugin_user/input_handle.py b/liteyuki/plugins/liteyuki_user/input_handle.py similarity index 100% rename from src/plugins/liteyuki_plugin_user/input_handle.py rename to liteyuki/plugins/liteyuki_user/input_handle.py diff --git a/src/plugins/liteyuki_plugin_user/profile_manager.py b/liteyuki/plugins/liteyuki_user/profile_manager.py similarity index 92% rename from src/plugins/liteyuki_plugin_user/profile_manager.py rename to liteyuki/plugins/liteyuki_user/profile_manager.py index 161fe785..a78e6e36 100644 --- a/src/plugins/liteyuki_plugin_user/profile_manager.py +++ b/liteyuki/plugins/liteyuki_user/profile_manager.py @@ -2,11 +2,11 @@ from typing import Optional from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna -from src.utils.data import LiteModel -from src.utils.data_manager import User, user_db -from src.utils.language import Language, get_all_lang, get_user_lang -from src.utils.ly_typing import T_Bot, T_MessageEvent -from src.utils.message import Markdown as md, send_markdown +from liteyuki.utils.data import LiteModel +from liteyuki.utils.data_manager import User, user_db +from liteyuki.utils.language import Language, get_all_lang, get_user_lang +from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent +from liteyuki.utils.message import Markdown as md, send_markdown profile_alc = on_alconna( Alconna( @@ -43,7 +43,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): r = set_profile(result.args["key"], result.args["value"]) if r: user.profile[result.args["key"]] = result.args["value"] - user_db.save(user) # 数据库保存 + user_db.upsert(user) # 数据库保存 await profile_alc.finish( ulang.get( "user.profile.set_success", diff --git a/src/plugins/liteyuki_plugin_weather/__init__.py b/liteyuki/plugins/liteyuki_weather/__init__.py similarity index 100% rename from src/plugins/liteyuki_plugin_weather/__init__.py rename to liteyuki/plugins/liteyuki_weather/__init__.py diff --git a/src/plugins/liteyuki_plugin_weather/data_model.py b/liteyuki/plugins/liteyuki_weather/data_model.py similarity index 100% rename from src/plugins/liteyuki_plugin_weather/data_model.py rename to liteyuki/plugins/liteyuki_weather/data_model.py diff --git a/src/resources/fonts/bold.ttf b/liteyuki/resources/fonts/bold.ttf similarity index 100% rename from src/resources/fonts/bold.ttf rename to liteyuki/resources/fonts/bold.ttf diff --git a/src/resources/fonts/heavy.ttf b/liteyuki/resources/fonts/heavy.ttf similarity index 100% rename from src/resources/fonts/heavy.ttf rename to liteyuki/resources/fonts/heavy.ttf diff --git a/src/resources/fonts/normal.ttf b/liteyuki/resources/fonts/normal.ttf similarity index 100% rename from src/resources/fonts/normal.ttf rename to liteyuki/resources/fonts/normal.ttf diff --git a/src/resources/lang/en.lang b/liteyuki/resources/lang/en.lang similarity index 93% rename from src/resources/lang/en.lang rename to liteyuki/resources/lang/en.lang index 319bddd9..4bf1a7a0 100644 --- a/src/resources/lang/en.lang +++ b/liteyuki/resources/lang/en.lang @@ -1,6 +1,12 @@ language.name=English -main.current_language=Current system language: {LANG} +log.debug=Debug +log.info=Info +log.warning=WARN +log.error=Error +log.success=Success + +main.current_language=Current config language: {LANG} main.enable_webdash=Web dashboard is enabled: {URL} main.monitor.title=Liteyuki Monitor main.monitor.description=Monitor your server with Liteyuki Monitor @@ -46,6 +52,7 @@ npm.prev_page=Prev npm.plugin_cannot_be_toggled=Plugin {NAME} cannot be toggled npm.plugin_already=Plugin {NAME} is already in {STATUS} state, no need for repeated operation npm.toggle_failed=Failed to {STATUS} plugin {NAME}: {ERROR} +npm.toggle_success=Succeeded in {STATUS} plugin {NAME} user.profile.edit=Edit user.profile.set=Set diff --git a/src/resources/lang/ja.lang b/liteyuki/resources/lang/ja.lang similarity index 95% rename from src/resources/lang/ja.lang rename to liteyuki/resources/lang/ja.lang index 88e82c8c..31f4162b 100644 --- a/src/resources/lang/ja.lang +++ b/liteyuki/resources/lang/ja.lang @@ -1,5 +1,11 @@ language.name = 日本語 +log.debug=デバッグ +log.info=情報 +log.warning=警告 +log.error=エラー +log.success=成功 + main.current_language = 現在のシステム言語: {LANG} main.enable_webdash = ウェブダッシュボードが有効になりました: {URL} main.monitor.title = Liteyukiモニタリングパネル @@ -46,6 +52,7 @@ npm.prev_page = 前のページ npm.plugin_cannot_be_toggled=プラグイン {NAME} は有効または無効にできません npm.plugin_already=プラグイン {NAME} はすでに {STATUS} 状態です。繰り返し操作する必要はありません npm.toggle_failed=プラグイン {NAME} を {STATUS} にするのに失敗しました: {ERROR} +npm.toggle_success=プラグイン {NAME} が {STATUS} になりました user.profile.edit=編集 user.profile.set=設定 diff --git a/src/resources/lang/zh-CN.lang b/liteyuki/resources/lang/zh-CN.lang similarity index 91% rename from src/resources/lang/zh-CN.lang rename to liteyuki/resources/lang/zh-CN.lang index f775f2d3..21876224 100644 --- a/src/resources/lang/zh-CN.lang +++ b/liteyuki/resources/lang/zh-CN.lang @@ -1,6 +1,12 @@ language.name=简体中文 -main.current_language=当前系统语言为: {LANG} +log.debug=调试 +log.info=信息 +log.warning=警告 +log.error=错误 +log.success=成功 + +main.current_language=当前配置语言为: {LANG} main.enable_webdash=已启用网页监控面板: {URL} main.monitor.title=轻雪监控面板 main.monitor.description=轻雪机器人监控面板 @@ -32,7 +38,7 @@ npm.search_no_result=无搜索结果 npm.too_many_results=内容过多,{HIDE_NUM}项已隐藏,请限制关键字搜索 npm.install_success={NAME} 安装成功 npm.install_failed={NAME} 安装失败,请查看日志获取详细信息,如不能解决,请访问{HOMEPAGE}寻求帮助 -npm.uninstall_success={NAME} 卸载成功 +npm.uninstall_success={NAME} 卸载成功,下次重启生效 npm.uninstall_failed={NAME} 卸载失败 npm.load_failed={NAME} 加载失败,请在控制台查看详细信息,检查依赖或配置是否正确,如不能解决,请访问{HOMEPAGE}寻求帮助 npm.plugin_not_found=未在商店中找到 {NAME},请尝试更新商店信息或检查拼写 @@ -46,6 +52,7 @@ npm.prev_page=上一页 npm.plugin_cannot_be_toggled=插件 {NAME} 无法被启用或停用 npm.plugin_already=插件 {NAME} 已经是 {STATUS} 状态,无需重复操作 npm.toggle_failed=插件 {NAME} {STATUS} 失败: {ERROR} +npm.toggle_success=插件 {NAME} {STATUS} 成功 user.profile.edit=修改 user.profile.set=设置 diff --git a/src/resources/lang/zh-HK.lang b/liteyuki/resources/lang/zh-HK.lang similarity index 94% rename from src/resources/lang/zh-HK.lang rename to liteyuki/resources/lang/zh-HK.lang index ae3aedfa..73be99a5 100644 --- a/src/resources/lang/zh-HK.lang +++ b/liteyuki/resources/lang/zh-HK.lang @@ -1,4 +1,10 @@ -language.name=繁體中文 +language.name=繁體中文(香港) + +log.debug=調試 +log.info=信息 +log.warning=警告 +log.error=錯誤 +log.success=成功 main.current_language=當前系統語言為:{LANG} main.enable_webdash=已啟用網頁監控面板:{URL} @@ -46,6 +52,8 @@ npm.prev_page=上一頁 npm.plugin_cannot_be_toggled=無法啟用或停用插件 {NAME} npm.plugin_already=插件 {NAME} 已處於 {STATUS} 狀態,無需重複操作 npm.toggle_failed=插件 {NAME} {STATUS} 失敗: {ERROR} +npm.toggle_success=插件 {NAME} {STATUS} 成功 + user.profile.edit=編輯 user.profile.set=設定 diff --git a/src/resources/lang/zh-Kawaii.lang b/liteyuki/resources/lang/zh-Kawaii.lang similarity index 96% rename from src/resources/lang/zh-Kawaii.lang rename to liteyuki/resources/lang/zh-Kawaii.lang index 002917f1..40712e71 100644 --- a/src/resources/lang/zh-Kawaii.lang +++ b/liteyuki/resources/lang/zh-Kawaii.lang @@ -1,5 +1,11 @@ language.name=简体中文(轻雪版) +log.debug=调试 +log.info=信息 +log.warning=有问题哦 +log.error=出错啦 +log.success=成功啦 + main.current_language=现在系统用的语言是:{LANG} 喔! main.enable_webdash=已经打开了网页监控板:{URL} 啦! main.monitor.title=监控板 diff --git a/src/resources/lang/zh-WY.lang b/liteyuki/resources/lang/zh-WY.lang similarity index 95% rename from src/resources/lang/zh-WY.lang rename to liteyuki/resources/lang/zh-WY.lang index bdaadfc5..ee70f6fb 100644 --- a/src/resources/lang/zh-WY.lang +++ b/liteyuki/resources/lang/zh-WY.lang @@ -1,4 +1,10 @@ -language.name=漢字 +language.name=中文(華夏) + +log.debug=調試 +log.info=信息 +log.warning=警告 +log.error=錯誤 +log.success=成功 main.current_language=當前之系統語言為:{LANG} main.enable_webdash=已啟用網頁監控板:{URL} diff --git a/src/resources/metadata.yml b/liteyuki/resources/metadata.yml similarity index 100% rename from src/resources/metadata.yml rename to liteyuki/resources/metadata.yml diff --git a/src/resources/unsorted/plugins.json b/liteyuki/resources/unsorted/plugins.json similarity index 100% rename from src/resources/unsorted/plugins.json rename to liteyuki/resources/unsorted/plugins.json diff --git a/liteyuki/utils/__init__.py b/liteyuki/utils/__init__.py new file mode 100644 index 00000000..a6db9648 --- /dev/null +++ b/liteyuki/utils/__init__.py @@ -0,0 +1,37 @@ +import nonebot + +from .log import logger +import sys + +__NAME__ = "LiteyukiBot" +__VERSION__ = "6.2.1" # 60201 +major, minor, patch = map(int, __VERSION__.split(".")) +__VERSION_I__ = major * 10000 + minor * 100 + patch + + +def init(): + """ + 初始化 + Returns: + + """ + # 检测python版本是否高于3.10 + if sys.version_info < (3, 10): + nonebot.logger.error("This project requires Python3.10+ to run, please upgrade your Python Environment.") + exit(1) + + print("\033[34m" + r""" __ ______ ________ ________ __ __ __ __ __ __ ______ +/ | / |/ |/ |/ \ / |/ | / |/ | / |/ | +$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ +$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ | +$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | +$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ | +$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_ +$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ | +$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ """ + "\033[0m") + nonebot.logger.info( + f"Run Liteyuki with Python{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} " + f"at {sys.executable}" + ) + + nonebot.logger.info(f"{__NAME__} {__VERSION__}({__VERSION_I__}) is running") diff --git a/src/utils/config.py b/liteyuki/utils/config.py similarity index 98% rename from src/utils/config.py rename to liteyuki/utils/config.py index 5395229f..73ce31ca 100644 --- a/src/utils/config.py +++ b/liteyuki/utils/config.py @@ -4,7 +4,7 @@ import nonebot import yaml from pydantic import BaseModel -config = None +config = {} class BasicConfig(BaseModel): diff --git a/src/utils/data.py b/liteyuki/utils/data.py similarity index 98% rename from src/utils/data.py rename to liteyuki/utils/data.py index 06d63e30..4ae3d2fe 100644 --- a/src/utils/data.py +++ b/liteyuki/utils/data.py @@ -31,7 +31,7 @@ class BaseORMAdapter(ABC): """ raise NotImplementedError - def save(self, *args, **kwargs): + def upsert(self, *args, **kwargs): """存储数据 Returns: @@ -171,7 +171,7 @@ class Database(BaseORMAdapter): self.conn.commit() nonebot.logger.debug(f'Table {table_name} migrated successfully') - def save(self, *models: LiteModel) -> int | tuple: + def upsert(self, *models: LiteModel) -> int | tuple: """存储数据,检查id字段,如果有id字段则更新,没有则插入 Args: @@ -192,7 +192,7 @@ class Database(BaseORMAdapter): for field, value in model.__dict__.items(): if isinstance(value, LiteModel): key_list.append(f'{self.FOREIGNID}{field}') - value_list.append(f'{self.ID}:{value.__class__.__name__}:{self.save(value)}') + value_list.append(f'{self.ID}:{value.__class__.__name__}:{self.upsert(value)}') elif isinstance(value, list): key_list.append(f'{self.LIST}{field}') value_list.append(self._flat(value)) @@ -225,7 +225,7 @@ class Database(BaseORMAdapter): return_data = {} for k, v in data.items(): if isinstance(v, LiteModel): - return_data[f'{self.FOREIGNID}{k}'] = f'{self.ID}:{v.__class__.__name__}:{self.save(v)}' + return_data[f'{self.FOREIGNID}{k}'] = f'{self.ID}:{v.__class__.__name__}:{self.upsert(v)}' elif isinstance(v, list): return_data[f'{self.LIST}{k}'] = self._flat(v) elif isinstance(v, dict): @@ -239,7 +239,7 @@ class Database(BaseORMAdapter): return_data = [] for v in data: if isinstance(v, LiteModel): - return_data.append(f'{self.ID}:{v.__class__.__name__}:{self.save(v)}') + return_data.append(f'{self.ID}:{v.__class__.__name__}:{self.upsert(v)}') elif isinstance(v, list): return_data.append(self._flat(v)) elif isinstance(v, dict): diff --git a/liteyuki/utils/data_manager.py b/liteyuki/utils/data_manager.py new file mode 100644 index 00000000..33773d7c --- /dev/null +++ b/liteyuki/utils/data_manager.py @@ -0,0 +1,45 @@ +import os + +from pydantic import Field + +from liteyuki.utils.data import LiteModel, Database as DB + +DATA_PATH = "data/liteyuki" + +user_db = DB(os.path.join(DATA_PATH, 'users.ldb')) +group_db = DB(os.path.join(DATA_PATH, 'groups.ldb')) +plugin_db = DB(os.path.join(DATA_PATH, 'plugins.ldb')) +common_db = DB(os.path.join(DATA_PATH, 'common.ldb')) + + +class User(LiteModel): + user_id: str = Field(str(), alias='user_id') + username: str = Field(str(), alias='username') + profile: dict[str, str] = Field(dict(), alias='profile') + enabled_plugins: list[str] = Field(list(), alias='enabled_plugins') + disabled_plugins: list[str] = Field(list(), alias='disabled_plugins') + + +class GroupChat(LiteModel): + # Group是一个关键字,所以这里用GroupChat + group_id: str = Field(str(), alias='group_id') + group_name: str = Field(str(), alias='group_name') + enabled_plugins: list[str] = Field([], alias='enabled_plugins') + disabled_plugins: list[str] = Field([], alias='disabled_plugins') + + +class InstalledPlugin(LiteModel): + module_name: str = Field(str(), alias='module_name') + version: str = Field(str(), alias='version') + + +class GlobalPlugin(LiteModel): + module_name: str = Field(str(), alias='module_name') + enabled: bool = Field(True, alias='enabled') + + +def auto_migrate(): + user_db.auto_migrate(User()) + group_db.auto_migrate(GroupChat()) + plugin_db.auto_migrate(InstalledPlugin()) + common_db.auto_migrate(GlobalPlugin()) diff --git a/src/utils/language.py b/liteyuki/utils/language.py similarity index 92% rename from src/utils/language.py rename to liteyuki/utils/language.py index c8fc9edb..130ce255 100644 --- a/src/utils/language.py +++ b/liteyuki/utils/language.py @@ -9,8 +9,8 @@ from typing import Any import nonebot -from src.utils.config import config -from src.utils.data_manager import User, user_db +from liteyuki.utils.config import config +from liteyuki.utils.data_manager import User, user_db _default_lang_code = "en" _language_data = { @@ -38,7 +38,6 @@ def load_from_lang(file_path: str, lang_code: str = None): if not line or line.startswith('#'): # 空行或注释 continue key, value = line.split('=', 1) - nonebot.logger.debug(f"Loaded language text: {key.strip()} -> {value.strip()}") data[key.strip()] = value.strip() if lang_code not in _language_data: _language_data[lang_code] = {} @@ -139,7 +138,7 @@ def get_user_lang(user_id: str) -> Language: username="Unknown" )) - return Language(user.profile.get('lang',config.get("default_language", get_system_lang_code()) )) + return Language(user.profile.get('lang', config.get("default_language", get_system_lang_code()))) def get_system_lang_code() -> str: @@ -149,11 +148,11 @@ def get_system_lang_code() -> str: return locale.getdefaultlocale()[0].replace('_', '-') -def get_system_lang() -> Language: +def get_default_lang() -> Language: """ - 获取系统语言 + 获取默认/系统语言 """ - return Language(get_system_lang_code()) + return Language(config.get("default_language", get_system_lang_code())) def get_all_lang() -> dict[str, str]: diff --git a/src/utils/log.py b/liteyuki/utils/log.py similarity index 60% rename from src/utils/log.py rename to liteyuki/utils/log.py index f888b0b1..a56e6dfb 100644 --- a/src/utils/log.py +++ b/liteyuki/utils/log.py @@ -1,35 +1,14 @@ import sys import logging from typing import TYPE_CHECKING - +from colored import fg +from .language import get_default_lang import loguru if TYPE_CHECKING: - # avoid sphinx autodoc resolve annotation failed - # because loguru module do not have `Logger` class actually from loguru import Logger, Record -# logger = logging.getLogger("nonebot") logger: "Logger" = loguru.logger -"""NoneBot 日志记录器对象。 - -默认信息: - -- 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s` -- 等级: `INFO` ,根据 `config.log_level` 配置改变 -- 输出: 输出至 stdout - -用法: - ```python - from nonebot.log import logger - ``` -""" - - -# default_handler = logging.StreamHandler(sys.stdout) -# default_handler.setFormatter( -# logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s")) -# logger.addHandler(default_handler) class LoguruHandler(logging.Handler): # pragma: no cover @@ -59,7 +38,7 @@ def default_filter(record: "Record"): default_format: str = ( - "{time:MM-DD HH:mm:ss} " + "{time:YYYY-MM-DD} {time:HH:mm:ss} " "[{level.icon}] " "<{name}> " "{message}" @@ -74,12 +53,12 @@ logger_id = logger.add( filter=default_filter, format=default_format, ) - -logger.level("DEBUG", color="", icon="㊙️DEBU") -logger.level("INFO", color="", icon="ℹ️INFO") -logger.level("SUCCESS", color="", icon="✅SUCC") -logger.level("WARNING", color="", icon="⚠️WARN") -logger.level("ERROR", color="", icon="☢️ERRO") +slang = get_default_lang() +logger.level("DEBUG", color="", icon=f"*️⃣ DDDEBUG") +logger.level("INFO", color="", icon=f"ℹ️ IIIINFO") +logger.level("SUCCESS", color="", icon=f"✅ SUCCESS") +logger.level("WARNING", color="", icon=f"⚠️ WARNING") +logger.level("ERROR", color="", icon=f"⭕ EEERROR") """默认日志处理器 id""" diff --git a/src/utils/ly_typing.py b/liteyuki/utils/ly_typing.py similarity index 100% rename from src/utils/ly_typing.py rename to liteyuki/utils/ly_typing.py diff --git a/src/utils/message.py b/liteyuki/utils/message.py similarity index 100% rename from src/utils/message.py rename to liteyuki/utils/message.py diff --git a/liteyuki/utils/orm.py b/liteyuki/utils/orm.py new file mode 100644 index 00000000..2fb79297 --- /dev/null +++ b/liteyuki/utils/orm.py @@ -0,0 +1,207 @@ +import os +import pickle +import sqlite3 +from types import NoneType +from typing import Any + +import nonebot +from pydantic import BaseModel, Field + + +class LiteModel(BaseModel): + """轻量级模型基类 + 类型注解统一使用Python3.9的PEP585标准,如需使用泛型请使用typing模块的泛型类型 + 不允许使用id, table_name以及其他SQLite关键字作为字段名,不允许使用JSON和ID,必须指定默认值,且默认值类型必须与字段类型一致 + """ + __ID__: int = Field(None, alias='id') + __TABLE_NAME__: str = Field(None, alias='table_name') + + +class Database: + TYPE_MAPPING = { + int : "INTEGER", + float : "REAL", + str : "TEXT", + bool : "INTEGER", + bytes : "BLOB", + NoneType: "NULL", + + dict : "BLOB", # LITEYUKIDICT{key_name} + list : "BLOB", # LITEYUKILIST{key_name} + tuple : "BLOB", # LITEYUKITUPLE{key_name} + set : "BLOB", # LITEYUKISET{key_name} + } + + # 基础类型 + BASIC_TYPE = [int, float, str, bool, bytes, NoneType] + # 可序列化类型 + ITERABLE_TYPE = [dict, list, tuple, set] + + LITEYUKI = "LITEYUKI" + + # 字段前缀映射,默认基础类型为"" + FIELD_PREFIX_MAPPING = { + dict : f"{LITEYUKI}DICT", + list : f"{LITEYUKI}LIST", + tuple : f"{LITEYUKI}TUPLE", + set : f"{LITEYUKI}SET", + type(LiteModel): f"{LITEYUKI}MODEL" + } + + def __init__(self, db_name: str): + if not os.path.exists(os.path.dirname(db_name)): + os.makedirs(os.path.dirname(db_name)) + self.conn = sqlite3.connect(db_name) # 连接对象 + self.conn.row_factory = sqlite3.Row # 以字典形式返回查询结果 + self.cursor = self.conn.cursor() # 游标对象 + + def auto_migrate(self, *args: LiteModel): + """ + 自动迁移模型 + Args: + *args: 模型类实例化对象,支持空默认值,不支持嵌套迁移 + + Returns: + + """ + for model in args: + if not model.__TABLE_NAME__: + raise ValueError(f"数据模型{model.__class__.__name__}未提供表名") + + # 若无则创建表 + self.cursor.execute( + f'CREATE TABLE IF NOT EXISTS {model.__TABLE_NAME__} (id INTEGER PRIMARY KEY AUTOINCREMENT)' + ) + + # 获取表结构 + new_fields, new_stored_types = ( + zip( + *[(self._get_stored_field_prefix(model.__getattribute__(field)) + field, self._get_stored_type(model.__getattribute__(field))) + for field in model.__annotations__] + ) + ) + + # 原有的字段列表 + existing_fields = self.cursor.execute(f'PRAGMA table_info({model.__TABLE_NAME__})').fetchall() + existing_types = [field['name'] for field in existing_fields] + + # 检测缺失字段,由于SQLite是动态类型,所以不需要检测类型 + for n_field, n_type in zip(new_fields, new_stored_types): + if n_field not in existing_types: + nonebot.logger.debug(f'ALTER TABLE {model.__TABLE_NAME__} ADD COLUMN {n_field} {n_type}') + self.cursor.execute( + f'ALTER TABLE {model.__TABLE_NAME__} ADD COLUMN {n_field} {n_type}' + ) + + # 检测多余字段进行删除 + for e_field in existing_types: + if e_field not in new_fields and e_field not in ['id']: + nonebot.logger.debug(f'ALTER TABLE {model.__TABLE_NAME__} DROP COLUMN {e_field}') + self.cursor.execute( + f'ALTER TABLE {model.__TABLE_NAME__} DROP COLUMN {e_field}' + ) + + self.conn.commit() + + def save(self, *args: LiteModel) -> [int | tuple[int, ...]]: + """ + 保存或更新模型 + Args: + *args: 模型类实例化对象,支持空默认值,不支持嵌套迁移 + Returns: + + """ + ids = [] + for model in args: + if not model.__TABLE_NAME__: + raise ValueError(f"数据模型{model.__class__.__name__}未提供表名") + if not self.cursor.execute(f'PRAGMA table_info({model.__TABLE_NAME__})').fetchall(): + raise ValueError(f"数据表{model.__TABLE_NAME__}不存在,请先迁移{model.__class__.__name__}模型") + + stored_fields, stored_values = [], [] + for r_field in model.__annotations__: + r_value = model.__getattribute__(r_field) + stored_fields.append(self._get_stored_field_prefix(r_value) + r_field) + + if type(r_value) in Database.BASIC_TYPE: + # int str float bool bytes NoneType + stored_values.append(r_value) + + elif type(r_value) in Database.ITERABLE_TYPE: + # dict list tuple set + stored_values.append(pickle.dumps(self._flat_save(r_value))) + + elif isinstance(r_value, LiteModel): + # LiteModel TABLE_NAME:ID + stored_values.append(f"{r_value.__TABLE_NAME__}:{self.save(r_value)}") + + else: + raise ValueError(f"不支持的数据类型{type(r_value)}") + nonebot.logger.debug(f"INSERT OR REPLACE INTO {model.__TABLE_NAME__} ({','.join(stored_fields)}) VALUES ({','.join([_ for _ in stored_values])})") + self.cursor.execute( + f"INSERT OR REPLACE INTO {model.__TABLE_NAME__} ({','.join(stored_fields)}) VALUES ({','.join(['?' for _ in stored_values])})", + stored_values + ) + ids.append(self.cursor.lastrowid) + self.conn.commit() + return tuple(ids) if len(ids) > 1 else ids[0] + + # 检测id字段是否有1,有则更新,无则插入 + + def _flat_save(self, obj) -> Any: + """扁平化存储 + + Args: + obj: 需要存储的对象 + + Returns: + 存储的字节流 + """ + # TODO 递归扁平化存储 + if type(obj) in Database.ITERABLE_TYPE: + for i, item in enumerate(obj) if type(obj) in [list, tuple, set] else obj.items(): + if type(item) in Database.BASIC_TYPE: + continue + elif type(item) in Database.ITERABLE_TYPE: + obj[i] = pickle.dumps(self._flat_save(item)) + elif isinstance(item, LiteModel): + obj[i] = f"{item.__TABLE_NAME__}:{self.save(item)}" + else: + raise ValueError(f"不支持的数据类型{type(item)}") + else: + raise ValueError(f"不支持的数据类型{type(obj)}") + + @staticmethod + def _get_stored_field_prefix(value) -> str: + """获取存储字段前缀,一定在后加上字段名 + + LiteModel -> LITEYUKIID + + dict -> LITEYUKIDICT + + list -> LITEYUKILIST + + tuple -> LITEYUKITUPLE + + set -> LITEYUKISET + + * -> "" + Args: + value: 储存的值 + + Returns: + Sqlite3存储字段 + """ + return Database.FIELD_PREFIX_MAPPING.get(type(value), "") + + @staticmethod + def _get_stored_type(value) -> str: + """获取存储类型 + + Args: + value: 储存的值 + + Returns: + Sqlite3存储类型 + """ + return Database.TYPE_MAPPING.get(type(value), "TEXT") diff --git a/src/utils/permission.py b/liteyuki/utils/permission.py similarity index 58% rename from src/utils/permission.py rename to liteyuki/utils/permission.py index fcc6508b..dbf092db 100644 --- a/src/utils/permission.py +++ b/liteyuki/utils/permission.py @@ -1,6 +1,6 @@ from nonebot.adapters.onebot import v11 -from src.utils.ly_typing import T_GroupMessageEvent, T_MessageEvent +from liteyuki.utils.ly_typing import T_GroupMessageEvent, T_MessageEvent GROUP_ADMIN = v11.GROUP_ADMIN GROUP_OWNER = v11.GROUP_OWNER diff --git a/src/utils/resource.py b/liteyuki/utils/resource.py similarity index 86% rename from src/utils/resource.py rename to liteyuki/utils/resource.py index 256dfba6..2ae0c5e4 100644 --- a/src/utils/resource.py +++ b/liteyuki/utils/resource.py @@ -4,7 +4,7 @@ import nonebot import yaml from typing import Any -from src.utils.data import LiteModel +from liteyuki.utils.data import LiteModel _resource_data = {} _loaded_resource_packs = [] # 按照加载顺序排序 @@ -31,7 +31,6 @@ def load_resource_from_dir(path: str): relative_path = os.path.relpath(os.path.join(root, file), path).replace("\\", "/") abs_path = os.path.join(root, file).replace("\\", "/") _resource_data[relative_path] = abs_path - nonebot.logger.debug(f"Loaded {relative_path} -> {abs_path}") if os.path.exists(os.path.join(path, "metadata.yml")): with open(os.path.join(path, "metadata.yml"), "r", encoding="utf-8") as f: metadata = yaml.safe_load(f) @@ -39,12 +38,12 @@ def load_resource_from_dir(path: str): metadata = ResourceMetadata() metadata["path"] = path if os.path.exists(os.path.join(path, "lang")): - from src.utils.language import load_from_dir + from liteyuki.utils.language import load_from_dir load_from_dir(os.path.join(path, "lang")) _loaded_resource_packs.append(ResourceMetadata(**metadata)) -def get_res(path: str, default: Any = None) -> str | Any: +def get(path: str, default: Any = None) -> str | Any: """ 获取资源包中的文件 Args: diff --git a/src/utils/tools.py b/liteyuki/utils/tools.py similarity index 88% rename from src/utils/tools.py rename to liteyuki/utils/tools.py index c7142e2a..c5d9ccbd 100644 --- a/src/utils/tools.py +++ b/liteyuki/utils/tools.py @@ -1,3 +1,4 @@ +from importlib.metadata import PackageNotFoundError, version from urllib.parse import quote @@ -72,3 +73,11 @@ def keywords_in_text(keywords: list[str], text: str, all_matched: bool) -> bool: if keyword in text: return True return False + + +def check_for_package(package_name: str) -> bool: + try: + version(package_name) + return True + except PackageNotFoundError: + return False diff --git a/main.py b/main.py index 7f53e7be..d43b5abd 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,9 @@ import nonebot - from nonebot.adapters.onebot import v11, v12 +from liteyuki.utils.config import load_from_yaml +from liteyuki.utils import init -from src.utils import logger -from src.utils.config import load_from_yaml - -nonebot.logger = logger +init() nonebot.init(**load_from_yaml("config.yml")) adapters = [v11.Adapter, v12.Adapter] @@ -14,7 +12,7 @@ driver = nonebot.get_driver() for adapter in adapters: driver.register_adapter(adapter) -nonebot.load_plugin("src.liteyuki_main") +nonebot.load_plugin("liteyuki.liteyuki_main") if __name__ == "__main__": nonebot.run() diff --git a/requirements.txt b/requirements.txt index 0911e553..16f0b783 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,13 +2,16 @@ aiohttp==3.9.3 aiofiles==23.2.1 arclet-alconna==1.8.5 arclet-alconna-tools==0.7.0 +colored==2.2.4 dash==2.16.1 nonebot2[fastapi]==2.2.1 nonebot-adapter-onebot==2.4.3 nonebot-plugin-alconna==0.41.0 pip==24.0 psutil==5.9.8 -pydantic==2.6.4 +pydantic==1.10.14 pytz==2024.1 PyYAML~=6.0.1 -starlette~=0.36.3 \ No newline at end of file +starlette~=0.36.3 +loguru==0.7.2 +importlib_metadata==7.0.2 \ No newline at end of file diff --git a/src/liteyuki_main/loader.py b/src/liteyuki_main/loader.py deleted file mode 100644 index a3fc4b5a..00000000 --- a/src/liteyuki_main/loader.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -import nonebot.plugin - -from src.utils.data_manager import InstalledPlugin, plugin_db -from src.utils.resource import load_resource_from_dir - -THIS_PLUGIN_NAME = os.path.basename(os.path.dirname(__file__)) -RESOURCE_PATH = "src/resources" -load_resource_from_dir(RESOURCE_PATH) - -nonebot.plugin.load_plugins("src/plugins") -nonebot.plugin.load_plugins("plugins") - -installed_plugins = plugin_db.all(InstalledPlugin) -if installed_plugins: - for install_plugin in plugin_db.all(InstalledPlugin): - nonebot.load_plugin(install_plugin.module_name) \ No newline at end of file diff --git a/src/plugins/liteyuki_plugin_eventpush.py b/src/plugins/liteyuki_plugin_eventpush.py deleted file mode 100644 index dc08644b..00000000 --- a/src/plugins/liteyuki_plugin_eventpush.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import Optional - -import nonebot -from nonebot import on_message -from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand, Arg -from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import on_alconna -from src.utils.data import LiteModel -from src.utils.message import send_markdown -from src.utils.ly_typing import T_Bot, T_MessageEvent -from src.utils.data import Database - - -class Node(LiteModel): - bot_id: str - session_type: str - session_id: str - - def __str__(self): - return f"{self.bot_id}.{self.session_type}.{self.session_id}" - - -class Push(LiteModel): - source: Node - target: Node - inde: int - - -pushes_db = Database("data/pushes.ldb") -pushes_db.auto_migrate(Push, Node) - -alc = Alconna( - "lep", - Subcommand( - "add", - Args["source", str], - Args["target", str], - Option("bidirectional", Args["bidirectional", bool]) - ), - Subcommand( - "rm", - Args["index", int], - - ), - Subcommand( - "list", - ) -) - -add_push = on_alconna(alc) - - -@add_push.handle() -async def _(result: Arparma): - """bot_id.session_type.session_id""" - if result.subcommands.get("add"): - source = result.subcommands["add"].args.get("source") - target = result.subcommands["add"].args.get("target") - if source and target: - source = source.split(".") - target = target.split(".") - push1 = Push( - source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]), - target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]), - inde=len(pushes_db.all(Push, default=[])) - ) - pushes_db.save(push1) - - if result.subcommands["add"].args.get("bidirectional"): - push2 = Push( - source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]), - target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]), - inde=len(pushes_db.all(Push, default=[])) - ) - pushes_db.save(push2) - await add_push.finish("添加成功") - else: - await add_push.finish("参数缺失") - elif result.subcommands.get("rm"): - index = result.subcommands["rm"].args.get("index") - if index is not None: - try: - pushes_db.delete(Push, "inde = ?", index) - await add_push.finish("删除成功") - except IndexError: - await add_push.finish("索引错误") - else: - await add_push.finish("参数缺失") - elif result.subcommands.get("list"): - await add_push.finish( - "\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> " - f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in - enumerate(pushes_db.all(Push, default=[]))])) - else: - await add_push.finish("参数错误") - - -@on_message(block=False).handle() -async def _(event: T_MessageEvent, bot: T_Bot): - for push in pushes_db.all(Push, default=[]): - if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}": - bot2 = nonebot.get_bot(push.target.bot_id) - msg_formatted = "" - for l in str(event.message).split("\n"): - msg_formatted += f"**{l.strip()}**\n" - push_message = ( - f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n" - f"{msg_formatted}") - await send_markdown(push_message, bot2, message_type=push.target.session_type, session_id=push.target.session_id) - return - - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="轻雪事件推送", - description="事件推送插件,支持单向和双向推送,支持跨Bot推送", - usage="", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - } -) diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index c99114d9..00000000 --- a/src/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .log import logger \ No newline at end of file diff --git a/src/utils/data_manager.py b/src/utils/data_manager.py deleted file mode 100644 index eddda80c..00000000 --- a/src/utils/data_manager.py +++ /dev/null @@ -1,35 +0,0 @@ -import os - -from src.utils.data import LiteModel, Database as DB - -DATA_PATH = "data/liteyuki" - -user_db = DB(os.path.join(DATA_PATH, 'users.ldb')) -group_db = DB(os.path.join(DATA_PATH, 'groups.ldb')) -plugin_db = DB(os.path.join(DATA_PATH, 'plugins.ldb')) - - -class User(LiteModel): - user_id: str - username: str = "" - profile: dict[str, str] = {} - enabled_plugins: list[str] = [] - disabled_plugins: list[str] = [] - - -class GroupChat(LiteModel): - # Group是一个关键字,所以这里用GroupChat - group_id: str - group_name: str = "" - enabled_plugins: list[str] = [] - disabled_plugins: list[str] = [] - - -class InstalledPlugin(LiteModel): - module_name: str - - -def auto_migrate(): - user_db.auto_migrate(User) - group_db.auto_migrate(GroupChat) - plugin_db.auto_migrate(InstalledPlugin)