1
0
forked from bot/app

fix: 插件列表显示错误问题

This commit is contained in:
远野千束 2024-03-24 09:43:34 +08:00
parent de0c073c26
commit fab5be70b3
45 changed files with 501 additions and 303 deletions

View File

@ -1,10 +1,10 @@
import nonebot
from nonebot.plugin import PluginMetadata
from src.utils.language import get_system_lang
from src.utils.data_manager import *
from liteyuki.utils.language import get_default_lang
from liteyuki.utils.data_manager import *
from .loader import *
from .webdash import *
from src.utils.config import config
from liteyuki.utils.config import config
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
@ -20,6 +20,6 @@ __plugin_meta__ = PluginMetadata(
auto_migrate() # 自动迁移数据库
sys_lang = get_system_lang()
sys_lang = get_default_lang()
nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name")))
nonebot.logger.info(sys_lang.get("main.enable_webdash", URL=f"http://127.0.0.1:{config.get('port', 8080)}"))
nonebot.logger.info(sys_lang.get("main.enable_webdash", URL=f"http://127.0.0.1:{config.get('port', 20216)}"))

View File

@ -0,0 +1,22 @@
import os
import nonebot.plugin
from liteyuki.utils.data_manager import InstalledPlugin, plugin_db
from liteyuki.utils.resource import load_resource_from_dir
from liteyuki.utils.tools import check_for_package
THIS_PLUGIN_NAME = os.path.basename(os.path.dirname(__file__))
RESOURCE_PATH = "liteyuki/resources"
load_resource_from_dir(RESOURCE_PATH)
nonebot.plugin.load_plugins("liteyuki/plugins")
nonebot.plugin.load_plugins("plugins")
installed_plugins = plugin_db.all(InstalledPlugin)
if installed_plugins:
for installed_plugin in plugin_db.all(InstalledPlugin):
if not check_for_package(installed_plugin.module_name):
nonebot.logger.error(f"{installed_plugin.module_name} not installed, but in loading database. please run `npm fixup` in chat to reinstall it.")
else:
nonebot.load_plugin(installed_plugin.module_name)

View File

@ -3,8 +3,8 @@ import psutil
from dash import Dash, Input, Output, dcc, html
from starlette.middleware.wsgi import WSGIMiddleware
from src.utils.language import Language
from src.utils.tools import convert_size
from liteyuki.utils.language import Language
from liteyuki.utils.tools import convert_size
app = nonebot.get_app()

View File

@ -6,8 +6,8 @@ from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from src.utils.message import send_markdown
from src.utils.ly_typing import T_Message, T_Bot, v11, T_MessageEvent
from liteyuki.utils.message import send_markdown
from liteyuki.utils.ly_typing import T_Message, T_Bot, v11, T_MessageEvent
md_test = on_command("mdts", aliases={"会话md"}, permission=SUPERUSER)
md_group = on_command("mdg", aliases={"群md"}, permission=SUPERUSER)

View File

@ -4,9 +4,9 @@ from typing import Optional
import aiofiles
import nonebot.plugin
from src.utils.data import Database, LiteModel
from src.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db
from src.utils.ly_typing import T_MessageEvent
from liteyuki.utils.data import Database, LiteModel
from liteyuki.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db
from liteyuki.utils.ly_typing import T_MessageEvent
LNPM_COMMAND_START = "lnpm"
@ -58,16 +58,17 @@ def get_plugin_default_enable(plugin_module_name: str) -> bool:
Returns:
bool: 插件默认状态
"""
return (nonebot.plugin.get_plugin(plugin_module_name).metadata.extra.get('default_enable', True)
if nonebot.plugin.get_plugin(plugin_module_name) and nonebot.plugin.get_plugin(plugin_module_name).metadata else True) \
if nonebot.plugin.get_plugin(plugin_module_name) else False
plug = nonebot.plugin.get_plugin_by_module_name(plugin_module_name)
return (plug.metadata.extra.get('default_enable', True)
if plug.metadata else True) if plug else True
def get_plugin_current_enable(event: T_MessageEvent, plugin_module_name: str) -> bool:
def get_plugin_session_enable(event: T_MessageEvent, plugin_module_name: str) -> bool:
"""
获取插件当前启用状态
获取插件当前会话启用状态
Args:
event: 会话事件
plugin_module_name (str): 插件模块名
Returns:
@ -88,6 +89,10 @@ def get_plugin_current_enable(event: T_MessageEvent, plugin_module_name: str) ->
return plugin_module_name in session.enabled_plugins
def get_plugin_global_enable(plugin_module_name: str) -> bool:
return True
def get_plugin_can_be_toggle(plugin_module_name: str) -> bool:
"""
获取插件是否可以被启用/停用
@ -98,5 +103,5 @@ def get_plugin_can_be_toggle(plugin_module_name: str) -> bool:
Returns:
bool: 插件是否可以被启用/停用
"""
return nonebot.plugin.get_plugin(plugin_module_name).metadata.extra.get('toggleable', True) \
if nonebot.plugin.get_plugin(plugin_module_name) and nonebot.plugin.get_plugin(plugin_module_name).metadata else True
plug = nonebot.plugin.get_plugin_by_module_name(plugin_module_name)
return plug.metadata.extra.get('toggleable', True) if plug and plug.metadata else True

View File

@ -9,9 +9,9 @@ from arclet.alconna import Arparma, MultiVar
from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import Alconna, Args, Subcommand, on_alconna
from src.utils.language import get_user_lang
from src.utils.ly_typing import T_Bot
from src.utils.message import Markdown as md, send_markdown
from liteyuki.utils.language import get_user_lang
from liteyuki.utils.ly_typing import T_Bot
from liteyuki.utils.message import Markdown as md, send_markdown
from .common import *
npm_alc = on_alconna(
@ -45,9 +45,6 @@ npm_alc = on_alconna(
)
@npm_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
ulang = get_user_lang(str(event.user_id))
@ -103,7 +100,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
if r_load:
if found_in_db_plugin is None:
plugin_db.save(installed_plugin)
plugin_db.upsert(installed_plugin)
info = ulang.get('npm.install_success', NAME=store_plugin.name).replace('_', r'\\_') # markdown转义
await send_markdown(
f"{info}\n\n"
@ -192,9 +189,6 @@ async def npm_search(keywords: list[str]) -> list[StorePlugin]:
return results
def npm_install(plugin_module_name) -> tuple[bool, str]:
"""
Args:
@ -209,8 +203,8 @@ def npm_install(plugin_module_name) -> tuple[bool, str]:
sys.stderr = buffer
mirrors = [
"https://pypi.mirrors.cqupt.edu.cn/simple", # 重庆邮电大学
"https://pypi.tuna.tsinghua.edu.cn/simple", # 清华大学
"https://pypi.mirrors.cqupt.edu.cn/simple", # 重庆邮电大学
"https://pypi.liteyuki.icu/simple", # 轻雪镜像
"https://pypi.org/simple", # 官方源
]

View File

@ -8,23 +8,35 @@ from nonebot.message import run_preprocessor
from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma
from src.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db
from src.utils.message import Markdown as md, send_markdown
from src.utils.permission import GROUP_ADMIN, GROUP_OWNER
from src.utils.ly_typing import T_Bot, T_MessageEvent
from src.utils.language import get_user_lang
from .common import get_plugin_can_be_toggle, get_plugin_current_enable, get_plugin_default_enable
from liteyuki.utils.data_manager import GroupChat, InstalledPlugin, User, group_db, plugin_db, user_db
from liteyuki.utils.message import Markdown as md, send_markdown
from liteyuki.utils.permission import GROUP_ADMIN, GROUP_OWNER
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.language import get_user_lang
from .common import get_plugin_can_be_toggle, get_plugin_global_enable, get_plugin_session_enable, get_plugin_default_enable
from .installer import get_store_plugin, npm_update
list_plugins = on_command("list-plugin", aliases={"列出插件", "插件列表"}, priority=0)
# toggle_plugin = on_command("enable-plugin", aliases={"启用插件", "停用插件", "disable-plugin"}, priority=0)
list_plugins = on_alconna(
Alconna(
['list-plugins', "插件列表", "列出插件"],
)
)
toggle_plugin = on_alconna(
Alconna(
['enable-plugin', 'disable-plugin'],
Args['plugin_name', str]['global', bool, False],
Args['plugin_name', str],
)
)
global_toggle = on_alconna(
Alconna(
['toggle-global'],
Args['plugin_name', str],
),
permission=SUPERUSER
)
@list_plugins.handle()
async def _(event: T_MessageEvent, bot: T_Bot):
@ -35,39 +47,59 @@ async def _(event: T_MessageEvent, bot: T_Bot):
for plugin in nonebot.get_loaded_plugins():
# 检查是否有 metadata 属性
# 添加帮助按钮
btn_usage = md.button(lang.get('npm.usage'), f'help {plugin.name}', False)
btn_usage = md.button(lang.get('npm.usage'), f'help {plugin.module_name}', False)
store_plugin = await get_store_plugin(plugin.module_name)
session_enable = get_plugin_session_enable(event, plugin.module_name)
default_enable = get_plugin_default_enable(plugin.module_name)
print(session_enable, default_enable, plugin.module_name)
if store_plugin:
btn_homepage = md.link(lang.get('npm.homepage'), store_plugin.homepage)
elif plugin.metadata and plugin.metadata.extra.get('liteyuki'):
show_name = store_plugin.name
show_desc = store_plugin.desc
elif plugin.metadata:
if plugin.metadata.extra.get('liteyuki'):
btn_homepage = md.link(lang.get('npm.homepage'), "https://github.com/snowykami/LiteyukiBot")
else:
btn_homepage = lang.get('npm.homepage')
show_name = plugin.metadata.name
show_desc = plugin.metadata.description
else:
btn_homepage = lang.get('npm.homepage')
show_name = plugin.name
show_desc = lang.get('npm.no_description')
if plugin.metadata:
reply += (f"\n**{md.escape(plugin.metadata.name)}**\n"
f"\n > {plugin.metadata.description}")
reply += (f"\n**{md.escape(show_name)}**\n"
f"\n > {md.escape(show_desc)}")
else:
reply += (f"**{md.escape(plugin.name)}**\n"
f"\n > {lang.get('npm.no_description')}")
reply += (f"**{md.escape(show_name)}**\n"
f"\n > {md.escape(show_desc)}")
reply += f"\n > {btn_usage} {btn_homepage}"
if await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event):
# 添加启用/停用插件按钮
btn_toggle = lang.get('npm.disable') if plugin.metadata and not plugin.metadata.extra.get('toggleable') \
else md.button(lang.get('npm.disable'), f'enable-plugin {plugin.module_name}')
cmd_toggle = f"{'disable' if session_enable else 'enable'}-plugin {plugin.module_name}"
text_toggle = lang.get('npm.disable' if session_enable else 'npm.enable')
can_be_toggle = get_plugin_can_be_toggle(plugin.module_name)
btn_toggle = text_toggle if not can_be_toggle else md.button(text_toggle, cmd_toggle)
reply += f" {btn_toggle}"
if await SUPERUSER(bot, event):
plugin_in_database = plugin_db.first(InstalledPlugin, 'module_name = ?', plugin.module_name)
# 添加移除插件
# 添加移除插件和全局切换按钮
global_enable = get_plugin_global_enable(plugin.module_name)
btn_uninstall = (
md.button(lang.get('npm.uninstall'), f'npm uninstall {plugin.module_name}')) if plugin_in_database else lang.get(
'npm.uninstall')
btn_toggle_global = lang.get('npm.disable') if plugin.metadata and not plugin.metadata.extra.get('toggleable') \
else md.button(lang.get('npm.disable_global'), f'disable-plugin {plugin.module_name} true')
btn_toggle_global_text = lang.get('npm.disable_global' if global_enable else 'npm.enable_global')
cmd_toggle_global = f'npm toggle-global {plugin.module_name}'
btn_toggle_global = btn_toggle_global_text if not can_be_toggle else md.button(btn_toggle_global_text, cmd_toggle_global)
reply += f" {btn_uninstall} {btn_toggle_global}"
reply += "\n\n***\n"
@ -83,9 +115,10 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
plugin_module_name = result.args.get("plugin_name")
toggle = result.header_result == 'enable-plugin' # 判断是启用还是停用
current_enable = get_plugin_current_enable(event, plugin_module_name) # 获取插件当前状态
current_enable = get_plugin_session_enable(event, plugin_module_name) # 获取插件当前状态
default_enable = get_plugin_default_enable(plugin_module_name) # 获取插件默认状态
can_be_toggled = get_plugin_can_be_toggle(plugin_module_name) # 获取插件是否可以被启用/停用
if not can_be_toggled:
@ -102,10 +135,6 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
session = group_db.first(GroupChat, "group_id = ?", event.group_id, default=GroupChat(group_id=event.group_id))
else:
raise FinishedException(ulang.get("Permission Denied"))
# 启用 已停用的默认启用插件 将其从停用列表移除
# 启用 已停用的默认停用插件 将其放到启用列表
# 停用 已启用的默认启用插件 将其放到停用列表
# 停用 已启用的默认停用插件 将其从启用列表移除
try:
if toggle:
if default_enable:
@ -126,14 +155,21 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
ERROR=str(e))
)
await toggle_plugin.finish(
ulang.get(
"npm.toggle_success",
NAME=plugin_module_name,
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))
)
if event.message_type == "private":
user_db.save(session)
user_db.upsert(session)
else:
group_db.save(session)
group_db.upsert(session)
@run_preprocessor
async def _(event: T_MessageEvent, matcher: Matcher):
plugin = matcher.plugin
# TODO 插件启用/停用检查hook
nonebot.logger.info(f"Plugin: {plugin.module_name}")
nonebot.logger.info(f"Plugin Callapi: {plugin.module_name}")

View File

@ -1,5 +1,5 @@
# 插件权限管理器对api调用进行hook限制防止插件滥用api
from src.utils.data import LiteModel
from liteyuki.utils.data import LiteModel
class PermissionAllow(LiteModel):

View File

@ -2,11 +2,11 @@ from typing import Optional
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
from src.utils.data import LiteModel
from src.utils.data_manager import User, user_db
from src.utils.language import Language, get_all_lang, get_user_lang
from src.utils.ly_typing import T_Bot, T_MessageEvent
from src.utils.message import Markdown as md, send_markdown
from liteyuki.utils.data import LiteModel
from liteyuki.utils.data_manager import User, user_db
from liteyuki.utils.language import Language, get_all_lang, get_user_lang
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message import Markdown as md, send_markdown
profile_alc = on_alconna(
Alconna(
@ -43,7 +43,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
r = set_profile(result.args["key"], result.args["value"])
if r:
user.profile[result.args["key"]] = result.args["value"]
user_db.save(user) # 数据库保存
user_db.upsert(user) # 数据库保存
await profile_alc.finish(
ulang.get(
"user.profile.set_success",

View File

@ -1,6 +1,12 @@
language.name=English
main.current_language=Current system language: {LANG}
log.debug=Debug
log.info=Info
log.warning=WARN
log.error=Error
log.success=Success
main.current_language=Current config language: {LANG}
main.enable_webdash=Web dashboard is enabled: {URL}
main.monitor.title=Liteyuki Monitor
main.monitor.description=Monitor your server with Liteyuki Monitor
@ -46,6 +52,7 @@ npm.prev_page=Prev
npm.plugin_cannot_be_toggled=Plugin {NAME} cannot be toggled
npm.plugin_already=Plugin {NAME} is already in {STATUS} state, no need for repeated operation
npm.toggle_failed=Failed to {STATUS} plugin {NAME}: {ERROR}
npm.toggle_success=Succeeded in {STATUS} plugin {NAME}
user.profile.edit=Edit
user.profile.set=Set

View File

@ -1,5 +1,11 @@
language.name = 日本語
log.debug=デバッグ
log.info=情報
log.warning=警告
log.error=エラー
log.success=成功
main.current_language = 現在のシステム言語: {LANG}
main.enable_webdash = ウェブダッシュボードが有効になりました: {URL}
main.monitor.title = Liteyukiモニタリングパネル
@ -46,6 +52,7 @@ npm.prev_page = 前のページ
npm.plugin_cannot_be_toggled=プラグイン {NAME} は有効または無効にできません
npm.plugin_already=プラグイン {NAME} はすでに {STATUS} 状態です。繰り返し操作する必要はありません
npm.toggle_failed=プラグイン {NAME} を {STATUS} にするのに失敗しました: {ERROR}
npm.toggle_success=プラグイン {NAME} が {STATUS} になりました
user.profile.edit=編集
user.profile.set=設定

View File

@ -1,6 +1,12 @@
language.name=简体中文
main.current_language=当前系统语言为: {LANG}
log.debug=调试
log.info=信息
log.warning=警告
log.error=错误
log.success=成功
main.current_language=当前配置语言为: {LANG}
main.enable_webdash=已启用网页监控面板: {URL}
main.monitor.title=轻雪监控面板
main.monitor.description=轻雪机器人监控面板
@ -32,7 +38,7 @@ npm.search_no_result=无搜索结果
npm.too_many_results=内容过多,{HIDE_NUM}项已隐藏,请限制关键字搜索
npm.install_success={NAME} 安装成功
npm.install_failed={NAME} 安装失败,请查看日志获取详细信息,如不能解决,请访问{HOMEPAGE}寻求帮助
npm.uninstall_success={NAME} 卸载成功
npm.uninstall_success={NAME} 卸载成功,下次重启生效
npm.uninstall_failed={NAME} 卸载失败
npm.load_failed={NAME} 加载失败,请在控制台查看详细信息,检查依赖或配置是否正确,如不能解决,请访问{HOMEPAGE}寻求帮助
npm.plugin_not_found=未在商店中找到 {NAME},请尝试更新商店信息或检查拼写
@ -46,6 +52,7 @@ npm.prev_page=上一页
npm.plugin_cannot_be_toggled=插件 {NAME} 无法被启用或停用
npm.plugin_already=插件 {NAME} 已经是 {STATUS} 状态,无需重复操作
npm.toggle_failed=插件 {NAME} {STATUS} 失败: {ERROR}
npm.toggle_success=插件 {NAME} {STATUS} 成功
user.profile.edit=修改
user.profile.set=设置

View File

@ -1,4 +1,10 @@
language.name=繁體中文
language.name=繁體中文(香港)
log.debug=調試
log.info=信息
log.warning=警告
log.error=錯誤
log.success=成功
main.current_language=當前系統語言為:{LANG}
main.enable_webdash=已啟用網頁監控面板:{URL}
@ -46,6 +52,8 @@ npm.prev_page=上一頁
npm.plugin_cannot_be_toggled=無法啟用或停用插件 {NAME}
npm.plugin_already=插件 {NAME} 已處於 {STATUS} 狀態,無需重複操作
npm.toggle_failed=插件 {NAME} {STATUS} 失敗: {ERROR}
npm.toggle_success=插件 {NAME} {STATUS} 成功
user.profile.edit=編輯
user.profile.set=設定

View File

@ -1,5 +1,11 @@
language.name=简体中文(轻雪版)
log.debug=调试
log.info=信息
log.warning=有问题哦
log.error=出错啦
log.success=成功啦
main.current_language=现在系统用的语言是:{LANG} 喔!
main.enable_webdash=已经打开了网页监控板:{URL} 啦!
main.monitor.title=监控板

View File

@ -1,4 +1,10 @@
language.name=漢字
language.name=中文(華夏)
log.debug=調試
log.info=信息
log.warning=警告
log.error=錯誤
log.success=成功
main.current_language=當前之系統語言為:{LANG}
main.enable_webdash=已啟用網頁監控板:{URL}

View File

@ -0,0 +1,37 @@
import nonebot
from .log import logger
import sys
__NAME__ = "LiteyukiBot"
__VERSION__ = "6.2.1" # 60201
major, minor, patch = map(int, __VERSION__.split("."))
__VERSION_I__ = major * 10000 + minor * 100 + patch
def init():
"""
初始化
Returns:
"""
# 检测python版本是否高于3.10
if sys.version_info < (3, 10):
nonebot.logger.error("This project requires Python3.10+ to run, please upgrade your Python Environment.")
exit(1)
print("\033[34m" + r""" __ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ """ + "\033[0m")
nonebot.logger.info(
f"Run Liteyuki with Python{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} "
f"at {sys.executable}"
)
nonebot.logger.info(f"{__NAME__} {__VERSION__}({__VERSION_I__}) is running")

View File

@ -4,7 +4,7 @@ import nonebot
import yaml
from pydantic import BaseModel
config = None
config = {}
class BasicConfig(BaseModel):

View File

@ -31,7 +31,7 @@ class BaseORMAdapter(ABC):
"""
raise NotImplementedError
def save(self, *args, **kwargs):
def upsert(self, *args, **kwargs):
"""存储数据
Returns:
@ -171,7 +171,7 @@ class Database(BaseORMAdapter):
self.conn.commit()
nonebot.logger.debug(f'Table {table_name} migrated successfully')
def save(self, *models: LiteModel) -> int | tuple:
def upsert(self, *models: LiteModel) -> int | tuple:
"""存储数据检查id字段如果有id字段则更新没有则插入
Args:
@ -192,7 +192,7 @@ class Database(BaseORMAdapter):
for field, value in model.__dict__.items():
if isinstance(value, LiteModel):
key_list.append(f'{self.FOREIGNID}{field}')
value_list.append(f'{self.ID}:{value.__class__.__name__}:{self.save(value)}')
value_list.append(f'{self.ID}:{value.__class__.__name__}:{self.upsert(value)}')
elif isinstance(value, list):
key_list.append(f'{self.LIST}{field}')
value_list.append(self._flat(value))
@ -225,7 +225,7 @@ class Database(BaseORMAdapter):
return_data = {}
for k, v in data.items():
if isinstance(v, LiteModel):
return_data[f'{self.FOREIGNID}{k}'] = f'{self.ID}:{v.__class__.__name__}:{self.save(v)}'
return_data[f'{self.FOREIGNID}{k}'] = f'{self.ID}:{v.__class__.__name__}:{self.upsert(v)}'
elif isinstance(v, list):
return_data[f'{self.LIST}{k}'] = self._flat(v)
elif isinstance(v, dict):
@ -239,7 +239,7 @@ class Database(BaseORMAdapter):
return_data = []
for v in data:
if isinstance(v, LiteModel):
return_data.append(f'{self.ID}:{v.__class__.__name__}:{self.save(v)}')
return_data.append(f'{self.ID}:{v.__class__.__name__}:{self.upsert(v)}')
elif isinstance(v, list):
return_data.append(self._flat(v))
elif isinstance(v, dict):

View File

@ -0,0 +1,45 @@
import os
from pydantic import Field
from liteyuki.utils.data import LiteModel, Database as DB
DATA_PATH = "data/liteyuki"
user_db = DB(os.path.join(DATA_PATH, 'users.ldb'))
group_db = DB(os.path.join(DATA_PATH, 'groups.ldb'))
plugin_db = DB(os.path.join(DATA_PATH, 'plugins.ldb'))
common_db = DB(os.path.join(DATA_PATH, 'common.ldb'))
class User(LiteModel):
user_id: str = Field(str(), alias='user_id')
username: str = Field(str(), alias='username')
profile: dict[str, str] = Field(dict(), alias='profile')
enabled_plugins: list[str] = Field(list(), alias='enabled_plugins')
disabled_plugins: list[str] = Field(list(), alias='disabled_plugins')
class GroupChat(LiteModel):
# Group是一个关键字所以这里用GroupChat
group_id: str = Field(str(), alias='group_id')
group_name: str = Field(str(), alias='group_name')
enabled_plugins: list[str] = Field([], alias='enabled_plugins')
disabled_plugins: list[str] = Field([], alias='disabled_plugins')
class InstalledPlugin(LiteModel):
module_name: str = Field(str(), alias='module_name')
version: str = Field(str(), alias='version')
class GlobalPlugin(LiteModel):
module_name: str = Field(str(), alias='module_name')
enabled: bool = Field(True, alias='enabled')
def auto_migrate():
user_db.auto_migrate(User())
group_db.auto_migrate(GroupChat())
plugin_db.auto_migrate(InstalledPlugin())
common_db.auto_migrate(GlobalPlugin())

View File

@ -9,8 +9,8 @@ from typing import Any
import nonebot
from src.utils.config import config
from src.utils.data_manager import User, user_db
from liteyuki.utils.config import config
from liteyuki.utils.data_manager import User, user_db
_default_lang_code = "en"
_language_data = {
@ -38,7 +38,6 @@ def load_from_lang(file_path: str, lang_code: str = None):
if not line or line.startswith('#'): # 空行或注释
continue
key, value = line.split('=', 1)
nonebot.logger.debug(f"Loaded language text: {key.strip()} -> {value.strip()}")
data[key.strip()] = value.strip()
if lang_code not in _language_data:
_language_data[lang_code] = {}
@ -139,7 +138,7 @@ def get_user_lang(user_id: str) -> Language:
username="Unknown"
))
return Language(user.profile.get('lang',config.get("default_language", get_system_lang_code()) ))
return Language(user.profile.get('lang', config.get("default_language", get_system_lang_code())))
def get_system_lang_code() -> str:
@ -149,11 +148,11 @@ def get_system_lang_code() -> str:
return locale.getdefaultlocale()[0].replace('_', '-')
def get_system_lang() -> Language:
def get_default_lang() -> Language:
"""
获取系统语言
获取默认/系统语言
"""
return Language(get_system_lang_code())
return Language(config.get("default_language", get_system_lang_code()))
def get_all_lang() -> dict[str, str]:

View File

@ -1,35 +1,14 @@
import sys
import logging
from typing import TYPE_CHECKING
from colored import fg
from .language import get_default_lang
import loguru
if TYPE_CHECKING:
# avoid sphinx autodoc resolve annotation failed
# because loguru module do not have `Logger` class actually
from loguru import Logger, Record
# logger = logging.getLogger("nonebot")
logger: "Logger" = loguru.logger
"""NoneBot 日志记录器对象。
默认信息:
- 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`
- 等级: `INFO` 根据 `config.log_level` 配置改变
- 输出: 输出至 stdout
用法:
```python
from nonebot.log import logger
```
"""
# default_handler = logging.StreamHandler(sys.stdout)
# default_handler.setFormatter(
# logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s"))
# logger.addHandler(default_handler)
class LoguruHandler(logging.Handler): # pragma: no cover
@ -59,7 +38,7 @@ def default_filter(record: "Record"):
default_format: str = (
"<g>{time:MM-DD HH:mm:ss}</g> "
"<c>{time:YYYY-MM-DD}</c> <blue>{time:HH:mm:ss}</blue> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}></c> "
"{message}"
@ -74,12 +53,12 @@ logger_id = logger.add(
filter=default_filter,
format=default_format,
)
logger.level("DEBUG", color="<cyan>", icon="DEBU")
logger.level("INFO", color="<white>", icon="INFO")
logger.level("SUCCESS", color="<green>", icon="SUCC")
logger.level("WARNING", color="<yellow>", icon="⚠️WARN")
logger.level("ERROR", color="<red>", icon="ERRO")
slang = get_default_lang()
logger.level("DEBUG", color="<blue>", icon=f"*️⃣ DDDEBUG")
logger.level("INFO", color="<white>", icon=f" IIIINFO")
logger.level("SUCCESS", color="<green>", icon=f" SUCCESS")
logger.level("WARNING", color="<yellow>", icon=f"⚠️ WARNING")
logger.level("ERROR", color="<red>", icon=f"⭕ EEERROR")
"""默认日志处理器 id"""

207
liteyuki/utils/orm.py Normal file
View File

@ -0,0 +1,207 @@
import os
import pickle
import sqlite3
from types import NoneType
from typing import Any
import nonebot
from pydantic import BaseModel, Field
class LiteModel(BaseModel):
"""轻量级模型基类
类型注解统一使用Python3.9的PEP585标准如需使用泛型请使用typing模块的泛型类型
不允许使用id, table_name以及其他SQLite关键字作为字段名不允许使用JSON和ID必须指定默认值且默认值类型必须与字段类型一致
"""
__ID__: int = Field(None, alias='id')
__TABLE_NAME__: str = Field(None, alias='table_name')
class Database:
TYPE_MAPPING = {
int : "INTEGER",
float : "REAL",
str : "TEXT",
bool : "INTEGER",
bytes : "BLOB",
NoneType: "NULL",
dict : "BLOB", # LITEYUKIDICT{key_name}
list : "BLOB", # LITEYUKILIST{key_name}
tuple : "BLOB", # LITEYUKITUPLE{key_name}
set : "BLOB", # LITEYUKISET{key_name}
}
# 基础类型
BASIC_TYPE = [int, float, str, bool, bytes, NoneType]
# 可序列化类型
ITERABLE_TYPE = [dict, list, tuple, set]
LITEYUKI = "LITEYUKI"
# 字段前缀映射,默认基础类型为""
FIELD_PREFIX_MAPPING = {
dict : f"{LITEYUKI}DICT",
list : f"{LITEYUKI}LIST",
tuple : f"{LITEYUKI}TUPLE",
set : f"{LITEYUKI}SET",
type(LiteModel): f"{LITEYUKI}MODEL"
}
def __init__(self, db_name: str):
if not os.path.exists(os.path.dirname(db_name)):
os.makedirs(os.path.dirname(db_name))
self.conn = sqlite3.connect(db_name) # 连接对象
self.conn.row_factory = sqlite3.Row # 以字典形式返回查询结果
self.cursor = self.conn.cursor() # 游标对象
def auto_migrate(self, *args: LiteModel):
"""
自动迁移模型
Args:
*args: 模型类实例化对象支持空默认值不支持嵌套迁移
Returns:
"""
for model in args:
if not model.__TABLE_NAME__:
raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
# 若无则创建表
self.cursor.execute(
f'CREATE TABLE IF NOT EXISTS {model.__TABLE_NAME__} (id INTEGER PRIMARY KEY AUTOINCREMENT)'
)
# 获取表结构
new_fields, new_stored_types = (
zip(
*[(self._get_stored_field_prefix(model.__getattribute__(field)) + field, self._get_stored_type(model.__getattribute__(field)))
for field in model.__annotations__]
)
)
# 原有的字段列表
existing_fields = self.cursor.execute(f'PRAGMA table_info({model.__TABLE_NAME__})').fetchall()
existing_types = [field['name'] for field in existing_fields]
# 检测缺失字段由于SQLite是动态类型所以不需要检测类型
for n_field, n_type in zip(new_fields, new_stored_types):
if n_field not in existing_types:
nonebot.logger.debug(f'ALTER TABLE {model.__TABLE_NAME__} ADD COLUMN {n_field} {n_type}')
self.cursor.execute(
f'ALTER TABLE {model.__TABLE_NAME__} ADD COLUMN {n_field} {n_type}'
)
# 检测多余字段进行删除
for e_field in existing_types:
if e_field not in new_fields and e_field not in ['id']:
nonebot.logger.debug(f'ALTER TABLE {model.__TABLE_NAME__} DROP COLUMN {e_field}')
self.cursor.execute(
f'ALTER TABLE {model.__TABLE_NAME__} DROP COLUMN {e_field}'
)
self.conn.commit()
def save(self, *args: LiteModel) -> [int | tuple[int, ...]]:
"""
保存或更新模型
Args:
*args: 模型类实例化对象支持空默认值不支持嵌套迁移
Returns:
"""
ids = []
for model in args:
if not model.__TABLE_NAME__:
raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
if not self.cursor.execute(f'PRAGMA table_info({model.__TABLE_NAME__})').fetchall():
raise ValueError(f"数据表{model.__TABLE_NAME__}不存在,请先迁移{model.__class__.__name__}模型")
stored_fields, stored_values = [], []
for r_field in model.__annotations__:
r_value = model.__getattribute__(r_field)
stored_fields.append(self._get_stored_field_prefix(r_value) + r_field)
if type(r_value) in Database.BASIC_TYPE:
# int str float bool bytes NoneType
stored_values.append(r_value)
elif type(r_value) in Database.ITERABLE_TYPE:
# dict list tuple set
stored_values.append(pickle.dumps(self._flat_save(r_value)))
elif isinstance(r_value, LiteModel):
# LiteModel TABLE_NAME:ID
stored_values.append(f"{r_value.__TABLE_NAME__}:{self.save(r_value)}")
else:
raise ValueError(f"不支持的数据类型{type(r_value)}")
nonebot.logger.debug(f"INSERT OR REPLACE INTO {model.__TABLE_NAME__} ({','.join(stored_fields)}) VALUES ({','.join([_ for _ in stored_values])})")
self.cursor.execute(
f"INSERT OR REPLACE INTO {model.__TABLE_NAME__} ({','.join(stored_fields)}) VALUES ({','.join(['?' for _ in stored_values])})",
stored_values
)
ids.append(self.cursor.lastrowid)
self.conn.commit()
return tuple(ids) if len(ids) > 1 else ids[0]
# 检测id字段是否有1有则更新无则插入
def _flat_save(self, obj) -> Any:
"""扁平化存储
Args:
obj: 需要存储的对象
Returns:
存储的字节流
"""
# TODO 递归扁平化存储
if type(obj) in Database.ITERABLE_TYPE:
for i, item in enumerate(obj) if type(obj) in [list, tuple, set] else obj.items():
if type(item) in Database.BASIC_TYPE:
continue
elif type(item) in Database.ITERABLE_TYPE:
obj[i] = pickle.dumps(self._flat_save(item))
elif isinstance(item, LiteModel):
obj[i] = f"{item.__TABLE_NAME__}:{self.save(item)}"
else:
raise ValueError(f"不支持的数据类型{type(item)}")
else:
raise ValueError(f"不支持的数据类型{type(obj)}")
@staticmethod
def _get_stored_field_prefix(value) -> str:
"""获取存储字段前缀,一定在后加上字段名
LiteModel -> LITEYUKIID
dict -> LITEYUKIDICT
list -> LITEYUKILIST
tuple -> LITEYUKITUPLE
set -> LITEYUKISET
* -> ""
Args:
value: 储存的值
Returns:
Sqlite3存储字段
"""
return Database.FIELD_PREFIX_MAPPING.get(type(value), "")
@staticmethod
def _get_stored_type(value) -> str:
"""获取存储类型
Args:
value: 储存的值
Returns:
Sqlite3存储类型
"""
return Database.TYPE_MAPPING.get(type(value), "TEXT")

View File

@ -1,6 +1,6 @@
from nonebot.adapters.onebot import v11
from src.utils.ly_typing import T_GroupMessageEvent, T_MessageEvent
from liteyuki.utils.ly_typing import T_GroupMessageEvent, T_MessageEvent
GROUP_ADMIN = v11.GROUP_ADMIN
GROUP_OWNER = v11.GROUP_OWNER

View File

@ -4,7 +4,7 @@ import nonebot
import yaml
from typing import Any
from src.utils.data import LiteModel
from liteyuki.utils.data import LiteModel
_resource_data = {}
_loaded_resource_packs = [] # 按照加载顺序排序
@ -31,7 +31,6 @@ def load_resource_from_dir(path: str):
relative_path = os.path.relpath(os.path.join(root, file), path).replace("\\", "/")
abs_path = os.path.join(root, file).replace("\\", "/")
_resource_data[relative_path] = abs_path
nonebot.logger.debug(f"Loaded {relative_path} -> {abs_path}")
if os.path.exists(os.path.join(path, "metadata.yml")):
with open(os.path.join(path, "metadata.yml"), "r", encoding="utf-8") as f:
metadata = yaml.safe_load(f)
@ -39,12 +38,12 @@ def load_resource_from_dir(path: str):
metadata = ResourceMetadata()
metadata["path"] = path
if os.path.exists(os.path.join(path, "lang")):
from src.utils.language import load_from_dir
from liteyuki.utils.language import load_from_dir
load_from_dir(os.path.join(path, "lang"))
_loaded_resource_packs.append(ResourceMetadata(**metadata))
def get_res(path: str, default: Any = None) -> str | Any:
def get(path: str, default: Any = None) -> str | Any:
"""
获取资源包中的文件
Args:

View File

@ -1,3 +1,4 @@
from importlib.metadata import PackageNotFoundError, version
from urllib.parse import quote
@ -72,3 +73,11 @@ def keywords_in_text(keywords: list[str], text: str, all_matched: bool) -> bool:
if keyword in text:
return True
return False
def check_for_package(package_name: str) -> bool:
try:
version(package_name)
return True
except PackageNotFoundError:
return False

10
main.py
View File

@ -1,11 +1,9 @@
import nonebot
from nonebot.adapters.onebot import v11, v12
from liteyuki.utils.config import load_from_yaml
from liteyuki.utils import init
from src.utils import logger
from src.utils.config import load_from_yaml
nonebot.logger = logger
init()
nonebot.init(**load_from_yaml("config.yml"))
adapters = [v11.Adapter, v12.Adapter]
@ -14,7 +12,7 @@ driver = nonebot.get_driver()
for adapter in adapters:
driver.register_adapter(adapter)
nonebot.load_plugin("src.liteyuki_main")
nonebot.load_plugin("liteyuki.liteyuki_main")
if __name__ == "__main__":
nonebot.run()

View File

@ -2,13 +2,16 @@ aiohttp==3.9.3
aiofiles==23.2.1
arclet-alconna==1.8.5
arclet-alconna-tools==0.7.0
colored==2.2.4
dash==2.16.1
nonebot2[fastapi]==2.2.1
nonebot-adapter-onebot==2.4.3
nonebot-plugin-alconna==0.41.0
pip==24.0
psutil==5.9.8
pydantic==2.6.4
pydantic==1.10.14
pytz==2024.1
PyYAML~=6.0.1
starlette~=0.36.3
loguru==0.7.2
importlib_metadata==7.0.2

View File

@ -1,18 +0,0 @@
import os
import nonebot.plugin
from src.utils.data_manager import InstalledPlugin, plugin_db
from src.utils.resource import load_resource_from_dir
THIS_PLUGIN_NAME = os.path.basename(os.path.dirname(__file__))
RESOURCE_PATH = "src/resources"
load_resource_from_dir(RESOURCE_PATH)
nonebot.plugin.load_plugins("src/plugins")
nonebot.plugin.load_plugins("plugins")
installed_plugins = plugin_db.all(InstalledPlugin)
if installed_plugins:
for install_plugin in plugin_db.all(InstalledPlugin):
nonebot.load_plugin(install_plugin.module_name)

View File

@ -1,122 +0,0 @@
from typing import Optional
import nonebot
from nonebot import on_message
from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand, Arg
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import on_alconna
from src.utils.data import LiteModel
from src.utils.message import send_markdown
from src.utils.ly_typing import T_Bot, T_MessageEvent
from src.utils.data import Database
class Node(LiteModel):
bot_id: str
session_type: str
session_id: str
def __str__(self):
return f"{self.bot_id}.{self.session_type}.{self.session_id}"
class Push(LiteModel):
source: Node
target: Node
inde: int
pushes_db = Database("data/pushes.ldb")
pushes_db.auto_migrate(Push, Node)
alc = Alconna(
"lep",
Subcommand(
"add",
Args["source", str],
Args["target", str],
Option("bidirectional", Args["bidirectional", bool])
),
Subcommand(
"rm",
Args["index", int],
),
Subcommand(
"list",
)
)
add_push = on_alconna(alc)
@add_push.handle()
async def _(result: Arparma):
"""bot_id.session_type.session_id"""
if result.subcommands.get("add"):
source = result.subcommands["add"].args.get("source")
target = result.subcommands["add"].args.get("target")
if source and target:
source = source.split(".")
target = target.split(".")
push1 = Push(
source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
inde=len(pushes_db.all(Push, default=[]))
)
pushes_db.save(push1)
if result.subcommands["add"].args.get("bidirectional"):
push2 = Push(
source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
inde=len(pushes_db.all(Push, default=[]))
)
pushes_db.save(push2)
await add_push.finish("添加成功")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("rm"):
index = result.subcommands["rm"].args.get("index")
if index is not None:
try:
pushes_db.delete(Push, "inde = ?", index)
await add_push.finish("删除成功")
except IndexError:
await add_push.finish("索引错误")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("list"):
await add_push.finish(
"\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> "
f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in
enumerate(pushes_db.all(Push, default=[]))]))
else:
await add_push.finish("参数错误")
@on_message(block=False).handle()
async def _(event: T_MessageEvent, bot: T_Bot):
for push in pushes_db.all(Push, default=[]):
if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}":
bot2 = nonebot.get_bot(push.target.bot_id)
msg_formatted = ""
for l in str(event.message).split("\n"):
msg_formatted += f"**{l.strip()}**\n"
push_message = (
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
f"{msg_formatted}")
await send_markdown(push_message, bot2, message_type=push.target.session_type, session_id=push.target.session_id)
return
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪事件推送",
description="事件推送插件支持单向和双向推送支持跨Bot推送",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -1 +0,0 @@
from .log import logger

View File

@ -1,35 +0,0 @@
import os
from src.utils.data import LiteModel, Database as DB
DATA_PATH = "data/liteyuki"
user_db = DB(os.path.join(DATA_PATH, 'users.ldb'))
group_db = DB(os.path.join(DATA_PATH, 'groups.ldb'))
plugin_db = DB(os.path.join(DATA_PATH, 'plugins.ldb'))
class User(LiteModel):
user_id: str
username: str = ""
profile: dict[str, str] = {}
enabled_plugins: list[str] = []
disabled_plugins: list[str] = []
class GroupChat(LiteModel):
# Group是一个关键字所以这里用GroupChat
group_id: str
group_name: str = ""
enabled_plugins: list[str] = []
disabled_plugins: list[str] = []
class InstalledPlugin(LiteModel):
module_name: str
def auto_migrate():
user_db.auto_migrate(User)
group_db.auto_migrate(GroupChat)
plugin_db.auto_migrate(InstalledPlugin)