1
0
forked from bot/app

20 Commits

Author SHA1 Message Date
5e454bc971 性能提升200倍 2024-10-20 23:36:49 +08:00
70bfb0fcee 性能提升200倍 2024-10-20 23:35:44 +08:00
13b95c2732 🐛 修复一些细节小问题 2024-10-20 20:59:30 +08:00
ef5866343d add: nonebot-plugin-gotify 2024-10-20 04:12:17 +08:00
d5ccd105a2 add: nonebot-plugin-gotify 2024-10-20 03:11:26 +08:00
20ad8dc53f 🐛 hotfix: from mypy import 2024-10-19 21:15:37 +08:00
de9c91d8bd 🐛 hotfix: from mypy import 2024-10-19 21:12:55 +08:00
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
117 changed files with 5303 additions and 4413 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

@ -5,4 +5,5 @@ nonebot:
nickname: [ "liteyuki" ]
default_language: zh
driver: ~fastapi+~httpx+~websockets
alconna_use_command_start: true
alconna_use_command_start: true
gotify_token: "empty token"

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] = []
@ -62,9 +75,6 @@ class Channel(Generic[T]):
if name in _channel:
raise ValueError(f"Channel {name} already exists")
_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")
def _get_generic_type(self) -> Optional[type]:
"""
@ -72,7 +82,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 +108,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 +126,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 +141,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 +157,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 +204,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 +263,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 +275,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 +308,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 +323,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

@ -118,6 +118,8 @@ def load_config_in_default(no_waring: bool = False) -> dict[str, Any]:
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
Args:
no_waring: 是否关闭警告
"""
config = load_configs_from_dirs("config", no_waring=no_waring)
config.update(

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

@ -12,26 +12,34 @@ import sys
import loguru
logger = loguru.logger
logger = loguru.logger.bind()
# DEBUG日志格式
debug_format: str = (
"<c>{time:YYYY-MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}.{module}.{function}:{line}></c> "
"{message}"
"<c>{time:YYYY-MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}{level}]</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}"
"<c>{time:MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}{level}]</lvl> "
"<c><{name}></c> "
"{message}"
)
def get_format(level: str) -> str:
"""
获取日志格式
Args:
level: 日志等级
Returns: 日志格式
"""
# DEBUG日志格式
if level == "DEBUG":
return debug_format
else:
@ -41,23 +49,27 @@ def get_format(level: str) -> str:
def init_log(config: dict):
"""
在语言加载完成后执行
Returns:
Args:
config: 配置
"""
global logger
level = config.get("log_level", "DEBUG")
logger.remove()
logger.add(
sys.stdout,
level=0,
level=level,
diagnose=False,
format=get_format(config.get("log_level", "INFO")),
format=get_format(level),
)
show_icon = config.get("log_icon", True)
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")
logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}")
logger.level("INFO", color="<normal>", icon=f"{'' if show_icon else ''}")
logger.level("SUCCESS", color="<green>", icon=f"{'' if show_icon else ''}")
logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}")
logger.level("ERROR", color="<red>", icon=f"{'' if show_icon else ''}")
logger.level("CRITICAL", color="<red>", icon=f"{'' if show_icon else ''}")
logger.level("TRACE", color="<cyan>", icon=f"{'🔍' if show_icon else ''}")
logger.bind()
init_log(config={})
init_log(config={"log_level": "DEBUG", "log_icon": True})

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,21 @@
# -*- coding: utf-8 -*-
"""
该模块参考并引用了nonebot-plugin-alconna的消息段定义
"""
from typing import Any
from typing import Iterable
from magicoca import Chan, select
from liteyuki.log import logger
def message_handler_thread(i_chans: Iterable[Chan[Any]]):
"""
Args:
i_chans: 多路输入管道组
Returns:
"""
for msg in select(*i_chans):
logger.debug(f"Recv from anybot {msg}")
logger.info(f"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", [])

515
pdm.lock generated Normal file
View File

@ -0,0 +1,515 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default", "dev"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:b79773f31b417a430bc17fa770a0ff937c3ed0a3d1ac18950fdf4d5c405a28af"
[[metadata.targets]]
requires_python = ">=3.10"
[[package]]
name = "annotated-types"
version = "0.7.0"
requires_python = ">=3.8"
summary = "Reusable constraint types to use with typing.Annotated"
groups = ["default"]
dependencies = [
"typing-extensions>=4.0.0; python_version < \"3.9\"",
]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "black"
version = "24.10.0"
requires_python = ">=3.9"
summary = "The uncompromising code formatter."
groups = ["dev"]
dependencies = [
"click>=8.0.0",
"mypy-extensions>=0.4.3",
"packaging>=22.0",
"pathspec>=0.9.0",
"platformdirs>=2",
"tomli>=1.1.0; python_version < \"3.11\"",
"typing-extensions>=4.0.1; python_version < \"3.11\"",
]
files = [
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
{file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
{file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
{file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
{file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
{file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
{file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
{file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
{file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
{file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
{file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
{file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
{file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
{file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
{file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
{file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
{file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
]
[[package]]
name = "click"
version = "8.1.7"
requires_python = ">=3.7"
summary = "Composable command line interface toolkit"
groups = ["dev"]
dependencies = [
"colorama; platform_system == \"Windows\"",
"importlib-metadata; python_version < \"3.8\"",
]
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[[package]]
name = "colorama"
version = "0.4.6"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Cross-platform colored terminal text."
groups = ["default", "dev"]
marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "croterline"
version = "1.0.9"
requires_python = ">=3.10"
summary = "Default template for PDM package"
groups = ["default"]
dependencies = [
"magicoca>=1.0.6",
]
files = [
{file = "croterline-1.0.9-py3-none-any.whl", hash = "sha256:d2d62bd8038ea0a845175b4a2ac127e4c4fcdb8dab5d4a04804c8af0a30986eb"},
{file = "croterline-1.0.9.tar.gz", hash = "sha256:417a3aa2b18ee362f6b22939d918db378dceb1992b0231f3b80cbbdde4cb2f0a"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
requires_python = ">=3.7"
summary = "Backport of PEP 654 (exception groups)"
groups = ["dev"]
marker = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[[package]]
name = "iniconfig"
version = "2.0.0"
requires_python = ">=3.7"
summary = "brain-dead simple config-ini parsing"
groups = ["dev"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "loguru"
version = "0.7.2"
requires_python = ">=3.5"
summary = "Python logging made (stupidly) simple"
groups = ["default"]
dependencies = [
"aiocontextvars>=0.2.0; python_version < \"3.7\"",
"colorama>=0.3.4; sys_platform == \"win32\"",
"win32-setctime>=1.0.0; sys_platform == \"win32\"",
]
files = [
{file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"},
{file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"},
]
[[package]]
name = "magicoca"
version = "1.0.6"
requires_python = ">=3.10"
summary = "A communication library for Python"
groups = ["default"]
files = [
{file = "magicoca-1.0.6-py3-none-any.whl", hash = "sha256:3c62d6c3e87b3b5a13e0fd5b5fb674374355326684ab4421f2ac8a4c5127455e"},
{file = "magicoca-1.0.6.tar.gz", hash = "sha256:e32ec57d67e64232dc7681c5095166e6a0ee3d27048d7115aa2b09ce4e7d9ff6"},
]
[[package]]
name = "mypy"
version = "1.12.1"
requires_python = ">=3.8"
summary = "Optional static typing for Python"
groups = ["dev"]
dependencies = [
"mypy-extensions>=1.0.0",
"tomli>=1.1.0; python_version < \"3.11\"",
"typing-extensions>=4.6.0",
]
files = [
{file = "mypy-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d7d4371829184e22fda4015278fbfdef0327a4b955a483012bd2d423a788801"},
{file = "mypy-1.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f59f1dfbf497d473201356966e353ef09d4daec48caeacc0254db8ef633a28a5"},
{file = "mypy-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b947097fae68004b8328c55161ac9db7d3566abfef72d9d41b47a021c2fba6b1"},
{file = "mypy-1.12.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96af62050971c5241afb4701c15189ea9507db89ad07794a4ee7b4e092dc0627"},
{file = "mypy-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:d90da248f4c2dba6c44ddcfea94bb361e491962f05f41990ff24dbd09969ce20"},
{file = "mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735"},
{file = "mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66"},
{file = "mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6"},
{file = "mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931"},
{file = "mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811"},
{file = "mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f"},
{file = "mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0"},
{file = "mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042"},
{file = "mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179"},
{file = "mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a"},
{file = "mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc"},
{file = "mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635"},
{file = "mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81"},
{file = "mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4"},
{file = "mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02"},
{file = "mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e"},
{file = "mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
requires_python = ">=3.5"
summary = "Type system extensions for programs checked with the mypy type checker."
groups = ["dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "24.1"
requires_python = ">=3.8"
summary = "Core utilities for Python packages"
groups = ["dev"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
requires_python = ">=3.8"
summary = "Utility library for gitignore style pattern matching of file paths."
groups = ["dev"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "pdm-backend"
version = "2.4.2"
requires_python = ">=3.8"
summary = "The build backend used by PDM that supports latest packaging standards"
groups = ["default"]
dependencies = [
"importlib-metadata>=3.6; python_version < \"3.10\"",
]
files = [
{file = "pdm_backend-2.4.2-py3-none-any.whl", hash = "sha256:8537a3273b19d6448eb07a4a1a92dedc0b60935344a037729ada7be33b5f71ad"},
{file = "pdm_backend-2.4.2.tar.gz", hash = "sha256:1f833e527ae172f34b4b84e2fcf1f65859a2a5ca746e496d8313b3ea6539969f"},
]
[[package]]
name = "platformdirs"
version = "4.3.6"
requires_python = ">=3.8"
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
groups = ["dev"]
files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[[package]]
name = "pluggy"
version = "1.5.0"
requires_python = ">=3.8"
summary = "plugin and hook calling mechanisms for python"
groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[[package]]
name = "pydantic"
version = "2.9.2"
requires_python = ">=3.8"
summary = "Data validation using Python type hints"
groups = ["default"]
dependencies = [
"annotated-types>=0.6.0",
"pydantic-core==2.23.4",
"typing-extensions>=4.12.2; python_version >= \"3.13\"",
"typing-extensions>=4.6.1; python_version < \"3.13\"",
]
files = [
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
]
[[package]]
name = "pydantic-core"
version = "2.23.4"
requires_python = ">=3.8"
summary = "Core functionality for Pydantic validation and serialization"
groups = ["default"]
dependencies = [
"typing-extensions!=4.7.0,>=4.6.0",
]
files = [
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
{file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
{file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
{file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
{file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
{file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
{file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
{file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
{file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
{file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
]
[[package]]
name = "pytest"
version = "8.3.3"
requires_python = ">=3.8"
summary = "pytest: simple powerful testing with Python"
groups = ["dev"]
dependencies = [
"colorama; sys_platform == \"win32\"",
"exceptiongroup>=1.0.0rc8; python_version < \"3.11\"",
"iniconfig",
"packaging",
"pluggy<2,>=1.5",
"tomli>=1; python_version < \"3.11\"",
]
files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
]
[[package]]
name = "pyyaml"
version = "6.0.2"
requires_python = ">=3.8"
summary = "YAML parser and emitter for Python"
groups = ["default"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "toml"
version = "0.10.2"
requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
summary = "Python Library for Tom's Obvious, Minimal Language"
groups = ["default"]
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
[[package]]
name = "tomli"
version = "2.0.2"
requires_python = ">=3.8"
summary = "A lil' TOML parser"
groups = ["dev"]
marker = "python_version < \"3.11\""
files = [
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
requires_python = ">=3.8"
summary = "Backported and Experimental Type Hints for Python 3.8+"
groups = ["default", "dev"]
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "uv"
version = "0.4.24"
requires_python = ">=3.8"
summary = "An extremely fast Python package and project manager, written in Rust."
groups = ["dev"]
files = [
{file = "uv-0.4.24-py3-none-linux_armv6l.whl", hash = "sha256:bbc24b232c5e874741d863c5bec2257533db86f91381f1a101872028a0502ec9"},
{file = "uv-0.4.24-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8d467d4c4746127b2121d6f67686957a2b5431935d26767aa02fa4516694293"},
{file = "uv-0.4.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7d076875e9fa4d8cda44d3e51c9b47efc578db830535c62f25884772bfa265bc"},
{file = "uv-0.4.24-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c40f75df1f2c45a7f67fcc69d80231760f6a017b7c8e889a16e21348651a34d7"},
{file = "uv-0.4.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b459913d8ba6edba2c4b299e87fccfbd7fca4b2e2abe5fd4fa0da56147e19fc8"},
{file = "uv-0.4.24-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e3ce0350e74b3dba6854789dd253faeab2fdf8e84f2671b68573070bb40ff17"},
{file = "uv-0.4.24-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d274f7ddc013697fb52962632bc7e77889a6ec87d2cd12316d218686cfece3d4"},
{file = "uv-0.4.24-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ef6914a7294ac7df5bd15b21652cbe61d1c12a0f29a94d178dce6192f858092"},
{file = "uv-0.4.24-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d8e5f66a8756d4908121cb59189e6f9992fdbd0f9c26a5a30a069b94f8acab3"},
{file = "uv-0.4.24-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a3ea6780e3451c81ce1635656abcd8a47e43f1b0f02542c433b4b6dd459df8e"},
{file = "uv-0.4.24-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:beaff8fdaad3bcd781a8d28b60843b8d1cd2a04229847dc314c1bb7e0bb39ca2"},
{file = "uv-0.4.24-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:70a76cb5b8a459d6f6931becf2b5689599382c2512341d566ce335b8304c44e8"},
{file = "uv-0.4.24-py3-none-musllinux_1_1_i686.whl", hash = "sha256:29c514752873c1be259afd82b975e528ec6783564a306fd24deee0cccb2dc566"},
{file = "uv-0.4.24-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:c03a411f1b86ce7de25d6271d90358ba2d33e87b4922dc5378c4c07674909363"},
{file = "uv-0.4.24-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a03bc4b2ca2236eece97fffb8b5605b7a2248cd8a4b9a9c67955ad08756a1ceb"},
{file = "uv-0.4.24-py3-none-win32.whl", hash = "sha256:a97c347af12deb687c09fed82dc829efd6e5fbc4d76a38e98b2eaa2b065e4cfe"},
{file = "uv-0.4.24-py3-none-win_amd64.whl", hash = "sha256:ec0570f5e2e4dbfd83a89e9a55d5f033050d749f684bd0e7d4c327fd49f89b12"},
{file = "uv-0.4.24.tar.gz", hash = "sha256:f71a00f10cfa15b4f4f0184a67da19f35c48683bba9bb49cebe9c206f1b2bc1f"},
]
[[package]]
name = "watchdog"
version = "5.0.3"
requires_python = ">=3.9"
summary = "Filesystem events monitoring"
groups = ["default"]
files = [
{file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea"},
{file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb"},
{file = "watchdog-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b"},
{file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818"},
{file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490"},
{file = "watchdog-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e"},
{file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8"},
{file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926"},
{file = "watchdog-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e"},
{file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7"},
{file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906"},
{file = "watchdog-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1"},
{file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7"},
{file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8"},
{file = "watchdog-5.0.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91"},
{file = "watchdog-5.0.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c"},
{file = "watchdog-5.0.3-py3-none-manylinux2014_i686.whl", hash = "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c"},
{file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221"},
{file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05"},
{file = "watchdog-5.0.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97"},
{file = "watchdog-5.0.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7"},
{file = "watchdog-5.0.3-py3-none-win32.whl", hash = "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49"},
{file = "watchdog-5.0.3-py3-none-win_amd64.whl", hash = "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9"},
{file = "watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45"},
{file = "watchdog-5.0.3.tar.gz", hash = "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176"},
]
[[package]]
name = "win32-setctime"
version = "1.1.0"
requires_python = ">=3.5"
summary = "A small Python utility to set file creation time on Windows"
groups = ["default"]
marker = "sys_platform == \"win32\""
files = [
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
{file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
]

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.6",
"croterline>=1.0.9",
]
[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
@ -6,9 +7,10 @@ httpx>=0.27.0
nonebot-plugin-htmlrender>=0.1.0
nonebot2[fastapi,httpx,websockets]>=2.3.3
nonebot-adapter-onebot>=2.4.3
nonebot-plugin-alconna>=0.46.3
nonebot-plugin-alconna>=0.53.1
nonebot_plugin_apscheduler>=0.4.0
nonebot-adapter-satori>=0.11.5
mysql-connector-python>=9.1.0
pyppeteer>=2.0.0
markdown>=3.3.6
zhDateTime>=1.0.3
@ -25,4 +27,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.7
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,37 @@
import os.path
from pathlib import Path
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):
import nonebot
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,109 @@
import asyncio
import aiohttp
from nonebot import logger, on_message, Bot
from nonebot.internal.adapter import Event
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
from .adapter_ctx import Context
from .config import plugin_config, Config, NOTICE, MESSAGE
from .gotify import Message, msg_chan, fetch_msg
__plugin_meta__ = PluginMetadata(
name="Gotify",
description="使用Gotify API发送消息",
usage="后台服务插件,无需用户交互",
)
# on plugin load
driver = get_driver()
async def push_thread():
async with aiohttp.ClientSession() as session:
while True:
msg = await fetch_msg()
try:
logger.debug(f"Pushing message: {msg}")
async with session.post(
url=plugin_config.gotify_url + "/message",
params={"token": plugin_config.gotify_token},
data={
"title": msg.title,
"message": msg.message,
"priority": msg.priority,
},
) as resp:
if resp.status != 200:
logger.error(
f"Push message to server failed: {await resp.text()}"
)
else:
logger.info(f"Push message to server success: {msg}")
except Exception as e:
logger.error(f"Push message to server failed: {e}")
@driver.on_startup
async def start_push_thread():
asyncio.create_task(push_thread())
logger.info(
f"Gotify plugin loaded, server: {plugin_config.gotify_url}, token: {plugin_config.gotify_token}"
)
if MESSAGE in plugin_config.gotify_includes:
@on_message(block=False, priority=100).handle()
async def _(event: Event):
ctx = Context(
user_id=event.get_user_id(),
nickname="",
message=event.get_plaintext(),
message_type=event.get_type(),
)
ctx.handle(event)
msg_chan << Message(
title=plugin_config.gotify_title.format(**ctx.model_dump()),
message=plugin_config.gotify_message.format(**ctx.model_dump()),
)
if NOTICE in plugin_config.gotify_includes:
@driver.on_startup
async def startup():
if NOTICE in plugin_config.gotify_includes:
msg_chan << Message(
title=plugin_config.gotify_nickname,
message="Bot started",
priority=plugin_config.gotify_priority,
)
@driver.on_shutdown
async def shutdown():
if NOTICE in plugin_config.gotify_includes:
msg_chan << Message(
title=plugin_config.gotify_nickname,
message="Bot stopped",
priority=plugin_config.gotify_priority,
)
@driver.on_bot_connect
async def bot_connect(bot: Bot):
if NOTICE in plugin_config.gotify_includes:
msg_chan << Message(
title=plugin_config.gotify_nickname,
message=f"Bot connected: {bot.self_id}",
priority=plugin_config.gotify_priority,
)
@driver.on_bot_disconnect
async def bot_disconnect(bot: Bot):
if NOTICE in plugin_config.gotify_includes:
msg_chan << Message(
title=plugin_config.gotify_nickname,
message=f"Bot disconnected: {bot.self_id}",
priority=plugin_config.gotify_priority,
)

View File

@ -0,0 +1,12 @@
from pydantic import BaseModel
class Context(BaseModel):
user_id: str
nickname: str
message: str
message_type: str | None = None
def handle(self, event):
pass

View File

@ -0,0 +1,22 @@
from nonebot import get_plugin_config
from pydantic import BaseModel
NOTICE = "notice"
MESSAGE = "message"
class Config(BaseModel):
# required fields
gotify_token: str
# optional fields
gotify_url: str = "http://127.0.0.1:40266"
gotify_priority: int = 5
gotify_nickname: str = "NoneBot"
gotify_title: str = "{message_type}: {nickname}({user_id})"
gotify_message: str = "{message}"
gotify_includes: list[str, ...] = [NOTICE, MESSAGE]
plugin_config = get_plugin_config(Config)
if plugin_config.gotify_url.endswith("/"):
plugin_config.gotify_url = plugin_config.gotify_url[:-1]

View File

@ -0,0 +1,20 @@
import asyncio
from asyncio import Future
from typing import Any
from magicoca import Chan
from pydantic import BaseModel
from .config import plugin_config
msg_chan = Chan["Message"]()
class Message(BaseModel):
title: str
message: str
priority: int = plugin_config.gotify_priority
def fetch_msg() -> Future[Any]:
return asyncio.get_event_loop().run_in_executor(func=msg_chan.recv, executor=None)

View File

@ -0,0 +1,33 @@
# -*- 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.log import logger
from nonebot import on_message
__plugin_meta__ = PluginMetadata(
name="轻雪push",
description="把消息事件传递给轻雪框架进行处理",
usage="用户无需使用",
)
ctx = get_ctx()
@on_message(block=False, priority=100).handle()
async def _(event: MessageEvent):
logger.debug("Pushing message to Liteyuki")
ctx.sub_chan << event.raw_message
logger.debug("Pushed message to Liteyuki")

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,298 @@
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 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 import reload # type: ignore
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_function import get_function # type: ignore
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.html_tool import md_to_pic
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from .api import update_liteyuki # type: ignore
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,36 @@
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.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()
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,40 @@
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 git import Repo
major, minor, patch = map(int, __VERSION__.split("."))
__VERSION_I__ = major * 10000 + minor * 100 + patch
def init():
"""
初始化
Returns:
"""
# 检测python版本是否高于3.10
if sys.version_info < (3, 10):
nonebot.logger.error("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

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