diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index cfef1b3d..039a40e9 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -10,10 +10,12 @@ export default defineUserConfig({ description: "LiteyukiBot | 轻雪机器人 | An OneBot Standard ChatBot | 一个OneBot标准的聊天机器人", head: [ // 设置 favor.ico,.vuepress/public 下 - [ - 'link', {rel: 'icon', href: 'https://cdn.liteyuki.icu/favicon.ico'}, + ['link', {rel: 'icon', href: 'https://cdn.liteyuki.icu/favicon.ico'},], + + ['link', {rel: 'stylesheet', href: 'https://fonts.cdnfonts.com/css/colortube-2'}], + + ['link', {rel: 'stylesheet', href: 'https://cdn.bootcdn.net/ajax/libs/firacode/6.2.0/fira_code.min.css'}], - ], [ "meta", { diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts index 4fc30c54..1639c925 100644 --- a/docs/.vuepress/navbar.ts +++ b/docs/.vuepress/navbar.ts @@ -13,7 +13,7 @@ export default navbar([ prefix: "usage/", }, { - text: "主题商店", + text: "资源商店", link: "/store/", prefix: "store/", } diff --git a/docs/.vuepress/public/assets/fonts/colortube.otf b/docs/.vuepress/public/assets/fonts/colortube.otf new file mode 100644 index 00000000..586f75c2 Binary files /dev/null and b/docs/.vuepress/public/assets/fonts/colortube.otf differ diff --git a/docs/.vuepress/public/assets/resources.json b/docs/.vuepress/public/assets/resources.json index 9ed0b281..07020e3a 100644 --- a/docs/.vuepress/public/assets/resources.json +++ b/docs/.vuepress/public/assets/resources.json @@ -19,10 +19,10 @@ "link": "https://cdn.liteyuki.icu/static/lrp/MapleMonoFonts.zip" }, { - "name": "示例包1", + "name": "野兽先辈主题HomoTheme", "author": "SnowyKami", - "description": "A simple bot that shows the status of the bot and the server.", - "link": "" + "description": "野兽先辈主题包,114514!", + "link": "https://cdn.liteyuki.icu/static/lrp/HomoTheme.zip" }, { "name": "示例包2", diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index 6e9bb8e2..e12a465d 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -1,19 +1,26 @@ -import { sidebar } from "vuepress-theme-hope"; +import {sidebar} from "vuepress-theme-hope"; export default sidebar({ - "/": [ - "", - { - text: "项目部署", - icon: "laptop-code", - prefix: "deployment/", - children: "structure", - }, - { - text: "使用手册", - icon: "book", - prefix: "usage/", - children: "structure", - }, - ], + "/": [ + "", + { + text: "项目部署", + icon: "laptop-code", + prefix: "deployment/", + children: "structure", + }, + { + text: "使用手册", + icon: "book", + prefix: "usage/", + children: "structure", + }, + { + text: "资源商店", + icon: "store", + prefix: "store/", + link: "/store/", + children: "structure", + } + ], }); diff --git a/docs/.vuepress/styles/index.scss b/docs/.vuepress/styles/index.scss index f6af3878..a8717fe4 100644 --- a/docs/.vuepress/styles/index.scss +++ b/docs/.vuepress/styles/index.scss @@ -1 +1,11 @@ // place your custom styles here + +#main-title { + font-family: "ColorTube", serif; + color: #ff0000 !important; /* 你想要的颜色 */ + line-height: 2; +} + +code { + font-family: "Fira Code", monospace !important; +} \ No newline at end of file diff --git a/docs/.vuepress/styles/palette.scss b/docs/.vuepress/styles/palette.scss index c797b6fd..ddab2950 100644 --- a/docs/.vuepress/styles/palette.scss +++ b/docs/.vuepress/styles/palette.scss @@ -1,2 +1,2 @@ // you can change colors here -$theme-color: #00a6ff; +$theme-color: #00a6ff; \ No newline at end of file diff --git a/docs/.vuepress/theme.js b/docs/.vuepress/theme.js index 9d12b5a5..3aef178f 100644 --- a/docs/.vuepress/theme.js +++ b/docs/.vuepress/theme.js @@ -1,193 +1,191 @@ -import { hopeTheme } from "vuepress-theme-hope"; +import {hopeTheme} from "vuepress-theme-hope"; import navbar from "./navbar.js"; import sidebar from "./sidebar.js"; export default hopeTheme({ - hostname: "https://vuepress-theme-hope-docs-demo.netlify.app", + hostname: "https://vuepress-theme-hope-docs-demo.netlify.app", - author: { - name: "远野千束", - url: "https://snowykami.me", - }, - - iconAssets: "fontawesome-with-brands", - - logo: "https://cdn.liteyuki.icu/static/img/logo.png", - - repo: "https://github.com/snowykami/LiteyukiBot", - - docsDir: "docs", - - // 导航栏 - navbar, - - // 侧边栏 - sidebar, - - // 页脚 - footer: "LiteyukiBot", - displayFooter: true, - - // 加密配置 - encrypt: { - config: { - "/demo/encrypt.html": ["1234"], - }, - }, - - // 多语言配置 - metaLocales: { - editLink: "在 GitHub 上编辑此页", - }, - - // 如果想要实时查看任何改变,启用它。注: 这对更新性能有很大负面影响 - // hotReload: true, - - // 在这里配置主题提供的插件 - plugins: { - searchPro: true, - // search: true, - - blog: true, - comment: { - provider: "Giscus", - repo: "snowykami/LiteyukiBot", - repoId: "R_kgDOHVNKpQ", - category: "Announcements", - categoryId: "DIC_kwDOHVNKpc4CeWxj", + author: { + name: "远野千束", + url: "https://snowykami.me", }, - components: { - components: ["Badge", "VPCard"], - }, + iconAssets: "fontawesome-with-brands", - // 此处开启了很多功能用于演示,你应仅保留用到的功能。 - mdEnhance: { - align: true, - attrs: true, - codetabs: true, - footnote: true, - component: true, - demo: true, - figure: true, - imgLazyload: true, - imgSize: true, - include: true, - mark: true, - stylize: [ - { - matcher: "Recommended", - replacer: ({ tag }) => { - if (tag === "em") - return { - tag: "Badge", - attrs: { type: "tip" }, - content: "Recommended", - }; - }, + logo: "https://cdn.liteyuki.icu/static/img/liteyuki_icon_640.png", + + repo: "https://github.com/snowykami/LiteyukiBot", + + docsDir: "docs", + + // 导航栏 + navbar, + + // 侧边栏 + sidebar, + + // 页脚 + footer: "LiteyukiBot", + displayFooter: true, + + // 加密配置 + encrypt: { + config: { + "/demo/encrypt.html": ["1234"], }, - ], - sub: true, - sup: true, - tabs: true, - vPre: true, - - - // 在启用之前安装 chart.js - // chart: true, - - // insert component easily - - // 在启用之前安装 echarts - // echarts: true, - - // 在启用之前安装 flowchart.ts - // flowchart: true, - - // gfm requires mathjax-full to provide tex support - // gfm: true, - - // 在启用之前安装 katex - // katex: true, - - // 在启用之前安装 mathjax-full - // mathjax: true, - - // 在启用之前安装 mermaid - // mermaid: true, - - // playground: { - // presets: ["ts", "vue"], - // }, - - // 在启用之前安装 reveal.js - // revealJs: { - // plugins: ["highlight", "math", "search", "notes", "zoom"], - // }, - - // 在启用之前安装 @vue/repl - // vuePlayground: true, - - // install sandpack-vue3 before enabling it - // sandpack: true, }, - // 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释 - // pwa: { - // favicon: "/favicon.ico", - // cacheHTML: true, - // cachePic: true, - // appendBase: true, - // apple: { - // icon: "/assets/icon/apple-icon-152.png", - // statusBarColor: "black", - // }, - // msTile: { - // image: "/assets/icon/ms-icon-144.png", - // color: "#ffffff", - // }, - // manifest: { - // icons: [ - // { - // src: "/assets/icon/chrome-mask-512.png", - // sizes: "512x512", - // purpose: "maskable", - // type: "image/png", - // }, - // { - // src: "/assets/icon/chrome-mask-192.png", - // sizes: "192x192", - // purpose: "maskable", - // type: "image/png", - // }, - // { - // src: "/assets/icon/chrome-512.png", - // sizes: "512x512", - // type: "image/png", - // }, - // { - // src: "/assets/icon/chrome-192.png", - // sizes: "192x192", - // type: "image/png", - // }, - // ], - // shortcuts: [ - // { - // name: "Demo", - // short_name: "Demo", - // url: "/demo/", - // icons: [ - // { - // src: "/assets/icon/guide-maskable.png", - // sizes: "192x192", - // purpose: "maskable", - // type: "image/png", - // }, - // ], - // }, - // ], - // }, - // }, - }, + // 多语言配置 + metaLocales: { + editLink: "在 GitHub 上编辑此页", + }, + + // 如果想要实时查看任何改变,启用它。注: 这对更新性能有很大负面影响 + // hotReload: true, + + // 在这里配置主题提供的插件 + plugins: { + searchPro: true, + // search: true, + comment: { + provider: "Giscus", + repo: "snowykami/LiteyukiBot", + repoId: "R_kgDOHVNKpQ", + category: "Announcements", + categoryId: "DIC_kwDOHVNKpc4CeWxj", + }, + + components: { + components: ["Badge", "VPCard"], + }, + + // 此处开启了很多功能用于演示,你应仅保留用到的功能。 + mdEnhance: { + align: true, + attrs: true, + codetabs: true, + footnote: true, + component: true, + demo: true, + figure: true, + imgLazyload: true, + imgSize: true, + include: true, + mark: true, + stylize: [ + { + matcher: "Recommended", + replacer: ({tag}) => { + if (tag === "em") + return { + tag: "Badge", + attrs: {type: "tip"}, + content: "Recommended", + }; + }, + }, + ], + sub: true, + sup: true, + tabs: true, + vPre: true, + + + // 在启用之前安装 chart.js + // chart: true, + + // insert component easily + + // 在启用之前安装 echarts + // echarts: true, + + // 在启用之前安装 flowchart.ts + // flowchart: true, + + // gfm requires mathjax-full to provide tex support + // gfm: true, + + // 在启用之前安装 katex + // katex: true, + + // 在启用之前安装 mathjax-full + // mathjax: true, + + // 在启用之前安装 mermaid + // mermaid: true, + + // playground: { + // presets: ["ts", "vue"], + // }, + + // 在启用之前安装 reveal.js + // revealJs: { + // plugins: ["highlight", "math", "search", "notes", "zoom"], + // }, + + // 在启用之前安装 @vue/repl + // vuePlayground: true, + + // install sandpack-vue3 before enabling it + // sandpack: true, + }, + + // 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释 + // pwa: { + // favicon: "/favicon.ico", + // cacheHTML: true, + // cachePic: true, + // appendBase: true, + // apple: { + // icon: "/assets/icon/apple-icon-152.png", + // statusBarColor: "black", + // }, + // msTile: { + // image: "/assets/icon/ms-icon-144.png", + // color: "#ffffff", + // }, + // manifest: { + // icons: [ + // { + // src: "/assets/icon/chrome-mask-512.png", + // sizes: "512x512", + // purpose: "maskable", + // type: "image/png", + // }, + // { + // src: "/assets/icon/chrome-mask-192.png", + // sizes: "192x192", + // purpose: "maskable", + // type: "image/png", + // }, + // { + // src: "/assets/icon/chrome-512.png", + // sizes: "512x512", + // type: "image/png", + // }, + // { + // src: "/assets/icon/chrome-192.png", + // sizes: "192x192", + // type: "image/png", + // }, + // ], + // shortcuts: [ + // { + // name: "Demo", + // short_name: "Demo", + // url: "/demo/", + // icons: [ + // { + // src: "/assets/icon/guide-maskable.png", + // sizes: "192x192", + // purpose: "maskable", + // type: "image/png", + // }, + // ], + // }, + // ], + // }, + // }, + }, }); diff --git a/docs/README.md b/docs/README.md index a4e58495..82039a9f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ bgImage: bgImageDark: bgImageStyle: background-attachment: fixed -heroText: LiteyukiBot 6 +heroText: LiteyukiBot tagline: 轻雪机器人,一个以轻量和简洁为设计理念基于Nonebot2的OneBot标准聊天机器人 actions: diff --git a/docs/store/README.md b/docs/store/README.md index 7252d22f..0e1146f1 100644 --- a/docs/store/README.md +++ b/docs/store/README.md @@ -1 +1,6 @@ - \ No newline at end of file +--- +title: 资源商店 +icon: store +index: false +--- + \ No newline at end of file diff --git a/docs/usage/basic_command.md b/docs/usage/basic_command.md index 04d9768e..f975231c 100644 --- a/docs/usage/basic_command.md +++ b/docs/usage/basic_command.md @@ -5,50 +5,70 @@ order: 1 category: 使用手册 --- -## 基础插件命令 +## 基础插件 -#### 命令前有[S]的表示仅超级用户可用,[O]和[A]分别为群主和群管可用,[P]为私聊可用 - -### 轻雪`liteyuki` +### **轻雪 `liteyuki`** ```shell -[S]reload-liteyuki # 重载轻雪 -[S]update-liteyuki # 更新轻雪 -[S]liteecho # 查看当前bot -[S]config set value # 添加配置项,若存在则会覆盖,输入值会被执行,以便于转换为正确的值,"10"和10是不一样的 -[S]config get [key] # 查询配置项,不带key返回配置项列表,推荐私聊使用 -[S]reload-resources # 重载资源 -[S]switch-image-mode # 切换图片模式,该功能需要commit:505468b及以后的Lagrange.OneBot,在普通图片和Markdown图片之间切换,后者更大但有失败的可能 +仅超级用户 +reload-liteyuki # 重载轻雪 +update-liteyuki # 更新轻雪 +liteecho # 查看当前bot +config set value # 添加配置项,若存在则会覆盖,输入值会被执行以转换为正确的类型,"10"和10是不一样的 +config get [key] # 查询配置项,不带key返回配置项列表,推荐私聊使用 +switch-image-mode # 在普通图片和Markdown大图之间切换,该功能需要commit:505468b及以后的Lagrange.OneBot, + +所有人可用 liteyuki-docs # 查看轻雪文档 -# 上述两个命令修改的配置项在数据库中保存,但是优先级低于配置文件,如果配置文件中存在相同的配置项,将会使用配置文件中的配置 ------- -别名: reload-liteyuki 重启轻雪, update-liteyuki 更新轻雪, reload-resources 重载资源, config 配置, set 设置, get 查询, -switch-image-mode 切换图片模式, liteyuki-docs 轻雪文档 ``` -### 轻雪包管理器 `liteyuki_npm` +命令别名 ```shell -[S]nps update # 更新插件索引 -[S]nps install # 安装插件 -[S]nps uninstall # 卸载插件 -[S]nps search # 通过关键词搜索插件 ------- -[AOSP]npm enable # 当前会话启用插件 -[AOSP]npm disable # 当前会话禁用插件 -[S]npm enable-global # 全局启用插件 -[S]npm disable-global # 全局禁用插件 -list-plugin [page] [num] # 列出所有插件 page为页数,num为每页显示数量 ------- -[S]rpm list [page] [num] # 列出所有资源包 page为页数,num为每页显示数量 -[S]rpm load # 加载资源包 -[S]rpm unload # 卸载资源包 -[S]rpm change # 修改优先级 -[S]rpm reload # 重载所有资源包 ------- -别名: nps 插件商店, npm 插件管理, update 更新, install 安装, uninstall 卸载, search 搜索, -enable 启用, disable 停用, enable-global 全局启用, disable-global 全局停用, list-plugin 列出插件/插件列表, -rpm 资源包, load 加载, unload 卸载, change 更改, reload 重载, list 列表/列出 +reload-liteyuki 重启轻雪, +update-liteyuki 更新轻雪, +reload-resources 重载资源, +config 配置 | set 设置 | get 查询, +switch-image-mode 切换图片模式, +liteyuki-docs 轻雪文档 +``` + +### **插件/包管理器 `liteyuki_pacman`** + +- 插件管理 + +```shell +# 仅超级用户 +npm update # 更新插件商店索引 +npm install # 安装插件 +npm uninstall # 卸载插件 +npm search # 通过关键词搜索插件 +npm enable-global # 全局启用插件 +npm disable-global # 全局禁用插件 + +# 群聊仅群主、管理员、超级用户可用,私聊所有人可用 +npm enable # 当前会话启用插件 +npm disable # 当前会话禁用插件 +npm list [page] [num] # 列出所有插件 page为页数,num为每页显示数量 +``` + +- 资源包管理 + +```shell +# 仅超级用户 +rpm list [page] [num] # 列出所有资源包 page为页数,num为每页显示数量 +rpm load # 加载资源包 +rpm unload # 卸载资源包 +rpm change # 修改优先级 +rpm reload # 重载所有资源包 +``` + +命令别名 + +```shell +npm 插件管理 | update 更新 | install 安装 | uninstall 卸载 | search 搜索 +enable 启用 | disable 停用 | enable-global 全局启用 | disable-global 全局停用 | list-plugin 插件列表 +rpm 资源包 | load 加载 | unload 卸载 | change 更改 | reload 重载 | list 列表 ``` ```shell @@ -58,14 +78,18 @@ rpm 资源包, load 加载, unload 卸载, change 更改, reload 重载, list ``` -### 轻雪用户管理`liteyuki_user` +### **用户管理`liteyuki_user`** ```shell profile # 查看用户信息菜单 profile set [value] # 设置用户信息或打开属性设置菜单 profile get # 获取用户信息 ------- -别名: profile 个人信息, set 设置, get 查询 +``` + +命令别名 + +```shell +profile 个人信息 | set 设置 | get 查询 ``` **参数**:``为必填参数,`[option]`为可选参数。 diff --git a/liteyuki/liteyuki_main/core.py b/liteyuki/liteyuki_main/core.py index 561cc0a3..3085d353 100644 --- a/liteyuki/liteyuki_main/core.py +++ b/liteyuki/liteyuki_main/core.py @@ -24,6 +24,7 @@ driver = get_driver() markdown_image = common_db.first(StoredConfig(), default=StoredConfig()).config.get("markdown_image", False) + @on_alconna( command=Alconna( "liteecho", @@ -165,8 +166,9 @@ async def _(event: T_MessageEvent, matcher: Matcher): async def _(matcher: Matcher): matcher.finish("https://bot.liteyuki.icu/usage") + # system hook -@Bot.on_calling_api # 图片模式检测 +@Bot.on_calling_api # 图片模式检测 async def test_for_md_image(bot: T_Bot, api: str, data: dict): if api in ["send_msg", "send_private_msg", "send_group_msg"] and markdown_image and data.get("user_id") != bot.self_id: if api == "send_msg" and data.get("message_type") == "private" or api == "send_private_msg": diff --git a/liteyuki/liteyuki_main/runtime.py b/liteyuki/liteyuki_main/runtime.py index f83eaf6a..e4f276c1 100644 --- a/liteyuki/liteyuki_main/runtime.py +++ b/liteyuki/liteyuki_main/runtime.py @@ -139,21 +139,24 @@ async def get_stats_data(self_id: str = None, lang: str = None) -> dict: disk_data = [] for disk in psutil.disk_partitions(all=True): - disk_usage = psutil.disk_usage(disk.mountpoint) - disk_total_show = convert_size(disk_usage.total, 1) - disk_free_show = convert_size(disk_usage.free, 1) - if disk_usage.total > 0: - disk_data.append( - { - "name" : disk.device, - "total" : disk_total_show, - "free" : disk_free_show, - "percent" : disk_usage.percent, - "usedValue" : disk_usage.used, - "freeValue" : disk_usage.free, - "totalValue": disk_usage.total, - } - ) + try: + disk_usage = psutil.disk_usage(disk.mountpoint) + disk_total_show = convert_size(disk_usage.total, 1) + disk_free_show = convert_size(disk_usage.free, 1) + if disk_usage.total > 0: + disk_data.append( + { + "name" : disk.device, + "total" : disk_total_show, + "free" : disk_free_show, + "percent" : disk_usage.percent, + "usedValue" : disk_usage.used, + "freeValue" : disk_usage.free, + "totalValue": disk_usage.total, + } + ) + except Exception: + pass cpu_info = get_cpu_info() if "AMD" in cpu_info.get("brand_raw", ""): diff --git a/liteyuki/plugins/liteyuki_npm/helper.py b/liteyuki/plugins/liteyuki_npm/helper.py deleted file mode 100644 index e69de29b..00000000 diff --git a/liteyuki/plugins/liteyuki_npm/installer.py b/liteyuki/plugins/liteyuki_npm/installer.py deleted file mode 100644 index c206ee7d..00000000 --- a/liteyuki/plugins/liteyuki_npm/installer.py +++ /dev/null @@ -1,230 +0,0 @@ -import os.path -import sys -from io import StringIO - -import aiohttp -import nonebot -import pip -from arclet.alconna import Arparma, MultiVar -from nonebot import require -from nonebot.permission import SUPERUSER -from liteyuki.utils.language import get_user_lang -from liteyuki.utils.ly_typing import T_Bot -from liteyuki.utils.message import Markdown as md -from .common import * - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import Alconna, Args, Subcommand, on_alconna - -nps = on_alconna( - Alconna( - "nps", - Subcommand( - "update", - alias=["u"], - ), - Subcommand( - "search", - Args["keywords", MultiVar(str)]["page", int, 1], - alias=["s", "搜索"], - ), - Subcommand( - "install", - Args["plugin_name", str], - alias=["i", "安装"], - ), - Subcommand( - "uninstall", - Args["plugin_name", str], - alias=["r", "rm", "卸载"], - ) - ), - aliases={"插件商店"}, - permission=SUPERUSER, -) - - -@nps.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"): - await npm_update() - - if result.subcommands.get("update"): - r = await npm_update() - if r: - await nps.finish(ulang.get("npm.store_update_success")) - else: - await nps.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) - max_show = 10 - for p in rs: - print(p.module_name, p.homepage) - 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.btn_cmd(ulang.get("npm.install"), "npm install %s" % plugin.module_name) - link_page = md.btn_link(ulang.get("npm.homepage"), plugin.homepage) - link_pypi = md.btn_link(ulang.get("npm.pypi"), plugin.homepage) - - 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: - reply = ulang.get("npm.search_no_result") - await md.send_md(reply, bot, event=event) - - 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 nps.send(ulang.get("npm.installing", NAME=plugin_module_name)) - r, log = npm_install(plugin_module_name) - log = log.replace("\\", "/") - - if not store_plugin: - await nps.finish(ulang.get("npm.plugin_not_found", NAME=plugin_module_name)) - - homepage_btn = md.btn_cmd(ulang.get("npm.homepage"), store_plugin.homepage) - if r: - - r_load = nonebot.load_plugin(plugin_module_name) # 加载插件 - installed_plugin = InstalledPlugin(module_name=plugin_module_name) # 构造插件信息模型 - found_in_db_plugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_module_name) # 查询数据库中是否已经安装 - - if r_load: - if found_in_db_plugin is None: - plugin_db.upsert(installed_plugin) - info = md.escape(ulang.get("npm.install_success", NAME=store_plugin.name)) # markdown转义 - await md.send_md( - f"{info}\n\n" - f"```\n{log}\n```", - bot, - event=event - ) - else: - await nps.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 md.send_md( - f"{info}\n\n" - f"```\n{log}\n```\n", - bot, - event=event - ) - else: - info = ulang.get("npm.install_failed", NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace("_", r"\\_") - await md.send_md( - f"{info}\n\n" - f"```\n{log}\n```", - bot, - event=event - ) - - elif result.subcommands.get("uninstall"): - plugin_module_name: str = result.subcommands["uninstall"].args.get("plugin_name") - found_installed_plugin: InstalledPlugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_module_name) - if found_installed_plugin: - plugin_db.delete(InstalledPlugin(), "module_name = ?", plugin_module_name) - reply = f"{ulang.get('npm.uninstall_success', NAME=found_installed_plugin.module_name)}" - await nps.finish(reply) - else: - await nps.finish(ulang.get("npm.plugin_not_installed", NAME=plugin_module_name)) - - -async def npm_update() -> bool: - """ - 更新本地插件json缓存 - - Returns: - bool: 是否成功更新 - """ - url_list = [ - "https://registry.nonebot.dev/plugins.json", - ] - 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) - 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, - ' '.join([tag.label for tag in plugin.tags]) - ] - ) - if all([keyword in plugin_text for keyword in keywords]): - results.append(plugin) - return results - - -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.mirrors.cqupt.edu.cn/simple", # 重庆邮电大学 - "https://pypi.liteyuki.icu/simple", # 轻雪镜像 - "https://pypi.org/simple", # 官方源 - ] - - # 使用pip安装包,对每个镜像尝试一次,成功后返回值 - success = False - for mirror in mirrors: - try: - nonebot.logger.info(f"npm_install try mirror: {mirror}") - result = pip.main(["install", plugin_module_name, "-i", mirror]) - success = result == 0 - if success: - break - else: - nonebot.logger.warning(f"npm_install failed, try next mirror.") - except Exception as e: - - success = False - continue - - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - - return success, buffer.getvalue() diff --git a/liteyuki/plugins/liteyuki_npm/manager.py b/liteyuki/plugins/liteyuki_npm/manager.py deleted file mode 100644 index 5398929f..00000000 --- a/liteyuki/plugins/liteyuki_npm/manager.py +++ /dev/null @@ -1,243 +0,0 @@ -import os - -import nonebot.plugin -from nonebot import require -from nonebot.exception import FinishedException, IgnoredException -from nonebot.internal.adapter import Event -from nonebot.internal.matcher import Matcher -from nonebot.message import run_preprocessor -from nonebot.permission import SUPERUSER -from nonebot.plugin import Plugin - -from liteyuki.utils.data_manager import GlobalPlugin, Group, InstalledPlugin, User, group_db, plugin_db, user_db -from liteyuki.utils.language import get_user_lang -from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent -from liteyuki.utils.message import Markdown as md -from liteyuki.utils.permission import GROUP_ADMIN, GROUP_OWNER -from .common import get_plugin_can_be_toggle, get_plugin_default_enable, get_plugin_global_enable, get_plugin_session_enable -from .installer import get_store_plugin, npm_update -from ...utils.tools import clamp - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma, Subcommand - -list_plugins = on_alconna( - Alconna( - "list-plugin", - Args["page", int, 1]["num", int, 10], - ), - aliases={"列出插件", "插件列表"} -) - -npm = on_alconna( - aliases={"插件管理"}, - command=Alconna( - "npm", - # Args["plugin_name", str], - Subcommand( - "enable", - Args["plugin_name", str], - alias=["启用"], - - ), - Subcommand( - "disable", - Args["plugin_name", str], - alias=["停用"], - ), - Subcommand( - "global-enable", - Args["plugin_name", str], - alias=["全局启用"], - ), - Subcommand( - "global-disable", - Args["plugin_name", str], - alias=["全局停用"], - ), - ), - -) - - -@list_plugins.handle() -async def _(event: T_MessageEvent, bot: T_Bot, result: Arparma): - ulang = get_user_lang(str(event.user_id)) - if not os.path.exists("data/liteyuki/plugins.json"): - await npm_update() - - loaded_plugin_list = sorted(nonebot.get_loaded_plugins(), key=lambda x: x.module_name) - num_per_page = result.args.get("num") - total = len(loaded_plugin_list) // num_per_page + (1 if len(loaded_plugin_list) % num_per_page else 0) - - page = clamp(result.args.get("page"), 1, total) - - # 已加载插件 | 总计10 | 第1/3页 - reply = (f"# {ulang.get('npm.loaded_plugins')} | " - f"{ulang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} | " - f"{ulang.get('npm.page', PAGE=page, TOTAL=total)} \n***\n") - - for plugin in loaded_plugin_list[(page - 1) * num_per_page: min(page * num_per_page, len(loaded_plugin_list))]: - # 检查是否有 metadata 属性 - # 添加帮助按钮 - btn_usage = md.btn_cmd(ulang.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) - if store_plugin: - btn_homepage = md.btn_link(ulang.get("npm.homepage"), store_plugin.homepage) - show_name = store_plugin.name - elif plugin.metadata: - if plugin.metadata.extra.get("liteyuki"): - btn_homepage = md.btn_link(ulang.get("npm.homepage"), "https://github.com/snowykami/LiteyukiBot") - else: - btn_homepage = ulang.get("npm.homepage") - show_name = plugin.metadata.name - else: - btn_homepage = ulang.get("npm.homepage") - show_name = plugin.name - ulang.get("npm.no_description") - - if plugin.metadata: - reply += f"\n**{md.escape(show_name)}**\n" - else: - reply += f"**{md.escape(show_name)}**\n" - - reply += f"\n > {btn_usage} {btn_homepage}" - - if await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event): - # 添加启用/停用插件按钮 - cmd_toggle = f"npm {'disable' if session_enable else 'enable'} {plugin.module_name}" - text_toggle = ulang.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.btn_cmd(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.btn_cmd(ulang.get("npm.uninstall"), f'npm uninstall {plugin.module_name}')) if plugin_in_database else ulang.get( - 'npm.uninstall') - btn_toggle_global_text = ulang.get("npm.disable_global" if global_enable else "npm.enable_global") - cmd_toggle_global = f"npm {'global-disable' if global_enable else 'global-enable'} {plugin.module_name}" - btn_toggle_global = btn_toggle_global_text if not can_be_toggle else md.btn_cmd(btn_toggle_global_text, cmd_toggle_global) - - reply += f" {btn_uninstall} {btn_toggle_global}" - reply += "\n\n***\n" - await md.send_md(reply, bot, event=event) - - -@npm.handle() -async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): - if not os.path.exists("data/liteyuki/plugins.json"): - await npm_update() - # 判断会话类型 - ulang = get_user_lang(str(event.user_id)) - plugin_module_name = result.args.get("plugin_name") - # 支持对自定义command_start的判断 - if result.subcommands.get("enable") or result.subcommands.get("disable"): - - toggle = result.subcommands.get("enable") is not None - - session_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: - await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_module_name)) - - if session_enable == toggle: - await npm.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(Group(), "group_id = ?", event.group_id, default=Group(group_id=str(event.group_id))) - else: - raise FinishedException(ulang.get("Permission Denied")) - 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) - if event.message_type == "private": - user_db.upsert(session) - else: - group_db.upsert(session) - except Exception as e: - print(e) - await npm.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)) - ) - - await npm.finish( - ulang.get( - "npm.toggle_success", - NAME=plugin_module_name, - STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")) - ) - elif result.subcommands.get("global-enable") or result.subcommands.get("global-disable") and await SUPERUSER(bot, event): - toggle = result.subcommands.get("global-enable") is not None - can_be_toggled = get_plugin_can_be_toggle(plugin_module_name) - if not can_be_toggled: - await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_module_name)) - - global_enable = get_plugin_global_enable(plugin_module_name) - if global_enable == toggle: - await npm.finish( - ulang.get("npm.plugin_already", NAME=plugin_module_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))) - - try: - plugin = plugin_db.first(GlobalPlugin(), "module_name = ?", plugin_module_name, default=GlobalPlugin(module_name=plugin_module_name)) - if toggle: - plugin.enabled = True - else: - plugin.enabled = False - plugin_db.upsert(plugin) - except Exception as e: - print(e) - await npm.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)) - ) - - await npm.finish( - ulang.get( - "npm.toggle_success", - NAME=plugin_module_name, - STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")) - ) - - -@run_preprocessor -async def pre_handle(event: Event, matcher: Matcher): - plugin: Plugin = matcher.plugin - plugin_global_enable = get_plugin_global_enable(plugin.module_name) - if not plugin_global_enable: - raise IgnoredException("Plugin disabled globally") - if event.get_type() == "message": - plugin_session_enable = get_plugin_session_enable(event, plugin.module_name) - if not plugin_session_enable: - raise IgnoredException("Plugin disabled in session") - -# @Bot.on_calling_api -# async def _(bot: Bot, api: str, data: dict[str, any]): -# nonebot.logger.info(f"Plugin Callapi: {api}: {data}") diff --git a/liteyuki/plugins/liteyuki_npm/permission.py b/liteyuki/plugins/liteyuki_npm/permission.py deleted file mode 100644 index ee346f48..00000000 --- a/liteyuki/plugins/liteyuki_npm/permission.py +++ /dev/null @@ -1,8 +0,0 @@ -# 插件权限管理器,对api调用进行hook限制,防止插件滥用api -from liteyuki.utils.data import LiteModel - - -class PermissionAllow(LiteModel): - plugin_name: str - api_name: str - allow: bool \ No newline at end of file diff --git a/liteyuki/plugins/liteyuki_npm/__init__.py b/liteyuki/plugins/liteyuki_pacman/__init__.py similarity index 90% rename from liteyuki/plugins/liteyuki_npm/__init__.py rename to liteyuki/plugins/liteyuki_pacman/__init__.py index c6f3f761..d10db8d3 100644 --- a/liteyuki/plugins/liteyuki_npm/__init__.py +++ b/liteyuki/plugins/liteyuki_pacman/__init__.py @@ -1,7 +1,5 @@ from nonebot.plugin import PluginMetadata -from .manager import * -from .installer import * -from .helper import * +from .npm import * from .rpm import * __author__ = "snowykami" diff --git a/liteyuki/plugins/liteyuki_npm/common.py b/liteyuki/plugins/liteyuki_pacman/common.py similarity index 58% rename from liteyuki/plugins/liteyuki_npm/common.py rename to liteyuki/plugins/liteyuki_pacman/common.py index 2e5717de..98185ac7 100644 --- a/liteyuki/plugins/liteyuki_npm/common.py +++ b/liteyuki/plugins/liteyuki_pacman/common.py @@ -4,8 +4,8 @@ from typing import Optional import aiofiles import nonebot.plugin -from liteyuki.utils.data import Database, LiteModel -from liteyuki.utils.data_manager import GlobalPlugin, Group, InstalledPlugin, User, group_db, plugin_db, user_db +from liteyuki.utils.data import LiteModel +from liteyuki.utils.data_manager import GlobalPlugin, Group, User, group_db, plugin_db, user_db from liteyuki.utils.ly_typing import T_MessageEvent @@ -17,9 +17,9 @@ class PluginTag(LiteModel): class StorePlugin(LiteModel): name: str desc: str - module_name: str + module_name: str # 插件商店中的模块名不等于本地的模块名,前者是文件夹名,后者是点分割模块名 project_link: str = "" - homepage: str ="" + homepage: str = "" author: str = "" type: str | None = None version: str | None = "" @@ -28,12 +28,27 @@ class StorePlugin(LiteModel): is_official: bool = False -async def get_store_plugin(plugin_module_name: str) -> Optional[StorePlugin]: +def get_plugin_exist(plugin_name: str) -> bool: + """ + 获取插件是否存在 + Args: + plugin_name: + + Returns: + + """ + for plugin in nonebot.plugin.get_loaded_plugins(): + if plugin.name == plugin_name: + return True + return False + + +async def get_store_plugin(plugin_name: str) -> Optional[StorePlugin]: """ 获取插件信息 Args: - plugin_module_name (str): 插件模块名 + plugin_name (str): 插件模块名 Returns: Optional[StorePlugin]: 插件信息 @@ -41,33 +56,33 @@ async def get_store_plugin(plugin_module_name: str) -> 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: + if plugin.name == plugin_name: return plugin return None -def get_plugin_default_enable(plugin_module_name: str) -> bool: +def get_plugin_default_enable(plugin_name: str) -> bool: """ 获取插件默认启用状态,由插件定义,不存在则默认为启用 Args: - plugin_module_name (str): 插件模块名 + plugin_name (str): 插件模块名 Returns: bool: 插件默认状态 """ - plug = nonebot.plugin.get_plugin_by_module_name(plugin_module_name) + plug = nonebot.plugin.get_plugin(plugin_name) return (plug.metadata.extra.get("default_enable", True) if plug.metadata else True) if plug else True -def get_plugin_session_enable(event: T_MessageEvent, plugin_module_name: str) -> bool: +def get_plugin_session_enable(event: T_MessageEvent, plugin_name: str) -> bool: """ 获取插件当前会话启用状态 Args: event: 会话事件 - plugin_module_name (str): 插件模块名 + plugin_name (str): 插件模块名 Returns: bool: 插件当前状态 @@ -80,31 +95,31 @@ def get_plugin_session_enable(event: T_MessageEvent, plugin_module_name: str) -> # 默认停用插件不在启用列表内表示停用 # 默认启用插件在停用列表内表示停用 # 默认启用插件不在停用列表内表示启用 - default_enable = get_plugin_default_enable(plugin_module_name) + default_enable = get_plugin_default_enable(plugin_name) if default_enable: - return plugin_module_name not in session.disabled_plugins + return plugin_name not in session.disabled_plugins else: - return plugin_module_name in session.enabled_plugins + return plugin_name in session.enabled_plugins -def get_plugin_global_enable(plugin_module_name: str) -> bool: - loaded_plugin = nonebot.plugin.get_plugin_by_module_name(plugin_module_name) +def get_plugin_global_enable(plugin_name: str) -> bool: + nonebot.plugin.get_plugin(plugin_name) return plugin_db.first( GlobalPlugin(), "module_name = ?", - plugin_module_name, - default=GlobalPlugin(module_name=plugin_module_name, enabled=True)).enabled + plugin_name, + default=GlobalPlugin(module_name=plugin_name, enabled=True)).enabled -def get_plugin_can_be_toggle(plugin_module_name: str) -> bool: +def get_plugin_can_be_toggle(plugin_name: str) -> bool: """ 获取插件是否可以被启用/停用 Args: - plugin_module_name (str): 插件模块名 + plugin_name (str): 插件模块名 Returns: bool: 插件是否可以被启用/停用 """ - plug = nonebot.plugin.get_plugin_by_module_name(plugin_module_name) + plug = nonebot.plugin.get_plugin(plugin_name) return plug.metadata.extra.get("toggleable", True) if plug and plug.metadata else True diff --git a/liteyuki/plugins/liteyuki_pacman/npm.py b/liteyuki/plugins/liteyuki_pacman/npm.py new file mode 100644 index 00000000..155b4130 --- /dev/null +++ b/liteyuki/plugins/liteyuki_pacman/npm.py @@ -0,0 +1,460 @@ +import os +import sys +import aiohttp +import nonebot.plugin +import pip +from io import StringIO +from arclet.alconna import MultiVar +from nonebot import require +from nonebot.exception import FinishedException, IgnoredException +from nonebot.internal.adapter import Event +from nonebot.internal.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.permission import SUPERUSER +from nonebot.plugin import Plugin +from liteyuki.utils.data_manager import InstalledPlugin +from liteyuki.utils.language import get_user_lang +from liteyuki.utils.ly_typing import T_Bot +from liteyuki.utils.message import Markdown as md +from liteyuki.utils.permission import GROUP_ADMIN, GROUP_OWNER +from liteyuki.utils.tools import clamp +from .common import * + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma, Subcommand + +# const +enable_global = "enable-global" +disable_global = "disable-global" +enable = "enable" +disable = "disable" + + +@on_alconna( + aliases={"插件"}, + command=Alconna( + "npm", + Subcommand( + "enable", + Args["plugin_name", str], + alias=["启用"], + + ), + Subcommand( + "disable", + Args["plugin_name", str], + alias=["停用"], + ), + Subcommand( + enable_global, + Args["plugin_name", str], + alias=["全局启用"], + ), + Subcommand( + disable_global, + Args["plugin_name", str], + alias=["全局停用"], + ), + # 安装部分 + Subcommand( + "update", + alias=["u"], + ), + Subcommand( + "search", + Args["keywords", MultiVar(str)]["show_num", int, 15], + alias=["s", "搜索"], + ), + Subcommand( + "install", + Args["plugin_name", str], + alias=["i", "安装"], + ), + Subcommand( + "uninstall", + Args["plugin_name", str], + alias=["r", "rm", "卸载"], + ), + Subcommand( + "list", + Args["num", int, 10]["page", int, 1], + alias=["ls", "列表"], + ), + Subcommand( + "usage", + Args["plugin_name", str], + alias=["详情"], + ) + ) +).handle() +async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher): + if not os.path.exists("data/liteyuki/plugins.json"): + await npm_update() + # 判断会话类型 + ulang = get_user_lang(str(event.user_id)) + plugin_name = result.args.get("plugin_name") + sc = result.subcommands # 获取子命令 + perm_s = await SUPERUSER(bot, event) # 判断是否为超级用户 + # 支持对自定义command_start的判断 + if sc.get("enable") or result.subcommands.get("disable"): + + toggle = result.subcommands.get("enable") is not None + + plugin_exist = get_plugin_exist(plugin_name) + + session_enable = get_plugin_session_enable(event, plugin_name) # 获取插件当前状态 + + default_enable = get_plugin_default_enable(plugin_name) # 获取插件默认状态 + + can_be_toggled = get_plugin_can_be_toggle(plugin_name) # 获取插件是否可以被启用/停用 + + if not plugin_exist: + await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + + if not can_be_toggled: + await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name)) + + if session_enable == toggle: + await npm.finish( + ulang.get("npm.plugin_already", NAME=plugin_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(Group(), "group_id = ?", event.group_id, default=Group(group_id=str(event.group_id))) + else: + raise FinishedException(ulang.get("Permission Denied")) + try: + if toggle: + if default_enable: + session.disabled_plugins.remove(plugin_name) + else: + session.enabled_plugins.append(plugin_name) + else: + if default_enable: + session.disabled_plugins.append(plugin_name) + else: + session.enabled_plugins.remove(plugin_name) + if event.message_type == "private": + user_db.upsert(session) + else: + group_db.upsert(session) + except Exception as e: + print(e) + await npm.finish( + ulang.get( + "npm.toggle_failed", + NAME=plugin_name, + STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"), + ERROR=str(e)) + ) + + await npm.finish( + ulang.get( + "npm.toggle_success", + NAME=plugin_name, + STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")) + ) + + elif sc.get(enable_global) or result.subcommands.get(disable_global) and await SUPERUSER(bot, event): + plugin_exist = get_plugin_exist(plugin_name) + + toggle = result.subcommands.get(enable_global) is not None + + can_be_toggled = get_plugin_can_be_toggle(plugin_name) + + if not plugin_exist: + await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + + if not can_be_toggled: + await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name)) + + global_enable = get_plugin_global_enable(plugin_name) + if global_enable == toggle: + await npm.finish( + ulang.get("npm.plugin_already", NAME=plugin_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))) + + try: + storePlugin = plugin_db.first(GlobalPlugin(), "module_name = ?", plugin_name, default=GlobalPlugin(module_name=plugin_name)) + if toggle: + storePlugin.enabled = True + else: + storePlugin.enabled = False + plugin_db.upsert(storePlugin) + except Exception as e: + print(e) + await npm.finish( + ulang.get( + "npm.toggle_failed", + NAME=plugin_name, + STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"), + ERROR=str(e)) + ) + + await npm.finish( + ulang.get( + "npm.toggle_success", + NAME=plugin_name, + STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")) + ) + + elif sc.get("update") and perm_s: + r = await npm_update() + if r: + await npm.finish(ulang.get("npm.store_update_success")) + else: + await npm.finish(ulang.get("npm.store_update_failed")) + + elif sc.get("search"): + keywords: list[str] = result.subcommands["search"].args.get("keywords") + rs = await npm_search(keywords) + max_show = result.subcommands.get("search").args.get("show_num") + if len(rs): + reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***" + for storePlugin in rs[:min(max_show, len(rs))]: + btn_install = md.btn_cmd(ulang.get("npm.install"), "npm install %s" % storePlugin.module_name) + link_page = md.btn_link(ulang.get("npm.homepage"), storePlugin.homepage) + link_pypi = md.btn_link(ulang.get("npm.pypi"), storePlugin.homepage) + + reply += (f"\n# **{storePlugin.name}**\n" + f"\n> **{storePlugin.desc}**\n" + f"\n> {ulang.get('npm.author')}: {storePlugin.author}" + f"\n> *{md.escape(storePlugin.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: + reply = ulang.get("npm.search_no_result") + await md.send_md(reply, bot, event=event) + + elif sc.get("install") and perm_s: + plugin_name: str = result.subcommands["install"].args.get("plugin_name") + store_plugin = await get_store_plugin(plugin_name) + await npm.send(ulang.get("npm.installing", NAME=plugin_name)) + r, log = npm_install(plugin_name) + log = log.replace("\\", "/") + + if not store_plugin: + await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + + homepage_btn = md.btn_cmd(ulang.get("npm.homepage"), store_plugin.homepage) + if r: + + r_load = nonebot.load_plugin(plugin_name) # 加载插件 + installed_plugin = InstalledPlugin(module_name=plugin_name) # 构造插件信息模型 + found_in_db_plugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_name) # 查询数据库中是否已经安装 + + if r_load: + if found_in_db_plugin is None: + plugin_db.upsert(installed_plugin) + info = md.escape(ulang.get("npm.install_success", NAME=store_plugin.name)) # markdown转义 + await md.send_md( + f"{info}\n\n" + f"```\n{log}\n```", + bot, + event=event + ) + else: + await npm.finish(ulang.get("npm.plugin_already_installed", NAME=store_plugin.name)) + else: + info = ulang.get("npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn).replace("_", r"\\_") + await md.send_md( + f"{info}\n\n" + f"```\n{log}\n```\n", + bot, + event=event + ) + else: + info = ulang.get("npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn).replace("_", r"\\_") + await md.send_md( + f"{info}\n\n" + f"```\n{log}\n```", + bot, + event=event + ) + + elif sc.get("uninstall") and perm_s: + plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") + found_installed_plugin: InstalledPlugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_name) + if found_installed_plugin: + plugin_db.delete(InstalledPlugin(), "module_name = ?", plugin_name) + reply = f"{ulang.get('npm.uninstall_success', NAME=found_installed_plugin.module_name)}" + await npm.finish(reply) + else: + await npm.finish(ulang.get("npm.plugin_not_installed", NAME=plugin_name)) + + elif sc.get("list"): + loaded_plugin_list = sorted(nonebot.get_loaded_plugins(), key=lambda x: x.name) + num_per_page = result.subcommands.get("list").args.get("num") + total = len(loaded_plugin_list) // num_per_page + (1 if len(loaded_plugin_list) % num_per_page else 0) + + page = clamp(result.subcommands.get("list").args.get("page"), 1, total) + + # 已加载插件 | 总计10 | 第1/3页 + reply = (f"# {ulang.get('npm.loaded_plugins')} | " + f"{ulang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} | " + f"{ulang.get('npm.page', PAGE=page, TOTAL=total)} \n***\n") + + permission_oas = await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event) + permission_s = await SUPERUSER(bot, event) + + for storePlugin in loaded_plugin_list[(page - 1) * num_per_page: min(page * num_per_page, len(loaded_plugin_list))]: + # 检查是否有 metadata 属性 + # 添加帮助按钮 + + btn_usage = md.btn_cmd(ulang.get("npm.usage"), f"npm usage {storePlugin.name}", False) + store_plugin = await get_store_plugin(storePlugin.name) + session_enable = get_plugin_session_enable(event, storePlugin.name) + if store_plugin: + btn_homepage = md.btn_link(ulang.get("npm.homepage"), store_plugin.homepage) + show_name = store_plugin.name + elif storePlugin.metadata: + if storePlugin.metadata.extra.get("liteyuki"): + btn_homepage = md.btn_link(ulang.get("npm.homepage"), "https://github.com/snowykami/LiteyukiBot") + else: + btn_homepage = ulang.get("npm.homepage") + show_name = storePlugin.metadata.name + else: + btn_homepage = ulang.get("npm.homepage") + show_name = storePlugin.name + ulang.get("npm.no_description") + + if storePlugin.metadata: + reply += f"\n**{md.escape(show_name)}**\n" + else: + reply += f"**{md.escape(show_name)}**\n" + + reply += f"\n > {btn_usage} {btn_homepage}" + + if permission_oas: + # 添加启用/停用插件按钮 + cmd_toggle = f"npm {'disable' if session_enable else 'enable'} {storePlugin.name}" + text_toggle = ulang.get("npm.disable" if session_enable else "npm.enable") + can_be_toggle = get_plugin_can_be_toggle(storePlugin.name) + btn_toggle = text_toggle if not can_be_toggle else md.btn_cmd(text_toggle, cmd_toggle) + reply += f" {btn_toggle}" + + if permission_s: + plugin_in_database = plugin_db.first(InstalledPlugin(), "module_name = ?", storePlugin.name) + # 添加移除插件和全局切换按钮 + global_enable = get_plugin_global_enable(storePlugin.name) + btn_uninstall = ( + md.btn_cmd(ulang.get("npm.uninstall"), f'npm uninstall {storePlugin.name}')) if plugin_in_database else ulang.get( + 'npm.uninstall') + btn_toggle_global_text = ulang.get("npm.disable_global" if global_enable else "npm.enable_global") + cmd_toggle_global = f"npm {'disable' if global_enable else 'enable'}-global {storePlugin.name}" + btn_toggle_global = btn_toggle_global_text if not can_be_toggle else md.btn_cmd(btn_toggle_global_text, cmd_toggle_global) + + reply += f" {btn_uninstall} {btn_toggle_global}" + reply += "\n\n***\n" + await md.send_md(reply, bot, event=event) + + elif sc.get("usage"): + # TODO + pass + else: + pass + + +@run_preprocessor +async def pre_handle(event: Event, matcher: Matcher): + plugin: Plugin = matcher.plugin + plugin_global_enable = get_plugin_global_enable(plugin.name) + if not plugin_global_enable: + raise IgnoredException("Plugin disabled globally") + if event.get_type() == "message": + plugin_session_enable = get_plugin_session_enable(event, plugin.name) + if not plugin_session_enable: + raise IgnoredException("Plugin disabled in session") + + +async def npm_update() -> bool: + """ + 更新本地插件json缓存 + + Returns: + bool: 是否成功更新 + """ + url_list = [ + "https://registry.nonebot.dev/plugins.json", + ] + 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) + 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, + ' '.join([tag.label for tag in plugin.tags]) + ] + ) + if all([keyword in plugin_text for keyword in keywords]): + results.append(plugin) + return results + + +def npm_install(plugin_package_name) -> tuple[bool, str]: + """ + Args: + plugin_package_name: + + Returns: + tuple[bool, str]: + + """ + buffer = StringIO() + sys.stdout = buffer + sys.stderr = buffer + + mirrors = [ + "https://pypi.tuna.tsinghua.edu.cn/simple", # 清华大学 + "https://pypi.mirrors.cqupt.edu.cn/simple", # 重庆邮电大学 + "https://pypi.liteyuki.icu/simple", # 轻雪代理镜像 + "https://pypi.org/simple", # 官方源 + ] + + # 使用pip安装包,对每个镜像尝试一次,成功后返回值 + success = False + for mirror in mirrors: + try: + nonebot.logger.info(f"npm_install try mirror: {mirror}") + result = pip.main(["install", plugin_package_name, "-i", mirror]) + success = result == 0 + if success: + break + else: + nonebot.logger.warning(f"npm_install failed, try next mirror.") + except Exception as e: + + success = False + continue + + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + + return success, buffer.getvalue() diff --git a/liteyuki/plugins/liteyuki_npm/rpm.py b/liteyuki/plugins/liteyuki_pacman/rpm.py similarity index 99% rename from liteyuki/plugins/liteyuki_npm/rpm.py rename to liteyuki/plugins/liteyuki_pacman/rpm.py index 876f00ff..39ca69ce 100644 --- a/liteyuki/plugins/liteyuki_npm/rpm.py +++ b/liteyuki/plugins/liteyuki_pacman/rpm.py @@ -13,7 +13,8 @@ from liteyuki.utils.resource import (ResourceMetadata, add_resource_pack, change require("nonebot_plugin_alconna") from nonebot_plugin_alconna import Alconna, Args, on_alconna, Arparma, Subcommand -rpm = on_alconna( + +@on_alconna( aliases={"资源包"}, command=Alconna( "rpm", @@ -53,10 +54,7 @@ rpm = on_alconna( ), ), permission=SUPERUSER -) - - -@rpm.handle() +).handle() async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma): ulang = get_user_lang(str(event.user_id)) reply = "" diff --git a/liteyuki/utils/__init__.py b/liteyuki/utils/__init__.py index 6bb5b637..8012f85d 100644 --- a/liteyuki/utils/__init__.py +++ b/liteyuki/utils/__init__.py @@ -6,7 +6,7 @@ import sys import nonebot __NAME__ = "LiteyukiBot" -__VERSION__ = "6.2.7" # 60201 +__VERSION__ = "6.2.8" # 60201 import requests