diff --git a/src/liteyuki_main/__init__.py b/src/liteyuki_main/__init__.py index 060c33d0..ff715d20 100644 --- a/src/liteyuki_main/__init__.py +++ b/src/liteyuki_main/__init__.py @@ -14,10 +14,11 @@ __plugin_meta__ = PluginMetadata( homepage="https://github.com/snowykami/LiteyukiBot", extra={ "liteyuki_plugin": True, + "toggleable": False, } ) -auto_migrate() +auto_migrate() # 自动迁移数据库 sys_lang = get_system_lang() nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))) diff --git a/src/plugins/liteyuki_markdowntest.py b/src/plugins/liteyuki_markdowntest.py index a6fe3cb0..6960e912 100644 --- a/src/plugins/liteyuki_markdowntest.py +++ b/src/plugins/liteyuki_markdowntest.py @@ -53,6 +53,6 @@ __plugin_meta__ = PluginMetadata( usage="", homepage="https://github.com/snowykami/LiteyukiBot", extra={ - "liteyuki_plugin": True, + "liteyuki": True, } ) diff --git a/src/plugins/liteyuki_plugin_eventpush.py b/src/plugins/liteyuki_plugin_eventpush.py index a6b8a4e9..8071cb25 100644 --- a/src/plugins/liteyuki_plugin_eventpush.py +++ b/src/plugins/liteyuki_plugin_eventpush.py @@ -117,6 +117,6 @@ __plugin_meta__ = PluginMetadata( usage="", homepage="https://github.com/snowykami/LiteyukiBot", extra={ - "liteyuki_plugin": True, + "liteyuki": True, } ) diff --git a/src/plugins/liteyuki_plugin_npm/__init__.py b/src/plugins/liteyuki_plugin_npm/__init__.py index 9bf41cab..a70ecd1b 100644 --- a/src/plugins/liteyuki_plugin_npm/__init__.py +++ b/src/plugins/liteyuki_plugin_npm/__init__.py @@ -2,6 +2,7 @@ from nonebot.plugin import PluginMetadata from .manager import * from .installer import * from .helper import * +from .permission import * __author__ = "snowykami" __plugin_meta__ = PluginMetadata( @@ -16,6 +17,8 @@ __plugin_meta__ = PluginMetadata( type="application", homepage="https://github.com/snowykami/LiteyukiBot", extra={ - "liteyuki_plugin": True, + "liteyuki": True, + "toggleable" : False, + "default_enable" : True, } ) diff --git a/src/plugins/liteyuki_plugin_npm/common.py b/src/plugins/liteyuki_plugin_npm/common.py index f8b307b4..dc511425 100644 --- a/src/plugins/liteyuki_plugin_npm/common.py +++ b/src/plugins/liteyuki_plugin_npm/common.py @@ -1,8 +1,102 @@ +import json +from typing import Optional + +import aiofiles +import nonebot.plugin + from src.utils.data import Database, LiteModel -from src.utils.data_manager import InstalledPlugin, plugin_db +from src.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db +from src.utils.typing import T_MessageEvent LNPM_COMMAND_START = "lnpm" +class PluginTag(LiteModel): + label: str + color: str = '#000000' +class StorePlugin(LiteModel): + name: str + desc: str + module_name: str + project_link: str = '' + homepage: str = '' + author: str = '' + type: str | None = None + version: str | None = '' + time: str = '' + tags: list[PluginTag] = [] + is_official: bool = False + + +async def get_store_plugin(plugin_module_name: str) -> Optional[StorePlugin]: + """ + 获取插件信息 + + Args: + plugin_module_name (str): 插件模块名 + + Returns: + Optional[StorePlugin]: 插件信息 + """ + async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f: + plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())] + for plugin in plugins: + if plugin.module_name == plugin_module_name: + return plugin + return None + + +def get_plugin_default_enable(plugin_module_name: str) -> bool: + """ + 获取插件默认启用状态,由插件定义,不存在则默认为启用 + + Args: + plugin_module_name (str): 插件模块名 + + 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 + + +def get_plugin_current_enable(event: T_MessageEvent, plugin_module_name: str) -> bool: + """ + 获取插件当前启用状态 + + Args: + plugin_module_name (str): 插件模块名 + + Returns: + bool: 插件当前状态 + """ + if event.message_type == "group": + session: GroupChat = group_db.first(GroupChat, 'group_id = ?', event.group_id, default=GroupChat(group_id=event.group_id)) + else: + session: User = user_db.first(User, 'user_id = ?', event.user_id, default=User(user_id=event.user_id)) + # 默认停用插件在启用列表内表示启用 + # 默认停用插件不在启用列表内表示停用 + # 默认启用插件在停用列表内表示停用 + # 默认启用插件不在停用列表内表示启用 + default_enable = get_plugin_default_enable(plugin_module_name) + if default_enable: + return plugin_module_name not in session.disabled_plugins + else: + return plugin_module_name in session.enabled_plugins + + +def get_plugin_can_be_toggle(plugin_module_name: str) -> bool: + """ + 获取插件是否可以被启用/停用 + + Args: + plugin_module_name (str): 插件模块名 + + 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 diff --git a/src/plugins/liteyuki_plugin_npm/installer.py b/src/plugins/liteyuki_plugin_npm/installer.py index 5909781e..cea89768 100644 --- a/src/plugins/liteyuki_plugin_npm/installer.py +++ b/src/plugins/liteyuki_plugin_npm/installer.py @@ -22,7 +22,7 @@ from src.utils.data_manager import InstalledPlugin npm_alc = on_alconna( Alconna( - ["lnpm", "插件管理"], + ["npm", "插件"], Subcommand( "update", alias=["u"], @@ -42,29 +42,17 @@ npm_alc = on_alconna( Args["plugin_name", str], alias=["rm", "移除", "卸载"], ), + Subcommand( + "list", + alias=["l", "ls", "列表"], + ) ), permission=SUPERUSER ) -class PluginTag(LiteModel): - label: str - color: str = '#000000' -class StorePlugin(LiteModel): - name: str - desc: str - module_name: str - project_link: str = '' - homepage: str = '' - author: str = '' - type: str | None = None - version: str | None = '' - time: str = '' - tags: list[PluginTag] = [] - is_official: bool = False - @npm_alc.handle() async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): @@ -88,12 +76,15 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): if len(rs): reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***" for plugin in rs[:min(max_show, len(rs))]: - btn_install = md.button(ulang.get('npm.install'), 'lnpm install %s' % plugin.module_name) + btn_install = md.button(ulang.get('npm.install'), 'npm install %s' % plugin.module_name) link_page = md.link(ulang.get('npm.homepage'), plugin.homepage) + link_pypi = md.link(ulang.get('npm.pypi'), plugin.homepage) - reply += (f"\n{btn_install} **{plugin.name}**\n" - f"\n > **{plugin.desc}**\n" - f"\n > {ulang.get('npm.author')}: {plugin.author} {link_page}\n\n***\n") + reply += (f"\n# **{plugin.name}**\n" + f"\n> **{plugin.desc}**\n" + f"\n> {ulang.get('npm.author')}: {plugin.author}" + f"\n> *{md.escape(plugin.module_name)}*" + f"\n> {btn_install} {link_page} {link_pypi}\n\n***\n") if len(rs) > max_show: reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}" else: @@ -102,27 +93,43 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): elif result.subcommands.get("install"): plugin_module_name: str = result.subcommands["install"].args.get("plugin_name") + store_plugin = await get_store_plugin(plugin_module_name) await npm_alc.send(ulang.get("npm.installing", NAME=plugin_module_name)) r, log = npm_install(plugin_module_name) log = log.replace("\\", "/") + + if not store_plugin: + await npm_alc.finish(ulang.get("npm.plugin_not_found", NAME=plugin_module_name)) + + homepage_btn = md.button(ulang.get('npm.homepage'), store_plugin.homepage) if r: - nonebot.load_plugin(plugin_module_name) # 加载插件 + + r_load = nonebot.load_plugin(plugin_module_name) # 加载插件 installed_plugin = InstalledPlugin(module_name=plugin_module_name) # 构造插件信息模型 - store_plugin = await get_store_plugin(plugin_module_name) # 获取商店中的插件信息 found_in_db_plugin = plugin_db.first(InstalledPlugin, "module_name = ?", plugin_module_name) # 查询数据库中是否已经安装 - if found_in_db_plugin is None: - plugin_db.save(installed_plugin) - info = ulang.get('npm.install_success', NAME=store_plugin.name).replace('_', r'\\_') # markdown转义 + + if r_load: + if found_in_db_plugin is None: + plugin_db.save(installed_plugin) + info = ulang.get('npm.install_success', NAME=store_plugin.name).replace('_', r'\\_') # markdown转义 + await send_markdown( + f"{info}\n\n" + f"```\n{log}\n```", + bot, + event=event + ) + else: + await npm_alc.finish(ulang.get('npm.plugin_already_installed', NAME=store_plugin.name)) + else: + info = ulang.get('npm.load_failed', NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace('_', r'\\_') await send_markdown( f"{info}\n\n" - f"```\n{log}\n```", + f"```\n{log}\n```\n", bot, event=event ) - else: - await npm_alc.finish(ulang.get('npm.plugin_already_installed', NAME=store_plugin.name)) else: - info = ulang.get('npm.install_failed', NAME=plugin_module_name).replace('_', r'\\_') + info = ulang.get('npm.install_failed', NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace('_', r'\\_') await send_markdown( f"{info}\n\n" f"```\n{log}\n```", @@ -192,22 +199,7 @@ async def npm_search(keywords: list[str]) -> list[StorePlugin]: return results -async def get_store_plugin(plugin_module_name: str) -> Optional[StorePlugin]: - """ - 获取插件信息 - Args: - plugin_module_name (str): 插件模块名 - - Returns: - Optional[StorePlugin]: 插件信息 - """ - async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f: - plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())] - for plugin in plugins: - if plugin.module_name == plugin_module_name: - return plugin - return None def npm_install(plugin_module_name) -> tuple[bool, str]: diff --git a/src/plugins/liteyuki_plugin_npm/manager.py b/src/plugins/liteyuki_plugin_npm/manager.py index 3945c9ed..895e1d8e 100644 --- a/src/plugins/liteyuki_plugin_npm/manager.py +++ b/src/plugins/liteyuki_plugin_npm/manager.py @@ -1,41 +1,131 @@ import nonebot.plugin from nonebot import on_command +from nonebot.internal.matcher import Matcher +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 InstalledPlugin, plugin_db +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.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 .installer import get_store_plugin list_plugins = on_command("list-plugin", aliases={"列出插件", "插件列表"}, priority=0) -toggle_plugin = on_command("enable-plugin", aliases={"启用插件", "停用插件", "disable-plugin"}, priority=0, permission=SUPERUSER) +# toggle_plugin = on_command("enable-plugin", aliases={"启用插件", "停用插件", "disable-plugin"}, priority=0) +toggle_plugin = on_alconna( + Alconna( + ['enable-plugin', 'disable-plugin'], + Args['plugin_name', str]['global', bool, False], + ) +) @list_plugins.handle() async def _(event: T_MessageEvent, bot: T_Bot): lang = get_user_lang(str(event.user_id)) - reply = f"# {lang.get('npm.loaded_plugins')} | {lang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} \n***" + reply = f"# {lang.get('npm.loaded_plugins')} | {lang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} \n***\n" for plugin in nonebot.get_loaded_plugins(): # 检查是否有 metadata 属性 - btn_help = md.button(lang.get('npm.help'), f'help {plugin.name}', False) - reply += f"\n{btn_help} " + # 添加帮助按钮 + btn_usage = md.button(lang.get('npm.usage'), f'help {plugin.name}', False) + store_plugin = await get_store_plugin(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") + else: + btn_homepage = lang.get('npm.homepage') + if plugin.metadata: - reply += (f"**{plugin.metadata.name}**\n" + reply += (f"\n**{md.escape(plugin.metadata.name)}**\n" f"\n > {plugin.metadata.description}") else: - reply += (f"**{plugin.name}**\n" + reply += (f"**{md.escape(plugin.name)}**\n" f"\n > {lang.get('npm.no_description')}") - # if await GROUP_ADMIN(bot=bot, event=event) or await GROUP_OWNER(bot=bot, event=event) or await SUPERUSER(bot=bot, event=event): + + 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_enable = md.button(lang.get('npm.enable'), f'enable-plugin {plugin.module_name}') - btn_disable = md.button(lang.get('npm.disable'), f'disable-plugin {plugin.module_name}') - reply += f"\n > {btn_enable} {btn_disable}" + # 添加启用/停用插件按钮 + 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}') + reply += f" {btn_toggle}" + if await SUPERUSER(bot, event): plugin_in_database = plugin_db.first(InstalledPlugin, 'module_name = ?', plugin.module_name) + # 添加移除插件 btn_remove = ( - md.button(lang.get('npm.uninstall'), f'lnpm remove {plugin.module_name}')) if plugin_in_database else lang.get( + md.button(lang.get('npm.uninstall'), f'npm remove {plugin.module_name}')) if plugin_in_database else lang.get( 'npm.uninstall') - reply += f" {btn_remove}" + 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} global') + reply += f" {btn_remove} {btn_toggle_global}" + reply += "\n\n***\n" await send_markdown(reply, bot, event=event) + + +@toggle_plugin.handle() +async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): + # 判断会话类型 + ulang = get_user_lang(str(event.user_id)) + plugin_module_name = result.args.get("plugin_name") + + toggle = result.header_result == 'enable-plugin' # 判断是启用还是停用 + current_enable = get_plugin_current_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: + await toggle_plugin.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_module_name)) + + if current_enable == toggle: + await toggle_plugin.finish( + ulang.get("npm.plugin_already", NAME=plugin_module_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))) + + if event.message_type == "private": + session = user_db.first(User, "user_id = ?", event.user_id, default=User(user_id=event.user_id)) + else: + if await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event): + session = group_db.first(GroupChat, "group_id = ?", event.group_id, default=GroupChat(group_id=event.group_id)) + else: + return + # 启用 已停用的默认启用插件 将其从停用列表移除 + # 启用 已停用的默认停用插件 将其放到启用列表 + # 停用 已启用的默认启用插件 将其放到停用列表 + # 停用 已启用的默认停用插件 将其从启用列表移除 + try: + if toggle: + if default_enable: + session.disabled_plugins.remove(plugin_module_name) + else: + session.enabled_plugins.append(plugin_module_name) + else: + if default_enable: + session.disabled_plugins.append(plugin_module_name) + else: + session.enabled_plugins.remove(plugin_module_name) + except Exception as e: + await toggle_plugin.finish( + ulang.get( + "npm.toggle_failed", + NAME=plugin_module_name, + STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"), + ERROR=str(e)) + ) + + if event.message_type == "private": + user_db.save(session) + else: + group_db.save(session) + + +@run_preprocessor +async def _(event: T_MessageEvent, matcher: Matcher): + plugin = matcher.plugin + nonebot.logger.info(f"Plugin: {plugin.module_name}") diff --git a/src/plugins/liteyuki_plugin_user/__init__.py b/src/plugins/liteyuki_plugin_user/__init__.py index 9bb7d4cd..ff3e4409 100644 --- a/src/plugins/liteyuki_plugin_user/__init__.py +++ b/src/plugins/liteyuki_plugin_user/__init__.py @@ -9,6 +9,8 @@ __plugin_meta__ = PluginMetadata( usage="", homepage="https://github.com/snowykami/LiteyukiBot", extra={ - "liteyuki_plugin": True, + "liteyuki": True, + "toggleable" : False, + "default_enable" : True, } ) diff --git a/src/resources/lang/en.lang b/src/resources/lang/en.lang index 368a3de4..c5eb6b79 100644 --- a/src/resources/lang/en.lang +++ b/src/resources/lang/en.lang @@ -15,8 +15,11 @@ data_manager.migrate_success=Model {NAME} migration successful npm.loaded_plugins=Loaded plugins npm.total=Total {TOTAL} npm.help=Help +npm.usage=Usage npm.disable=Disable +npm.disable_global=DisableGlobal npm.enable=Enable +npm.enable_global=EnableGlobal npm.install=Install npm.uninstall=Uninstall npm.installing=Installing {NAME}... @@ -28,13 +31,20 @@ npm.search_result=Search results npm.search_no_result=No result found npm.too_many_results=Too many results found, {HIDE_NUM} hidden, please refine your search npm.install_success={NAME} installed successfully -npm.install_failed={NAME} installation failed +npm.install_failed={NAME} installation failed, please check the console for more information, or visit the plugin's {HOMEPAGE} +npm.remove_success={NAME} uninstalled successfully +npm.remove_failed={NAME} uninstallation failed +npm.load_failed={NAME} loading failed, please check the console for more information, or visit the plugin's {HOMEPAGE} +npm.plugin_not_found={NAME} not found, please check the plugin's name npm.plugin_not_installed={NAME} is not installed npm.plugin_already_installed={NAME} is already installed npm.author=Author npm.homepage=Homepage +npm.pypi=PyPI npm.next_page=Next npm.prev_page=Prev +npm.plugin_cannot_be_toggled=This plugin {NAME} cannot be toggled +npm.toggle_failed=Failed to {STATUS} {NAME}: {ERROR} user.profile_manager.query=Your {ATTR} is {VALUE} user.profile_manager.set=Your {ATTR} has been set to {VALUE} \ No newline at end of file diff --git a/src/resources/lang/ja.lang b/src/resources/lang/ja.lang index 66c619a9..205db7a0 100644 --- a/src/resources/lang/ja.lang +++ b/src/resources/lang/ja.lang @@ -15,8 +15,11 @@ data_manager.migrate_success=データが正常に移行されました {NAME} npm.loaded_plugins=読み込まれたプラグイン npm.total=合計 {TOTAL} npm.help=ヘルプ +npm.usage=使用法 npm.disable=無効 +npm.disable_global=グローバル無効 npm.enable=有効 +npm.enable_global=グローバル有効 npm.install=インストール npm.uninstall=アンインストール npm.installing={NAME} インストール中 @@ -28,13 +31,20 @@ npm.search_result=検索結果 npm.search_no_result=検索結果がありません npm.too_many_results=検索結果が多すぎます。{HIDE_NUM} 件の結果が非表示になりました npm.install_success={NAME} が正常にインストールされました -npm.install_failed={NAME} のインストールに失敗しました +npm.install_failed={NAME} のインストールに失敗しました, 詳細はログを参照してください, またはプラグインの作者に連絡してください{HOMEPAGE} +npm.remove_success={NAME} が正常にアンインストールされました +npm.remove_failed={NAME} のアンインストールに失敗しました +npm.load_failed={NAME} の読み込みに失敗しました,詳細はログを参照してください, またはプラグインの作者に連絡してください{HOMEPAGE} +npm.plugin_not_found={NAME} は見つかりません,スペルをチェックしてください npm.plugin_not_installed={NAME} はインストールされていません npm.plugin_already_installed={NAME} は既にインストールされています npm.author=著者 npm.homepage=ホームページ +npm.pypi=PyPI npm.next_page=次のページ npm.prev_page=前のページ +npm.plugin_cannot_be_toggled=このプラグイン {NAME} は無効にできません +npm.toggle_failed=プラグイン {NAME} の{STATUS}切り替えに失敗しました:{ERROR} user.profile_manager.query=あなたのプロファイル情報 {ATTR} は {VALUE} です -user.profile_manager.set=あなたのプロファイル情報 {ATTR} が {VALUE} に設定されました +user.profile_manager.set=あなたのプロファイル情報 {ATTR} が {VALUE} に設定されました \ No newline at end of file diff --git a/src/resources/lang/zh-CN.lang b/src/resources/lang/zh-CN.lang index e6ae8e93..04be77b5 100644 --- a/src/resources/lang/zh-CN.lang +++ b/src/resources/lang/zh-CN.lang @@ -15,8 +15,11 @@ data_manager.migrate_success=数据模型{NAME}迁移成功 npm.loaded_plugins=已加载插件 npm.total=总计 {TOTAL} npm.help=帮助 +npm.usage=用法 npm.disable=停用 +npm.disable_global=全局停用 npm.enable=启用 +npm.enable_global=全局启用 npm.install=安装 npm.uninstall=卸载 npm.installing=正在安装 {NAME} @@ -26,17 +29,23 @@ npm.store_update_success=插件商店数据更新成功 npm.store_update_failed=插件商店数据更新失败 npm.search_result=搜索结果 npm.search_no_result=无搜索结果 -npm.too_many_results=搜索结果过多,{HIDE_NUM}已隐藏,请限制关键字 +npm.too_many_results=内容过多,{HIDE_NUM}项已隐藏,请限制关键字搜索 npm.install_success={NAME} 安装成功 -npm.install_failed={NAME} 安装失败 +npm.install_failed={NAME} 安装失败,请查看日志获取详细信息,如不能解决,请访问{HOMEPAGE}寻求帮助 npm.remove_success={NAME} 卸载成功 npm.remove_failed={NAME} 卸载失败 +npm.load_failed={NAME} 加载失败,请在控制台查看详细信息,检查依赖或配置是否正确,如不能解决,请访问{HOMEPAGE}寻求帮助 +npm.plugin_not_found=未在商店中找到 {NAME},请尝试更新商店信息或检查拼写 npm.plugin_not_installed={NAME} 未安装 npm.plugin_already_installed={NAME} 已安装,请勿重复安装 npm.author=作者 -npm.homepage=项目主页 +npm.homepage=主页 +npm.pypi=PyPI npm.next_page=下一页 npm.prev_page=上一页 +npm.plugin_cannot_be_toggled=插件 {NAME} 无法被启用或停用 +npm.plugin_already=插件 {NAME} 已经是 {STATUS} 状态,无需重复操作 +npm.toggle_failed=插件 {NAME} {STATUS} 失败: {ERROR} user.profile_manager.query=你的个人信息 {ATTR} 为 {VALUE} user.profile_manager.set=你的个人信息 {ATTR} 已设置为 {VALUE} \ No newline at end of file diff --git a/src/utils/data.py b/src/utils/data.py index b0bad9d9..2fd9babb 100644 --- a/src/utils/data.py +++ b/src/utils/data.py @@ -87,7 +87,7 @@ class Database(BaseORMAdapter): list : 'TEXT' } - DEFAULT_TYPE = { + DEFAULT_VALUE = { 'TEXT' : '', 'INTEGER': 0, 'REAL' : 0.0 @@ -95,6 +95,8 @@ class Database(BaseORMAdapter): FOREIGNID = 'FOREIGNID' JSON = 'JSON' + LIST = 'LIST' + DICT = 'DICT' ID = '$ID' def __init__(self, db_name: str): @@ -131,28 +133,34 @@ class Database(BaseORMAdapter): table_fields = self.cursor.fetchall() table_fields = [field[1] for field in table_fields] - raw_fields = model.__annotations__.keys() + raw_fields, raw_types = zip(*model.__annotations__.items()) # 获取模型字段,若有模型则添加FOREIGNID前缀,若为BaseIterable则添加JSON前缀,用多行if判断 model_fields = [] model_types = [] - for field in raw_fields: - if isinstance(model.__annotations__[field], type(LiteModel)): + for field, r_type in zip(raw_fields, raw_types): + if isinstance(r_type, type(LiteModel)): model_fields.append(f'{self.FOREIGNID}{field}') model_types.append('TEXT') - elif isinstance(model.__annotations__[field], types.GenericAlias): + elif isinstance(r_type, list): + model_fields.append(f'{self.LIST}{field}') + model_types.append('TEXT') + elif isinstance(r_type, dict): + model_fields.append(f'{self.DICT}{field}') + model_types.append('TEXT') + elif isinstance(r_type, types.GenericAlias): model_fields.append(f'{self.JSON}{field}') model_types.append('TEXT') else: model_fields.append(field) - model_types.append(self.type_map.get(model.__annotations__[field], 'TEXT')) + model_types.append(self.type_map.get(r_type, 'TEXT')) - # 检测新字段 - for field, type_ in zip(model_fields, model_types): + # 检测新字段或字段类型是否有变化,有则增删字段,已经加了前缀类型 + for field, type_, r_type in zip(model_fields, model_types, raw_types): if field not in table_fields: nonebot.logger.debug(f'ALTER TABLE {table_name} ADD COLUMN {field} {type_}') self.cursor.execute(f'ALTER TABLE {table_name} ADD COLUMN {field} {type_}') # 在原有的行中添加新字段对应类型的默认值,从DEFAULT_TYPE中获取 - self.cursor.execute(f'UPDATE {table_name} SET {field} = ? WHERE {field} IS NULL', (self.DEFAULT_TYPE.get(type_, ""),)) + self.cursor.execute(f'UPDATE {table_name} SET {field} = ? WHERE {field} IS NULL', (self.DEFAULT_VALUE.get(type_, ""),)) # 检测多余字段,除了id字段 for field in table_fields: @@ -161,7 +169,7 @@ class Database(BaseORMAdapter): self.cursor.execute(f'ALTER TABLE {table_name} DROP COLUMN {field}') self.conn.commit() - nonebot.logger.success(f'Table {table_name} migrated successfully') + nonebot.logger.debug(f'Table {table_name} migrated successfully') def save(self, *models: LiteModel) -> int | tuple: """存储数据,检查id字段,如果有id字段则更新,没有则插入 @@ -326,7 +334,14 @@ class Database(BaseORMAdapter): new_d[k.replace(self.FOREIGNID, '')] = load( dict(self.cursor.execute(f'SELECT * FROM {v.split(":", 2)[1]} WHERE id = ?', (v.split(":", 2)[2],)).fetchone())) elif k.startswith(self.JSON): + if v == '': v = '[]' new_d[k.replace(self.JSON, '')] = load(json.loads(v)) + elif k.startswith(self.LIST): + if v == '': v = '[]' + new_d[k.replace(self.LIST, '')] = load(json.loads(v)) + elif k.startswith(self.DICT): + if v == '': v = '{}' + new_d[k.replace(self.DICT, '')] = load(json.loads(v)) else: new_d[k] = v elif isinstance(d, list | tuple | set): diff --git a/src/utils/data_manager.py b/src/utils/data_manager.py index 4e850cc4..f70256ee 100644 --- a/src/utils/data_manager.py +++ b/src/utils/data_manager.py @@ -5,6 +5,7 @@ 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')) @@ -12,6 +13,16 @@ class User(LiteModel): user_id: str username: str = "" lang: str = "en" + 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): @@ -20,4 +31,5 @@ class InstalledPlugin(LiteModel): def auto_migrate(): user_db.auto_migrate(User) + group_db.auto_migrate(GroupChat) plugin_db.auto_migrate(InstalledPlugin) diff --git a/src/utils/message.py b/src/utils/message.py index 81dd83a1..4049b5d5 100644 --- a/src/utils/message.py +++ b/src/utils/message.py @@ -76,7 +76,7 @@ async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None, class Markdown: @staticmethod def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str: - """生成点击按钮 + """生成点击回调按钮 Args: name: 按钮显示内容 cmd: 发送的命令,已在函数内url编码,不需要再次编码 @@ -91,7 +91,7 @@ class Markdown: @staticmethod def link(name: str, url: str) -> str: - """生成链接 + """生成点击链接按钮 Args: name: 链接显示内容 url: 链接地址 @@ -101,3 +101,18 @@ class Markdown: """ return f"[🔗{name}]({url})" + + @staticmethod + def escape(text: str) -> str: + """转义特殊字符 + Args: + text: 需要转义的文本,请勿直接把整个markdown文本传入,否则会转义掉所有字符 + + Returns: + 转义后的文本 + + """ + chars = "*[]()~_-`>#+-=|{}.!" + for char in chars: + text = text.replace(char, f"\\\\{char}") + return text