diff --git a/requirements.txt b/requirements.txt index caa15ec8..731c79dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +aiohttp==3.9.3 +aiofiles==23.2.1 arclet-alconna==1.8.5 arclet-alconna-tools==0.7.0 dash==2.16.1 diff --git a/src/plugins/liteyuki_plugin_npm/__init__.py b/src/plugins/liteyuki_plugin_npm/__init__.py index 5172c8de..53606cc0 100644 --- a/src/plugins/liteyuki_plugin_npm/__init__.py +++ b/src/plugins/liteyuki_plugin_npm/__init__.py @@ -1,6 +1,7 @@ from nonebot.plugin import PluginMetadata from .manager import * from .installer import * +from .helper import * __author__ = "snowykami" diff --git a/src/plugins/liteyuki_plugin_npm/installer.py b/src/plugins/liteyuki_plugin_npm/installer.py index 6e21aa42..078ecc93 100644 --- a/src/plugins/liteyuki_plugin_npm/installer.py +++ b/src/plugins/liteyuki_plugin_npm/installer.py @@ -1,5 +1,154 @@ +import json +import os.path +import shutil +from typing import Optional + +import nonebot +from arclet.alconna import Arparma, MultiVar +from nonebot.permission import SUPERUSER +from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand import pip +import aiohttp, aiofiles +from typing_extensions import Any + +from src.utils.data import LiteModel +from src.utils.language import get_user_lang +from src.utils.message import button, send_markdown +from src.utils.resource import get_res +from src.utils.typing import T_Bot, T_MessageEvent + +npm_alc = on_alconna( + Alconna( + "lnpm", + Subcommand( + "update", + alias=["u"], + ), + Subcommand( + "search", + Args["keywords", MultiVar(str)]["page", int, 1], + alias=["s"], + ), + Subcommand( + "install", + Args["plugin_name", str], + alias=["i"], + ), + Subcommand( + "remove", + Args["plugin_name", str], + alias=["rm"], + ), + ), + 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): + ulang = get_user_lang(str(event.user_id)) + + if not os.path.exists("data/liteyuki/plugins.json"): + shutil.move(get_res('unsorted/plugins.json'), "data/liteyuki/plugins.json") + nonebot.logger.info("Please update plugin store data file.") + + if result.subcommands.get("update"): + r = await npm_update() + if r: + await npm_alc.finish(ulang.get("npm.store_update_success")) + else: + await npm_alc.finish(ulang.get("npm.store_update_failed")) + + elif result.subcommands.get("search"): + keywords: list[str] = result.subcommands["search"].args.get("keywords") + rs = await npm_search(keywords) + if len(rs): + reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***" + for plugin in rs[:min(10, len(rs))]: + reply += (f"\n{button(ulang.get('npm.install'), 'lnpm install %s' % plugin.module_name)} | **{plugin.name}**\n" + f"\n > **{plugin.desc}**\n" + f"\n > {ulang.get('npm.author')}: {plugin.author} | [🔗{ulang.get('npm.homepage')}]({plugin.homepage})\n\n***\n") + if len(rs) > 10: + reply += (f"\n{ulang.get('npm.too_many_results')}" + f"\n{button(ulang.get('npm.prev_page'), 'lnpm search %s %s' % (' '.join(keywords), 2))} | " + f"{button(ulang.get('npm.next_page'), 'lnpm search %s %s' % (' '.join(keywords), 2))}") + else: + reply = ulang.get("npm.search_no_result") + await send_markdown(reply, bot, event=event) + + +async def npm_update() -> bool: + """ + 更新本地插件json缓存 + + Returns: + bool: 是否成功更新 + """ + url_list = [ + "https://registry.nonebot.dev/plugins.json", + ] + # 用aiohttp请求json文件,成功就覆盖本地文件,否则尝试下一个url + for url in url_list: + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status == 200: + async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f: + data = await resp.read() + await f.write(data) + nonebot.logger.info() + return True + return False + + +async def npm_search(keywords: list[str]) -> list[StorePlugin]: + """ + 搜索插件 + + Args: + keywords (list[str]): 关键词列表 + + Returns: + list[StorePlugin]: 插件列表 + """ + results = [] + 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: + plugin_text = ' '.join( + [ + plugin.name, + plugin.desc, + plugin.author, + plugin.module_name, + plugin.project_link, + plugin.homepage, + ' '.join([tag.label for tag in plugin.tags]) + ] + ) + if all([keyword in plugin_text for keyword in keywords]): + results.append(plugin) + return results + def install(plugin_name) -> bool: try: diff --git a/src/plugins/liteyuki_plugin_npm/manager.py b/src/plugins/liteyuki_plugin_npm/manager.py index f954a42e..68784fac 100644 --- a/src/plugins/liteyuki_plugin_npm/manager.py +++ b/src/plugins/liteyuki_plugin_npm/manager.py @@ -6,7 +6,7 @@ from src.utils.message import button, send_markdown from src.utils.typing import T_Bot, T_MessageEvent from src.utils.language import get_user_lang -list_plugins = on_command("list-plugin", aliases={"列出插件"}, priority=0, permission=SUPERUSER) +list_plugins = on_command("list-plugin", aliases={"列出插件", "插件列表"}, priority=0, permission=SUPERUSER) toggle_plugin = on_command("enable-plugin", aliases={"启用插件", "禁用插件", "disable-plugin"}, priority=0, permission=SUPERUSER) @@ -17,9 +17,11 @@ async def _(event: T_MessageEvent, bot: T_Bot): for plugin in nonebot.get_loaded_plugins(): # 检查是否有 metadata 属性 if plugin.metadata: - reply += (f"\n{button(lang.get('npm.disable'), 'disable-plugin %s' % plugin.name, False, False)} **{plugin.metadata.name}**\n" + reply += (f"\n{button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} " + f"**{plugin.metadata.name}**\n" f"\n > {plugin.metadata.description}\n\n***\n") else: - reply += (f"\n{button(lang.get('npm.disable'), 'disable-plugin %s' % plugin.name, False, False)} **{plugin.name}**\n" + reply += (f"\n{button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} " + f"**{plugin.name}**\n" f"\n > {lang.get('npm.no_description')}\n\n***\n") await send_markdown(reply, bot, event=event) diff --git a/src/resources/lang/en.lang b/src/resources/lang/en.lang index b0e56ed8..5d0c9f5b 100644 --- a/src/resources/lang/en.lang +++ b/src/resources/lang/en.lang @@ -12,9 +12,21 @@ main.monitor.usage=Usage npm.loaded_plugins=Loaded plugins npm.total=Total {TOTAL} +npm.help=Help npm.disable=Disable npm.enable=Enable +npm.install=Install +npm.uninstall=Uninstall npm.no_description=No description +npm.store_update_success=Plugin store data updated successfully +npm.store_update_failed=Plugin store data update failed +npm.search_result=Search results +npm.search_no_result=No result found +npm.too_many_results=Too many results found +npm.author=Author +npm.homepage=Homepage +npm.next_page=Next +npm.prev_page=Prev 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/zh-CN.lang b/src/resources/lang/zh-CN.lang index 2d1563c4..9a795662 100644 --- a/src/resources/lang/zh-CN.lang +++ b/src/resources/lang/zh-CN.lang @@ -12,9 +12,21 @@ main.monitor.usage=使用率 npm.loaded_plugins=已加载插件 npm.total=总计 {TOTAL} +npm.help=帮助 npm.disable=停用 npm.enable=启用 +npm.install=安装 +npm.uninstall=卸载 npm.no_description=无描述 +npm.store_update_success=插件商店数据更新成功 +npm.store_update_failed=插件商店数据更新失败 +npm.search_result=搜索结果 +npm.search_no_result=无搜索结果 +npm.too_many_results=搜索结果过多,请翻页查看 +npm.author=作者 +npm.homepage=主页 +npm.next_page=下一页 +npm.prev_page=上一页 user.profile_manager.query=你的个人信息 {ATTR} 为 {VALUE} user.profile_manager.set=你的个人信息 {ATTR} 已设置为 {VALUE} \ No newline at end of file diff --git a/src/utils/language.py b/src/utils/language.py index b6bd9cdd..2c41f968 100644 --- a/src/utils/language.py +++ b/src/utils/language.py @@ -5,7 +5,7 @@ import json import locale import os -from typing import Any +from typing_extensions import Any import nonebot diff --git a/src/utils/message.py b/src/utils/message.py index 2e2d5c80..f29806c3 100644 --- a/src/utils/message.py +++ b/src/utils/message.py @@ -74,15 +74,15 @@ async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None, def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str: - """生成点击按钮的链接 + """生成点击按钮 Args: - name: - cmd: + name: 按钮显示内容 + cmd: 发送的命令,已在函数内url编码,不需要再次编码 reply: 是否以回复的方式发送消息 - enter: 自动发送消息则为True + enter: 自动发送消息则为True,否则填充到输入框 Returns: - markdown格式的链接 + markdown格式的可点击回调按钮 """ return f"[{name}](mqqapi://aio/inlinecmd?command={encode_url(cmd)}&reply={str(reply).lower()}&enter={str(enter).lower()})" diff --git a/src/utils/resource.py b/src/utils/resource.py index 51974ef7..5255cc28 100644 --- a/src/utils/resource.py +++ b/src/utils/resource.py @@ -2,11 +2,12 @@ import os import nonebot import yaml +from typing_extensions import Any from src.utils.data import LiteModel _resource_data = {} -_loaded_resource_packs = [] # 按照加载顺序排序 +_loaded_resource_packs = [] # 按照加载顺序排序 class ResourceMetadata(LiteModel): @@ -41,3 +42,14 @@ def load_resource_from_dir(path: str): from src.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: + """ + 获取资源包中的文件 + Args: + default: 默认 + path: 文件相对路径 + Returns: 文件绝对路径 + """ + return _resource_data.get(path, default) diff --git a/src/utils/tools.py b/src/utils/tools.py index 62deac4a..c7142e2a 100644 --- a/src/utils/tools.py +++ b/src/utils/tools.py @@ -49,3 +49,26 @@ def de_escape(text: str) -> str: def encode_url(text: str) -> str: return quote(text, safe="") + + +def keywords_in_text(keywords: list[str], text: str, all_matched: bool) -> bool: + """ + 检查关键词是否在文本中 + Args: + keywords: 关键词列表 + text: 文本 + all_matched: 是否需要全部匹配 + + Returns: + + """ + if all_matched: + for keyword in keywords: + if keyword not in text: + return False + return True + else: + for keyword in keywords: + if keyword in text: + return True + return False