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

@ -0,0 +1,25 @@
import nonebot
from nonebot.plugin import PluginMetadata
from liteyuki.utils.language import get_default_lang
from liteyuki.utils.data_manager import *
from .loader import *
from .webdash import *
from liteyuki.utils.config import config
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪主程序",
description="轻雪主程序插件,包含了许多初始化的功能",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable": False,
}
)
auto_migrate() # 自动迁移数据库
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', 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

@ -0,0 +1,91 @@
import nonebot
import psutil
from dash import Dash, Input, Output, dcc, html
from starlette.middleware.wsgi import WSGIMiddleware
from liteyuki.utils.language import Language
from liteyuki.utils.tools import convert_size
app = nonebot.get_app()
def get_system_info():
cpu_percent = psutil.cpu_percent()
memory_info = psutil.virtual_memory()
memory_percent = memory_info.percent
return {
"cpu_percent" : cpu_percent,
"memory_percent": memory_percent
}
@app.get("/system_info")
async def system_info():
return get_system_info()
lang = Language()
dash_app = Dash(__name__)
dash_app.layout = dash_app.layout = html.Div(children=[
html.H1(children=lang.get("main.monitor.title"), style={
'textAlign': 'center'
}),
dcc.Graph(id='live-update-graph'),
dcc.Interval(
id='interval-component',
interval=1 * 1000, # in milliseconds
n_intervals=0
)
])
@dash_app.callback(Output('live-update-graph', 'figure'),
[Input('interval-component', 'n_intervals')])
def update_graph_live(n):
lang = Language()
system_inf = get_system_info()
dash_app.layout = html.Div(children=[
html.H1(children=lang.get("main.monitor.title"), style={
'textAlign': 'center'
}),
dcc.Graph(id='live-update-graph'),
dcc.Interval(
id='interval-component',
interval=2 * 1000, # in milliseconds
n_intervals=0
)
])
mem = psutil.virtual_memory()
cpu_f = psutil.cpu_freq()
figure = {
'data' : [
{
'x' : [f"{cpu_f.current / 1000:.2f}GHz {psutil.cpu_count(logical=False)}c{psutil.cpu_count()}t"],
'y' : [system_inf['cpu_percent']],
'type': 'bar',
'name': f"{lang.get('main.monitor.cpu')} {lang.get('main.monitor.usage')}"
},
{
'x' : [f"{convert_size(mem.used, add_unit=False)}/{convert_size(mem.total)}({mem.used / mem.total * 100:.2f}%)"],
'y' : [system_inf['memory_percent']],
'type': 'bar',
'name': f"{lang.get('main.monitor.memory')} {lang.get('main.monitor.usage')}"
},
],
'layout': {
'title': lang.get('main.monitor.description'),
# 'xaxis': {
# 'range': [0, 10]
# }, # 设置x轴的范围
'yaxis': {
'range': [0, 100]
}, # 设置y轴的范围
}
}
return figure
app.mount("/", WSGIMiddleware(dash_app.server))

View File

@ -0,0 +1,59 @@
import nonebot
from nonebot import on_command, on_message
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.exception import FinishedException
from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
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)
md_conv = on_command("md", block=False, permission=SUPERUSER)
placeholder = {
"[": "[",
"]": "]",
"&": "&",
",": ",",
"\n" : r"\n",
"\"" : r'\\\"'
}
@md_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await send_markdown(
str(arg),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
ignore_msg_ids = []
last_sent = None
@md_conv.handle()
async def _(bot: v11.Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
if str(event.user_id) == str(bot.self_id) and str(bot.self_id) in ["2751454815"]:
nonebot.logger.info("开始处理:%s" % str(event.message_id))
data = await send_markdown(str(arg), bot, message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id)
await bot.delete_msg(message_id=event.message_id)
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪Markdown测试",
description="用于测试Markdown的插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -0,0 +1,14 @@
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="轻雪小游戏",
description="内置了一些小游戏",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)

View File

@ -0,0 +1,23 @@
from nonebot.plugin import PluginMetadata
from .manager import *
from .installer import *
from .helper import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪插件管理",
description="本地插件管理和插件商店支持,支持启用/停用,安装/卸载插件",
usage=(
"npm list\n"
"npm enable/disable <plugin_name>\n"
"npm search <keywords...>\n"
"npm install/uninstall <plugin_name>\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : True,
}
)

View File

@ -0,0 +1,107 @@
import json
from typing import Optional
import aiofiles
import nonebot.plugin
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"
class PluginTag(LiteModel):
label: str
color: str = '#000000'
class StorePlugin(LiteModel):
name: str
desc: str
module_name: str
project_link: str = ''
homepage: str = ''
author: str = ''
type: str | None = None
version: str | None = ''
time: str = ''
tags: list[PluginTag] = []
is_official: bool = False
async def get_store_plugin(plugin_module_name: str) -> Optional[StorePlugin]:
"""
获取插件信息
Args:
plugin_module_name (str): 插件模块名
Returns:
Optional[StorePlugin]: 插件信息
"""
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
for plugin in plugins:
if plugin.module_name == plugin_module_name:
return plugin
return None
def get_plugin_default_enable(plugin_module_name: str) -> bool:
"""
获取插件默认启用状态,由插件定义,不存在则默认为启用
Args:
plugin_module_name (str): 插件模块名
Returns:
bool: 插件默认状态
"""
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_session_enable(event: T_MessageEvent, plugin_module_name: str) -> bool:
"""
获取插件当前会话启用状态
Args:
event: 会话事件
plugin_module_name (str): 插件模块名
Returns:
bool: 插件当前状态
"""
if event.message_type == "group":
session: GroupChat = group_db.first(GroupChat, 'group_id = ?', event.group_id, default=GroupChat(group_id=event.group_id))
else:
session: User = user_db.first(User, 'user_id = ?', event.user_id, default=User(user_id=event.user_id))
# 默认停用插件在启用列表内表示启用
# 默认停用插件不在启用列表内表示停用
# 默认启用插件在停用列表内表示停用
# 默认启用插件不在停用列表内表示启用
default_enable = get_plugin_default_enable(plugin_module_name)
if default_enable:
return plugin_module_name not in session.disabled_plugins
else:
return plugin_module_name in session.enabled_plugins
def get_plugin_global_enable(plugin_module_name: str) -> bool:
return True
def get_plugin_can_be_toggle(plugin_module_name: str) -> bool:
"""
获取插件是否可以被启用/停用
Args:
plugin_module_name (str): 插件模块名
Returns:
bool: 插件是否可以被启用/停用
"""
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

View File

@ -0,0 +1,231 @@
import os.path
import sys
from io import StringIO
import aiohttp
import nonebot
import pip
from arclet.alconna import Arparma, MultiVar
from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import Alconna, Args, Subcommand, on_alconna
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(
Alconna(
["npm", "插件"],
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=["rm", "移除", "卸载"],
),
Subcommand(
"list",
alias=["l", "ls", "列表"],
)
),
permission=SUPERUSER
)
@npm_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
ulang = get_user_lang(str(event.user_id))
if not os.path.exists("data/liteyuki/plugins.json"):
await npm_update()
if result.subcommands.get("update"):
r = await npm_update()
if r:
await npm_alc.finish(ulang.get("npm.store_update_success"))
else:
await npm_alc.finish(ulang.get("npm.store_update_failed"))
elif result.subcommands.get("search"):
keywords: list[str] = result.subcommands["search"].args.get("keywords")
rs = await npm_search(keywords)
max_show = 10
if len(rs):
reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***"
for plugin in rs[:min(max_show, len(rs))]:
btn_install = md.button(ulang.get('npm.install'), 'npm install %s' % plugin.module_name)
link_page = md.link(ulang.get('npm.homepage'), plugin.homepage)
link_pypi = md.link(ulang.get('npm.pypi'), plugin.homepage)
reply += (f"\n# **{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 send_markdown(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 npm_alc.send(ulang.get("npm.installing", NAME=plugin_module_name))
r, log = npm_install(plugin_module_name)
log = log.replace("\\", "/")
if not store_plugin:
await npm_alc.finish(ulang.get("npm.plugin_not_found", NAME=plugin_module_name))
homepage_btn = md.button(ulang.get('npm.homepage'), store_plugin.homepage)
if r:
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 = ulang.get('npm.install_success', NAME=store_plugin.name).replace('_', r'\\_') # markdown转义
await send_markdown(
f"{info}\n\n"
f"```\n{log}\n```",
bot,
event=event
)
else:
await npm_alc.finish(ulang.get('npm.plugin_already_installed', NAME=store_plugin.name))
else:
info = ulang.get('npm.load_failed', NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace('_', r'\\_')
await send_markdown(
f"{info}\n\n"
f"```\n{log}\n```\n",
bot,
event=event
)
else:
info = ulang.get('npm.install_failed', NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace('_', r'\\_')
await send_markdown(
f"{info}\n\n"
f"```\n{log}\n```",
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 npm_alc.finish(reply)
else:
await npm_alc.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,
plugin.project_link,
plugin.homepage,
' '.join([tag.label for tag in plugin.tags])
]
)
if all([keyword in plugin_text for keyword in keywords]):
results.append(plugin)
return results
def 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()

View File

@ -0,0 +1,175 @@
import os
import nonebot.plugin
from nonebot import on_command
from nonebot.exception import FinishedException
from nonebot.internal.matcher import Matcher
from nonebot.message import run_preprocessor
from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma
from 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_alconna(
Alconna(
['list-plugins', "插件列表", "列出插件"],
)
)
toggle_plugin = on_alconna(
Alconna(
['enable-plugin', 'disable-plugin'],
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):
if not os.path.exists("data/liteyuki/plugins.json"):
await npm_update()
lang = get_user_lang(str(event.user_id))
reply = f"# {lang.get('npm.loaded_plugins')} | {lang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} \n***\n"
for plugin in nonebot.get_loaded_plugins():
# 检查是否有 metadata 属性
# 添加帮助按钮
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)
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(show_name)}**\n"
f"\n > {md.escape(show_desc)}")
else:
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):
# 添加启用/停用插件按钮
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_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"
await send_markdown(reply, bot, event=event)
@toggle_plugin.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")
toggle = result.header_result == 'enable-plugin' # 判断是启用还是停用
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:
await toggle_plugin.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_module_name))
if current_enable == toggle:
await toggle_plugin.finish(
ulang.get("npm.plugin_already", NAME=plugin_module_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")))
if event.message_type == "private":
session = user_db.first(User, "user_id = ?", event.user_id, default=User(user_id=event.user_id))
else:
if await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event):
session = group_db.first(GroupChat, "group_id = ?", event.group_id, default=GroupChat(group_id=event.group_id))
else:
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)
except Exception as e:
await toggle_plugin.finish(
ulang.get(
"npm.toggle_failed",
NAME=plugin_module_name,
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"),
ERROR=str(e))
)
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.upsert(session)
else:
group_db.upsert(session)
@run_preprocessor
async def _(event: T_MessageEvent, matcher: Matcher):
plugin = matcher.plugin
# TODO 插件启用/停用检查hook
nonebot.logger.info(f"Plugin Callapi: {plugin.module_name}")

View File

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

View File

@ -0,0 +1,16 @@
from nonebot.plugin import PluginMetadata
from .profile_manager import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪用户管理",
description="用户管理插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : True,
}
)

View File

@ -0,0 +1,2 @@
def detect_lang(input_str: str) -> str:
return "zh-CN" if input_str == "zh" else "en"

View File

@ -0,0 +1,135 @@
from typing import Optional
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
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(
["profile", "个人信息"],
Subcommand(
"set",
Args["key", str]["value", str, None],
alias=["s", "设置"],
),
Subcommand(
"get",
Args["key", str],
alias=["g", "查询"],
),
)
)
# json储存
class Profile(LiteModel):
lang: str = "zh-CN"
nickname: str = ""
timezone: str = "Asia/Shanghai"
location: str = ""
@profile_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
user: User = user_db.first(User, "user_id = ?", event.user_id, default=User(user_id=str(event.user_id)))
ulang = get_user_lang(str(event.user_id))
if result.subcommands.get("set"):
if result.subcommands["set"].args.get("value"):
# 对合法性进行校验后设置
r = set_profile(result.args["key"], result.args["value"])
if r:
user.profile[result.args["key"]] = result.args["value"]
user_db.upsert(user) # 数据库保存
await profile_alc.finish(
ulang.get(
"user.profile.set_success",
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
VALUE=result.args["value"]
)
)
else:
await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
else:
# 未输入值,尝试呼出菜单
menu = get_profile_menu(result.args["key"], ulang)
if menu:
await send_markdown(menu, bot, event=event)
else:
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
user.profile[result.args["key"]] = result.args["value"]
elif result.subcommands.get("get"):
if result.args["key"] in user.profile:
await profile_alc.finish(user.profile[result.args["key"]])
else:
await profile_alc.finish("无此键值")
else:
profile = Profile(**user.profile)
for k, v in user.profile.items():
profile.__setattr__(k, v)
reply = f"# {ulang.get('user.profile.info')}\n***\n"
hidden_attr = ["id"]
enter_attr = ["lang", "timezone"]
for key in sorted(profile.dict().keys()):
if key in hidden_attr:
continue
val = profile.dict()[key]
key_text = ulang.get(f"user.profile.{key}")
btn_set = md.button(ulang.get("user.profile.edit"), f"profile set {key}",
enter=True if key in enter_attr else False)
reply += (f"\n**{key_text}** **{val}**\n"
f"\n > {btn_set} {ulang.get(f'user.profile.{key}.desc')}\n\n***\n")
await send_markdown(reply, bot, event=event)
def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
"""获取属性的markdown菜单
Args:
ulang: 用户语言
key: 属性键
Returns:
"""
setting_name = ulang.get(f"user.profile.{key}")
no_menu = ["id", "nickname", "location"]
if key in no_menu:
return None
reply = f"{setting_name} {ulang.get('user.profile.settings')}\n***\n"
if key == "lang":
for lang_code, lang_name in get_all_lang().items():
btn_set = md.button(ulang.get('user.profile.set'), f"profile set {key} {lang_code}")
reply += (f"\n**{lang_name}** **{lang_code}**\n"
f"\n> {btn_set}\n\n***")
return reply
def set_profile(key: str, value: str) -> bool:
"""设置属性使用if分支对每一个合法性进行检查
Args:
key:
value:
Returns:
是否成功设置输入合法性不通过返回False
"""
if key == 'lang':
if value in get_all_lang():
return True
return False
elif key == 'timezone':
# TODO 其他个人信息项目的实现
pass

View File

@ -0,0 +1,15 @@
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="轻雪天气",
description="基于和风天气api的天气插件",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,76 @@
language.name=English
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
main.monitor.cpu=CPU
main.monitor.memory=MEM
main.monitor.swap=SWAP
main.monitor.disk=Disk
main.monitor.usage=Usage
data_manager.migrate_success=Model {NAME} migration successful
npm.loaded_plugins=Loaded plugins
npm.total=Total {TOTAL}
npm.help=Help
npm.usage=Usage
npm.disable=Disable
npm.disable_global=DisableGlobal
npm.enable=Enable
npm.enable_global=EnableGlobal
npm.install=Install
npm.uninstall=Uninstall
npm.installing=Installing {NAME}...
npm.cannot_uninstall=Cannot uninstall
npm.no_description=No description
npm.store_update_success=Plugin store data updated successfully
npm.store_update_failed=Plugin store data update failed
npm.search_result=Search results
npm.search_no_result=No result found
npm.too_many_results=Too many results found, {HIDE_NUM} hidden, please refine your search
npm.install_success={NAME} installed successfully
npm.install_failed={NAME} installation failed, please check the console for more information, or visit the plugin's {HOMEPAGE}
npm.uninstall_success={NAME} uninstalled successfully
npm.uninstall_failed={NAME} uninstallation failed
npm.load_failed={NAME} loading failed, please check the console for more information, or visit the plugin's {HOMEPAGE}
npm.plugin_not_found={NAME} not found, please check the plugin's name
npm.plugin_not_installed={NAME} is not installed
npm.plugin_already_installed={NAME} is already installed
npm.author=Author
npm.homepage=Homepage
npm.pypi=PyPI
npm.next_page=Next
npm.prev_page=Prev
npm.plugin_cannot_be_toggled=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
user.profile_manager.query=Your {ATTR} is {VALUE}
user.profile_manager.set=Yours {ATTR} has been set to {VALUE}
user.profile.settings=settings
user.profile.info=Personal information
user.profile.lang=Language
user.profile.lang.desc=Set user language
user.profile.timezone=Timezone
user.profile.timezone.desc=Set user's current timezone
user.profile.theme=Theme
user.profile.theme.desc=Set user interface theme
user.profile.location=Location
user.profile.location.desc=Set user location information
user.profile.nickname=Nickname
user.profile.nickname.desc=Set Bot's nickname for the user
user.profile.input_value=Please enter a value of {ATTR}
user.profile.set_success=Succeeded in setting {ATTR} as {VALUE}
user.profile.set_failed=Setting {ATTR} failed, please check the input value

View File

@ -0,0 +1,76 @@
language.name = 日本語
log.debug=デバッグ
log.info=情報
log.warning=警告
log.error=エラー
log.success=成功
main.current_language = 現在のシステム言語: {LANG}
main.enable_webdash = ウェブダッシュボードが有効になりました: {URL}
main.monitor.title = Liteyukiモニタリングパネル
main.monitor.description = Liteyukiロボットモニタリングパネル
main.monitor.cpu = CPU
main.monitor.memory = メモリ
main.monitor.swap = スワップ
main.monitor.disk = ディスク
main.monitor.usage = 使用率
data_manager.migrate_success = データが正常に移行されました {NAME}
npm.loaded_plugins = 読み込まれたプラグイン
npm.total = 合計 {TOTAL}
npm.help = ヘルプ
npm.usage = 使用法
npm.disable = 無効
npm.disable_global = グローバル無効
npm.enable = 有効
npm.enable_global = グローバル有効
npm.install = インストール
npm.uninstall = アンインストール
npm.installing = {NAME} インストール中
npm.cannot_uninstall = このプラグインはアンインストールできません
npm.no_description = 説明なし
npm.store_update_success = プラグインストアのデータが正常に更新されました
npm.store_update_failed = プラグインストアのデータの更新に失敗しました
npm.search_result = 検索結果
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_failed = {NAME} のアンインストールに失敗しました
npm.load_failed = {NAME} の読み込みに失敗しました,詳細はログを参照してください, またはプラグインの作者に連絡してください{HOMEPAGE}
npm.plugin_not_found = {NAME} は見つかりません,スペルをチェックしてください
npm.plugin_not_installed = {NAME} はインストールされていません
npm.plugin_already_installed = {NAME} は既にインストールされています
npm.author = 著者
npm.homepage = ホームページ
npm.pypi = PyPI
npm.next_page = 次のページ
npm.prev_page = 前のページ
npm.plugin_cannot_be_toggled=プラグイン {NAME} は有効または無効にできません
npm.plugin_already=プラグイン {NAME} はすでに {STATUS} 状態です。繰り返し操作する必要はありません
npm.toggle_failed=プラグイン {NAME} を {STATUS} にするのに失敗しました: {ERROR}
npm.toggle_success=プラグイン {NAME} が {STATUS} になりました
user.profile.edit=編集
user.profile.set=設定
user.profile_manager.query=あなたの個人情報 {ATTR} は {VALUE} です
user.profile_manager.set=あなたの個人情報 {ATTR} は {VALUE} に設定されました
user.profile.settings=設定
user.profile.info=個人情報
user.profile.lang=言語
user.profile.lang.desc=ユーザーの言語を設定します
user.profile.timezone=タイムゾーン
user.profile.timezone.desc=ユーザーの現在のタイムゾーンを設定します
user.profile.theme=テーマ
user.profile.theme.desc=ユーザーインターフェースのテーマを設定します
user.profile.location=場所
user.profile.location.desc=ユーザーの位置情報を設定します
user.profile.nickname=ニックネーム
user.profile.nickname.desc=ユーザーのニックネームを設定します
user.profile.input_value=新しい{ATTR}を入力してください
user.profile.set_success=設定{ATTR}成功{VALUE}
user.profile.set_failed=設定{ATTR}失敗詳細はInputを参照してください

View File

@ -0,0 +1,76 @@
language.name=简体中文
log.debug=调试
log.info=信息
log.warning=警告
log.error=错误
log.success=成功
main.current_language=当前配置语言为: {LANG}
main.enable_webdash=已启用网页监控面板: {URL}
main.monitor.title=轻雪监控面板
main.monitor.description=轻雪机器人监控面板
main.monitor.cpu=处理器
main.monitor.memory=内存
main.monitor.swap=交换空间
main.monitor.disk=磁盘
main.monitor.usage=使用率
data_manager.migrate_success=数据模型{NAME}迁移成功
npm.loaded_plugins=已加载插件
npm.total=总计 {TOTAL}
npm.help=帮助
npm.usage=用法
npm.disable=停用
npm.disable_global=全局停用
npm.enable=启用
npm.enable_global=全局启用
npm.install=安装
npm.uninstall=卸载
npm.installing=正在安装 {NAME}
npm.cannot_uninstall=无法卸载
npm.no_description=无描述
npm.store_update_success=插件商店数据更新成功
npm.store_update_failed=插件商店数据更新失败
npm.search_result=搜索结果
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_failed={NAME} 卸载失败
npm.load_failed={NAME} 加载失败,请在控制台查看详细信息,检查依赖或配置是否正确,如不能解决,请访问{HOMEPAGE}寻求帮助
npm.plugin_not_found=未在商店中找到 {NAME},请尝试更新商店信息或检查拼写
npm.plugin_not_installed={NAME} 未安装
npm.plugin_already_installed={NAME} 已安装,请勿重复安装
npm.author=作者
npm.homepage=主页
npm.pypi=PyPI
npm.next_page=下一页
npm.prev_page=上一页
npm.plugin_cannot_be_toggled=插件 {NAME} 无法被启用或停用
npm.plugin_already=插件 {NAME} 已经是 {STATUS} 状态,无需重复操作
npm.toggle_failed=插件 {NAME} {STATUS} 失败: {ERROR}
npm.toggle_success=插件 {NAME} {STATUS} 成功
user.profile.edit=修改
user.profile.set=设置
user.profile_manager.query=你的个人信息 {ATTR} 为 {VALUE}
user.profile_manager.set=你的个人信息 {ATTR} 已设置为 {VALUE}
user.profile.settings=设置
user.profile.info=个人信息
user.profile.lang=语言
user.profile.lang.desc=设置用户语言
user.profile.timezone=时区
user.profile.timezone.desc=设置用户当前时区
user.profile.theme=主题
user.profile.theme.desc=设置用户界面主题
user.profile.location=位置
user.profile.location.desc=设置用户位置信息
user.profile.nickname=称呼
user.profile.nickname.desc=设置Bot对用户的称呼
user.profile.input_value=请输入 {ATTR} 的值
user.profile.set_success=成功将 {ATTR} 设置为 {VALUE}
user.profile.set_failed=设置 {ATTR} 失败,请检查输入是否合法

View File

@ -0,0 +1,77 @@
language.name=繁體中文(香港)
log.debug=調試
log.info=信息
log.warning=警告
log.error=錯誤
log.success=成功
main.current_language=當前系統語言為:{LANG}
main.enable_webdash=已啟用網頁監控面板:{URL}
main.monitor.title=輕雪監控面板
main.monitor.description=輕雪機器人監控面板
main.monitor.cpu=處理器
main.monitor.memory=記憶體
main.monitor.swap=交換空間
main.monitor.disk=磁碟
main.monitor.usage=使用率
data_manager.migrate_success=數據模型{NAME}遷移成功
npm.loaded_plugins=已載入插件
npm.total=總計 {TOTAL}
npm.help=幫助
npm.usage=使用方法
npm.disable=停用
npm.disable_global=全域停用
npm.enable=啟用
npm.enable_global=全域啟用
npm.install=安裝
npm.uninstall=卸載
npm.installing=正在安裝 {NAME}
npm.cannot_uninstall=無法卸載
npm.no_description=無描述
npm.store_update_success=插件商店資料更新成功
npm.store_update_failed=插件商店資料更新失敗
npm.search_result=搜尋結果
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_failed={NAME} 卸載失敗
npm.load_failed={NAME} 載入失敗,請在控制台檢查詳細資訊,確認依賴項目或配置是否正確,如無法解決,請訪問{HOMEPAGE}尋求協助
npm.plugin_not_found=在商店中找不到 {NAME},請嘗試更新商店資訊或檢查拼寫
npm.plugin_not_installed={NAME} 未安裝
npm.plugin_already_installed={NAME} 已安裝,請勿重複安裝
npm.author=作者
npm.homepage=主頁
npm.pypi=PyPI
npm.next_page=下一頁
npm.prev_page=上一頁
npm.plugin_cannot_be_toggled=無法啟用或停用插件 {NAME}
npm.plugin_already=插件 {NAME} 已處於 {STATUS} 狀態,無需重複操作
npm.toggle_failed=插件 {NAME} {STATUS} 失敗: {ERROR}
npm.toggle_success=插件 {NAME} {STATUS} 成功
user.profile.edit=編輯
user.profile.set=設定
user.profile_manager.query=您的個人資訊 {ATTR} 為 {VALUE}
user.profile_manager.set=您的個人資訊 {ATTR} 已設定為 {VALUE}
user.profile.settings=設定
user.profile.info=個人資訊
user.profile.lang=語言
user.profile.lang.desc=設定用戶語言
user.profile.timezone=時區
user.profile.timezone.desc=設定用戶時區
user.profile.theme=主題
user.profile.theme.desc=設定用戶界面主題
user.profile.location=位置
user.profile.location.desc=設定用戶位置資訊
user.profile.nickname=稱呼
user.profile.nickname.desc=設定 Bot 對用戶的稱呼
user.profile.input_value=請輸入 {ATTR} 的值
user.profile.set_success=成功將 {ATTR} 設定為 {VALUE}
user.profile.set_failed=設定 {ATTR} 失敗,請檢查輸入是否合法

View File

@ -0,0 +1,75 @@
language.name=简体中文(轻雪版)
log.debug=调试
log.info=信息
log.warning=有问题哦
log.error=出错啦
log.success=成功啦
main.current_language=现在系统用的语言是:{LANG} 喔!
main.enable_webdash=已经打开了网页监控板:{URL} 啦!
main.monitor.title=监控板
main.monitor.description=机器人监控板
main.monitor.cpu=CPU中心处理器
main.monitor.memory=记忆体
main.monitor.swap=换来换去
main.monitor.disk=硬碟
main.monitor.usage=用得多吗?嘻嘻~
data_manager.migrate_success=资料的模型 {NAME} 成功移动了!
npm.loaded_plugins=已经装载插件啦~
npm.total=总数 {TOTAL} 哦!
npm.help=需要帮助吗?来咨询我吧!
npm.usage=怎么用呢?跟我学学吧!
npm.disable=停下来~
npm.disable_global=全球停下来!
npm.enable=继续~
npm.enable_global=全球继续吧!
npm.install=安装起来吧!
npm.uninstall=卸下来吧~
npm.installing=正在安装 {NAME} 啦~
npm.cannot_uninstall=不能卸载呢~
npm.no_description=没有描述哦~
npm.store_update_success=商店的资料成功更新了!
npm.store_update_failed=商店的资料更新失败了~
npm.search_result=找到了一些东西呢~
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_failed={NAME} 卸载失败了~
npm.load_failed={NAME} 装载失败了,去 {HOMEPAGE} 寻求帮助吧!
npm.plugin_not_found=没在商店里找到 {NAME} 呢~
npm.plugin_not_installed={NAME} 没有安装~
npm.plugin_already_installed={NAME} 已经安装了呢~
npm.author=作者
npm.homepage=主页
npm.pypi=PyPI
npm.next_page=下一页
npm.prev_page=上一页
npm.plugin_cannot_be_toggled=插件 {NAME} 无法被启用或停用呢~
npm.plugin_already=插件 {NAME} 已经是 {STATUS} 状态了~
npm.toggle_failed=插件 {NAME} {STATUS} 失败了:{ERROR}
user.profile.edit=修改
user.profile.set=设定
user.profile_manager.query=您的资料 {ATTR} 是 {VALUE} 呢~
user.profile_manager.set=您的资料 {ATTR} 设定为 {VALUE} 了呢~
user.profile.settings=设定
user.profile.info=个人资料
user.profile.lang=语言
user.profile.lang.desc=设定您的语言哦~
user.profile.timezone=时区
user.profile.timezone.desc=设定您的时区呢~
user.profile.theme=主题
user.profile.theme.desc=设定您的界面主题哦~
user.profile.location=地点
user.profile.location.desc=设定您的地点信息呢~
user.profile.nickname=昵称
user.profile.nickname.desc=设定 Bot 对您的称呼吧~
user.profile.input_value=请输入 {ATTR} 的值吧!
user.profile.set_success={ATTR} 设定为 {VALUE} 成功啦!
user.profile.set_failed={ATTR} 设定失败了哦~请检查输入是否合法呢~

View File

@ -0,0 +1,75 @@
language.name=中文(華夏)
log.debug=調試
log.info=信息
log.warning=警告
log.error=錯誤
log.success=成功
main.current_language=當前之系統語言為:{LANG}
main.enable_webdash=已啟用網頁監控板:{URL}
main.monitor.title=輕雪監控板
main.monitor.description=輕雪巨神之監控板
main.monitor.cpu=處理器
main.monitor.memory=記憶體
main.monitor.swap=交換空間
main.monitor.disk=磁盤
main.monitor.usage=使用率
data_manager.migrate_success=資料之模型{NAME}遷移成功
npm.loaded_plugins=已載入之插件
npm.total=總計 {TOTAL}
npm.help=助己
npm.usage=用法
npm.disable=停用
npm.disable_global=全域停用
npm.enable=啓用
npm.enable_global=全域啓用
npm.install=安裝
npm.uninstall=卸載
npm.installing=正在安裝 {NAME}
npm.cannot_uninstall=無法卸載
npm.no_description=無說明
npm.store_update_success=插件商店之資料更新成功
npm.store_update_failed=插件商店之資料更新失敗
npm.search_result=搜尋之結果
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_failed={NAME} 卸載失敗
npm.load_failed={NAME} 載入失敗,詳細信息可於控制臺查詢,檢查依賴或設置是否正確,如不能解決,請訪{HOMEPAGE}求助
npm.plugin_not_found=未在商店中見{NAME},可嘗試更新商店信息或檢查拼寫
npm.plugin_not_installed={NAME} 未安裝
npm.plugin_already_installed={NAME} 已安裝,請勿重複安裝
npm.author=作者
npm.homepage=主頁
npm.pypi=PyPI
npm.next_page=次頁
npm.prev_page=上頁
npm.plugin_cannot_be_toggled=插件{NAME}無法被啓用或停用
npm.plugin_already=插件{NAME}已為{STATUS}狀態,無需重複操作
npm.toggle_failed=插件{NAME}{STATUS}失敗: {ERROR}
user.profile.edit=修改
user.profile.set=設置
user.profile_manager.query=汝之個人信息{ATTR}為{VALUE}
user.profile_manager.set=汝之個人信息{ATTR}已設為{VALUE}
user.profile.settings=設置
user.profile.info=個人信息
user.profile.lang=言語
user.profile.lang.desc=設置用者之言語
user.profile.timezone=時區
user.profile.timezone.desc=設置用者之時區
user.profile.theme=主題
user.profile.theme.desc=設置用者之界面主題
user.profile.location=所在
user.profile.location.desc=設置用者之所在信息
user.profile.nickname=稱呼
user.profile.nickname.desc=設置巨神對用者之稱呼
user.profile.input_value=請輸入{ATTR}之值
user.profile.set_success=成功將{ATTR}設為{VALUE}
user.profile.set_failed=設置{ATTR}失敗,請檢查輸入是否合法

View File

@ -0,0 +1,3 @@
name: 轻雪资源包
description: 轻雪内置资源包,不可卸载
version: 1.0.0

File diff suppressed because it is too large Load Diff

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")

32
liteyuki/utils/config.py Normal file
View File

@ -0,0 +1,32 @@
import os
import nonebot
import yaml
from pydantic import BaseModel
config = {}
class BasicConfig(BaseModel):
host: str = "127.0.0.1"
port: int = 20216
superusers: list[str] = []
command_start: list[str] = ["/", ""]
nickname: list[str] = ["liteyuki"]
def load_from_yaml(file: str) -> dict:
nonebot.logger.debug("Loading config from %s" % file)
global config
if not os.path.exists(file):
nonebot.logger.warning(f'Config file {file} not found, created default config, please modify it and restart')
with open(file, 'w', encoding='utf-8') as f:
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
with open(file, 'r', encoding='utf-8') as f:
conf = yaml.load(f, Loader=yaml.FullLoader)
config = conf
if conf is None:
nonebot.logger.warning(f'Config file {file} is empty, use default config. please modify it and restart')
conf = BasicConfig().dict()
return conf

373
liteyuki/utils/data.py Normal file
View File

@ -0,0 +1,373 @@
import json
import os
import sqlite3
import types
from abc import ABC
from collections.abc import Iterable
import nonebot
from pydantic import BaseModel
from typing import Any
BaseIterable = list | tuple | set | dict
class LiteModel(BaseModel):
"""轻量级模型基类
类型注解统一使用Python3.9的PEP585标准如需使用泛型请使用typing模块的泛型类型
"""
id: int = None
class BaseORMAdapter(ABC):
def __init__(self):
pass
def auto_migrate(self, *args, **kwargs):
"""自动迁移
Returns:
"""
raise NotImplementedError
def upsert(self, *args, **kwargs):
"""存储数据
Returns:
"""
raise NotImplementedError
def first(self, *args, **kwargs):
"""查询第一条数据
Returns:
"""
raise NotImplementedError
def all(self, *args, **kwargs):
"""查询所有数据
Returns:
"""
raise NotImplementedError
def delete(self, *args, **kwargs):
"""删除数据
Returns:
"""
raise NotImplementedError
def update(self, *args, **kwargs):
"""更新数据
Returns:
"""
raise NotImplementedError
class Database(BaseORMAdapter):
"""SQLiteORM适配器严禁使用`FORIEGNID`和`JSON`作为主键前缀,严禁使用`$ID:`作为字符串值前缀
Attributes:
"""
type_map = {
# default: TEXT
str : 'TEXT',
int : 'INTEGER',
float: 'REAL',
bool : 'INTEGER',
list : 'TEXT'
}
DEFAULT_VALUE = {
'TEXT' : '',
'INTEGER': 0,
'REAL' : 0.0
}
FOREIGNID = 'FOREIGNID'
JSON = 'JSON'
LIST = 'LIST'
DICT = 'DICT'
ID = '$ID'
def __init__(self, db_name: str):
super().__init__()
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: type(LiteModel)):
"""自动迁移,检测新模型字段和原有表字段的差异,如有差异自动增删新字段
Args:
*args: 模型类
Returns:
"""
table_name = ''
for model in args:
model: type(LiteModel)
# 检测并创建表若模型未定义id字段则使用自增主键有定义的话使用id字段且id有可能为字符串
table_name = model.__name__
if 'id' in model.__annotations__ and model.__annotations__['id'] is not None:
# 如果模型定义了id字段那么使用模型的id字段
id_type = self.type_map.get(model.__annotations__['id'], 'TEXT')
self.cursor.execute(f'CREATE TABLE IF NOT EXISTS {table_name} (id {id_type} PRIMARY KEY)')
else:
# 如果模型未定义id字段那么使用自增主键
self.cursor.execute(f'CREATE TABLE IF NOT EXISTS {table_name} (id INTEGER PRIMARY KEY AUTOINCREMENT)')
# 获取表字段
self.cursor.execute(f'PRAGMA table_info({table_name})')
table_fields = self.cursor.fetchall()
table_fields = [field[1] for field in table_fields]
raw_fields, raw_types = zip(*model.__annotations__.items())
# 获取模型字段若有模型则添加FOREIGNID前缀若为BaseIterable则添加JSON前缀用多行if判断
model_fields = []
model_types = []
for field, r_type in zip(raw_fields, raw_types):
if isinstance(r_type, type(LiteModel)):
model_fields.append(f'{self.FOREIGNID}{field}')
model_types.append('TEXT')
elif r_type in [list[str], list[int], list[float], list[bool], list]:
model_fields.append(f'{self.LIST}{field}')
model_types.append('TEXT')
elif r_type in [dict[str, str], dict[str, int], dict[str, float], dict[str, bool], dict]:
model_fields.append(f'{self.DICT}{field}')
model_types.append('TEXT')
elif isinstance(r_type, types.GenericAlias):
model_fields.append(f'{self.JSON}{field}')
model_types.append('TEXT')
else:
model_fields.append(field)
model_types.append(self.type_map.get(r_type, 'TEXT'))
# 检测新字段或字段类型是否有变化,有则增删字段,已经加了前缀类型
for field_changed, type_, r_type in zip(model_fields, model_types, raw_types):
if field_changed not in table_fields:
nonebot.logger.debug(f'ALTER TABLE {table_name} ADD COLUMN {field_changed} {type_}')
self.cursor.execute(f'ALTER TABLE {table_name} ADD COLUMN {field_changed} {type_}')
# 在原有的行中添加新字段对应类型的默认值从DEFAULT_TYPE中获取
self.cursor.execute(f'UPDATE {table_name} SET {field_changed} = ? WHERE {field_changed} IS NULL', (self.DEFAULT_VALUE.get(type_, ""),))
# 检测多余字段除了id字段
for field in table_fields:
if field not in model_fields and field != 'id':
nonebot.logger.debug(f'ALTER TABLE {table_name} DROP COLUMN {field}')
self.cursor.execute(f'ALTER TABLE {table_name} DROP COLUMN {field}')
self.conn.commit()
nonebot.logger.debug(f'Table {table_name} migrated successfully')
def upsert(self, *models: LiteModel) -> int | tuple:
"""存储数据检查id字段如果有id字段则更新没有则插入
Args:
models: 数据
Returns:
id: 数据id如果有多个数据则返回id元组
"""
ids = []
for model in models:
table_name = model.__class__.__name__
if not self._detect_for_table(table_name):
raise ValueError(f'{table_name}不存在,请先迁移')
key_list = []
value_list = []
# 处理外键,添加前缀'$IDFieldName'
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.upsert(value)}')
elif isinstance(value, list):
key_list.append(f'{self.LIST}{field}')
value_list.append(self._flat(value))
elif isinstance(value, dict):
key_list.append(f'{self.DICT}{field}')
value_list.append(self._flat(value))
elif isinstance(value, BaseIterable):
key_list.append(f'{self.JSON}{field}')
value_list.append(self._flat(value))
else:
key_list.append(field)
value_list.append(value)
# 更新或插入数据,用?占位
nonebot.logger.debug(f'INSERT OR REPLACE INTO {table_name} ({",".join(key_list)}) VALUES ({",".join(["?" for _ in key_list])})')
self.cursor.execute(f'INSERT OR REPLACE INTO {table_name} ({",".join(key_list)}) VALUES ({",".join(["?" for _ in key_list])})', value_list)
ids.append(self.cursor.lastrowid)
self.conn.commit()
return ids[0] if len(ids) == 1 else tuple(ids)
def _flat(self, data: Iterable) -> str:
"""扁平化数据,返回扁平化对象
Args:
data: 数据,可迭代对象
Returns: json字符串
"""
if isinstance(data, dict):
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.upsert(v)}'
elif isinstance(v, list):
return_data[f'{self.LIST}{k}'] = self._flat(v)
elif isinstance(v, dict):
return_data[f'{self.DICT}{k}'] = self._flat(v)
elif isinstance(v, BaseIterable):
return_data[f'{self.JSON}{k}'] = self._flat(v)
else:
return_data[k] = v
elif isinstance(data, list | tuple | set):
return_data = []
for v in data:
if isinstance(v, LiteModel):
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):
return_data.append(self._flat(v))
elif isinstance(v, BaseIterable):
return_data.append(self._flat(v))
else:
return_data.append(v)
else:
raise ValueError('数据类型错误')
return json.dumps(return_data)
def _detect_for_table(self, table_name: str) -> bool:
"""在进行增删查改前检测表是否存在
Args:
table_name: 表名
Returns:
"""
return self.cursor.execute(f'SELECT * FROM sqlite_master WHERE type = "table" AND name = ?', (table_name,)).fetchone()
def first(self, model: type(LiteModel), conditions, *args, default: Any = None) -> LiteModel | None:
"""查询第一条数据
Args:
model: 模型
conditions: 查询条件
*args: 参数化查询条件参数
default: 未查询到结果默认返回值
Returns: 数据
"""
table_name = model.__name__
if not self._detect_for_table(table_name):
return default
self.cursor.execute(f'SELECT * FROM {table_name} WHERE {conditions}', args)
if row_data := self.cursor.fetchone():
data = dict(row_data)
return model(**self.convert_to_dict(data))
return default
def all(self, model: type(LiteModel), conditions=None, *args, default: Any = None) -> list[LiteModel] | None:
"""查询所有数据
Args:
model: 模型
conditions: 查询条件
*args: 参数化查询条件参数
default: 未查询到结果默认返回值
Returns: 数据
"""
table_name = model.__name__
if not self._detect_for_table(table_name):
return default
if conditions:
self.cursor.execute(f'SELECT * FROM {table_name} WHERE {conditions}', args)
else:
self.cursor.execute(f'SELECT * FROM {table_name}')
if row_datas := self.cursor.fetchall():
datas = [dict(row_data) for row_data in row_datas]
return [model(**self.convert_to_dict(d)) for d in datas] if datas else default
return default
def delete(self, model: type(LiteModel), conditions, *args):
"""删除数据
Args:
model: 模型
conditions: 查询条件
*args: 参数化查询条件参数
Returns:
"""
table_name = model.__name__
if not self._detect_for_table(table_name):
return
nonebot.logger.debug(f'DELETE FROM {table_name} WHERE {conditions}')
self.cursor.execute(f'DELETE FROM {table_name} WHERE {conditions}', args)
self.conn.commit()
def convert_to_dict(self, data: dict) -> dict:
"""将json字符串转换为字典
Args:
data: json字符串
Returns: 字典
"""
def load(d: BaseIterable) -> BaseIterable:
"""递归加载数据,去除前缀"""
if isinstance(d, dict):
new_d = {}
for k, v in d.items():
if k.startswith(self.FOREIGNID):
new_d[k.replace(self.FOREIGNID, '')] = load(
dict(self.cursor.execute(f'SELECT * FROM {v.split(":", 2)[1]} WHERE id = ?', (v.split(":", 2)[2],)).fetchone()))
elif k.startswith(self.LIST):
if v == '': v = '[]'
new_d[k.replace(self.LIST, '')] = load(json.loads(v))
elif k.startswith(self.DICT):
if v == '': v = '{}'
new_d[k.replace(self.DICT, '')] = load(json.loads(v))
elif k.startswith(self.JSON):
if v == '': v = '[]'
new_d[k.replace(self.JSON, '')] = load(json.loads(v))
else:
new_d[k] = v
elif isinstance(d, list | tuple | set):
new_d = []
for i, v in enumerate(d):
if isinstance(v, str) and v.startswith(self.ID):
new_d.append(load(dict(self.cursor.execute(f'SELECT * FROM {v.split(":", 2)[1]} WHERE id = ?', (v.split(":", 2)[2],)).fetchone())))
elif isinstance(v, BaseIterable):
new_d.append(load(v))
else:
new_d = d
return new_d
return load(data)

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())

167
liteyuki/utils/language.py Normal file
View File

@ -0,0 +1,167 @@
"""
语言模块,添加对多语言的支持
"""
import json
import locale
import os
from typing import Any
import nonebot
from liteyuki.utils.config import config
from liteyuki.utils.data_manager import User, user_db
_default_lang_code = "en"
_language_data = {
"en": {
"name": "English",
}
}
def load_from_lang(file_path: str, lang_code: str = None):
"""
从lang文件中加载语言数据用于简单的文本键值对
Args:
file_path: lang文件路径
lang_code: 语言代码如果为None则从文件名中获取
"""
try:
if lang_code is None:
lang_code = os.path.basename(file_path).split('.')[0]
with open(file_path, 'r', encoding='utf-8') as file:
data = {}
for line in file:
line = line.strip()
if not line or line.startswith('#'): # 空行或注释
continue
key, value = line.split('=', 1)
data[key.strip()] = value.strip()
if lang_code not in _language_data:
_language_data[lang_code] = {}
_language_data[lang_code].update(data)
except Exception as e:
nonebot.logger.error(f"Failed to load language data from {file_path}: {e}")
def load_from_json(file_path: str, lang_code: str = None):
"""
从json文件中加载语言数据可以定义一些变量
Args:
lang_code: 语言代码如果为None则从文件名中获取
file_path: json文件路径
"""
try:
if lang_code is None:
lang_code = os.path.basename(file_path).split('.')[0]
with open(file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
if lang_code not in _language_data:
_language_data[lang_code] = {}
_language_data[lang_code].update(data)
nonebot.logger.debug(f"Loaded language data from {file_path}")
except Exception as e:
nonebot.logger.error(f"Failed to load language data from {file_path}: {e}")
def load_from_dir(dir_path: str):
"""
从目录中加载语言数据
Args:
dir_path: 目录路径
"""
for file in os.listdir(dir_path):
try:
file_path = os.path.join(dir_path, file)
if os.path.isfile(file_path):
if file.endswith('.lang'):
load_from_lang(file_path)
elif file.endswith('.json'):
load_from_json(file_path)
except Exception as e:
nonebot.logger.error(f"Failed to load language data from {file}: {e}")
continue
def load_from_dict(data: dict, lang_code: str):
"""
从字典中加载语言数据
Args:
lang_code: 语言代码
data: 字典数据
"""
if lang_code not in _language_data:
_language_data[lang_code] = {}
_language_data[lang_code].update(data)
class Language:
def __init__(self, lang_code: str = None, fallback_lang_code: str = "en"):
if lang_code is None:
lang_code = get_system_lang_code()
self.lang_code = lang_code
self.fallback_lang_code = fallback_lang_code
def get(self, item: str, *args, **kwargs) -> str | Any:
"""
获取当前语言文本
Args:
item: 文本键
*args: 格式化参数
Returns:
str: 当前语言的文本
"""
try:
if self.lang_code in _language_data and item in _language_data[self.lang_code]:
return _language_data[self.lang_code][item].format(*args, **kwargs)
if self.fallback_lang_code in _language_data and item in _language_data[self.fallback_lang_code]:
return _language_data[self.fallback_lang_code][item].format(*args, **kwargs)
return item
except Exception as e:
nonebot.logger.error(f"Failed to get language text or format: {e}")
return item
def get_user_lang(user_id: str) -> Language:
"""
获取用户的语言代码
"""
user = user_db.first(User, "user_id = ?", user_id, default=User(
user_id=user_id,
username="Unknown"
))
return Language(user.profile.get('lang', config.get("default_language", get_system_lang_code())))
def get_system_lang_code() -> str:
"""
获取系统语言代码
"""
return locale.getdefaultlocale()[0].replace('_', '-')
def get_default_lang() -> Language:
"""
获取默认/系统语言
"""
return Language(config.get("default_language", get_system_lang_code()))
def get_all_lang() -> dict[str, str]:
"""
获取所有语言
Returns
{'en': 'English'}
"""
d = {}
for key in _language_data:
d[key] = _language_data[key].get("language.name", key)
return d

67
liteyuki/utils/log.py Normal file
View File

@ -0,0 +1,67 @@
import sys
import logging
from typing import TYPE_CHECKING
from colored import fg
from .language import get_default_lang
import loguru
if TYPE_CHECKING:
from loguru import Logger, Record
logger: "Logger" = loguru.logger
class LoguruHandler(logging.Handler): # pragma: no cover
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
def emit(self, record: logging.LogRecord):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
)
def default_filter(record: "Record"):
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
log_level = record["extra"].get("nonebot_log_level", "INFO")
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
return record["level"].no >= levelno
default_format: str = (
"<c>{time:YYYY-MM-DD}</c> <blue>{time:HH:mm:ss}</blue> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}></c> "
"{message}"
)
"""默认日志格式"""
logger.remove()
logger_id = logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=default_format,
)
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"""
__autodoc__ = {
"logger_id": False
}

View File

@ -0,0 +1,7 @@
from nonebot.adapters.onebot import v11, v12
T_Bot = v11.Bot | v12.Bot
T_GroupMessageEvent = v11.GroupMessageEvent | v12.GroupMessageEvent
T_PrivateMessageEvent = v11.PrivateMessageEvent | v12.PrivateMessageEvent
T_MessageEvent = v11.MessageEvent | v12.MessageEvent
T_Message = v11.Message | v12.Message

120
liteyuki/utils/message.py Normal file
View File

@ -0,0 +1,120 @@
import nonebot
from nonebot.adapters.onebot import v11, v12
from typing import Any
from .tools import de_escape, encode_url
from .ly_typing import T_Bot, T_MessageEvent
async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None, session_id: str | int = None, event: T_MessageEvent = None, **kwargs) -> dict[
str, Any]:
formatted_md = de_escape(markdown).replace("\n", r"\n").replace("\"", r'\\\"')
if event is not None and message_type is None:
message_type = event.message_type
session_id = event.user_id if event.message_type == "private" else event.group_id
try:
forward_id = await bot.call_api(
api="send_forward_msg",
messages=[
v11.MessageSegment(
type="node",
data={
"name" : "Liteyuki.OneBot",
"uin" : bot.self_id,
"content": [
{
"type": "markdown",
"data": {
"content": '{"content":"%s"}' % formatted_md
}
},
]
},
)
]
)
data = await bot.send_msg(
user_id=session_id,
group_id=session_id,
message_type=message_type,
message=[
v11.MessageSegment(
type="longmsg",
data={
"id": forward_id
}
),
],
**kwargs
)
except Exception as e:
nonebot.logger.warning("send_markdown error, send as plain text: %s" % e.__repr__())
if isinstance(bot, v11.Bot):
data = await bot.send_msg(
message_type=message_type,
message=markdown,
user_id=int(session_id),
group_id=int(session_id),
**kwargs
)
elif isinstance(bot, v12.Bot):
data = await bot.send_message(
detail_type=message_type,
message=v12.Message(
v12.MessageSegment.text(
text=markdown
)
),
user_id=str(session_id),
group_id=str(session_id),
**kwargs
)
else:
nonebot.logger.error("send_markdown: bot type not supported")
data = {}
return data
class Markdown:
@staticmethod
def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str:
"""生成点击回调按钮
Args:
name: 按钮显示内容
cmd: 发送的命令已在函数内url编码不需要再次编码
reply: 是否以回复的方式发送消息
enter: 自动发送消息则为True否则填充到输入框
Returns:
markdown格式的可点击回调按钮
"""
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}]({url})"
@staticmethod
def escape(text: str) -> str:
"""转义特殊字符
Args:
text: 需要转义的文本请勿直接把整个markdown文本传入否则会转义掉所有字符
Returns:
转义后的文本
"""
chars = "*[]()~_`>#+=|{}.!"
for char in chars:
text = text.replace(char, f"\\\\{char}")
return text

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

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

View File

@ -0,0 +1,54 @@
import os
import nonebot
import yaml
from typing import Any
from liteyuki.utils.data import LiteModel
_resource_data = {}
_loaded_resource_packs = [] # 按照加载顺序排序
class ResourceMetadata(LiteModel):
name: str = "Unknown"
version: str = "0.0.1"
description: str = "Unknown"
path: str
def load_resource_from_dir(path: str):
"""
把资源包按照文件相对路径加载到资源包中,后加载的优先级更高,顺便加载语言
Args:
path: 资源文件夹
Returns:
"""
for root, dirs, files in os.walk(path):
for file in files:
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
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)
else:
metadata = ResourceMetadata()
metadata["path"] = path
if os.path.exists(os.path.join(path, "lang")):
from liteyuki.utils.language import load_from_dir
load_from_dir(os.path.join(path, "lang"))
_loaded_resource_packs.append(ResourceMetadata(**metadata))
def get(path: str, default: Any = None) -> str | Any:
"""
获取资源包中的文件
Args:
default: 默认
path: 文件相对路径
Returns: 文件绝对路径
"""
return _resource_data.get(path, default)

83
liteyuki/utils/tools.py Normal file
View File

@ -0,0 +1,83 @@
from importlib.metadata import PackageNotFoundError, version
from urllib.parse import quote
def convert_size(size: int, precision: int = 2, add_unit: bool = True, suffix: str = "iB") -> str:
"""把字节数转换为人类可读的字符串,计算正负
Args:
add_unit: 是否添加单位False后则suffix无效
suffix: iB或B
precision: 浮点数的小数点位数
size (int): 字节数
Returns:
str: The human-readable string, e.g. "1.23 GB".
"""
is_negative = False
if size < 0:
is_negative = True
size = -size
for unit in ["", "K", "M", "G", "T", "P", "E", "Z", "Y"]:
if size < 1024:
if add_unit:
result = f"{size:.{precision}f} {unit}" + suffix
return f"-{result}" if is_negative else result
else:
return f"{size:.{precision}f}"
size /= 1024
if add_unit:
return f"{size:.{precision}f} Y" + suffix
else:
return f"{size:.{precision}f}"
def de_escape(text: str) -> str:
str_map = {
"&#91;": "[",
"&#93;": "]",
"&amp;": "&",
"&#44;": ",",
}
for k, v in str_map.items():
text = text.replace(k, v)
return text
def encode_url(text: str) -> str:
return quote(text, safe="")
def keywords_in_text(keywords: list[str], text: str, all_matched: bool) -> bool:
"""
检查关键词是否在文本中
Args:
keywords: 关键词列表
text: 文本
all_matched: 是否需要全部匹配
Returns:
"""
if all_matched:
for keyword in keywords:
if keyword not in text:
return False
return True
else:
for keyword in keywords:
if keyword in text:
return True
return False
def check_for_package(package_name: str) -> bool:
try:
version(package_name)
return True
except PackageNotFoundError:
return False