1
0
forked from bot/app

feat: 添加了自动安装插件功能

This commit is contained in:
远野千束 2024-03-21 12:10:24 +08:00
parent ca997f727a
commit e24c5c912e
8 changed files with 146 additions and 37 deletions

View File

@ -1 +1,13 @@
from src.utils.data import Database, LiteModel
from src.utils.data_manager import plugin_db
LNPM_COMMAND_START = "lnpm" LNPM_COMMAND_START = "lnpm"
class InstalledPlugin(LiteModel):
module_name: str
plugin_db.auto_migrate(InstalledPlugin)

View File

@ -1,23 +1,27 @@
import json import json
import os.path import os.path
import shutil import shutil
import sys
from io import StringIO
from typing import Optional from typing import Optional
import nonebot import nonebot
from arclet.alconna import Arparma, MultiVar from arclet.alconna import Arparma, MultiVar
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from nonebot.utils import run_sync
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand
import pip import pip
import aiohttp, aiofiles import aiohttp, aiofiles
from typing_extensions import Any from typing_extensions import Any
from src.utils.data import LiteModel
from src.utils.language import get_user_lang from src.utils.language import get_user_lang
from src.utils.message import button, send_markdown from src.utils.message import Markdown as md, send_markdown
from src.utils.resource import get_res from src.utils.resource import get_res
from src.utils.typing import T_Bot, T_MessageEvent from src.utils.typing import T_Bot, T_MessageEvent
from .common import *
npm_alc = on_alconna( npm_alc = on_alconna(
Alconna( Alconna(
"lnpm", "lnpm",
@ -28,17 +32,17 @@ npm_alc = on_alconna(
Subcommand( Subcommand(
"search", "search",
Args["keywords", MultiVar(str)]["page", int, 1], Args["keywords", MultiVar(str)]["page", int, 1],
alias=["s"], alias=["s", "搜索"],
), ),
Subcommand( Subcommand(
"install", "install",
Args["plugin_name", str], Args["plugin_name", str],
alias=["i"], alias=["i", "安装"],
), ),
Subcommand( Subcommand(
"remove", "remove",
Args["plugin_name", str], Args["plugin_name", str],
alias=["rm"], alias=["rm", "移除", "卸载"],
), ),
), ),
permission=SUPERUSER permission=SUPERUSER
@ -82,20 +86,49 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
elif result.subcommands.get("search"): elif result.subcommands.get("search"):
keywords: list[str] = result.subcommands["search"].args.get("keywords") keywords: list[str] = result.subcommands["search"].args.get("keywords")
rs = await npm_search(keywords) rs = await npm_search(keywords)
max_show = 20
if len(rs): if len(rs):
reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***" reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***"
for plugin in rs[:min(10, len(rs))]: for plugin in rs[:min(max_show, len(rs))]:
reply += (f"\n{button(ulang.get('npm.install'), 'lnpm install %s' % plugin.module_name)} | **{plugin.name}**\n" btn_install = md.button(ulang.get('npm.install'), 'lnpm install %s' % plugin.module_name)
link_page = md.link(ulang.get('npm.homepage'), plugin.homepage)
reply += (f"\n{btn_install} | **{plugin.name}**\n"
f"\n > **{plugin.desc}**\n" f"\n > **{plugin.desc}**\n"
f"\n > {ulang.get('npm.author')}: {plugin.author} | [🔗{ulang.get('npm.homepage')}]({plugin.homepage})\n\n***\n") f"\n > {ulang.get('npm.author')}: {plugin.author} | {link_page}\n\n***\n")
if len(rs) > 10: if len(rs) > max_show:
reply += (f"\n{ulang.get('npm.too_many_results')}" 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: else:
reply = ulang.get("npm.search_no_result") reply = ulang.get("npm.search_no_result")
await send_markdown(reply, bot, event=event) await send_markdown(reply, bot, event=event)
elif result.subcommands.get("install"):
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
r, log = npm_install(plugin_name)
log = log.replace("\\", "/")
if r:
nonebot.load_plugin(plugin_name)
installed_plugin = InstalledPlugin(module_name=plugin_name)
store_plugin = await get_store_plugin(plugin_name)
plugin_db.save(installed_plugin)
await send_markdown(
f"**{ulang.get('npm.install_success', NAME=store_plugin.name)}**\n\n"
f"```\n{log}\n```",
bot,
event=event
)
else:
await send_markdown(
f"{ulang.get('npm.install_success', NAME=plugin_name)}\n\n"
f"```\n{log}\n```",
bot,
event=event
)
elif result.subcommands.get("remove"):
plugin_name: str = result.subcommands["remove"].args.get("plugin_name")
await npm_alc.finish(ulang.get("npm.remove_success"))
async def npm_update() -> bool: async def npm_update() -> bool:
""" """
@ -107,7 +140,6 @@ async def npm_update() -> bool:
url_list = [ url_list = [
"https://registry.nonebot.dev/plugins.json", "https://registry.nonebot.dev/plugins.json",
] ]
# 用aiohttp请求json文件成功就覆盖本地文件否则尝试下一个url
for url in url_list: for url in url_list:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as resp: async with session.get(url) as resp:
@ -115,7 +147,6 @@ async def npm_update() -> bool:
async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f: async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f:
data = await resp.read() data = await resp.read()
await f.write(data) await f.write(data)
return True return True
return False return False
@ -150,10 +181,54 @@ async def npm_search(keywords: list[str]) -> list[StorePlugin]:
return results return results
def install(plugin_name) -> bool: 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]:
"""
Args:
plugin_module_name:
Returns:
tuple[bool, str]:
"""
buffer = StringIO()
sys.stdout = buffer
sys.stderr = buffer
mirrors = [
"https://pypi.tuna.tsinghua.edu.cn/simple",
"https://pypi.org/simple",
]
# 使用pip安装包对每个镜像尝试一次成功后返回值
success = False
for mirror in mirrors:
try: try:
pip.main(['install', plugin_name]) result = pip.main(['install', plugin_module_name, "-i", mirror])
return True success = result == 0
break
except Exception as e: except Exception as e:
print(e) success = False
return False continue
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
return success, buffer.getvalue()

View File

@ -2,7 +2,7 @@ import nonebot.plugin
from nonebot import on_command from nonebot import on_command
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from src.utils.message import button, send_markdown from src.utils.message import Markdown as md, send_markdown
from src.utils.typing import T_Bot, T_MessageEvent from src.utils.typing import T_Bot, T_MessageEvent
from src.utils.language import get_user_lang from src.utils.language import get_user_lang
@ -17,11 +17,11 @@ async def _(event: T_MessageEvent, bot: T_Bot):
for plugin in nonebot.get_loaded_plugins(): for plugin in nonebot.get_loaded_plugins():
# 检查是否有 metadata 属性 # 检查是否有 metadata 属性
if plugin.metadata: if plugin.metadata:
reply += (f"\n{button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} " reply += (f"\n{md.button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} "
f"**{plugin.metadata.name}**\n" f"**{plugin.metadata.name}**\n"
f"\n > {plugin.metadata.description}\n\n***\n") f"\n > {plugin.metadata.description}\n\n***\n")
else: else:
reply += (f"\n{button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} " reply += (f"\n{md.button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} "
f"**{plugin.name}**\n" f"**{plugin.name}**\n"
f"\n > {lang.get('npm.no_description')}\n\n***\n") f"\n > {lang.get('npm.no_description')}\n\n***\n")
await send_markdown(reply, bot, event=event) await send_markdown(reply, bot, event=event)

View File

@ -22,7 +22,9 @@ npm.store_update_success=Plugin store data updated successfully
npm.store_update_failed=Plugin store data update failed npm.store_update_failed=Plugin store data update failed
npm.search_result=Search results npm.search_result=Search results
npm.search_no_result=No result found npm.search_no_result=No result found
npm.too_many_results=Too many results found npm.too_many_results=Too many results found, please refine your search
npm.install_success={NAME} installed successfully
npm.install_failed={NAME} installation failed
npm.author=Author npm.author=Author
npm.homepage=Homepage npm.homepage=Homepage
npm.next_page=Next npm.next_page=Next

View File

@ -23,6 +23,8 @@ npm.store_update_failed=プラグインストアのデータの更新に失敗
npm.search_result=検索結果 npm.search_result=検索結果
npm.search_no_result=検索結果がありません npm.search_no_result=検索結果がありません
npm.too_many_results=検索結果が多すぎます。ページをめくってください npm.too_many_results=検索結果が多すぎます。ページをめくってください
npm.install_success={NAME} が正常にインストールされました
npm.install_failed={NAME} のインストールに失敗しました
npm.author=著者 npm.author=著者
npm.homepage=ホームページ npm.homepage=ホームページ
npm.next_page=次のページ npm.next_page=次のページ

View File

@ -22,7 +22,9 @@ npm.store_update_success=插件商店数据更新成功
npm.store_update_failed=插件商店数据更新失败 npm.store_update_failed=插件商店数据更新失败
npm.search_result=搜索结果 npm.search_result=搜索结果
npm.search_no_result=无搜索结果 npm.search_no_result=无搜索结果
npm.too_many_results=搜索结果过多,请翻页查看 npm.too_many_results=搜索结果过多,请限制关键字
npm.install_success={NAME} 安装成功
npm.install_failed={NAME} 安装失败
npm.author=作者 npm.author=作者
npm.homepage=主页 npm.homepage=主页
npm.next_page=下一页 npm.next_page=下一页

View File

@ -5,6 +5,7 @@ from src.utils.data import LiteModel, Database as DB
DATA_PATH = "data/liteyuki" DATA_PATH = "data/liteyuki"
user_db = DB(os.path.join(DATA_PATH, 'users.ldb')) user_db = DB(os.path.join(DATA_PATH, 'users.ldb'))
plugin_db = DB(os.path.join(DATA_PATH, 'plugins.ldb'))
class User(LiteModel): class User(LiteModel):

View File

@ -73,7 +73,9 @@ async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None,
return data return data
def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str: class Markdown:
@staticmethod
def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str:
"""生成点击按钮 """生成点击按钮
Args: Args:
name: 按钮显示内容 name: 按钮显示内容
@ -86,3 +88,16 @@ def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str:
""" """
return f"[{name}](mqqapi://aio/inlinecmd?command={encode_url(cmd)}&reply={str(reply).lower()}&enter={str(enter).lower()})" return f"[{name}](mqqapi://aio/inlinecmd?command={encode_url(cmd)}&reply={str(reply).lower()}&enter={str(enter).lower()})"
@staticmethod
def link(name: str, url: str) -> str:
"""生成链接
Args:
name: 链接显示内容
url: 链接地址
Returns:
markdown格式的链接
"""
return f"[链接{name}]({encode_url(url)})"