1
0
forked from bot/app

13 Commits

Author SHA1 Message Date
6b64a0c379 🐛 hotfix: from mypy import 2024-10-19 21:10:18 +08:00
f117da7ff3 🎨 更新轻雪依赖版本 2024-10-14 20:57:30 +08:00
f548a07230 📝 文档删除常规部署,强制使用虚拟环境 2024-10-14 20:51:37 +08:00
e2e53c21fa 📝 文档删除常规部署,强制使用虚拟环境 2024-10-14 01:03:06 +08:00
3eaf23a56b 📝 文档删除常规部署,强制使用虚拟环境 2024-10-14 01:02:57 +08:00
4a5dd1f727 🐛 修复一些细节小问题 2024-10-14 00:57:33 +08:00
c2cb416b4e 🐛 hotfix: ubl 2024-10-13 17:44:24 +08:00
5cd528d5e9 🐛 hotfix: ubl 2024-10-13 17:44:17 +08:00
980fca650b 🐛 hotfix: ubl 2024-10-13 13:44:07 +08:00
9c525141f6 分离magicocacroterline 2024-10-13 02:56:29 +08:00
3d218a0e8d Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/liteyuki_plugins/nonebot/__init__.py
#	src/liteyuki_plugins/nonebot/nb_utils/adapter_manager/__init__.py
#	src/liteyuki_plugins/nonebot/nb_utils/adapter_manager/onebot.py
#	src/liteyuki_plugins/nonebot/nb_utils/adapter_manager/satori.py
#	src/liteyuki_plugins/nonebot/nb_utils/driver_manager/__init__.py
#	src/liteyuki_plugins/nonebot/nb_utils/driver_manager/auto_set_env.py
#	src/liteyuki_plugins/nonebot/nb_utils/driver_manager/defines.py
2024-10-13 02:55:04 +08:00
db385f597b 分离magicocacroterline 2024-10-13 02:54:47 +08:00
98a9d6413a 分离magicocacroterline 2024-10-13 02:51:33 +08:00
109 changed files with 4577 additions and 4387 deletions

View File

@ -1,8 +1,9 @@
name: Publish
on:
release:
types: [published]
push:
tags:
- 'v*'
jobs:
pypi-publish:

View File

@ -25,6 +25,10 @@
### 感谢
- 所有贡献者们
### 参考及鸣谢
- [nonebot-plugin-uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo)为会话部分用户信息提供了参考
- [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna/)为消息部分提供了参考
[OneBot]: https://img.shields.io/badge/OneBot-11/12-blue?style=for-the-badge

View File

@ -9,13 +9,23 @@ order: 1
1. Install [`Git`](https://git-scm.com/download/) and [`Python3.10+`](https://www.python.org/downloads/release/python-31010/) Environment.
```bash
# Clone the project
# Clone Repo
git clone https://github.com/LiteyukiStudio/LiteyukiBot --depth=1
# change directory
# Change directory
cd LiteyukiBot
# install dependencies
# Create virtual environment
python -m venv venv
# Activate virtual environment
.\venv\Scripts\activate # Windows
source venv/bin/activate # Linux
# Install dependencies
pip install -r requirements.txt
# start the bot!
# Run Liteyuki
python main.py
```
@ -37,9 +47,6 @@ python main.py
> If you are using Windows, please use the absolute project directory `/path/to/LiteyukiBot` instead of `$&#40;pwd&#41;` <br>
> If you have modified the port number, please replace `20216:20216` with your port number
## **Use TRSS Script**
[TRSS_Liteyuki Management Script](https://timerainstarsky.github.io/TRSS_Liteyuki/), which provides a more convenient way to manage LiteyukiBot, recommended to use `Arch Linux`
## **Device Requirements**
- Windows system version minimum `Windows10+`/`Windows Server 2019+`

2248
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ order: 2
```yaml
nonebot:
# Nonebot机器人的配置以前的最外层配置项仍可为Nonebot服务,但是部分内容会被覆盖,请尽快迁移
# Nonebot机器人的配置6.3.10版本后NoneBot下配置已迁移至nonebot键下不再使用外层配置,但是部分内容会被覆盖,请尽快迁移
command_start: [ "/", "" ] # 指令前缀,若没有""空命令头请开启alconna_use_command_start保证alconna解析正常
host: 127.0.0.1 # 监听地址默认为本机若要接收外部请求请填写0.0.0.0
port: 20216 # 绑定端口

View File

@ -2,11 +2,13 @@
title: 安装
order: 1
---
# 安装
## **常规部署**
1. 安装 [`Git`](https://git-scm.com/download/) 和 [`Python3.10+`](https://www.python.org/downloads/release/python-31010/) 环境
1. 安装 [`Git`](https://git-scm.com/download/) 和 [
`Python3.10+`](https://www.python.org/downloads/release/python-31010/) 环境
```bash
# 克隆项目到本地轻雪使用Git进行版本管理该步骤为必要项
@ -14,19 +16,25 @@ git clone https://github.com/LiteyukiStudio/LiteyukiBot --depth=1 # 若你不能
# 切换到Bot目录下
cd LiteyukiBot
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
.\venv\Scripts\activate # Windows
source venv/bin/activate # Linux
# 安装依赖
pip install -r requirements.txt
# 启动Bot
python main.py
```
> [!tip]
> 推荐使用虚拟环境来运行轻雪,以避免依赖冲突,你可以使用`python -m venv .venv`来创建虚拟环境,然后使用`.venv\Scripts\activate`来激活虚拟环境Linux下使用`source .venv/bin/activate`激活)
## **使用Docker构建**
1. 安装 [`Docker`](https://docs.docker.com/get-docker/)
2. 克隆项目 `git clone https://github.com/LiteyukiStudio/LiteyukiBot --depth=1`
2. 克隆项目 `git clone https://github.com/LiteyukiStudio/LiteyukiBot --depth=1`
3. 进入轻雪目录 `cd LiteyukiBot`
4. 构建镜像 `docker build -t liteyukibot .`
5. 启动容器 `docker run -p 20216:20216 -v $(pwd):/liteyukibot -v $(pwd)/.cache:/root/.cache liteyukibot`
@ -35,10 +43,6 @@ python main.py
> Windows请使用项目绝对目录`/path/to/LiteyukiBot`代替`$(pwd)` <br>
> 若你修改了端口号请将`20216:20216`中的`20216`替换为你的端口号
## **使用TRSS Scripts部署**
[TRSS_Liteyuki轻雪机器人管理脚本](https://timerainstarsky.github.io/TRSS_Liteyuki/)该功能由TRSS提供支持不是LiteyukiBot官方提供的功能推荐使用`Arch Linux`
## **装置要求**
- Windows系统版本最低`Windows10+`/`Windows Server 2019+`
@ -48,7 +52,8 @@ python main.py
- 硬盘: 至少`1GB`空间
> [!warning]
> 如果装置上有多个环境,请使用`path/to/python -m pip install -r requirements.txt`来安装依赖,`path/to/python`为你的Python可执行文件路径
> 如果装置上有多个环境,请使用`path/to/python -m pip install -r requirements.txt`来安装依赖,`path/to/python`
> 为你的Python可执行文件路径
> [!warning]
> 轻雪的更新功能依赖Git如果你没有安装Git直接下载源代码运行你将无法使用更新功能

View File

@ -1,25 +1,28 @@
import asyncio
import atexit
import os
import platform
import signal
import sys
import threading
import time
from typing import Any, Optional
from liteyuki.bot.lifespan import (LIFESPAN_FUNC, Lifespan, PROCESS_LIFESPAN_FUNC)
from magicoca import Chan
from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan, PROCESS_LIFESPAN_FUNC
from liteyuki.comm.channel import get_channel
from liteyuki.core.manager import ProcessManager
# new version
from liteyuki.core.manager import sub_process_manager
from liteyuki.log import init_log, logger
from liteyuki.plugin import load_plugin
from liteyuki.session import message_handler_thread
from liteyuki.utils import IS_MAIN_PROCESS
__all__ = [
"LiteyukiBot",
"get_bot",
"get_config",
"get_config_with_compat",
"LiteyukiBot",
"get_bot",
"get_config",
"get_config_with_compat",
]
@ -30,6 +33,10 @@ class LiteyukiBot:
Args:
**kwargs: 配置
"""
"""总通道"""
self.i_chan = Chan[Any]() # 外部输入通道
self.o_chan = Chan[Any]() # 外部输出通道
"""常规操作"""
print_logo()
global _BOT_INSTANCE
@ -60,8 +67,9 @@ class LiteyukiBot:
启动逻辑
"""
await self.lifespan.before_start() # 启动前钩子
sub_process_manager.start_all()
await self.lifespan.after_start() # 启动后钩子
await self.keep_alive()
message_handler_thread([_.ctx.sub_chan for _ in sub_process_manager.processes.values()])
def run(self):
"""
@ -73,19 +81,7 @@ class LiteyukiBot:
except KeyboardInterrupt:
logger.opt(colors=True).info("<y>Liteyuki is stopping...</y>")
self.stop()
logger.opt(colors=True).info("<y>Liteyuki is stopped...</y>")
async def keep_alive(self):
"""
保持轻雪运行
"""
logger.info("Liteyuki is keeping alive...")
try:
while not self.stop_event.is_set():
await asyncio.sleep(0.1)
except Exception:
logger.info("Liteyuki is exiting...")
self.stop()
logger.opt(colors=True).info("<y>Liteyuki is stopped !</y>")
def restart(self, delay: int = 0):
"""
@ -108,7 +104,11 @@ class LiteyukiBot:
cmd = "nohup"
self.process_manager.terminate_all()
# 进程退出后重启
threading.Thread(target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",), daemon=True).start()
threading.Thread(
target=os.system,
args=(f"{cmd} {executable} {' '.join(args)}",),
daemon=True,
).start()
sys.exit(0)
self.call_restart_count += 1
@ -189,7 +189,9 @@ class LiteyukiBot:
"""
return self.lifespan.on_before_process_shutdown(func)
def on_before_process_restart(self, func: PROCESS_LIFESPAN_FUNC) -> PROCESS_LIFESPAN_FUNC:
def on_before_process_restart(
self, func: PROCESS_LIFESPAN_FUNC
) -> PROCESS_LIFESPAN_FUNC:
"""
注册进程重启前的函数,为子进程重启时调用
Args:
@ -211,7 +213,7 @@ class LiteyukiBot:
return self.lifespan.on_after_restart(func)
_BOT_INSTANCE: LiteyukiBot
_BOT_INSTANCE: LiteyukiBot | None = None
def get_bot() -> LiteyukiBot:
@ -241,7 +243,9 @@ def get_config(key: str, default: Any = None) -> Any:
return get_bot().config.get(key, default)
def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any = None) -> Any:
def get_config_with_compat(
key: str, compat_keys: tuple[str], default: Any = None
) -> Any:
"""
获取配置,兼容旧版本
Args:
@ -256,14 +260,18 @@ def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any = Non
return get_bot().config[key]
for compat_key in compat_keys:
if compat_key in get_bot().config:
logger.warning(f"Config key \"{compat_key}\" will be deprecated, use \"{key}\" instead.")
logger.warning(
f'Config key "{compat_key}" will be deprecated, use "{key}" instead.'
)
return get_bot().config[compat_key]
return default
def print_logo():
"""@litedoc-hide"""
print("\033[34m" + r"""
print(
"\033[34m"
+ r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
@ -273,4 +281,6 @@ def print_logo():
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
"""
+ "\033[0m"
)

View File

@ -4,20 +4,31 @@
"""
import asyncio
from multiprocessing import Pipe
from typing import Any, Callable, Coroutine, Generic, Optional, TypeAlias, TypeVar, get_args
from typing import (
Any,
Callable,
Coroutine,
Generic,
Optional,
TypeAlias,
TypeVar,
get_args,
)
from liteyuki.log import logger
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable
T = TypeVar("T")
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Any] # 同步接收函数
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, Any]] # 异步接收函数
ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC # 接收函数
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Any] # 同步接收函数
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[
[T], Coroutine[Any, Any, Any]
] # 异步接收函数
ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC # 接收函数
SYNC_FILTER_FUNC: TypeAlias = Callable[[T], bool] # 同步过滤函数
ASYNC_FILTER_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, bool]] # 异步过滤函数
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC # 过滤函数
SYNC_FILTER_FUNC: TypeAlias = Callable[[T], bool] # 同步过滤函数
ASYNC_FILTER_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, bool]] # 异步过滤函数
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC # 过滤函数
_func_id: int = 0
_channel: dict[str, "Channel"] = {}
@ -39,7 +50,9 @@ class Channel(Generic[T]):
"""
self.conn_send, self.conn_recv = Pipe()
self._conn_send_inner, self._conn_recv_inner = Pipe() # 内部通道,用于子进程通信
self._conn_send_inner, self._conn_recv_inner = (
Pipe()
) # 内部通道,用于子进程通信
self._closed = False
self._on_main_receive_func_ids: list[int] = []
self._on_sub_receive_func_ids: list[int] = []
@ -64,7 +77,9 @@ class Channel(Generic[T]):
_channel[name] = self
logger.debug(f"Channel {name} initialized in main process")
else:
logger.debug(f"Channel {name} initialized in sub process, should manually set in main process")
logger.debug(
f"Channel {name} initialized in sub process, should manually set in main process"
)
def _get_generic_type(self) -> Optional[type]:
"""
@ -72,7 +87,7 @@ class Channel(Generic[T]):
Returns:
Optional[type]: 泛型类型
"""
if hasattr(self, '__orig_class__'):
if hasattr(self, "__orig_class__"):
return get_args(self.__orig_class__)[0]
return None
@ -98,7 +113,10 @@ class Channel(Generic[T]):
elif isinstance(structure, dict):
if not isinstance(data, dict):
return False
return all(k in data and self._validate_structure(data[k], structure[k]) for k in structure)
return all(
k in data and self._validate_structure(data[k], structure[k])
for k in structure
)
return False
def __str__(self):
@ -113,10 +131,12 @@ class Channel(Generic[T]):
if self.type_check:
_type = self._get_generic_type()
if _type is not None and not self._validate_structure(data, _type):
raise TypeError(f"Data must be an instance of {_type}, {type(data)} found")
raise TypeError(
f"Data must be an instance of {_type}, {type(data)} found"
)
if self._closed:
raise RuntimeError("Cannot send to a closed channel_")
raise RuntimeError("Cannot send to a closed channel")
self.conn_send.send(data)
def receive(self) -> T:
@ -126,7 +146,7 @@ class Channel(Generic[T]):
T: 数据
"""
if self._closed:
raise RuntimeError("Cannot receive from a closed channel_")
raise RuntimeError("Cannot receive from a closed channel")
while True:
data = self.conn_recv.recv()
@ -142,7 +162,9 @@ class Channel(Generic[T]):
data = await loop.run_in_executor(None, self.receive)
return data
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
def on_receive(
self, filter_func: Optional[FILTER_FUNC] = None
) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
"""
接收数据并执行函数
Args:
@ -187,37 +209,52 @@ class Channel(Generic[T]):
data: 数据
"""
if IS_MAIN_PROCESS:
[asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_main_receive_func_ids]
[
asyncio.create_task(_callback_funcs[func_id](data))
for func_id in self._on_main_receive_func_ids
]
else:
[asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_sub_receive_func_ids]
[
asyncio.create_task(_callback_funcs[func_id](data))
for func_id in self._on_sub_receive_func_ids
]
"""子进程可用的主动和被动通道"""
active_channel: Channel = Channel(name="active_channel") # 主动通道
active_channel: Channel = Channel(name="active_channel") # 主动通道
passive_channel: Channel = Channel(name="passive_channel") # 被动通道
publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(name="publish_channel") # 发布通道
publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(
name="publish_channel"
) # 发布通道
"""通道传递通道,主进程创建单例,子进程初始化时实例化"""
channel_deliver_active_channel: Channel[Channel[Any]] # 主动通道传递通道
channel_deliver_passive_channel: Channel[tuple[str, dict[str, Any]]] # 被动通道传递通道
channel_deliver_active_channel: Channel[Channel[Any]] # 主动通道传递通道
channel_deliver_passive_channel: Channel[tuple[str, dict[str, Any]]] # 被动通道传递通道
if IS_MAIN_PROCESS:
channel_deliver_active_channel = Channel(name="channel_deliver_active_channel") # 主动通道传递通道
channel_deliver_passive_channel = Channel(name="channel_deliver_passive_channel") # 被动通道传递通道
channel_deliver_active_channel = Channel(
name="channel_deliver_active_channel"
) # 主动通道传递通道
channel_deliver_passive_channel = Channel(
name="channel_deliver_passive_channel"
) # 被动通道传递通道
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "set_channel")
@channel_deliver_passive_channel.on_receive(
filter_func=lambda data: data[0] == "set_channel"
)
def on_set_channel(data: tuple[str, dict[str, Any]]):
name, channel = data[1]["name"], data[1]["channel_"]
set_channel(name, channel)
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "get_channel")
@channel_deliver_passive_channel.on_receive(
filter_func=lambda data: data[0] == "get_channel"
)
def on_get_channel(data: tuple[str, dict[str, Any]]):
name, recv_chan = data[1]["name"], data[1]["recv_chan"]
recv_chan.send(get_channel(name))
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "get_channels")
@channel_deliver_passive_channel.on_receive(
filter_func=lambda data: data[0] == "get_channels"
)
def on_get_channels(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]["recv_chan"]
recv_chan.send(get_channels())
@ -231,7 +268,9 @@ def set_channel(name: str, channel: "Channel"):
channel ([`Channel`](#class-channel-generic-t)): 通道实例
"""
if not isinstance(channel, Channel):
raise TypeError(f"channel_ must be an instance of Channel, {type(channel)} found")
raise TypeError(
f"channel_ must be an instance of Channel, {type(channel)} found"
)
if IS_MAIN_PROCESS:
if name in _channel:
@ -241,10 +280,11 @@ def set_channel(name: str, channel: "Channel"):
# 请求主进程设置通道
channel_deliver_passive_channel.send(
(
"set_channel", {
"name" : name,
"channel_": channel,
}
"set_channel",
{
"name": name,
"channel_": channel,
},
)
)
@ -273,13 +313,7 @@ def get_channel(name: str) -> "Channel":
else:
recv_chan = Channel[Channel[Any]]("recv_chan")
channel_deliver_passive_channel.send(
(
"get_channel",
{
"name" : name,
"recv_chan": recv_chan
}
)
("get_channel", {"name": name, "recv_chan": recv_chan})
)
return recv_chan.receive()
@ -294,12 +328,5 @@ def get_channels() -> dict[str, "Channel"]:
return _channel
else:
recv_chan = Channel[dict[str, Channel[Any]]]("recv_chan")
channel_deliver_passive_channel.send(
(
"get_channels",
{
"recv_chan": recv_chan
}
)
)
channel_deliver_passive_channel.send(("get_channels", {"recv_chan": recv_chan}))
return recv_chan.receive()

View File

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
"""
本模块用于实现RPC(基于IPC)通信
"""
from typing import TypeAlias, Callable, Any
from liteyuki.comm.channel import Channel
ON_CALLING_FUNC: TypeAlias = Callable[[tuple, dict], Any]
class RPC:
"""
RPC类
"""
def __init__(self, on_calling: ON_CALLING_FUNC) -> None:
self.on_calling = on_calling
def call(self, args: tuple, kwargs: dict) -> Any:
"""
调用
"""
# 获取self.calling函数名
return self.on_calling(args, kwargs)

View File

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
"""
基于socket的通道
"""
class SocksChannel:
"""
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
"""
def __init__(self, name: str):
"""
初始化通道
Args:
name: 通道ID
"""
self._name = name
self._conn_send = None
self._conn_recv = None
self._closed = False
def send(self, data):
"""
发送数据
Args:
data: 数据
"""
pass
def receive(self):
"""
接收数据
Returns:
data: 数据
"""
pass
def close(self):
"""
关闭通道
"""
pass

View File

@ -14,6 +14,9 @@ import threading
from multiprocessing import Process
from typing import Any, Callable, TYPE_CHECKING, TypeAlias
from croterline.context import Context
from croterline.process import SubProcess, ProcessFuncType
from liteyuki.log import logger
from liteyuki.utils import IS_MAIN_PROCESS
@ -26,7 +29,10 @@ from liteyuki.comm import Channel
if IS_MAIN_PROCESS:
from liteyuki.comm.channel import get_channel, publish_channel, get_channels
from liteyuki.comm.storage import shared_memory
from liteyuki.comm.channel import channel_deliver_active_channel, channel_deliver_passive_channel
from liteyuki.comm.channel import (
channel_deliver_active_channel,
channel_deliver_passive_channel,
)
else:
from liteyuki.comm import channel
from liteyuki.comm import storage
@ -34,20 +40,18 @@ else:
TARGET_FUNC: TypeAlias = Callable[..., Any]
TIMEOUT = 10
__all__ = [
"ProcessManager"
]
__all__ = ["ProcessManager", "sub_process_manager"]
multiprocessing.set_start_method("spawn", force=True)
class ChannelDeliver:
def __init__(
self,
active: Channel[Any],
passive: Channel[Any],
channel_deliver_active: Channel[Channel[Any]],
channel_deliver_passive: Channel[tuple[str, dict]],
publish: Channel[tuple[str, Any]],
self,
active: Channel[Any],
passive: Channel[Any],
channel_deliver_active: Channel[Channel[Any]],
channel_deliver_passive: Channel[tuple[str, dict]],
publish: Channel[tuple[str, Any]],
):
self.active = active
self.passive = passive
@ -57,7 +61,9 @@ class ChannelDeliver:
# 函数处理一些跨进程通道的
def _delivery_channel_wrapper(func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyValueStore", *args, **kwargs):
def _delivery_channel_wrapper(
func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyValueStore", *args, **kwargs
):
"""
子进程入口函数
处理一些操作
@ -68,8 +74,12 @@ def _delivery_channel_wrapper(func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyVal
channel.active_channel = cd.active # 子进程主动通道
channel.passive_channel = cd.passive # 子进程被动通道
channel.channel_deliver_active_channel = cd.channel_deliver_active # 子进程通道传递主动通道
channel.channel_deliver_passive_channel = cd.channel_deliver_passive # 子进程通道传递被动通道
channel.channel_deliver_active_channel = (
cd.channel_deliver_active
) # 子进程通道传递主动通道
channel.channel_deliver_passive_channel = (
cd.channel_deliver_passive
) # 子进程通道传递被动通道
channel.publish_channel = cd.publish # 子进程发布通道
# 给子进程创建共享内存实例
@ -102,8 +112,12 @@ class ProcessManager:
chan_active = get_channel(f"{name}-active")
def _start_process():
process = Process(target=self.targets[name][0], args=self.targets[name][1],
kwargs=self.targets[name][2], daemon=True)
process = Process(
target=self.targets[name][0],
args=self.targets[name][1],
kwargs=self.targets[name][2],
daemon=True,
)
self.processes[name] = process
process.start()
@ -133,7 +147,9 @@ class ProcessManager:
for name in self.targets:
logger.debug(f"Starting process {name}")
threading.Thread(target=self._run_process, args=(name, ), daemon=True).start()
threading.Thread(
target=self._run_process, args=(name,), daemon=True
).start()
def add_target(self, name: str, target: TARGET_FUNC, args: tuple = (), kwargs=None):
"""
@ -154,10 +170,14 @@ class ProcessManager:
passive=chan_passive,
channel_deliver_active=channel_deliver_active_channel,
channel_deliver_passive=channel_deliver_passive_channel,
publish=publish_channel
publish=publish_channel,
)
self.targets[name] = (_delivery_channel_wrapper, (target, channel_deliver, shared_memory, *args), kwargs)
self.targets[name] = (
_delivery_channel_wrapper,
(target, channel_deliver, shared_memory, *args),
kwargs,
)
# 主进程通道
def join_all(self):
@ -199,3 +219,79 @@ class ProcessManager:
if name not in self.targets:
logger.warning(f"Process {name} not found.")
return self.processes[name].is_alive()
# new version
class _SubProcessManager:
"""
子进程管理器
若要子进程间通信请先在子进程A中发送通信事件给主进程包含当前进程信息及上下文信息主进程再将信息发送给子进程B子进程B再根据信息进行操作
"""
def __init__(self):
self.processes: dict[str, SubProcess] = {}
def add(self, name: str, func: ProcessFuncType, *args, **kwargs):
"""
添加子进程
Args:
func: 子进程函数
name: 子进程名称
args: 子进程函数参数
kwargs: 子进程函数关键字参数
Returns:
"""
self.processes[name] = SubProcess(name, func, *args, **kwargs)
def start(self, name: str):
"""
启动指定子进程
Args:
name: 子进程名称
Returns:
"""
if name not in self.processes:
raise KeyError(f"Process {name} not found.")
self.processes[name].start()
def start_all(self):
"""
启动所有子进程
"""
for name, process in self.processes.items():
process.start()
logger.debug(f"Starting process {name}")
def terminate(self, name: str):
"""
终止指定子进程
Args:
name: 子进程名称
Returns:
"""
if name not in self.processes:
raise KeyError(f"Process {name} not found.")
self.processes[name].terminate()
def terminate_all(self):
"""
终止所有子进程
"""
for name, process in self.processes.items():
process.terminate()
logger.debug(f"Terminating process {name}")
def get_process(self, name: str) -> SubProcess | None:
"""
获取指定子进程
Args:
name: 子进程名称
Returns:
"""
return self.processes.get(name, None)
sub_process_manager = _SubProcessManager()

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:44
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:47
@Author : snowykami
@Email : snowykami@outlook.com
@File : session.py
@Software: PyCharm
"""

View File

@ -60,7 +60,6 @@ def load_plugin(module_path: str | Path) -> Optional[Plugin]:
f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type
)
else:
logger.opt(colors=True).warning(
f'The metadata of Liteyuki plugin "{module.__name__}" is not specified, use empty.'
)

View File

@ -9,9 +9,9 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Software: PyCharm
"""
from liteyuki.message.on import on_startswith
from liteyuki.message.event import MessageEvent
from liteyuki.message.rule import is_su_rule
from liteyuki.session.on import on_startswith
from liteyuki.session.event import MessageEvent
from liteyuki.session.rule import is_su_rule
@on_startswith(["liteecho"], rule=is_su_rule).handle()

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
该模块参考并引用了nonebot-plugin-alconna的消息段定义
"""
from typing import Any
from magicoca import Chan, select
from mypy.server.objgraph import Iterable
from six import Iterator
def message_handler_thread(i_chans: Iterable[Chan[Any]]):
"""
Args:
i_chans: 多路输入管道组
Returns:
"""
for msg in select(*i_chans):
print("Recv from anybot", msg)

View File

@ -11,7 +11,6 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
from typing import Any, Optional
from liteyuki import Channel
from liteyuki.comm.storage import shared_memory
class MessageEvent:

View File

@ -11,8 +11,8 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
import traceback
from typing import Any, TypeAlias, Callable, Coroutine
from liteyuki.message.event import MessageEvent
from liteyuki.message.rule import Rule
from liteyuki.session.event import MessageEvent
from liteyuki.session.rule import Rule
EventHandler: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, Any]]

View File

@ -0,0 +1,51 @@
from pydantic import BaseModel
class User(BaseModel):
"""
用户信息
Attributes:
id: 用户ID
name: 用户名
nick: 用户昵称
avatar: 用户头像图链接
"""
id: str
name: str | None
nick: str | None
avatar: str | None
class Scene(BaseModel):
"""
场景信息
Attributes:
id: 场景ID
type: 场景类型
name: 场景名
avatar: 场景头像图链接
parent: 父场景
"""
id: str
type: str
name: str | None
avatar: str | None
parent: "Scene | None"
class Session(BaseModel):
"""
会话信息
Attributes:
self_id: 机器人ID
adapter: 适配器ID
scope: 会话范围
scene: 场景信息
user: 用户信息
member: 成员信息,仅频道及群聊有效
operator: 操作者信息,仅频道及群聊有效
"""
self_id: str
adapter: str
scope: str
scene: Scene
user: User
member: "Member | None"
operator: "Member | None"

View File

@ -11,33 +11,13 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
from queue import Queue
from liteyuki.comm.storage import shared_memory
from liteyuki.log import logger
from liteyuki.message.event import MessageEvent
from liteyuki.message.matcher import Matcher
from liteyuki.message.rule import Rule, empty_rule
from liteyuki.session.event import MessageEvent
from liteyuki.session.matcher import Matcher
from liteyuki.session.rule import Rule, empty_rule
_matcher_list: list[Matcher] = []
_queue: Queue = Queue()
@shared_memory.on_subscriber_receive("event_to_liteyuki")
async def _(event: MessageEvent):
print("AA")
current_priority = -1
for i, matcher in enumerate(_matcher_list):
logger.info(f"Running matcher {matcher} for event: {event}")
await matcher.run(event)
# 同优先级不阻断,不同优先级阻断
if current_priority != matcher.priority:
current_priority = matcher.priority
if matcher.block:
break
else:
logger.info(f"No matcher matched for event: {event}")
print("BB")
def add_matcher(matcher: Matcher):
for i, m in enumerate(_matcher_list):
if m.priority < matcher.priority:

View File

@ -11,7 +11,7 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
import inspect
from typing import Optional, TypeAlias, Callable, Coroutine
from liteyuki.message.event import MessageEvent
from liteyuki.session.event import MessageEvent
from liteyuki import get_config
_superusers: list[str] = get_config("liteyuki.superusers", [])

View File

@ -10,17 +10,18 @@ readme = "README.md"
requires-python = ">=3.10"
authors = [
{ name = "snowykami", email = "snowykami@outlook.com" },
{ name = "LiteyukiStudio", email = "studio@liteyuki.icu" },
]
license = { text = "MIT&LSO" }
dependencies = [
"loguru~=0.7.2",
"pydantic==2.8.2",
"PyYAML==6.0.2",
"toml==0.10.2",
"watchdog==4.0.1",
"pdm-backend==2.3.3"
"pydantic>=2.9.2",
"PyYAML>=6.0.2",
"toml>=0.10.2",
"watchdog>=4.0.1",
"pdm-backend>=2.3.3",
"magicoca>=1.0.5",
"croterline~=1.0.5",
]
[project.urls]
@ -38,5 +39,14 @@ includes = ["liteyuki/", "LICENSE", "README.md"]
excludes = ["tests/", "docs/", "src/"]
[tool.pdm.version]
source = "file"
path = "liteyuki/__init__.py"
source = "scm"
tag_filter = "v*"
tag_regex = '^v(?:\D*)?(?P<version>([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|c|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$)$'
[tool.pdm.dev-dependencies]
dev = [
"pytest>=8.3.3",
"black>=24.10.0",
"uv>=0.4.20",
"mypy>=1.11.2",
]

View File

@ -1,3 +1,4 @@
# app dependencies
aiohttp>=3.9.3
aiofiles>=23.2.1
colored>=2.2.4
@ -25,4 +26,12 @@ toml>=0.10.2
importlib_metadata>=7.0.2
watchdog>=4.0.0
jieba>=0.42.1
python-dotenv>=1.0.1
python-dotenv>=1.0.1
loguru~=0.7.2
pydantic~=2.9.2
pip~=23.2.1
fastapi~=0.115.0
# liteyuki dependencies
croterline>=1.0.5
magicoca>=1.0.5

View File

@ -9,8 +9,8 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Software: PyCharm
"""
from liteyuki.plugin import PluginMetadata, PluginType
from liteyuki.message.on import on_message
from liteyuki.message.event import MessageEvent
from liteyuki.session.on import on_message
from liteyuki.session.event import MessageEvent
__plugin_meta__ = PluginMetadata(
name="你好轻雪",

View File

@ -1,53 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/11 下午5:24
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""
import nonebot
from liteyuki.utils import IS_MAIN_PROCESS
from liteyuki.plugin import PluginMetadata, PluginType
from .nb_utils import adapter_manager, driver_manager # type: ignore
from liteyuki.log import logger
__plugin_meta__ = PluginMetadata(
name="NoneBot2启动器",
type=PluginType.APPLICATION,
)
def nb_run(*args, **kwargs):
"""
初始化NoneBot并运行在子进程
Args:
**kwargs:
Returns:
"""
# 给子进程传递通道对象
kwargs.update(kwargs.get("nonebot", {})) # nonebot配置优先
nonebot.init(**kwargs)
driver_manager.init(config=kwargs)
adapter_manager.init(kwargs)
adapter_manager.register()
try:
# nonebot.load_plugin("nonebot-plugin-lnpm") # 尝试加载轻雪NoneBot插件加载器Nonebot插件
nonebot.load_plugin("src.liteyuki_main") # 尝试加载轻雪主插件Nonebot插件
except Exception as e:
pass
nonebot.run()
if IS_MAIN_PROCESS:
from liteyuki import get_bot
from .dev_reloader import *
liteyuki = get_bot()
liteyuki.process_manager.add_target(name="nonebot", target=nb_run, args=(), kwargs=liteyuki.config)

View File

@ -0,0 +1,33 @@
import os.path
from pathlib import Path
import nonebot
from croterline.utils import IsMainProcess
from liteyuki.core import sub_process_manager
from liteyuki.plugin import PluginMetadata, PluginType
__plugin_meta__ = PluginMetadata(
name="NoneBot2启动器",
type=PluginType.APPLICATION,
)
def nb_run(*args, **kwargs):
nonebot.init(**kwargs)
from .nb_utils import driver_manager, adapter_manager
driver_manager.init(config=kwargs)
adapter_manager.init(kwargs)
adapter_manager.register()
nonebot.load_plugin(Path(os.path.dirname(__file__)) / "np_main")
nonebot.run()
if IsMainProcess:
from .dev_reloader import *
bot = get_bot()
sub_process_manager.add(
name="nonebot", func=nb_run, **bot.config.get("nonebot", {})
)

View File

@ -10,15 +10,17 @@ from liteyuki.utils import IS_MAIN_PROCESS
from watchdog.events import FileSystemEvent
liteyuki = get_bot()
bot = get_bot()
exclude_extensions = (".pyc", ".pyo")
@observer.on_file_system_event(
directories=("src/nonebot_plugins",),
event_filter=lambda event: not event.src_path.endswith(exclude_extensions) and ("__pycache__" not in event.src_path ) and os.path.isfile(event.src_path)
event_filter=lambda event: not event.src_path.endswith(exclude_extensions)
and ("__pycache__" not in event.src_path)
and os.path.isfile(event.src_path),
)
def restart_nonebot_process(event: FileSystemEvent):
logger.debug(f"File {event.src_path} changed, reloading nonebot...")
liteyuki.restart_process("nonebot")
bot.restart_process("nonebot")

View File

@ -10,7 +10,7 @@ from .common import MessageEventModel, msg_db
from src.utils.base.language import Language
from src.utils.base.resource import get_path
from src.utils.message.string_tool import convert_seconds_to_time
from ...utils.external.logo import get_group_icon, get_user_icon
from src.utils.external.logo import get_group_icon, get_user_icon
async def count_msg_by_bot_id(bot_id: str) -> int:

View File

@ -15,7 +15,7 @@ __plugin_meta__ = PluginMetadata(
}
)
from ...utils.base.data_manager import set_memory_data
from src.utils.base.data_manager import set_memory_data
driver = get_driver()

View File

@ -3,8 +3,8 @@ import aiohttp
from .qw_models import *
import httpx
from ...utils.base.data_manager import get_memory_data
from ...utils.base.language import Language
from src.utils.base.data_manager import get_memory_data
from src.utils.base.language import Language
dev_url = "https://devapi.qweather.com/" # 开发HBa
com_url = "https://api.qweather.com/" # 正式环境

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/20 上午5:10
@Author : snowykami
@Email : snowykami@outlook.com
@File : to_liteyuki.py
@Software: PyCharm
"""
from croterline.process import get_ctx
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot.plugin import PluginMetadata
from nonebot import on_message
__plugin_meta__ = PluginMetadata(
name="轻雪push",
description="把消息事件传递给轻雪框架进行处理",
usage="用户无需使用",
)
ctx = get_ctx()
@on_message().handle()
async def _(event: MessageEvent):
print("Push message to Liteyuki")
ctx.sub_chan << event.raw_message

View File

@ -1,20 +1,20 @@
from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪核心插件",
description="轻雪主程序插件,包含了许多初始化的功能",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable": False,
}
)
from ..utils.base.language import Language, get_default_lang_code
sys_lang = Language(get_default_lang_code())
from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪核心插件",
description="轻雪主程序插件,包含了许多初始化的功能",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable": False,
}
)
from src.utils.base.language import Language, get_default_lang_code
sys_lang = Language(get_default_lang_code())
nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name")))

View File

@ -1,47 +1,47 @@
import nonebot
from git import Repo
from src.utils.base.config import get_config
remote_urls = [
"https://github.com/LiteyukiStudio/LiteyukiBot.git",
"https://gitee.com/snowykami/LiteyukiBot.git"
]
def detect_update() -> bool:
# 对每个远程仓库进行检查只要有一个仓库有更新就返回True
for remote_url in remote_urls:
repo = Repo(".")
repo.remotes.origin.set_url(remote_url)
repo.remotes.origin.fetch()
if repo.head.commit != repo.commit('origin/main'):
return True
def update_liteyuki() -> tuple[bool, str]:
"""更新轻雪
:return: 是否更新成功更新变动"""
if get_config("allow_update", True):
new_commit_detected = detect_update()
if new_commit_detected:
repo = Repo(".")
logs = ""
# 对每个远程仓库进行更新
for remote_url in remote_urls:
try:
logs += f"\nremote: {remote_url}"
repo.remotes.origin.set_url(remote_url)
repo.remotes.origin.pull()
diffs = repo.head.commit.diff("origin/main")
for diff in diffs.iter_change_type('M'):
logs += f"\n{diff.a_path}"
return True, logs
except:
continue
else:
return False, "Nothing Changed"
else:
raise PermissionError("Update is not allowed.")
import nonebot
from git import Repo
from src.utils.base.config import get_config
remote_urls = [
"https://github.com/LiteyukiStudio/LiteyukiBot.git",
"https://gitee.com/snowykami/LiteyukiBot.git"
]
def detect_update() -> bool:
# 对每个远程仓库进行检查只要有一个仓库有更新就返回True
for remote_url in remote_urls:
repo = Repo(".")
repo.remotes.origin.set_url(remote_url)
repo.remotes.origin.fetch()
if repo.head.commit != repo.commit('origin/main'):
return True
def update_liteyuki() -> tuple[bool, str]:
"""更新轻雪
:return: 是否更新成功更新变动"""
if get_config("allow_update", True):
new_commit_detected = detect_update()
if new_commit_detected:
repo = Repo(".")
logs = ""
# 对每个远程仓库进行更新
for remote_url in remote_urls:
try:
logs += f"\nremote: {remote_url}"
repo.remotes.origin.set_url(remote_url)
repo.remotes.origin.pull()
diffs = repo.head.commit.diff("origin/main")
for diff in diffs.iter_change_type('M'):
logs += f"\n{diff.a_path}"
return True, logs
except:
continue
else:
return False, "Nothing Changed"
else:
raise PermissionError("Update is not allowed.")

View File

@ -1,301 +1,301 @@
import time
from typing import AnyStr
import time
from typing import AnyStr
import nonebot
import pip
from nonebot import get_driver, require
from nonebot.adapters import onebot, satori
from nonebot.adapters.onebot.v11 import Message, unescape
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
# from src.liteyuki.core import Reloader
from src.utils import event as event_utils, satori_utils
from src.utils.base.config import get_config
from src.utils.base.data_manager import TempConfig, common_db
from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from .api import update_liteyuki # type: ignore
from ..utils.base import reload # type: ignore
from ..utils.base.ly_function import get_function # type: ignore
from ..utils.message.html_tool import md_to_pic
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler
driver = get_driver()
@on_alconna(
command=Alconna(
"liteecho",
Args["text", str, ""],
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if text := result.main_args.get("text"):
await matcher.finish(Message(unescape(text)))
else:
await matcher.finish(f"Hello, Liteyuki!\nBot {bot.self_id}")
@on_alconna(
aliases={"更新轻雪"},
command=Alconna(
"update-liteyuki"
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
# 使用git pull更新
ulang = get_user_lang(str(event.user.id if isinstance(event, satori.event.Event) else event.user_id))
success, logs = update_liteyuki()
reply = "Liteyuki updated!\n"
reply += f"```\n{logs}\n```\n"
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
pip.main(["install", "-r", "requirements.txt"])
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
# await md.send_md(reply, bot)
img_bytes = await md_to_pic(reply)
await UniMessage.send(UniMessage.image(raw=img_bytes))
@on_alconna(
aliases={"重启轻雪"},
command=Alconna(
"reload-liteyuki"
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
await matcher.send("Liteyuki reloading")
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
temp_data.data.update(
{
"reload" : True,
"reload_time" : time.time(),
"reload_bot_id" : bot.self_id,
"reload_session_type": event_utils.get_message_type(event),
"reload_session_id" : (event.group_id if event.message_type == "group" else event.user_id)
if not isinstance(event, satori.event.Event) else event.chan_active.id,
"delta_time" : 0
}
)
common_db.save(temp_data)
reload()
@on_alconna(
command=Alconna(
"liteyuki-docs",
),
aliases={"轻雪文档"},
).handle()
# Satori OK
async def _(matcher: Matcher):
await matcher.finish("https://bot.liteyuki.icu/")
@on_alconna(
command=Alconna(
"/function",
Args["function", str]["args", MultiVar(str), ()],
),
permission=SUPERUSER
).handle()
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
"""
调用轻雪函数
Args:
result:
bot:
event:
Returns:
"""
function_name = result.main_args.get("function")
args: tuple[str] = result.main_args.get("args", ())
_args = []
_kwargs = {
"USER_ID" : str(event.user_id),
"GROUP_ID": str(event.group_id) if event.message_type == "group" else "0",
"BOT_ID" : str(bot.self_id)
}
for arg in args:
arg = arg.replace("\\=", "EQUAL_SIGN")
if "=" in arg:
key, value = arg.split("=", 1)
value = unescape(value.replace("EQUAL_SIGN", "="))
try:
value = eval(value)
except:
value = value
_kwargs[key] = value
else:
_args.append(arg.replace("EQUAL_SIGN", "="))
ly_func = get_function(function_name)
ly_func.bot = bot if "BOT_ID" not in _kwargs else nonebot.get_bot(_kwargs["BOT_ID"])
ly_func.matcher = matcher
await ly_func(*tuple(_args), **_kwargs)
@on_alconna(
command=Alconna(
"/api",
Args["api", str]["args", MultiVar(AnyStr), ()],
),
permission=SUPERUSER
).handle()
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
"""
调用API
Args:
result:
bot:
event:
Returns:
"""
api_name = result.main_args.get("api")
args: tuple[str] = result.main_args.get("args", ()) # 类似于url参数但每个参数间用空格分隔空格是%20
args_dict = {}
for arg in args:
key, value = arg.split("=", 1)
args_dict[key] = unescape(value.replace("%20", " "))
if api_name in need_user_id and "user_id" not in args_dict:
args_dict["user_id"] = str(event.user_id)
if api_name in need_group_id and "group_id" not in args_dict and event.message_type == "group":
args_dict["group_id"] = str(event.group_id)
if "message" in args_dict:
args_dict["message"] = Message(eval(args_dict["message"]))
if "messages" in args_dict:
args_dict["messages"] = Message(eval(args_dict["messages"]))
try:
result = await bot.call_api(api_name, **args_dict)
except Exception as e:
result = str(e)
args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items())
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
@driver.on_startup
async def on_startup():
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
# 储存重启信息
if temp_data.data.get("reload", False):
delta_time = time.time() - temp_data.data.get("reload_time", 0)
temp_data.data["delta_time"] = delta_time
common_db.save(temp_data) # 更新数据
"""
该部分将迁移至轻雪生命周期
Returns:
"""
@driver.on_shutdown
async def on_shutdown():
pass
@driver.on_bot_connect
async def _(bot: T_Bot):
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
if isinstance(bot, satori.Bot):
await satori_utils.user_infos.load_friends(bot)
# 用于重启计时
if temp_data.data.get("reload", False):
temp_data.data["reload"] = False
reload_bot_id = temp_data.data.get("reload_bot_id", 0)
if reload_bot_id != bot.self_id:
return
reload_session_type = temp_data.data.get("reload_session_type", "private")
reload_session_id = temp_data.data.get("reload_session_id", 0)
delta_time = temp_data.data.get("delta_time", 0)
common_db.save(temp_data) # 更新数据
if delta_time <= 20.0: # 启动时间太长就别发了,丢人
if isinstance(bot, satori.Bot):
await bot.send_message(
channel_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time
)
elif isinstance(bot, onebot.v11.Bot):
await bot.send_msg(
message_type=reload_session_type,
user_id=reload_session_id,
group_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time
)
elif isinstance(bot, onebot.v12.Bot):
await bot.send_message(
message_type=reload_session_type,
user_id=reload_session_id,
group_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time,
detail_type="group"
)
# 每天4点更新
@scheduler.scheduled_job("cron", hour=4)
async def every_day_update():
if get_config("auto_update", default=True):
result, logs = update_liteyuki()
pip.main(["install", "-r", "requirements.txt"])
if result:
await broadcast_to_superusers(f"Liteyuki updated: ```\n{logs}\n```")
nonebot.logger.info(f"Liteyuki updated: {logs}")
reload()
else:
nonebot.logger.info(logs)
# 需要用户id的api
need_user_id = (
"send_private_msg",
"send_msg",
"set_group_card",
"set_group_special_title",
"get_stranger_info",
"get_group_member_info"
)
need_group_id = (
"send_group_msg",
"send_msg",
"set_group_card",
"set_group_name",
"set_group_special_title",
"get_group_member_info",
"get_group_member_list",
"get_group_honor_info"
)
import time
from typing import AnyStr
import time
from typing import AnyStr
import nonebot
import pip
from nonebot import get_driver, require
from nonebot.adapters import onebot, satori
from nonebot.adapters.onebot.v11 import Message, unescape
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
# from src.liteyuki.core import Reloader
from src.utils import event as event_utils, satori_utils
from src.utils.base.config import get_config
from src.utils.base.data_manager import TempConfig, common_db
from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from .api import update_liteyuki # type: ignore
from src.utils.base import reload # type: ignore
from src.utils.base.ly_function import get_function # type: ignore
from src.utils.message.html_tool import md_to_pic
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler
driver = get_driver()
@on_alconna(
command=Alconna(
"liteecho",
Args["text", str, ""],
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if text := result.main_args.get("text"):
await matcher.finish(Message(unescape(text)))
else:
await matcher.finish(f"Hello, Liteyuki!\nBot {bot.self_id}")
@on_alconna(
aliases={"更新轻雪"},
command=Alconna(
"update-liteyuki"
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
# 使用git pull更新
ulang = get_user_lang(str(event.user.id if isinstance(event, satori.event.Event) else event.user_id))
success, logs = update_liteyuki()
reply = "Liteyuki updated!\n"
reply += f"```\n{logs}\n```\n"
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
pip.main(["install", "-r", "requirements.txt"])
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
# await md.send_md(reply, bot)
img_bytes = await md_to_pic(reply)
await UniMessage.send(UniMessage.image(raw=img_bytes))
@on_alconna(
aliases={"重启轻雪"},
command=Alconna(
"reload-liteyuki"
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
await matcher.send("Liteyuki reloading")
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
temp_data.data.update(
{
"reload" : True,
"reload_time" : time.time(),
"reload_bot_id" : bot.self_id,
"reload_session_type": event_utils.get_message_type(event),
"reload_session_id" : (event.group_id if event.message_type == "group" else event.user_id)
if not isinstance(event, satori.event.Event) else event.chan_active.id,
"delta_time" : 0
}
)
common_db.save(temp_data)
reload()
@on_alconna(
command=Alconna(
"liteyuki-docs",
),
aliases={"轻雪文档"},
).handle()
# Satori OK
async def _(matcher: Matcher):
await matcher.finish("https://bot.liteyuki.icu/")
@on_alconna(
command=Alconna(
"/function",
Args["function", str]["args", MultiVar(str), ()],
),
permission=SUPERUSER
).handle()
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
"""
调用轻雪函数
Args:
result:
bot:
event:
Returns:
"""
function_name = result.main_args.get("function")
args: tuple[str] = result.main_args.get("args", ())
_args = []
_kwargs = {
"USER_ID" : str(event.user_id),
"GROUP_ID": str(event.group_id) if event.message_type == "group" else "0",
"BOT_ID" : str(bot.self_id)
}
for arg in args:
arg = arg.replace("\\=", "EQUAL_SIGN")
if "=" in arg:
key, value = arg.split("=", 1)
value = unescape(value.replace("EQUAL_SIGN", "="))
try:
value = eval(value)
except:
value = value
_kwargs[key] = value
else:
_args.append(arg.replace("EQUAL_SIGN", "="))
ly_func = get_function(function_name)
ly_func.bot = bot if "BOT_ID" not in _kwargs else nonebot.get_bot(_kwargs["BOT_ID"])
ly_func.matcher = matcher
await ly_func(*tuple(_args), **_kwargs)
@on_alconna(
command=Alconna(
"/api",
Args["api", str]["args", MultiVar(AnyStr), ()],
),
permission=SUPERUSER
).handle()
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
"""
调用API
Args:
result:
bot:
event:
Returns:
"""
api_name = result.main_args.get("api")
args: tuple[str] = result.main_args.get("args", ()) # 类似于url参数但每个参数间用空格分隔空格是%20
args_dict = {}
for arg in args:
key, value = arg.split("=", 1)
args_dict[key] = unescape(value.replace("%20", " "))
if api_name in need_user_id and "user_id" not in args_dict:
args_dict["user_id"] = str(event.user_id)
if api_name in need_group_id and "group_id" not in args_dict and event.message_type == "group":
args_dict["group_id"] = str(event.group_id)
if "message" in args_dict:
args_dict["message"] = Message(eval(args_dict["message"]))
if "messages" in args_dict:
args_dict["messages"] = Message(eval(args_dict["messages"]))
try:
result = await bot.call_api(api_name, **args_dict)
except Exception as e:
result = str(e)
args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items())
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
@driver.on_startup
async def on_startup():
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
# 储存重启信息
if temp_data.data.get("reload", False):
delta_time = time.time() - temp_data.data.get("reload_time", 0)
temp_data.data["delta_time"] = delta_time
common_db.save(temp_data) # 更新数据
"""
该部分将迁移至轻雪生命周期
Returns:
"""
@driver.on_shutdown
async def on_shutdown():
pass
@driver.on_bot_connect
async def _(bot: T_Bot):
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
if isinstance(bot, satori.Bot):
await satori_utils.user_infos.load_friends(bot)
# 用于重启计时
if temp_data.data.get("reload", False):
temp_data.data["reload"] = False
reload_bot_id = temp_data.data.get("reload_bot_id", 0)
if reload_bot_id != bot.self_id:
return
reload_session_type = temp_data.data.get("reload_session_type", "private")
reload_session_id = temp_data.data.get("reload_session_id", 0)
delta_time = temp_data.data.get("delta_time", 0)
common_db.save(temp_data) # 更新数据
if delta_time <= 20.0: # 启动时间太长就别发了,丢人
if isinstance(bot, satori.Bot):
await bot.send_message(
channel_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time
)
elif isinstance(bot, onebot.v11.Bot):
await bot.send_msg(
message_type=reload_session_type,
user_id=reload_session_id,
group_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time
)
elif isinstance(bot, onebot.v12.Bot):
await bot.send_message(
message_type=reload_session_type,
user_id=reload_session_id,
group_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time,
detail_type="group"
)
# 每天4点更新
@scheduler.scheduled_job("cron", hour=4)
async def every_day_update():
if get_config("auto_update", default=True):
result, logs = update_liteyuki()
pip.main(["install", "-r", "requirements.txt"])
if result:
await broadcast_to_superusers(f"Liteyuki updated: ```\n{logs}\n```")
nonebot.logger.info(f"Liteyuki updated: {logs}")
reload()
else:
nonebot.logger.info(logs)
# 需要用户id的api
need_user_id = (
"send_private_msg",
"send_msg",
"set_group_card",
"set_group_special_title",
"get_stranger_info",
"get_group_member_info"
)
need_group_id = (
"send_group_msg",
"send_msg",
"set_group_card",
"set_group_name",
"set_group_special_title",
"get_group_member_info",
"get_group_member_list",
"get_group_honor_info"
)

View File

@ -1,33 +1,38 @@
import asyncio
import nonebot.plugin
from nonebot import get_driver
from src.utils import init_log
from src.utils.base.config import get_config
from src.utils.base.data_manager import InstalledPlugin, plugin_db
from src.utils.base.resource import load_resources
from src.utils.message.tools import check_for_package
load_resources()
init_log()
driver = get_driver()
@driver.on_startup
async def load_plugins():
nonebot.plugin.load_plugins("src/nonebot_plugins")
# 从数据库读取已安装的插件
if not get_config("safe_mode", False):
# 安全模式下,不加载插件
installed_plugins: list[InstalledPlugin] = plugin_db.where_all(InstalledPlugin())
if installed_plugins:
for installed_plugin in installed_plugins:
if not check_for_package(installed_plugin.module_name):
nonebot.logger.error(
f"{installed_plugin.module_name} not installed, but still in loader index.")
else:
nonebot.load_plugin(installed_plugin.module_name)
nonebot.plugin.load_plugins("plugins")
else:
nonebot.logger.info("Safe mode is on, no plugin loaded.")
import asyncio
import os.path
from pathlib import Path
import nonebot.plugin
from nonebot import get_driver
from src.utils import init_log
from src.utils.base.config import get_config
from src.utils.base.data_manager import InstalledPlugin, plugin_db
from src.utils.base.resource import load_resources
from src.utils.message.tools import check_for_package
load_resources()
init_log()
driver = get_driver()
@driver.on_startup
async def load_plugins():
nonebot.plugin.load_plugins(os.path.abspath(os.path.join(os.path.dirname(__file__), "../nonebot_plugins")))
# 从数据库读取已安装的插件
if not get_config("safe_mode", False):
# 安全模式下,不加载插件
installed_plugins: list[InstalledPlugin] = plugin_db.where_all(
InstalledPlugin()
)
if installed_plugins:
for installed_plugin in installed_plugins:
if not check_for_package(installed_plugin.module_name):
nonebot.logger.error(
f"{installed_plugin.module_name} not installed, but still in loader index."
)
else:
nonebot.load_plugin(installed_plugin.module_name)
nonebot.plugin.load_plugins("plugins")
else:
nonebot.logger.info("Safe mode is on, no plugin loaded.")

View File

@ -1,16 +0,0 @@
from nonebot.plugin import PluginMetadata
from .auto_update import *
__author__ = "expliyh"
__plugin_meta__ = PluginMetadata(
name="Satori 用户数据自动更新(临时措施)",
description="",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)

View File

@ -1,20 +0,0 @@
import nonebot
from nonebot.message import event_preprocessor
from src.utils.base.ly_typing import T_MessageEvent
from src.utils import satori_utils
from nonebot.adapters import satori
from nonebot_plugin_alconna.typings import Event
from src.nonebot_plugins.liteyuki_status.counter_for_satori import satori_counter
@event_preprocessor
async def pre_handle(event: Event):
if isinstance(event, satori.MessageEvent):
if event.user.id == event.self_id:
satori_counter.msg_sent += 1
else:
satori_counter.msg_received += 1
if event.user.name is not None:
if await satori_utils.user_infos.put(event.user):
nonebot.logger.info(f"Satori user {event.user.name}<{event.user.id}> updated")

View File

@ -1,17 +0,0 @@
from nonebot.plugin import PluginMetadata
from .api import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="联合黑名单(测试中...)",
description="",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)

View File

@ -1,58 +0,0 @@
import datetime
import aiohttp
import nonebot
from nonebot import require
from nonebot.exception import IgnoredException
from nonebot.message import event_preprocessor
from nonebot_plugin_alconna.typings import Event
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
blacklist_data: dict[str, set[str]] = {}
blacklist: set[str] = set()
@scheduler.scheduled_job("interval", minutes=10, next_run_time=datetime.datetime.now())
async def update_blacklist():
await request_for_blacklist()
async def request_for_blacklist():
global blacklist
urls = [
"https://cdn.liteyuki.icu/static/ubl/"
]
platforms = [
"qq"
]
for plat in platforms:
for url in urls:
url += f"{plat}.txt"
async with aiohttp.ClientSession() as client:
resp = await client.get(url)
blacklist_data[plat] = set((await resp.text()).splitlines())
blacklist = get_uni_set()
nonebot.logger.info("blacklists updated")
def get_uni_set() -> set:
s = set()
for new_set in blacklist_data.values():
s.update(new_set)
return s
@event_preprocessor
async def pre_handle(event: Event):
try:
user_id = str(event.get_user_id())
except:
return
if user_id in get_uni_set():
raise IgnoredException("UserId in blacklist")

View File

@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/20 上午5:10
@Author : snowykami
@Email : snowykami@outlook.com
@File : to_liteyuki.py
@Software: PyCharm
"""
import asyncio
from nonebot import Bot, get_bot, on_message, get_driver
from nonebot.plugin import PluginMetadata
from nonebot.adapters.onebot.v11 import MessageEvent, Bot
from liteyuki import Channel
from liteyuki.comm import get_channel
from liteyuki.comm.storage import shared_memory
from liteyuki.message.event import MessageEvent as LiteyukiMessageEvent
__plugin_meta__ = PluginMetadata(
name="轻雪push",
description="把消息事件传递给轻雪框架进行处理",
usage="用户无需使用",
)
recv_channel = Channel[LiteyukiMessageEvent](name="event_to_nonebot")
# @on_message().handle()
# async def _(bot: Bot, event: MessageEvent):
# liteyuki_event = LiteyukiMessageEvent(
# message_type=event.message_type,
# message=event.dict()["message"],
# raw_message=event.raw_message,
# data=event.dict(),
# bot_id=bot.self_id,
# user_id=str(event.user_id),
# session_id=str(event.user_id if event.message_type == "private" else event.group_id),
# session_type=event.message_type,
# receive_channel=recv_channel,
# )
# shared_memory.publish("event_to_liteyuki", liteyuki_event)
# @get_driver().on_bot_connect
# async def _():
# while True:
# event = await recv_channel.async_receive()
# bot: Bot = get_bot(event.bot_id) # type: ignore
# if event.message_type == "private":
# await bot.send_private_msg(user_id=int(event.session_id), message=event.data["message"])
# elif event.message_type == "group":
# await bot.send_group_msg(group_id=int(event.session_id), message=event.data["message"])

View File

@ -1,42 +1,42 @@
import sys
import nonebot
__NAME__ = "LiteyukiBot"
__VERSION__ = "6.3.2" # 60201
from src.utils.base.config import load_from_yaml, config
from src.utils.base.log import init_log
from git import Repo
major, minor, patch = map(int, __VERSION__.split("."))
__VERSION_I__ = major * 10000 + minor * 100 + patch
def init():
"""
初始化
Returns:
"""
# 检测python版本是否高于3.10
init_log()
if sys.version_info < (3, 10):
nonebot.logger.error("Requires Python3.10+ to run, please upgrade your Python Environment.")
exit(1)
try:
# 检测git仓库
repo = Repo(".")
except Exception as e:
nonebot.logger.error(f"Failed to load git repository: {e}, please clone this project from GitHub instead of downloading the zip file.")
# temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
# temp_data.data["start_time"] = time.time()
# common_db.save(temp_data)
nonebot.logger.info(
f"Run Liteyuki-NoneBot 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")
import sys
import nonebot
__NAME__ = "LiteyukiBot"
__VERSION__ = "6.3.2" # 60201
from src.utils.base.config import load_from_yaml, config
from src.utils.base.log import init_log
from git import Repo
major, minor, patch = map(int, __VERSION__.split("."))
__VERSION_I__ = major * 10000 + minor * 100 + patch
def init():
"""
初始化
Returns:
"""
# 检测python版本是否高于3.10
init_log()
if sys.version_info < (3, 10):
nonebot.logger.error("Requires Python3.10+ to run, please upgrade your Python Environment.")
exit(1)
try:
# 检测git仓库
repo = Repo(".")
except Exception as e:
nonebot.logger.error(f"Failed to load git repository: {e}, please clone this project from GitHub instead of downloading the zip file.")
# temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
# temp_data.data["start_time"] = time.time()
# common_db.save(temp_data)
nonebot.logger.info(
f"Run Liteyuki-NoneBot 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

@ -1,109 +1,109 @@
import os
import platform
from typing import List
import nonebot
import yaml
from pydantic import BaseModel
from ..message.tools import random_hex_string
config = {} # 全局配置,确保加载后读取
class SatoriNodeConfig(BaseModel):
host: str = ""
port: str = "5500"
path: str = ""
token: str = ""
class SatoriConfig(BaseModel):
comment: str = (
"These features are still in development. Do not enable in production environment."
)
enable: bool = False
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
class BasicConfig(BaseModel):
host: str = "127.0.0.1"
port: int = 20216
superusers: list[str] = []
command_start: list[str] = ["/", ""]
nickname: list[str] = [f"LiteyukiBot-{random_hex_string(6)}"]
satori: SatoriConfig = SatoriConfig()
data_path: str = "data/liteyuki"
chromium_path: str = (
"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" # type: ignore
if platform.system() == "Darwin"
else (
"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
if platform.system() == "Windows"
else "/usr/bin/chromium-browser"
)
)
def load_from_yaml(file_: str) -> dict:
global config
nonebot.logger.debug("Loading config from %s" % file_)
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 = init_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
def get_config(key: str, default=None):
"""获取配置项优先级bot > config > db > yaml"""
try:
bot = nonebot.get_bot()
except:
bot = None
if bot is None:
bot_config = {}
else:
bot_config = bot.config.dict()
if key in bot_config:
return bot_config[key]
elif key in config:
return config[key]
elif key in load_from_yaml("config.yml"):
return load_from_yaml("config.yml")[key]
else:
return default
def init_conf(conf: dict) -> dict:
"""
初始化配置文件,确保配置文件中的必要字段存在,且不会冲突
Args:
conf:
Returns:
"""
# 若command_start中无""则添加必要命令头开启alconna_use_command_start防止冲突
# 以下内容由于issue #53 被注释
# if "" not in conf.get("command_start", []):
# conf["alconna_use_command_start"] = True
return conf
pass
import os
import platform
from typing import List
import nonebot
import yaml
from pydantic import BaseModel
from ..message.tools import random_hex_string
config = {} # 全局配置,确保加载后读取
class SatoriNodeConfig(BaseModel):
host: str = ""
port: str = "5500"
path: str = ""
token: str = ""
class SatoriConfig(BaseModel):
comment: str = (
"These features are still in development. Do not enable in production environment."
)
enable: bool = False
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
class BasicConfig(BaseModel):
host: str = "127.0.0.1"
port: int = 20216
superusers: list[str] = []
command_start: list[str] = ["/", ""]
nickname: list[str] = [f"LiteyukiBot-{random_hex_string(6)}"]
satori: SatoriConfig = SatoriConfig()
data_path: str = "data/liteyuki"
chromium_path: str = (
"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" # type: ignore
if platform.system() == "Darwin"
else (
"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
if platform.system() == "Windows"
else "/usr/bin/chromium-browser"
)
)
def load_from_yaml(file_: str) -> dict:
global config
nonebot.logger.debug("Loading config from %s" % file_)
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 = init_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
def get_config(key: str, default=None):
"""获取配置项优先级bot > config > db > yaml"""
try:
bot = nonebot.get_bot()
except:
bot = None
if bot is None:
bot_config = {}
else:
bot_config = bot.config.dict()
if key in bot_config:
return bot_config[key]
elif key in config:
return config[key]
elif key in load_from_yaml("config.yml"):
return load_from_yaml("config.yml")[key]
else:
return default
def init_conf(conf: dict) -> dict:
"""
初始化配置文件,确保配置文件中的必要字段存在,且不会冲突
Args:
conf:
Returns:
"""
# 若command_start中无""则添加必要命令头开启alconna_use_command_start防止冲突
# 以下内容由于issue #53 被注释
# if "" not in conf.get("command_start", []):
# conf["alconna_use_command_start"] = True
return conf
pass

View File

@ -1,436 +1,436 @@
import inspect
import os
import pickle
import sqlite3
from types import NoneType
from typing import Any, Callable
from nonebot import logger
from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel
class LiteModel(BaseModel):
TABLE_NAME: str = None
id: int = None
def dump(self, *args, **kwargs):
if PYDANTIC_V2:
return self.model_dump(*args, **kwargs)
else:
return self.dict(*args, **kwargs)
class Database:
def __init__(self, db_name: str):
if os.path.dirname(db_name) != "" and not os.path.exists(os.path.dirname(db_name)):
os.makedirs(os.path.dirname(db_name))
self.db_name = db_name
self.conn = sqlite3.connect(db_name, check_same_thread=False)
self.cursor = self.conn.cursor()
self._on_save_callbacks = []
self._is_locked = False
def lock(self):
self.cursor.execute("BEGIN TRANSACTION")
self._is_locked = True
def lock_query(self, query: str, *args):
"""锁定查询"""
self.cursor.execute(query, args).fetchall()
def lock_model(self, model: LiteModel) -> LiteModel | Any | None:
"""锁定行
Args:
model: 数据模型实例
Returns:
"""
pass
def unlock(self):
self.cursor.execute("COMMIT")
self._is_locked = False
def where_one(self, model: LiteModel, condition: str = "", *args: Any, default: Any = None) -> LiteModel | Any | None:
"""查询第一个
Args:
model: 数据模型实例
condition: 查询条件,不给定则查询所有
*args: 参数化查询参数
default: 默认值
Returns:
"""
all_results = self.where_all(model, condition, *args)
return all_results[0] if all_results else default
def where_all(self, model: LiteModel, condition: str = "", *args: Any, default: Any = None) -> list[LiteModel | Any] | None:
"""查询所有
Args:
model: 数据模型实例
condition: 查询条件,不给定则查询所有
*args: 参数化查询参数
default: 默认值
Returns:
"""
table_name = model.TABLE_NAME
model_type = type(model)
logger.debug(f"Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}")
if not table_name:
raise ValueError(f"数据模型{model_type.__name__}未提供表名")
# condition = f"WHERE {condition}"
# print(f"SELECT * FROM {table_name} {condition}", args)
# if len(args) == 0:
# results = self.cursor.execute(f"SELECT * FROM {table_name} {condition}").fetchall()
# else:
# results = self.cursor.execute(f"SELECT * FROM {table_name} {condition}", args).fetchall()
if condition:
results = self.cursor.execute(f"SELECT * FROM {table_name} WHERE {condition}", args).fetchall()
else:
results = self.cursor.execute(f"SELECT * FROM {table_name}").fetchall()
fields = [description[0] for description in self.cursor.description]
if not results:
return default
else:
return [model_type(**self._load(dict(zip(fields, result)))) for result in results]
def save(self, *args: LiteModel):
self.returns_ = """增/改操作
Args:
*args:
Returns:
"""
table_list = [item[0] for item in self.cursor.execute("SELECT name FROM sqlite_master WHERE type ='table'").fetchall()]
for model in args:
logger.debug(f"Upserting {model}")
if not model.TABLE_NAME:
raise ValueError(f"数据模型 {model.__class__.__name__} 未提供表名")
elif model.TABLE_NAME not in table_list:
raise ValueError(f"数据模型 {model.__class__.__name__}{model.TABLE_NAME} 不存在,请先迁移")
else:
self._save(model.dump(by_alias=True))
for callback in self._on_save_callbacks:
callback(model)
def _save(self, obj: Any) -> Any:
# obj = copy.deepcopy(obj)
if isinstance(obj, dict):
table_name = obj.get("TABLE_NAME")
row_id = obj.get("id")
new_obj = {}
for field, value in obj.items():
if isinstance(value, self.ITERABLE_TYPE):
new_obj[self._get_stored_field_prefix(value) + field] = self._save(value) # self._save(value) # -> bytes
elif isinstance(value, self.BASIC_TYPE):
new_obj[field] = value
else:
raise ValueError(f"数据模型{table_name}包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}")
if table_name:
fields, values = [], []
for n_field, n_value in new_obj.items():
if n_field not in ["TABLE_NAME", "id"]:
fields.append(n_field)
values.append(n_value)
# 移除TABLE_NAME和id
fields = list(fields)
values = list(values)
if row_id is not None:
# 如果 _id 不为空,将 'id' 插入到字段列表的开始
fields.insert(0, 'id')
# 将 _id 插入到值列表的开始
values.insert(0, row_id)
fields = ', '.join([f'"{field}"' for field in fields])
placeholders = ', '.join('?' for _ in values)
self.cursor.execute(f"INSERT OR REPLACE INTO {table_name}({fields}) VALUES ({placeholders})", tuple(values))
# self.conn.commit()
if self._is_locked:
pass
else:
self.conn.commit()
foreign_id = self.cursor.execute("SELECT last_insert_rowid()").fetchone()[0]
return f"{self.FOREIGN_KEY_PREFIX}{foreign_id}@{table_name}" # -> FOREIGN_KEY_123456@{table_name} id@{table_name}
else:
return pickle.dumps(new_obj) # -> bytes
elif isinstance(obj, (list, set, tuple)):
obj_type = type(obj) # 到时候转回去
new_obj = []
for item in obj:
if isinstance(item, self.ITERABLE_TYPE):
new_obj.append(self._save(item))
elif isinstance(item, self.BASIC_TYPE):
new_obj.append(item)
else:
raise ValueError(f"数据模型包含不支持的数据类型,值:{item} 值类型:{type(item)}")
return pickle.dumps(obj_type(new_obj)) # -> bytes
else:
raise ValueError(f"数据模型包含不支持的数据类型,值:{obj} 值类型:{type(obj)}")
def _load(self, obj: Any) -> Any:
if isinstance(obj, dict):
new_obj = {}
for field, value in obj.items():
field: str
if field.startswith(self.BYTES_PREFIX):
if isinstance(value, bytes):
new_obj[field.replace(self.BYTES_PREFIX, "")] = self._load(pickle.loads(value))
else: # 从value字段可能为Nonefix at 2024/6/13
pass
# 暂时不作处理,后面再修
elif field.startswith(self.FOREIGN_KEY_PREFIX):
new_obj[field.replace(self.FOREIGN_KEY_PREFIX, "")] = self._load(self._get_foreign_data(value))
else:
new_obj[field] = value
return new_obj
elif isinstance(obj, (list, set, tuple)):
new_obj = []
for item in obj:
if isinstance(item, bytes):
# 对bytes进行尝试解析解析失败则返回原始bytes
try:
new_obj.append(self._load(pickle.loads(item)))
except Exception as e:
new_obj.append(self._load(item))
elif isinstance(item, str) and item.startswith(self.FOREIGN_KEY_PREFIX):
new_obj.append(self._load(self._get_foreign_data(item)))
else:
new_obj.append(self._load(item))
return new_obj
else:
return obj
def delete(self, model: LiteModel, condition: str, *args: Any, allow_empty: bool = False):
"""
删除满足条件的数据
Args:
allow_empty: 允许空条件删除整个表
model:
condition:
*args:
Returns:
"""
table_name = model.TABLE_NAME
logger.debug(f"Deleting {model} WHERE {condition} {args}")
if not table_name:
raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
if model.id is not None:
condition = f"id = {model.id}"
if not condition and not allow_empty:
raise ValueError("删除操作必须提供条件")
self.cursor.execute(f"DELETE FROM {table_name} WHERE {condition}", args)
if self._is_locked:
pass
else:
self.conn.commit()
def auto_migrate(self, *args: LiteModel):
"""
自动迁移模型
Args:
*args: 模型类实例化对象,支持空默认值,不支持嵌套迁移
Returns:
"""
for model in args:
if not model.TABLE_NAME:
raise ValueError(f"数据模型{type(model).__name__}未提供表名")
# 若无则创建表
self.cursor.execute(
f'CREATE TABLE IF NOT EXISTS "{model.TABLE_NAME}" (id INTEGER PRIMARY KEY AUTOINCREMENT)'
)
# 获取表结构,field -> SqliteType
new_structure = {}
for n_field, n_value in model.dump(by_alias=True).items():
if n_field not in ["TABLE_NAME", "id"]:
new_structure[self._get_stored_field_prefix(n_value) + n_field] = self._get_stored_type(n_value)
# 原有的字段列表
existing_structure = dict([(column[1], column[2]) for column in self.cursor.execute(f'PRAGMA table_info({model.TABLE_NAME})').fetchall()])
# 检测缺失字段由于SQLite是动态类型所以不需要检测类型
for n_field, n_type in new_structure.items():
if n_field not in existing_structure.keys() and n_field.lower() not in ["id", "table_name"]:
default_value = self.DEFAULT_MAPPING.get(n_type, 'NULL')
self.cursor.execute(
f"ALTER TABLE '{model.TABLE_NAME}' ADD COLUMN {n_field} {n_type} DEFAULT {self.DEFAULT_MAPPING.get(n_type, default_value)}"
)
# 检测多余字段进行删除
for e_field in existing_structure.keys():
if e_field not in new_structure.keys() and e_field.lower() not in ['id']:
self.cursor.execute(
f'ALTER TABLE "{model.TABLE_NAME}" DROP COLUMN "{e_field}"'
)
self.conn.commit()
# 已完成
def _get_stored_field_prefix(self, value) -> str:
"""根据类型获取存储字段前缀,一定在后加上字段名
* -> ""
Args:
value: 储存的值
Returns:
Sqlite3存储字段
"""
if isinstance(value, LiteModel) or isinstance(value, dict) and "TABLE_NAME" in value:
return self.FOREIGN_KEY_PREFIX
elif type(value) in self.ITERABLE_TYPE:
return self.BYTES_PREFIX
return ""
def _get_stored_type(self, value) -> str:
"""获取存储类型
Args:
value: 储存的值
Returns:
Sqlite3存储类型
"""
if isinstance(value, dict) and "TABLE_NAME" in value:
# 是一个模型字典,储存外键
return "INTEGER"
return self.TYPE_MAPPING.get(type(value), "TEXT")
def _get_foreign_data(self, foreign_value: str) -> dict:
"""
获取外键数据
Args:
foreign_value:
Returns:
"""
foreign_value = foreign_value.replace(self.FOREIGN_KEY_PREFIX, "")
table_name = foreign_value.split("@")[-1]
foreign_id = foreign_value.split("@")[0]
fields = [description[1] for description in self.cursor.execute(f"PRAGMA table_info({table_name})").fetchall()]
result = self.cursor.execute(f"SELECT * FROM {table_name} WHERE id = ?", (foreign_id,)).fetchone()
return dict(zip(fields, result))
def on_save(self, func: Callable[[LiteModel | Any], None]):
"""
装饰一个可调用对象使其在储存数据模型时被调用
Args:
func:
Returns:
"""
def wrapper(model):
# 检查被装饰函数声明的model类型和传入的model类型是否一致
sign = inspect.signature(func)
if param := sign.parameters.get("model"):
if isinstance(model, param.annotation):
pass
else:
return
else:
return
result = func(model)
for callback in self._on_save_callbacks:
callback(result)
return result
self._on_save_callbacks.append(wrapper)
return wrapper
TYPE_MAPPING = {
int : "INTEGER",
float : "REAL",
str : "TEXT",
bool : "INTEGER",
bytes : "BLOB",
NoneType : "NULL",
# dict : "TEXT",
# list : "TEXT",
# tuple : "TEXT",
# set : "TEXT",
dict : "BLOB", # LITEYUKIDICT{key_name}
list : "BLOB", # LITEYUKILIST{key_name}
tuple : "BLOB", # LITEYUKITUPLE{key_name}
set : "BLOB", # LITEYUKISET{key_name}
LiteModel: "TEXT" # FOREIGN_KEY_{table_name}
}
DEFAULT_MAPPING = {
"TEXT" : "''",
"INTEGER": 0,
"REAL" : 0.0,
"BLOB" : None,
"NULL" : None
}
# 基础类型
BASIC_TYPE = (int, float, str, bool, bytes, NoneType)
# 可序列化类型
ITERABLE_TYPE = (dict, list, tuple, set, LiteModel)
# 外键前缀
FOREIGN_KEY_PREFIX = "FOREIGN_KEY_"
# 转换为的字节前缀
BYTES_PREFIX = "PICKLE_BYTES_"
# transaction tx 事务操作
def first(self, model: LiteModel) -> "Database":
pass
def where(self, condition: str, *args) -> "Database":
pass
def limit(self, limit: int) -> "Database":
pass
def order(self, order: str) -> "Database":
pass
def check_sqlite_keyword(name):
sqlite_keywords = [
"ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ANALYZE", "AND", "AS", "ASC",
"ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE",
"CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE",
"CROSS", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT",
"DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DROP", "EACH",
"ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FOR",
"FOREIGN", "FROM", "FULL", "GLOB", "GROUP", "HAVING", "IF", "IGNORE", "IMMEDIATE",
"IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT",
"INTO", "IS", "ISNULL", "JOIN", "KEY", "LEFT", "LIKE", "LIMIT", "MATCH", "NATURAL",
"NO", "NOT", "NOTNULL", "NULL", "OF", "OFFSET", "ON", "OR", "ORDER", "OUTER", "PLAN",
"PRAGMA", "PRIMARY", "QUERY", "RAISE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX",
"RELEASE", "RENAME", "REPLACE", "RESTRICT", "RIGHT", "ROLLBACK", "ROW", "SAVEPOINT",
"SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TO", "TRANSACTION", "TRIGGER",
"UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN",
"WHERE", "WITH", "WITHOUT"
]
return True
# if name.upper() in sqlite_keywords:
import inspect
import os
import pickle
import sqlite3
from types import NoneType
from typing import Any, Callable
from nonebot import logger
from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel
class LiteModel(BaseModel):
TABLE_NAME: str = None
id: int = None
def dump(self, *args, **kwargs):
if PYDANTIC_V2:
return self.model_dump(*args, **kwargs)
else:
return self.dict(*args, **kwargs)
class Database:
def __init__(self, db_name: str):
if os.path.dirname(db_name) != "" and not os.path.exists(os.path.dirname(db_name)):
os.makedirs(os.path.dirname(db_name))
self.db_name = db_name
self.conn = sqlite3.connect(db_name, check_same_thread=False)
self.cursor = self.conn.cursor()
self._on_save_callbacks = []
self._is_locked = False
def lock(self):
self.cursor.execute("BEGIN TRANSACTION")
self._is_locked = True
def lock_query(self, query: str, *args):
"""锁定查询"""
self.cursor.execute(query, args).fetchall()
def lock_model(self, model: LiteModel) -> LiteModel | Any | None:
"""锁定行
Args:
model: 数据模型实例
Returns:
"""
pass
def unlock(self):
self.cursor.execute("COMMIT")
self._is_locked = False
def where_one(self, model: LiteModel, condition: str = "", *args: Any, default: Any = None) -> LiteModel | Any | None:
"""查询第一个
Args:
model: 数据模型实例
condition: 查询条件,不给定则查询所有
*args: 参数化查询参数
default: 默认值
Returns:
"""
all_results = self.where_all(model, condition, *args)
return all_results[0] if all_results else default
def where_all(self, model: LiteModel, condition: str = "", *args: Any, default: Any = None) -> list[LiteModel | Any] | None:
"""查询所有
Args:
model: 数据模型实例
condition: 查询条件,不给定则查询所有
*args: 参数化查询参数
default: 默认值
Returns:
"""
table_name = model.TABLE_NAME
model_type = type(model)
logger.debug(f"Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}")
if not table_name:
raise ValueError(f"数据模型{model_type.__name__}未提供表名")
# condition = f"WHERE {condition}"
# print(f"SELECT * FROM {table_name} {condition}", args)
# if len(args) == 0:
# results = self.cursor.execute(f"SELECT * FROM {table_name} {condition}").fetchall()
# else:
# results = self.cursor.execute(f"SELECT * FROM {table_name} {condition}", args).fetchall()
if condition:
results = self.cursor.execute(f"SELECT * FROM {table_name} WHERE {condition}", args).fetchall()
else:
results = self.cursor.execute(f"SELECT * FROM {table_name}").fetchall()
fields = [description[0] for description in self.cursor.description]
if not results:
return default
else:
return [model_type(**self._load(dict(zip(fields, result)))) for result in results]
def save(self, *args: LiteModel):
self.returns_ = """增/改操作
Args:
*args:
Returns:
"""
table_list = [item[0] for item in self.cursor.execute("SELECT name FROM sqlite_master WHERE type ='table'").fetchall()]
for model in args:
logger.debug(f"Upserting {model}")
if not model.TABLE_NAME:
raise ValueError(f"数据模型 {model.__class__.__name__} 未提供表名")
elif model.TABLE_NAME not in table_list:
raise ValueError(f"数据模型 {model.__class__.__name__}{model.TABLE_NAME} 不存在,请先迁移")
else:
self._save(model.dump(by_alias=True))
for callback in self._on_save_callbacks:
callback(model)
def _save(self, obj: Any) -> Any:
# obj = copy.deepcopy(obj)
if isinstance(obj, dict):
table_name = obj.get("TABLE_NAME")
row_id = obj.get("id")
new_obj = {}
for field, value in obj.items():
if isinstance(value, self.ITERABLE_TYPE):
new_obj[self._get_stored_field_prefix(value) + field] = self._save(value) # self._save(value) # -> bytes
elif isinstance(value, self.BASIC_TYPE):
new_obj[field] = value
else:
raise ValueError(f"数据模型{table_name}包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}")
if table_name:
fields, values = [], []
for n_field, n_value in new_obj.items():
if n_field not in ["TABLE_NAME", "id"]:
fields.append(n_field)
values.append(n_value)
# 移除TABLE_NAME和id
fields = list(fields)
values = list(values)
if row_id is not None:
# 如果 _id 不为空,将 'id' 插入到字段列表的开始
fields.insert(0, 'id')
# 将 _id 插入到值列表的开始
values.insert(0, row_id)
fields = ', '.join([f'"{field}"' for field in fields])
placeholders = ', '.join('?' for _ in values)
self.cursor.execute(f"INSERT OR REPLACE INTO {table_name}({fields}) VALUES ({placeholders})", tuple(values))
# self.conn.commit()
if self._is_locked:
pass
else:
self.conn.commit()
foreign_id = self.cursor.execute("SELECT last_insert_rowid()").fetchone()[0]
return f"{self.FOREIGN_KEY_PREFIX}{foreign_id}@{table_name}" # -> FOREIGN_KEY_123456@{table_name} id@{table_name}
else:
return pickle.dumps(new_obj) # -> bytes
elif isinstance(obj, (list, set, tuple)):
obj_type = type(obj) # 到时候转回去
new_obj = []
for item in obj:
if isinstance(item, self.ITERABLE_TYPE):
new_obj.append(self._save(item))
elif isinstance(item, self.BASIC_TYPE):
new_obj.append(item)
else:
raise ValueError(f"数据模型包含不支持的数据类型,值:{item} 值类型:{type(item)}")
return pickle.dumps(obj_type(new_obj)) # -> bytes
else:
raise ValueError(f"数据模型包含不支持的数据类型,值:{obj} 值类型:{type(obj)}")
def _load(self, obj: Any) -> Any:
if isinstance(obj, dict):
new_obj = {}
for field, value in obj.items():
field: str
if field.startswith(self.BYTES_PREFIX):
if isinstance(value, bytes):
new_obj[field.replace(self.BYTES_PREFIX, "")] = self._load(pickle.loads(value))
else: # 从value字段可能为Nonefix at 2024/6/13
pass
# 暂时不作处理,后面再修
elif field.startswith(self.FOREIGN_KEY_PREFIX):
new_obj[field.replace(self.FOREIGN_KEY_PREFIX, "")] = self._load(self._get_foreign_data(value))
else:
new_obj[field] = value
return new_obj
elif isinstance(obj, (list, set, tuple)):
new_obj = []
for item in obj:
if isinstance(item, bytes):
# 对bytes进行尝试解析解析失败则返回原始bytes
try:
new_obj.append(self._load(pickle.loads(item)))
except Exception as e:
new_obj.append(self._load(item))
elif isinstance(item, str) and item.startswith(self.FOREIGN_KEY_PREFIX):
new_obj.append(self._load(self._get_foreign_data(item)))
else:
new_obj.append(self._load(item))
return new_obj
else:
return obj
def delete(self, model: LiteModel, condition: str, *args: Any, allow_empty: bool = False):
"""
删除满足条件的数据
Args:
allow_empty: 允许空条件删除整个表
model:
condition:
*args:
Returns:
"""
table_name = model.TABLE_NAME
logger.debug(f"Deleting {model} WHERE {condition} {args}")
if not table_name:
raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
if model.id is not None:
condition = f"id = {model.id}"
if not condition and not allow_empty:
raise ValueError("删除操作必须提供条件")
self.cursor.execute(f"DELETE FROM {table_name} WHERE {condition}", args)
if self._is_locked:
pass
else:
self.conn.commit()
def auto_migrate(self, *args: LiteModel):
"""
自动迁移模型
Args:
*args: 模型类实例化对象,支持空默认值,不支持嵌套迁移
Returns:
"""
for model in args:
if not model.TABLE_NAME:
raise ValueError(f"数据模型{type(model).__name__}未提供表名")
# 若无则创建表
self.cursor.execute(
f'CREATE TABLE IF NOT EXISTS "{model.TABLE_NAME}" (id INTEGER PRIMARY KEY AUTOINCREMENT)'
)
# 获取表结构,field -> SqliteType
new_structure = {}
for n_field, n_value in model.dump(by_alias=True).items():
if n_field not in ["TABLE_NAME", "id"]:
new_structure[self._get_stored_field_prefix(n_value) + n_field] = self._get_stored_type(n_value)
# 原有的字段列表
existing_structure = dict([(column[1], column[2]) for column in self.cursor.execute(f'PRAGMA table_info({model.TABLE_NAME})').fetchall()])
# 检测缺失字段由于SQLite是动态类型所以不需要检测类型
for n_field, n_type in new_structure.items():
if n_field not in existing_structure.keys() and n_field.lower() not in ["id", "table_name"]:
default_value = self.DEFAULT_MAPPING.get(n_type, 'NULL')
self.cursor.execute(
f"ALTER TABLE '{model.TABLE_NAME}' ADD COLUMN {n_field} {n_type} DEFAULT {self.DEFAULT_MAPPING.get(n_type, default_value)}"
)
# 检测多余字段进行删除
for e_field in existing_structure.keys():
if e_field not in new_structure.keys() and e_field.lower() not in ['id']:
self.cursor.execute(
f'ALTER TABLE "{model.TABLE_NAME}" DROP COLUMN "{e_field}"'
)
self.conn.commit()
# 已完成
def _get_stored_field_prefix(self, value) -> str:
"""根据类型获取存储字段前缀,一定在后加上字段名
* -> ""
Args:
value: 储存的值
Returns:
Sqlite3存储字段
"""
if isinstance(value, LiteModel) or isinstance(value, dict) and "TABLE_NAME" in value:
return self.FOREIGN_KEY_PREFIX
elif type(value) in self.ITERABLE_TYPE:
return self.BYTES_PREFIX
return ""
def _get_stored_type(self, value) -> str:
"""获取存储类型
Args:
value: 储存的值
Returns:
Sqlite3存储类型
"""
if isinstance(value, dict) and "TABLE_NAME" in value:
# 是一个模型字典,储存外键
return "INTEGER"
return self.TYPE_MAPPING.get(type(value), "TEXT")
def _get_foreign_data(self, foreign_value: str) -> dict:
"""
获取外键数据
Args:
foreign_value:
Returns:
"""
foreign_value = foreign_value.replace(self.FOREIGN_KEY_PREFIX, "")
table_name = foreign_value.split("@")[-1]
foreign_id = foreign_value.split("@")[0]
fields = [description[1] for description in self.cursor.execute(f"PRAGMA table_info({table_name})").fetchall()]
result = self.cursor.execute(f"SELECT * FROM {table_name} WHERE id = ?", (foreign_id,)).fetchone()
return dict(zip(fields, result))
def on_save(self, func: Callable[[LiteModel | Any], None]):
"""
装饰一个可调用对象使其在储存数据模型时被调用
Args:
func:
Returns:
"""
def wrapper(model):
# 检查被装饰函数声明的model类型和传入的model类型是否一致
sign = inspect.signature(func)
if param := sign.parameters.get("model"):
if isinstance(model, param.annotation):
pass
else:
return
else:
return
result = func(model)
for callback in self._on_save_callbacks:
callback(result)
return result
self._on_save_callbacks.append(wrapper)
return wrapper
TYPE_MAPPING = {
int : "INTEGER",
float : "REAL",
str : "TEXT",
bool : "INTEGER",
bytes : "BLOB",
NoneType : "NULL",
# dict : "TEXT",
# list : "TEXT",
# tuple : "TEXT",
# set : "TEXT",
dict : "BLOB", # LITEYUKIDICT{key_name}
list : "BLOB", # LITEYUKILIST{key_name}
tuple : "BLOB", # LITEYUKITUPLE{key_name}
set : "BLOB", # LITEYUKISET{key_name}
LiteModel: "TEXT" # FOREIGN_KEY_{table_name}
}
DEFAULT_MAPPING = {
"TEXT" : "''",
"INTEGER": 0,
"REAL" : 0.0,
"BLOB" : None,
"NULL" : None
}
# 基础类型
BASIC_TYPE = (int, float, str, bool, bytes, NoneType)
# 可序列化类型
ITERABLE_TYPE = (dict, list, tuple, set, LiteModel)
# 外键前缀
FOREIGN_KEY_PREFIX = "FOREIGN_KEY_"
# 转换为的字节前缀
BYTES_PREFIX = "PICKLE_BYTES_"
# transaction tx 事务操作
def first(self, model: LiteModel) -> "Database":
pass
def where(self, condition: str, *args) -> "Database":
pass
def limit(self, limit: int) -> "Database":
pass
def order(self, order: str) -> "Database":
pass
def check_sqlite_keyword(name):
sqlite_keywords = [
"ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ANALYZE", "AND", "AS", "ASC",
"ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE",
"CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE",
"CROSS", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT",
"DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DROP", "EACH",
"ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FOR",
"FOREIGN", "FROM", "FULL", "GLOB", "GROUP", "HAVING", "IF", "IGNORE", "IMMEDIATE",
"IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT",
"INTO", "IS", "ISNULL", "JOIN", "KEY", "LEFT", "LIKE", "LIMIT", "MATCH", "NATURAL",
"NO", "NOT", "NOTNULL", "NULL", "OF", "OFFSET", "ON", "OR", "ORDER", "OUTER", "PLAN",
"PRAGMA", "PRIMARY", "QUERY", "RAISE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX",
"RELEASE", "RENAME", "REPLACE", "RESTRICT", "RIGHT", "ROLLBACK", "ROW", "SAVEPOINT",
"SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TO", "TRANSACTION", "TRIGGER",
"UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN",
"WHERE", "WITH", "WITHOUT"
]
return True
# if name.upper() in sqlite_keywords:
# raise ValueError(f"'{name}' 是SQLite保留字不建议使用请更换名称")

View File

@ -1,99 +1,99 @@
import os
from pydantic import Field
from .data import Database, LiteModel
DATA_PATH = "data/liteyuki"
user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb"))
group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb"))
plugin_db: Database = Database(os.path.join(DATA_PATH, "plugins.ldb"))
common_db: Database = Database(os.path.join(DATA_PATH, "common.ldb"))
# 内存数据库,临时用于存储数据
memory_database = {
}
class User(LiteModel):
TABLE_NAME: str = "user"
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 Group(LiteModel):
TABLE_NAME: str = "group_chat"
# 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")
enable: bool = Field(True, alias="enable") # 群聊全局机器人是否启用
config: dict = Field({}, alias="config")
class InstalledPlugin(LiteModel):
TABLE_NAME: str = "installed_plugin"
module_name: str = Field(str(), alias="module_name")
version: str = Field(str(), alias="version")
class GlobalPlugin(LiteModel):
TABLE_NAME: str = "global_plugin"
liteyuki: bool = Field(True, alias="liteyuki") # 是否为LiteYuki插件
module_name: str = Field(str(), alias="module_name")
enabled: bool = Field(True, alias="enabled")
class StoredConfig(LiteModel):
TABLE_NAME: str = "stored_config"
config: dict = {}
class TempConfig(LiteModel):
"""储存临时键值对的表"""
TABLE_NAME: str = "temp_data"
data: dict = {}
def auto_migrate():
user_db.auto_migrate(User())
group_db.auto_migrate(Group())
plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin())
common_db.auto_migrate(GlobalPlugin(), TempConfig())
auto_migrate()
def set_memory_data(key: str, value) -> None:
"""
设置内存数据库的数据类似于redis
Args:
key:
value:
Returns:
"""
return memory_database.update({
key: value
})
def get_memory_data(key: str, default=None) -> any:
"""
获取内存数据库的数据,类似于redis
Args:
key:
default:
Returns:
"""
return memory_database.get(key, default)
import os
from pydantic import Field
from .data import Database, LiteModel
DATA_PATH = "data/liteyuki"
user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb"))
group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb"))
plugin_db: Database = Database(os.path.join(DATA_PATH, "plugins.ldb"))
common_db: Database = Database(os.path.join(DATA_PATH, "common.ldb"))
# 内存数据库,临时用于存储数据
memory_database = {
}
class User(LiteModel):
TABLE_NAME: str = "user"
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 Group(LiteModel):
TABLE_NAME: str = "group_chat"
# 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")
enable: bool = Field(True, alias="enable") # 群聊全局机器人是否启用
config: dict = Field({}, alias="config")
class InstalledPlugin(LiteModel):
TABLE_NAME: str = "installed_plugin"
module_name: str = Field(str(), alias="module_name")
version: str = Field(str(), alias="version")
class GlobalPlugin(LiteModel):
TABLE_NAME: str = "global_plugin"
liteyuki: bool = Field(True, alias="liteyuki") # 是否为LiteYuki插件
module_name: str = Field(str(), alias="module_name")
enabled: bool = Field(True, alias="enabled")
class StoredConfig(LiteModel):
TABLE_NAME: str = "stored_config"
config: dict = {}
class TempConfig(LiteModel):
"""储存临时键值对的表"""
TABLE_NAME: str = "temp_data"
data: dict = {}
def auto_migrate():
user_db.auto_migrate(User())
group_db.auto_migrate(Group())
plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin())
common_db.auto_migrate(GlobalPlugin(), TempConfig())
auto_migrate()
def set_memory_data(key: str, value) -> None:
"""
设置内存数据库的数据类似于redis
Args:
key:
value:
Returns:
"""
return memory_database.update({
key: value
})
def get_memory_data(key: str, default=None) -> any:
"""
获取内存数据库的数据,类似于redis
Args:
key:
default:
Returns:
"""
return memory_database.get(key, default)

View File

@ -1,237 +1,237 @@
"""
语言模块,添加对多语言的支持
"""
import json
import locale
import os
from typing import Any, overload
import nonebot
from .config import config, get_config
from .data_manager import User, user_db
_language_data = {
"en": {
"name": "English",
}
}
_user_lang = {"user_id": "zh-CN"}
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)
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_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:
# 三重fallback
# 用户语言 > 默认语言/系统语言 > zh-CN
def __init__(self, lang_code: str = None, fallback_lang_code: str = None):
self.lang_code = lang_code
if self.lang_code is None:
self.lang_code = get_default_lang_code()
self.fallback_lang_code = fallback_lang_code
if self.fallback_lang_code is None:
self.fallback_lang_code = config.get(
"default_language", get_system_lang_code()
)
def _get(self, item: str, *args, **kwargs) -> str | Any:
"""
获取当前语言文本kwargs中的default参数为默认文本
**请不要重写本函数**
Args:
item: 文本键
*args: 格式化参数
**kwargs: 格式化参数
Returns:
str: 当前语言的文本
"""
default = kwargs.pop("default", None)
fallback = (self.lang_code, self.fallback_lang_code, "zh-CN")
for lang_code in fallback:
if lang_code in _language_data and item in _language_data[lang_code]:
trans: str = _language_data[lang_code][item]
try:
return trans.format(*args, **kwargs)
except Exception as e:
nonebot.logger.warning(f"Failed to format language data: {e}")
return trans
return default or item
def get(self, item: str, *args, **kwargs) -> str | Any:
"""
获取当前语言文本kwargs中的default参数为默认文本
Args:
item: 文本键
*args: 格式化参数
**kwargs: 格式化参数
Returns:
str: 当前语言的文本
"""
return self._get(item, *args, **kwargs)
def get_many(self, *args: str, **kwargs) -> dict[str, str]:
"""
获取多个文本
Args:
*args: 文本键
**kwargs: 文本键和默认文本
Returns:
dict: 多个文本
"""
args_data = {item: self.get(item) for item in args}
kwargs_data = {
item: self.get(item, default=default) for item, default in kwargs.items()
}
args_data.update(kwargs_data)
return args_data
def change_user_lang(user_id: str, lang_code: str):
"""
修改用户的语言,同时储存到数据库和内存中
"""
user = user_db.where_one(
User(), "user_id = ?", user_id, default=User(user_id=user_id)
)
user.profile["lang"] = lang_code
user_db.save(user)
_user_lang[user_id] = lang_code
def get_user_lang(user_id: str) -> Language:
"""
获取用户的语言实例,优先从内存中获取
"""
user_id = str(user_id)
if user_id not in _user_lang:
nonebot.logger.debug(f"Loading user language for {user_id}")
user = user_db.where_one(
User(),
"user_id = ?",
user_id,
default=User(user_id=user_id, username="Unknown"),
)
lang_code = user.profile.get("lang", get_default_lang_code())
_user_lang[user_id] = lang_code
return Language(_user_lang[user_id])
def get_system_lang_code() -> str:
"""
获取系统语言代码
"""
return locale.getdefaultlocale()[0].replace("_", "-")
def get_default_lang_code() -> str:
"""
获取默认语言代码,若没有设置则使用系统语言
Returns:
"""
return get_config("default_language", default=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
"""
语言模块,添加对多语言的支持
"""
import json
import locale
import os
from typing import Any, overload
import nonebot
from .config import config, get_config
from .data_manager import User, user_db
_language_data = {
"en": {
"name": "English",
}
}
_user_lang = {"user_id": "zh-CN"}
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)
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_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:
# 三重fallback
# 用户语言 > 默认语言/系统语言 > zh-CN
def __init__(self, lang_code: str = None, fallback_lang_code: str = None):
self.lang_code = lang_code
if self.lang_code is None:
self.lang_code = get_default_lang_code()
self.fallback_lang_code = fallback_lang_code
if self.fallback_lang_code is None:
self.fallback_lang_code = config.get(
"default_language", get_system_lang_code()
)
def _get(self, item: str, *args, **kwargs) -> str | Any:
"""
获取当前语言文本kwargs中的default参数为默认文本
**请不要重写本函数**
Args:
item: 文本键
*args: 格式化参数
**kwargs: 格式化参数
Returns:
str: 当前语言的文本
"""
default = kwargs.pop("default", None)
fallback = (self.lang_code, self.fallback_lang_code, "zh-CN")
for lang_code in fallback:
if lang_code in _language_data and item in _language_data[lang_code]:
trans: str = _language_data[lang_code][item]
try:
return trans.format(*args, **kwargs)
except Exception as e:
nonebot.logger.warning(f"Failed to format language data: {e}")
return trans
return default or item
def get(self, item: str, *args, **kwargs) -> str | Any:
"""
获取当前语言文本kwargs中的default参数为默认文本
Args:
item: 文本键
*args: 格式化参数
**kwargs: 格式化参数
Returns:
str: 当前语言的文本
"""
return self._get(item, *args, **kwargs)
def get_many(self, *args: str, **kwargs) -> dict[str, str]:
"""
获取多个文本
Args:
*args: 文本键
**kwargs: 文本键和默认文本
Returns:
dict: 多个文本
"""
args_data = {item: self.get(item) for item in args}
kwargs_data = {
item: self.get(item, default=default) for item, default in kwargs.items()
}
args_data.update(kwargs_data)
return args_data
def change_user_lang(user_id: str, lang_code: str):
"""
修改用户的语言,同时储存到数据库和内存中
"""
user = user_db.where_one(
User(), "user_id = ?", user_id, default=User(user_id=user_id)
)
user.profile["lang"] = lang_code
user_db.save(user)
_user_lang[user_id] = lang_code
def get_user_lang(user_id: str) -> Language:
"""
获取用户的语言实例,优先从内存中获取
"""
user_id = str(user_id)
if user_id not in _user_lang:
nonebot.logger.debug(f"Loading user language for {user_id}")
user = user_db.where_one(
User(),
"user_id = ?",
user_id,
default=User(user_id=user_id, username="Unknown"),
)
lang_code = user.profile.get("lang", get_default_lang_code())
_user_lang[user_id] = lang_code
return Language(_user_lang[user_id])
def get_system_lang_code() -> str:
"""
获取系统语言代码
"""
return locale.getdefaultlocale()[0].replace("_", "-")
def get_default_lang_code() -> str:
"""
获取默认语言代码,若没有设置则使用系统语言
Returns:
"""
return get_config("default_language", default=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

View File

@ -1,79 +1,79 @@
import sys
import loguru
from typing import TYPE_CHECKING
from .config import load_from_yaml
from .language import Language, get_default_lang_code
logger = loguru.logger
if TYPE_CHECKING:
# avoid sphinx autodoc resolve annotation failed
# because loguru module do not have `Logger` class actually
from loguru import Record
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
# DEBUG日志格式
debug_format: str = (
"<c>{time:YYYY-MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}.{module}.{function}:{line}></c> "
"{message}"
)
# 默认日志格式
default_format: str = (
"<c>{time:MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}></c> "
"{message}"
)
def get_format(level: str) -> str:
if level == "DEBUG":
return debug_format
else:
return default_format
logger = loguru.logger.bind()
def init_log():
"""
在语言加载完成后执行
Returns:
"""
global logger
config = load_from_yaml("config.yml")
logger.remove()
logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=get_format(config.get("log_level", "INFO")),
)
show_icon = config.get("log_icon", True)
lang = Language(get_default_lang_code())
debug = lang.get("log.debug", default="==DEBUG")
info = lang.get("log.info", default="===INFO")
success = lang.get("log.success", default="SUCCESS")
warning = lang.get("log.warning", default="WARNING")
error = lang.get("log.error", default="==ERROR")
logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}{debug}")
logger.level("INFO", color="<normal>", icon=f"{'' if show_icon else ''}{info}")
logger.level("SUCCESS", color="<green>", icon=f"{'' if show_icon else ''}{success}")
logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}{warning}")
logger.level("ERROR", color="<red>", icon=f"{'' if show_icon else ''}{error}")
import sys
import loguru
from typing import TYPE_CHECKING
from .config import load_from_yaml
from .language import Language, get_default_lang_code
logger = loguru.logger
if TYPE_CHECKING:
# avoid sphinx autodoc resolve annotation failed
# because loguru module do not have `Logger` class actually
from loguru import Record
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
# DEBUG日志格式
debug_format: str = (
"<c>{time:YYYY-MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}.{module}.{function}:{line}></c> "
"{message}"
)
# 默认日志格式
default_format: str = (
"<c>{time:MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}></c> "
"{message}"
)
def get_format(level: str) -> str:
if level == "DEBUG":
return debug_format
else:
return default_format
logger = loguru.logger.bind()
def init_log():
"""
在语言加载完成后执行
Returns:
"""
global logger
config = load_from_yaml("config.yml")
logger.remove()
logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=get_format(config.get("log_level", "INFO")),
)
show_icon = config.get("log_icon", True)
lang = Language(get_default_lang_code())
debug = lang.get("log.debug", default="==DEBUG")
info = lang.get("log.info", default="===INFO")
success = lang.get("log.success", default="SUCCESS")
warning = lang.get("log.warning", default="WARNING")
error = lang.get("log.error", default="==ERROR")
logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}{debug}")
logger.level("INFO", color="<normal>", icon=f"{'' if show_icon else ''}{info}")
logger.level("SUCCESS", color="<green>", icon=f"{'' if show_icon else ''}{success}")
logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}{warning}")
logger.level("ERROR", color="<red>", icon=f"{'' if show_icon else ''}{error}")

View File

@ -1,197 +1,197 @@
"""
liteyuki function是一种类似于mcfunction的函数用于在liteyuki中实现一些功能例如自定义指令等也可与Python函数绑定
使用 /function function_name *args **kwargs来调用
例如 /function test/hello user_id=123456
可以用于一些轻量级插件的编写无需Python代码
SnowyKami
"""
import asyncio
import functools
# cmd *args **kwargs
# api api_name **kwargs
import os
from typing import Any, Awaitable, Callable, Coroutine
import nonebot
from nonebot import Bot
from nonebot.adapters.satori import bot
from nonebot.internal.matcher import Matcher
ly_function_extensions = (
"lyf",
"lyfunction",
"mcfunction"
)
loaded_functions = dict()
class LiteyukiFunction:
def __init__(self, name: str):
self.name = name
self.functions: list[str] = list()
self.bot: Bot = None
self.kwargs_data = dict()
self.args_data = list()
self.matcher: Matcher = None
self.end = False
self.sub_tasks: list[asyncio.Task] = list()
async def __call__(self, *args, **kwargs):
self.kwargs_data.update(kwargs)
self.args_data = list(set(self.args_data + list(args)))
for i, cmd in enumerate(self.functions):
r = await self.execute_line(cmd, i, *args, **kwargs)
if r == 0:
msg = f"End function {self.name} by line {i}"
nonebot.logger.debug(msg)
for task in self.sub_tasks:
task.cancel(msg)
return
def __str__(self):
return f"LiteyukiFunction({self.name})"
def __repr__(self):
return self.__str__()
async def execute_line(self, cmd: str, line: int = 0, *args, **kwargs) -> Any:
"""
解析一行轻雪函数
Args:
cmd: 命令
line: 行数
Returns:
"""
try:
if "${" in cmd:
# 此种情况下,{}内容不用管,只对${}内的内容进行format
for i in range(len(cmd) - 1):
if cmd[i] == "$" and cmd[i + 1] == "{":
end = cmd.find("}", i)
key = cmd[i + 2:end]
cmd = cmd.replace(f"${{{key}}}", str(self.kwargs_data.get(key, "")))
else:
cmd = cmd.format(*self.args_data, **self.kwargs_data)
except Exception as e:
pass
no_head = cmd.split(" ", 1)[1] if len(cmd.split(" ")) > 1 else ""
try:
head, cmd_args, cmd_kwargs = self.get_args(cmd)
except Exception as e:
error_msg = f"Parsing error in {self.name} at line {line}: {e}"
nonebot.logger.error(error_msg)
await self.matcher.send(error_msg)
return
if head == "var":
# 变量定义
self.kwargs_data.update(cmd_kwargs)
elif head == "cmd":
# 在当前计算机上执行命令
os.system(no_head)
elif head == "api":
# 调用Bot API 需要Bot实例
await self.bot.call_api(cmd_args[1], **cmd_kwargs)
elif head == "function":
# 调用轻雪函数
func = get_function(cmd_args[1])
func.bot = self.bot
func.matcher = self.matcher
await func(*cmd_args[2:], **cmd_kwargs)
elif head == "sleep":
# 等待一段时间
await asyncio.sleep(float(cmd_args[1]))
elif head == "nohup":
# 挂起运行
task = asyncio.create_task(self.execute_line(no_head))
self.sub_tasks.append(task)
elif head == "end":
# 结束所有函数
self.end = True
return 0
elif head == "await":
# 等待所有协程执行完毕
await asyncio.gather(*self.sub_tasks)
def get_args(self, line: str) -> tuple[str, tuple[str, ...], dict[str, Any]]:
"""
获取参数
Args:
line: 命令
Returns:
命令头 参数 关键字
"""
line = line.replace("\\=", "EQUAL_SIGN")
head = ""
args = list()
kwargs = dict()
for i, arg in enumerate(line.split(" ")):
if "=" in arg:
key, value = arg.split("=", 1)
value = value.replace("EQUAL_SIGN", "=")
try:
value = eval(value)
except:
value = self.kwargs_data.get(value, value)
kwargs[key] = value
else:
if i == 0:
head = arg
args.append(arg)
return head, tuple(args), kwargs
def get_function(name: str) -> LiteyukiFunction | None:
"""
获取一个轻雪函数
Args:
name: 函数名
Returns:
"""
return loaded_functions.get(name)
def load_from_dir(path: str):
"""
从目录及其子目录中递归加载所有轻雪函数类似mcfunction
Args:
path: 目录路径
"""
for f in os.listdir(path):
f = os.path.join(path, f)
if os.path.isfile(f):
if f.endswith(ly_function_extensions):
load_from_file(f)
if os.path.isdir(f):
load_from_dir(f)
def load_from_file(path: str):
"""
从文件中加载轻雪函数
Args:
path:
Returns:
"""
with open(path, "r", encoding="utf-8") as f:
name = ".".join(os.path.basename(path).split(".")[:-1])
func = LiteyukiFunction(name)
for i, line in enumerate(f.read().split("\n")):
if line.startswith("#") or line.strip() == "":
continue
func.functions.append(line)
loaded_functions[name] = func
nonebot.logger.debug(f"Loaded function {name}")
"""
liteyuki function是一种类似于mcfunction的函数用于在liteyuki中实现一些功能例如自定义指令等也可与Python函数绑定
使用 /function function_name *args **kwargs来调用
例如 /function test/hello user_id=123456
可以用于一些轻量级插件的编写无需Python代码
SnowyKami
"""
import asyncio
import functools
# cmd *args **kwargs
# api api_name **kwargs
import os
from typing import Any, Awaitable, Callable, Coroutine
import nonebot
from nonebot import Bot
from nonebot.adapters.satori import bot
from nonebot.internal.matcher import Matcher
ly_function_extensions = (
"lyf",
"lyfunction",
"mcfunction"
)
loaded_functions = dict()
class LiteyukiFunction:
def __init__(self, name: str):
self.name = name
self.functions: list[str] = list()
self.bot: Bot = None
self.kwargs_data = dict()
self.args_data = list()
self.matcher: Matcher = None
self.end = False
self.sub_tasks: list[asyncio.Task] = list()
async def __call__(self, *args, **kwargs):
self.kwargs_data.update(kwargs)
self.args_data = list(set(self.args_data + list(args)))
for i, cmd in enumerate(self.functions):
r = await self.execute_line(cmd, i, *args, **kwargs)
if r == 0:
msg = f"End function {self.name} by line {i}"
nonebot.logger.debug(msg)
for task in self.sub_tasks:
task.cancel(msg)
return
def __str__(self):
return f"LiteyukiFunction({self.name})"
def __repr__(self):
return self.__str__()
async def execute_line(self, cmd: str, line: int = 0, *args, **kwargs) -> Any:
"""
解析一行轻雪函数
Args:
cmd: 命令
line: 行数
Returns:
"""
try:
if "${" in cmd:
# 此种情况下,{}内容不用管,只对${}内的内容进行format
for i in range(len(cmd) - 1):
if cmd[i] == "$" and cmd[i + 1] == "{":
end = cmd.find("}", i)
key = cmd[i + 2:end]
cmd = cmd.replace(f"${{{key}}}", str(self.kwargs_data.get(key, "")))
else:
cmd = cmd.format(*self.args_data, **self.kwargs_data)
except Exception as e:
pass
no_head = cmd.split(" ", 1)[1] if len(cmd.split(" ")) > 1 else ""
try:
head, cmd_args, cmd_kwargs = self.get_args(cmd)
except Exception as e:
error_msg = f"Parsing error in {self.name} at line {line}: {e}"
nonebot.logger.error(error_msg)
await self.matcher.send(error_msg)
return
if head == "var":
# 变量定义
self.kwargs_data.update(cmd_kwargs)
elif head == "cmd":
# 在当前计算机上执行命令
os.system(no_head)
elif head == "api":
# 调用Bot API 需要Bot实例
await self.bot.call_api(cmd_args[1], **cmd_kwargs)
elif head == "function":
# 调用轻雪函数
func = get_function(cmd_args[1])
func.bot = self.bot
func.matcher = self.matcher
await func(*cmd_args[2:], **cmd_kwargs)
elif head == "sleep":
# 等待一段时间
await asyncio.sleep(float(cmd_args[1]))
elif head == "nohup":
# 挂起运行
task = asyncio.create_task(self.execute_line(no_head))
self.sub_tasks.append(task)
elif head == "end":
# 结束所有函数
self.end = True
return 0
elif head == "await":
# 等待所有协程执行完毕
await asyncio.gather(*self.sub_tasks)
def get_args(self, line: str) -> tuple[str, tuple[str, ...], dict[str, Any]]:
"""
获取参数
Args:
line: 命令
Returns:
命令头 参数 关键字
"""
line = line.replace("\\=", "EQUAL_SIGN")
head = ""
args = list()
kwargs = dict()
for i, arg in enumerate(line.split(" ")):
if "=" in arg:
key, value = arg.split("=", 1)
value = value.replace("EQUAL_SIGN", "=")
try:
value = eval(value)
except:
value = self.kwargs_data.get(value, value)
kwargs[key] = value
else:
if i == 0:
head = arg
args.append(arg)
return head, tuple(args), kwargs
def get_function(name: str) -> LiteyukiFunction | None:
"""
获取一个轻雪函数
Args:
name: 函数名
Returns:
"""
return loaded_functions.get(name)
def load_from_dir(path: str):
"""
从目录及其子目录中递归加载所有轻雪函数类似mcfunction
Args:
path: 目录路径
"""
for f in os.listdir(path):
f = os.path.join(path, f)
if os.path.isfile(f):
if f.endswith(ly_function_extensions):
load_from_file(f)
if os.path.isdir(f):
load_from_dir(f)
def load_from_file(path: str):
"""
从文件中加载轻雪函数
Args:
path:
Returns:
"""
with open(path, "r", encoding="utf-8") as f:
name = ".".join(os.path.basename(path).split(".")[:-1])
func = LiteyukiFunction(name)
for i, line in enumerate(f.read().split("\n")):
if line.startswith("#") or line.strip() == "":
continue
func.functions.append(line)
loaded_functions[name] = func
nonebot.logger.debug(f"Loaded function {name}")

View File

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

View File

@ -1,5 +1,5 @@
from nonebot.adapters.onebot import v11
GROUP_ADMIN = v11.GROUP_ADMIN
GROUP_OWNER = v11.GROUP_OWNER
from nonebot.adapters.onebot import v11
GROUP_ADMIN = v11.GROUP_ADMIN
GROUP_OWNER = v11.GROUP_OWNER

View File

@ -1,355 +1,355 @@
import json
import os
import shutil
import zipfile
from typing import Any
from pathlib import Path
import aiofiles
import nonebot
import yaml
from .data import LiteModel
from .language import Language, get_default_lang_code
from .ly_function import loaded_functions
_loaded_resource_packs: list["ResourceMetadata"] = [] # 按照加载顺序排序
temp_resource_root = Path("data/liteyuki/resources")
temp_extract_root = Path("data/liteyuki/temp")
lang = Language(get_default_lang_code())
class ResourceMetadata(LiteModel):
name: str = "Unknown"
version: str = "0.0.1"
description: str = "Unknown"
path: str = ""
folder: str = ""
def load_resource_from_dir(path: str):
"""
把资源包按照文件相对路径复制到运行临时文件夹data/liteyuki/resources
Args:
path: 资源文件夹
Returns:
"""
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)
elif os.path.isfile(path) and path.endswith(".zip"):
# zip文件
# 临时解压并读取metadata.yml
with zipfile.ZipFile(path, "r") as zip_ref:
# 解压至临时目录 data/liteyuki/temp/{pack_name}.zip
zip_ref.extractall(os.path.join(temp_extract_root, os.path.basename(path)))
with zip_ref.open("metadata.yml") as f:
metadata = yaml.safe_load(f)
path = os.path.join(temp_extract_root, os.path.basename(path))
else:
# 没有metadata.yml文件不是一个资源包
return
for root, dirs, files in os.walk(path):
for file in files:
relative_path = os.path.relpath(os.path.join(root, file), path)
copy_file(
os.path.join(root, file),
os.path.join(temp_resource_root, relative_path),
)
metadata["path"] = path
metadata["folder"] = os.path.basename(path)
if os.path.exists(os.path.join(path, "lang")):
# 加载语言
from src.utils.base.language import load_from_dir
load_from_dir(os.path.join(path, "lang"))
if os.path.exists(os.path.join(path, "functions")):
# 加载功能
from src.utils.base.ly_function import load_from_dir
load_from_dir(os.path.join(path, "functions"))
if os.path.exists(os.path.join(path, "word_bank")):
# 加载词库
from src.utils.base.word_bank import load_from_dir
load_from_dir(os.path.join(path, "word_bank"))
_loaded_resource_packs.insert(0, ResourceMetadata(**metadata))
def get_path(
path: os.PathLike[str,] | Path | str,
abs_path: bool = True,
default: Any = None,
debug: bool = False,
) -> str | Any:
"""
获取资源包中的路径,且该路径必须存在
Args:
path: 相对路径
abs_path: 是否返回绝对路径
default: 默认解,当该路径不存在时使用
debug: 启用调试,每次都会先重载资源
Returns: 所需求之路径
"""
if debug:
nonebot.logger.debug("Resource path debug enabled, reloading")
load_resources()
resource_relative_path = temp_resource_root / path
if resource_relative_path.exists():
return str(
resource_relative_path.resolve() if abs_path else resource_relative_path
)
else:
return default
def get_resource_path(
path: os.PathLike[str,] | Path | str,
abs_path: bool = True,
only_exist: bool = False,
default: Any = None,
debug: bool = False,
) -> Path:
"""
获取资源包中的路径
Args:
path: 相对路径
abs_path: 是否返回绝对路径
only_exist: 检查该路径是否存在
default: [当 `only_exist` 为 **真** 时启用]默认解,当该路径不存在时使用
debug: 启用调试,每次都会先重载资源
Returns: 所需求之路径
"""
if debug:
nonebot.logger.debug("Resource path debug enabled, reloading")
load_resources()
resource_relative_path = (
(temp_resource_root / path).resolve()
if abs_path
else (temp_resource_root / path)
)
if only_exist:
if resource_relative_path.exists():
return resource_relative_path
else:
return default
else:
return resource_relative_path
def get_files(
path: os.PathLike[str,] | Path | str, abs_path: bool = False
) -> list[str]:
"""
获取资源包中一个目录的所有内容
Args:
path: 该目录的相对路径
abs_path: 是否返回绝对路径
Returns: 目录内容路径所构成之列表
"""
resource_relative_path = temp_resource_root / path
if resource_relative_path.exists():
return [
(
str((resource_relative_path / file_).resolve())
if abs_path
else str((resource_relative_path / file_))
)
for file_ in os.listdir(resource_relative_path)
]
else:
return []
def get_resource_files(
path: os.PathLike[str,] | Path | str, abs_path: bool = False
) -> list[Path]:
"""
获取资源包中一个目录的所有内容
Args:
path: 该目录的相对路径
abs_path: 是否返回绝对路径
Returns: 目录内容路径所构成之列表
"""
resource_relative_path = temp_resource_root / path
if resource_relative_path.exists():
return [
(
(resource_relative_path / file_).resolve()
if abs_path
else (resource_relative_path / file_)
)
for file_ in os.listdir(resource_relative_path)
]
else:
return []
def get_loaded_resource_packs() -> list[ResourceMetadata]:
"""
获取已加载的资源包,优先级从前到后
Returns: 资源包列表
"""
return _loaded_resource_packs
def copy_file(src, dst):
# 获取目标文件的目录
dst_dir = os.path.dirname(dst)
# 如果目标目录不存在,创建它
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
# 复制文件
shutil.copy(src, dst)
def load_resources():
"""用于外部主程序调用的资源加载函数
Returns:
"""
# 加载默认资源和语言
# 清空临时资源包路径data/liteyuki/resources
_loaded_resource_packs.clear()
loaded_functions.clear()
if os.path.exists(temp_resource_root):
shutil.rmtree(temp_resource_root)
os.makedirs(temp_resource_root, exist_ok=True)
# 加载内置资源
standard_resources_path = "src/resources"
for resource_dir in os.listdir(standard_resources_path):
load_resource_from_dir(os.path.join(standard_resources_path, resource_dir))
# 加载其他资源包
if not os.path.exists("resources"):
os.makedirs("resources", exist_ok=True)
if not os.path.exists("resources/index.json"):
json.dump([], open("resources/index.json", "w", encoding="utf-8"))
resource_index: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
resource_index.reverse() # 优先级高的后加载,但是排在前面
for resource in resource_index:
load_resource_from_dir(os.path.join("resources", resource))
def check_status(name: str) -> bool:
"""
检查资源包是否已加载
Args:
name: 资源包名称,文件夹名
Returns: 是否已加载
"""
return name in [rp.folder for rp in get_loaded_resource_packs()]
def check_exist(name: str) -> bool:
"""
检查资源包文件夹是否存在于resources文件夹
Args:
name: 资源包名称,文件夹名
Returns: 是否存在
"""
path = os.path.join("resources", name)
return os.path.exists(os.path.join(path, "metadata.yml")) or (
os.path.isfile(path) and name.endswith(".zip")
)
def add_resource_pack(name: str) -> bool:
"""
添加资源包该操作仅修改index.json文件不会加载资源包要生效请重载资源
Args:
name: 资源包名称,文件夹名
Returns:
"""
if check_exist(name):
old_index: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
if name not in old_index:
old_index.append(name)
json.dump(old_index, open("resources/index.json", "w", encoding="utf-8"))
load_resource_from_dir(os.path.join("resources", name))
return True
else:
nonebot.logger.warning(lang.get("liteyuki.resource_loaded", name=name))
return False
else:
nonebot.logger.warning(lang.get("liteyuki.resource_not_exist", name=name))
return False
def remove_resource_pack(name: str) -> bool:
"""
移除资源包,该操作仅修改加载索引,要生效请重载资源
Args:
name: 资源包名称,文件夹名
Returns:
"""
if check_exist(name):
old_index: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
if name in old_index:
old_index.remove(name)
json.dump(old_index, open("resources/index.json", "w", encoding="utf-8"))
return True
else:
nonebot.logger.warning(lang.get("liteyuki.resource_not_loaded", name=name))
return False
else:
nonebot.logger.warning(lang.get("liteyuki.resource_not_exist", name=name))
return False
def change_priority(name: str, delta: int) -> bool:
"""
修改资源包优先级
Args:
name: 资源包名称,文件夹名
delta: 优先级变化正数表示后移负数表示前移0表示移到最前
Returns:
"""
# 正数表示前移,负数表示后移
old_resource_list: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
new_resource_list = old_resource_list.copy()
if name in old_resource_list:
index = old_resource_list.index(name)
if 0 <= index + delta < len(old_resource_list):
new_index = index + delta
new_resource_list.remove(name)
new_resource_list.insert(new_index, name)
json.dump(
new_resource_list, open("resources/index.json", "w", encoding="utf-8")
)
return True
else:
nonebot.logger.warning("Priority change failed, out of range")
return False
else:
nonebot.logger.debug("Priority change failed, resource not loaded")
return False
def get_resource_metadata(name: str) -> ResourceMetadata:
"""
获取资源包元数据
Args:
name: 资源包名称,文件夹名
Returns:
"""
for rp in get_loaded_resource_packs():
if rp.folder == name:
return rp
return ResourceMetadata()
import json
import os
import shutil
import zipfile
from typing import Any
from pathlib import Path
import aiofiles
import nonebot
import yaml
from .data import LiteModel
from .language import Language, get_default_lang_code
from .ly_function import loaded_functions
_loaded_resource_packs: list["ResourceMetadata"] = [] # 按照加载顺序排序
temp_resource_root = Path("data/liteyuki/resources")
temp_extract_root = Path("data/liteyuki/temp")
lang = Language(get_default_lang_code())
class ResourceMetadata(LiteModel):
name: str = "Unknown"
version: str = "0.0.1"
description: str = "Unknown"
path: str = ""
folder: str = ""
def load_resource_from_dir(path: str):
"""
把资源包按照文件相对路径复制到运行临时文件夹data/liteyuki/resources
Args:
path: 资源文件夹
Returns:
"""
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)
elif os.path.isfile(path) and path.endswith(".zip"):
# zip文件
# 临时解压并读取metadata.yml
with zipfile.ZipFile(path, "r") as zip_ref:
# 解压至临时目录 data/liteyuki/temp/{pack_name}.zip
zip_ref.extractall(os.path.join(temp_extract_root, os.path.basename(path)))
with zip_ref.open("metadata.yml") as f:
metadata = yaml.safe_load(f)
path = os.path.join(temp_extract_root, os.path.basename(path))
else:
# 没有metadata.yml文件不是一个资源包
return
for root, dirs, files in os.walk(path):
for file in files:
relative_path = os.path.relpath(os.path.join(root, file), path)
copy_file(
os.path.join(root, file),
os.path.join(temp_resource_root, relative_path),
)
metadata["path"] = path
metadata["folder"] = os.path.basename(path)
if os.path.exists(os.path.join(path, "lang")):
# 加载语言
from src.utils.base.language import load_from_dir
load_from_dir(os.path.join(path, "lang"))
if os.path.exists(os.path.join(path, "functions")):
# 加载功能
from src.utils.base.ly_function import load_from_dir
load_from_dir(os.path.join(path, "functions"))
if os.path.exists(os.path.join(path, "word_bank")):
# 加载词库
from src.utils.base.word_bank import load_from_dir
load_from_dir(os.path.join(path, "word_bank"))
_loaded_resource_packs.insert(0, ResourceMetadata(**metadata))
def get_path(
path: os.PathLike[str,] | Path | str,
abs_path: bool = True,
default: Any = None,
debug: bool = False,
) -> str | Any:
"""
获取资源包中的路径,且该路径必须存在
Args:
path: 相对路径
abs_path: 是否返回绝对路径
default: 默认解,当该路径不存在时使用
debug: 启用调试,每次都会先重载资源
Returns: 所需求之路径
"""
if debug:
nonebot.logger.debug("Resource path debug enabled, reloading")
load_resources()
resource_relative_path = temp_resource_root / path
if resource_relative_path.exists():
return str(
resource_relative_path.resolve() if abs_path else resource_relative_path
)
else:
return default
def get_resource_path(
path: os.PathLike[str,] | Path | str,
abs_path: bool = True,
only_exist: bool = False,
default: Any = None,
debug: bool = False,
) -> Path:
"""
获取资源包中的路径
Args:
path: 相对路径
abs_path: 是否返回绝对路径
only_exist: 检查该路径是否存在
default: [当 `only_exist` 为 **真** 时启用]默认解,当该路径不存在时使用
debug: 启用调试,每次都会先重载资源
Returns: 所需求之路径
"""
if debug:
nonebot.logger.debug("Resource path debug enabled, reloading")
load_resources()
resource_relative_path = (
(temp_resource_root / path).resolve()
if abs_path
else (temp_resource_root / path)
)
if only_exist:
if resource_relative_path.exists():
return resource_relative_path
else:
return default
else:
return resource_relative_path
def get_files(
path: os.PathLike[str,] | Path | str, abs_path: bool = False
) -> list[str]:
"""
获取资源包中一个目录的所有内容
Args:
path: 该目录的相对路径
abs_path: 是否返回绝对路径
Returns: 目录内容路径所构成之列表
"""
resource_relative_path = temp_resource_root / path
if resource_relative_path.exists():
return [
(
str((resource_relative_path / file_).resolve())
if abs_path
else str((resource_relative_path / file_))
)
for file_ in os.listdir(resource_relative_path)
]
else:
return []
def get_resource_files(
path: os.PathLike[str,] | Path | str, abs_path: bool = False
) -> list[Path]:
"""
获取资源包中一个目录的所有内容
Args:
path: 该目录的相对路径
abs_path: 是否返回绝对路径
Returns: 目录内容路径所构成之列表
"""
resource_relative_path = temp_resource_root / path
if resource_relative_path.exists():
return [
(
(resource_relative_path / file_).resolve()
if abs_path
else (resource_relative_path / file_)
)
for file_ in os.listdir(resource_relative_path)
]
else:
return []
def get_loaded_resource_packs() -> list[ResourceMetadata]:
"""
获取已加载的资源包,优先级从前到后
Returns: 资源包列表
"""
return _loaded_resource_packs
def copy_file(src, dst):
# 获取目标文件的目录
dst_dir = os.path.dirname(dst)
# 如果目标目录不存在,创建它
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
# 复制文件
shutil.copy(src, dst)
def load_resources():
"""用于外部主程序调用的资源加载函数
Returns:
"""
# 加载默认资源和语言
# 清空临时资源包路径data/liteyuki/resources
_loaded_resource_packs.clear()
loaded_functions.clear()
if os.path.exists(temp_resource_root):
shutil.rmtree(temp_resource_root)
os.makedirs(temp_resource_root, exist_ok=True)
# 加载内置资源
standard_resources_path = "src/resources"
for resource_dir in os.listdir(standard_resources_path):
load_resource_from_dir(os.path.join(standard_resources_path, resource_dir))
# 加载其他资源包
if not os.path.exists("resources"):
os.makedirs("resources", exist_ok=True)
if not os.path.exists("resources/index.json"):
json.dump([], open("resources/index.json", "w", encoding="utf-8"))
resource_index: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
resource_index.reverse() # 优先级高的后加载,但是排在前面
for resource in resource_index:
load_resource_from_dir(os.path.join("resources", resource))
def check_status(name: str) -> bool:
"""
检查资源包是否已加载
Args:
name: 资源包名称,文件夹名
Returns: 是否已加载
"""
return name in [rp.folder for rp in get_loaded_resource_packs()]
def check_exist(name: str) -> bool:
"""
检查资源包文件夹是否存在于resources文件夹
Args:
name: 资源包名称,文件夹名
Returns: 是否存在
"""
path = os.path.join("resources", name)
return os.path.exists(os.path.join(path, "metadata.yml")) or (
os.path.isfile(path) and name.endswith(".zip")
)
def add_resource_pack(name: str) -> bool:
"""
添加资源包该操作仅修改index.json文件不会加载资源包要生效请重载资源
Args:
name: 资源包名称,文件夹名
Returns:
"""
if check_exist(name):
old_index: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
if name not in old_index:
old_index.append(name)
json.dump(old_index, open("resources/index.json", "w", encoding="utf-8"))
load_resource_from_dir(os.path.join("resources", name))
return True
else:
nonebot.logger.warning(lang.get("liteyuki.resource_loaded", name=name))
return False
else:
nonebot.logger.warning(lang.get("liteyuki.resource_not_exist", name=name))
return False
def remove_resource_pack(name: str) -> bool:
"""
移除资源包,该操作仅修改加载索引,要生效请重载资源
Args:
name: 资源包名称,文件夹名
Returns:
"""
if check_exist(name):
old_index: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
if name in old_index:
old_index.remove(name)
json.dump(old_index, open("resources/index.json", "w", encoding="utf-8"))
return True
else:
nonebot.logger.warning(lang.get("liteyuki.resource_not_loaded", name=name))
return False
else:
nonebot.logger.warning(lang.get("liteyuki.resource_not_exist", name=name))
return False
def change_priority(name: str, delta: int) -> bool:
"""
修改资源包优先级
Args:
name: 资源包名称,文件夹名
delta: 优先级变化正数表示后移负数表示前移0表示移到最前
Returns:
"""
# 正数表示前移,负数表示后移
old_resource_list: list[str] = json.load(
open("resources/index.json", "r", encoding="utf-8")
)
new_resource_list = old_resource_list.copy()
if name in old_resource_list:
index = old_resource_list.index(name)
if 0 <= index + delta < len(old_resource_list):
new_index = index + delta
new_resource_list.remove(name)
new_resource_list.insert(new_index, name)
json.dump(
new_resource_list, open("resources/index.json", "w", encoding="utf-8")
)
return True
else:
nonebot.logger.warning("Priority change failed, out of range")
return False
else:
nonebot.logger.debug("Priority change failed, resource not loaded")
return False
def get_resource_metadata(name: str) -> ResourceMetadata:
"""
获取资源包元数据
Args:
name: 资源包名称,文件夹名
Returns:
"""
for rp in get_loaded_resource_packs():
if rp.folder == name:
return rp
return ResourceMetadata()

View File

@ -1,57 +1,57 @@
import json
import os
import random
from typing import Iterable
import nonebot
word_bank: dict[str, set[str]] = {}
def load_from_file(file_path: str):
"""
从json文件中加载词库
Args:
file_path: 文件路径
"""
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
for key, value_list in data.items():
if key not in word_bank:
word_bank[key] = set()
word_bank[key].update(value_list)
nonebot.logger.debug(f"Loaded word bank from {file_path}")
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(".json"):
load_from_file(file_path)
except Exception as e:
nonebot.logger.error(f"Failed to load language data from {file}: {e}")
continue
def get_reply(kws: Iterable[str]) -> str | None:
"""
获取回复
Args:
kws: 关键词
Returns:
"""
for kw in kws:
if kw in word_bank:
return random.choice(list(word_bank[kw]))
return None
import json
import os
import random
from typing import Iterable
import nonebot
word_bank: dict[str, set[str]] = {}
def load_from_file(file_path: str):
"""
从json文件中加载词库
Args:
file_path: 文件路径
"""
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
for key, value_list in data.items():
if key not in word_bank:
word_bank[key] = set()
word_bank[key].update(value_list)
nonebot.logger.debug(f"Loaded word bank from {file_path}")
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(".json"):
load_from_file(file_path)
except Exception as e:
nonebot.logger.error(f"Failed to load language data from {file}: {e}")
continue
def get_reply(kws: Iterable[str]) -> str | None:
"""
获取回复
Args:
kws: 关键词
Returns:
"""
for kw in kws:
if kw in word_bank:
return random.choice(list(word_bank[kw]))
return None

View File

@ -1 +1 @@
from .get_info import *
from .get_info import *

View File

@ -1,26 +1,26 @@
from nonebot.adapters import satori
from nonebot.adapters import onebot
from src.utils.base.ly_typing import T_MessageEvent, T_GroupMessageEvent
def get_user_id(event: T_MessageEvent):
if isinstance(event, satori.event.Event):
return event.user.id
else:
return event.user_id
def get_group_id(event: T_GroupMessageEvent):
if isinstance(event, satori.event.Event):
return event.guild.id
elif isinstance(event, onebot.v11.GroupMessageEvent):
return event.group_id
else:
return None
def get_message_type(event: T_MessageEvent) -> str:
if isinstance(event, satori.event.Event):
return "private" if event.guild is None else "group"
else:
return event.message_type
from nonebot.adapters import satori
from nonebot.adapters import onebot
from src.utils.base.ly_typing import T_MessageEvent, T_GroupMessageEvent
def get_user_id(event: T_MessageEvent):
if isinstance(event, satori.event.Event):
return event.user.id
else:
return event.user_id
def get_group_id(event: T_GroupMessageEvent):
if isinstance(event, satori.event.Event):
return event.guild.id
elif isinstance(event, onebot.v11.GroupMessageEvent):
return event.group_id
else:
return None
def get_message_type(event: T_MessageEvent) -> str:
if isinstance(event, satori.event.Event):
return "private" if event.guild is None else "group"
else:
return event.message_type

View File

@ -1,40 +1,40 @@
async def get_user_icon(platform: str, user_id: str) -> str:
"""
获取用户头像
Args:
platform: qq, telegram, discord...
user_id: 1234567890
Returns:
str: 头像链接
"""
match platform:
case "qq":
return f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
case "telegram":
return f"https://t.me/i/userpic/320/{user_id}.jpg"
case "discord":
return f"https://cdn.discordapp.com/avatars/{user_id}/"
case _:
return ""
async def get_group_icon(platform: str, group_id: str) -> str:
"""
获取群组头像
Args:
platform: qq, telegram, discord...
group_id: 1234567890
Returns:
str: 头像链接
"""
match platform:
case "qq":
return f"http://p.qlogo.cn/gh/{group_id}/{group_id}/640"
case "telegram":
return f"https://t.me/c/{group_id}/"
case "discord":
return f"https://cdn.discordapp.com/icons/{group_id}/"
case _:
return ""
async def get_user_icon(platform: str, user_id: str) -> str:
"""
获取用户头像
Args:
platform: qq, telegram, discord...
user_id: 1234567890
Returns:
str: 头像链接
"""
match platform:
case "qq":
return f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
case "telegram":
return f"https://t.me/i/userpic/320/{user_id}.jpg"
case "discord":
return f"https://cdn.discordapp.com/avatars/{user_id}/"
case _:
return ""
async def get_group_icon(platform: str, group_id: str) -> str:
"""
获取群组头像
Args:
platform: qq, telegram, discord...
group_id: 1234567890
Returns:
str: 头像链接
"""
match platform:
case "qq":
return f"http://p.qlogo.cn/gh/{group_id}/{group_id}/640"
case "telegram":
return f"https://t.me/c/{group_id}/"
case "discord":
return f"https://cdn.discordapp.com/icons/{group_id}/"
case _:
return ""

View File

@ -1,89 +1,89 @@
import os
import aiofiles # type: ignore
import nonebot
from nonebot import require
# require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import ( # type: ignore
template_to_html,
template_to_pic,
md_to_pic
) # type: ignore
async def template2html(
template: str,
templates: dict,
) -> str:
"""
Args:
template: str: 模板文件
**templates: dict: 模板参数
Returns:
HTML 正文
"""
template_path = os.path.dirname(template)
template_name = os.path.basename(template)
return await template_to_html(template_path, template_name, **templates)
async def template2image(
template: str,
templates: dict,
pages=None,
wait: int = 0,
scale_factor: float = 1,
debug: bool = False,
) -> bytes:
"""
template -> html -> image
Args:
debug: 输入渲染好的 html
wait: 等待时间,单位秒
pages: 页面参数
template: str: 模板文件
templates: dict: 模板参数
scale_factor: 缩放因子,越高越清晰
Returns:
图片二进制数据
"""
###
if pages is None:
pages = {
"viewport": {
"width" : 1080,
"height": 10
},
}
template_path = os.path.dirname(template)
template_name = os.path.basename(template)
if debug:
# 重载资源
raw_html = await template_to_html(
template_name=template_name,
template_path=template_path,
**templates,
)
random_file_name = f"debug.html"
async with aiofiles.open(
os.path.join(template_path, random_file_name), "w", encoding="utf-8"
) as f:
await f.write(raw_html)
nonebot.logger.info("Debug HTML: %s" % f"{random_file_name}")
return await template_to_pic(
template_name=template_name,
template_path=template_path,
templates=templates,
wait=wait,
###
pages=pages,
device_scale_factor=scale_factor
###
)
import os
import aiofiles # type: ignore
import nonebot
from nonebot import require
# require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import ( # type: ignore
template_to_html,
template_to_pic,
md_to_pic
) # type: ignore
async def template2html(
template: str,
templates: dict,
) -> str:
"""
Args:
template: str: 模板文件
**templates: dict: 模板参数
Returns:
HTML 正文
"""
template_path = os.path.dirname(template)
template_name = os.path.basename(template)
return await template_to_html(template_path, template_name, **templates)
async def template2image(
template: str,
templates: dict,
pages=None,
wait: int = 0,
scale_factor: float = 1,
debug: bool = False,
) -> bytes:
"""
template -> html -> image
Args:
debug: 输入渲染好的 html
wait: 等待时间,单位秒
pages: 页面参数
template: str: 模板文件
templates: dict: 模板参数
scale_factor: 缩放因子,越高越清晰
Returns:
图片二进制数据
"""
###
if pages is None:
pages = {
"viewport": {
"width" : 1080,
"height": 10
},
}
template_path = os.path.dirname(template)
template_name = os.path.basename(template)
if debug:
# 重载资源
raw_html = await template_to_html(
template_name=template_name,
template_path=template_path,
**templates,
)
random_file_name = f"debug.html"
async with aiofiles.open(
os.path.join(template_path, random_file_name), "w", encoding="utf-8"
) as f:
await f.write(raw_html)
nonebot.logger.info("Debug HTML: %s" % f"{random_file_name}")
return await template_to_pic(
template_name=template_name,
template_path=template_path,
templates=templates,
wait=wait,
###
pages=pages,
device_scale_factor=scale_factor
###
)

Some files were not shown because too many files have changed in this diff Show More