mirror of
https://github.com/TriM-Organization/LiteyukiBot-TriM.git
synced 2024-12-01 01:34:45 +08:00
⚡合并轻雪f22f4d2更新,增加人性化语言内容。
This commit is contained in:
parent
0cb38d8ce7
commit
da08f8d085
2
.gitignore
vendored
2
.gitignore
vendored
@ -25,7 +25,7 @@ src/plugins/trimo_plugin_msctconverter/MusicPreview/assets/wav
|
|||||||
|
|
||||||
# vuepress
|
# vuepress
|
||||||
.github
|
.github
|
||||||
pyproject.toml
|
# pyproject.toml
|
||||||
|
|
||||||
test.py
|
test.py
|
||||||
line_count.py
|
line_count.py
|
||||||
|
12
liteyuki/__init__.py
Normal file
12
liteyuki/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from liteyuki.bot import (
|
||||||
|
LiteyukiBot,
|
||||||
|
get_bot
|
||||||
|
)
|
||||||
|
|
||||||
|
# def get_bot_instance() -> LiteyukiBot | None:
|
||||||
|
# """
|
||||||
|
# 获取轻雪实例
|
||||||
|
# Returns:
|
||||||
|
# LiteyukiBot: 当前的轻雪实例
|
||||||
|
# """
|
||||||
|
# return _BOT_INSTANCE
|
270
liteyuki/bot/__init__.py
Normal file
270
liteyuki/bot/__init__.py
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import asyncio
|
||||||
|
import multiprocessing
|
||||||
|
from typing import Any, Coroutine, Optional
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
|
||||||
|
from liteyuki.plugin.load import load_plugin, load_plugins
|
||||||
|
from src.utils import (
|
||||||
|
adapter_manager,
|
||||||
|
driver_manager,
|
||||||
|
)
|
||||||
|
from src.utils.base.log import logger
|
||||||
|
from liteyuki.bot.lifespan import (
|
||||||
|
Lifespan,
|
||||||
|
LIFESPAN_FUNC,
|
||||||
|
)
|
||||||
|
from liteyuki.core.spawn_process import nb_run, ProcessingManager
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LiteyukiBot",
|
||||||
|
"get_bot"
|
||||||
|
]
|
||||||
|
|
||||||
|
_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
||||||
|
|
||||||
|
|
||||||
|
class LiteyukiBot:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
global _BOT_INSTANCE
|
||||||
|
_BOT_INSTANCE = self # 引用
|
||||||
|
self.running = False
|
||||||
|
self.config: dict[str, Any] = kwargs
|
||||||
|
self.lifespan: Lifespan = Lifespan()
|
||||||
|
self.init(**self.config) # 初始化
|
||||||
|
|
||||||
|
print("\033[34m" + r"""
|
||||||
|
▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ██ ▅▅▅▅▅▅▅▅▅▅██
|
||||||
|
▛ ██ ██ ▛ ██ ███ ██ ██
|
||||||
|
██ ██ ███████████████ ██ ████████▅ ██
|
||||||
|
███████████████ ██ ███ ██ ██
|
||||||
|
██ ██ ▅██████████████▛ ██ ████████████
|
||||||
|
██ ██ ███ ███
|
||||||
|
████████████████ ██▅ ███ ██ ▅▅▅▅▅▅▅▅▅▅▅██
|
||||||
|
███ █ ▜███████ ██ ███ ██ ██ ██ ██
|
||||||
|
███ ███ █████▛ ██ ██ ██ ██ ██
|
||||||
|
███ ██ ███ █ ██ ██ ██ ██ ██
|
||||||
|
███ █████ ██████ ███ ██████████████
|
||||||
|
""" + "\033[0m")
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
|
||||||
|
if _MAIN_PROCESS:
|
||||||
|
load_plugins("liteyuki/plugins")
|
||||||
|
asyncio.run(self.lifespan.before_start())
|
||||||
|
self._run_nb_in_spawn_process(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
# 子进程启动
|
||||||
|
|
||||||
|
driver_manager.init(config=self.config)
|
||||||
|
adapter_manager.init(self.config)
|
||||||
|
adapter_manager.register()
|
||||||
|
nonebot.load_plugin("src.liteyuki_main")
|
||||||
|
|
||||||
|
def _run_nb_in_spawn_process(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
在新的进程中运行nonebot.run方法
|
||||||
|
Args:
|
||||||
|
*args:
|
||||||
|
**kwargs:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
|
||||||
|
timeout_limit: int = 20
|
||||||
|
should_exit = False
|
||||||
|
|
||||||
|
while not should_exit:
|
||||||
|
ctx = multiprocessing.get_context("spawn")
|
||||||
|
event = ctx.Event()
|
||||||
|
ProcessingManager.event = event
|
||||||
|
process = ctx.Process(
|
||||||
|
target=nb_run,
|
||||||
|
args=(event,) + args,
|
||||||
|
kwargs=kwargs,
|
||||||
|
)
|
||||||
|
process.start() # 启动进程
|
||||||
|
|
||||||
|
asyncio.run(self.lifespan.after_start())
|
||||||
|
|
||||||
|
while not should_exit:
|
||||||
|
if ProcessingManager.event.wait(1):
|
||||||
|
logger.info("接收到重启活动信息")
|
||||||
|
process.terminate()
|
||||||
|
process.join(timeout_limit)
|
||||||
|
if process.is_alive():
|
||||||
|
logger.warning(
|
||||||
|
f"进程 {process.pid} 在 {timeout_limit} 秒后依旧存在,强制清灭。"
|
||||||
|
)
|
||||||
|
process.kill()
|
||||||
|
break
|
||||||
|
elif process.is_alive():
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
should_exit = True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _run_coroutine(*coro: Coroutine):
|
||||||
|
"""
|
||||||
|
运行协程
|
||||||
|
Args:
|
||||||
|
coro:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# 检测是否有现有的事件循环
|
||||||
|
new_loop = False
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
new_loop = True
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
if new_loop:
|
||||||
|
for c in coro:
|
||||||
|
loop.run_until_complete(c)
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
else:
|
||||||
|
for c in coro:
|
||||||
|
loop.create_task(c)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> int:
|
||||||
|
"""
|
||||||
|
获取轻雪状态
|
||||||
|
Returns:
|
||||||
|
int: 0:未启动 1:运行中
|
||||||
|
"""
|
||||||
|
return 1 if self.running else 0
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
"""
|
||||||
|
停止轻雪
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.info("正在停止灵温活动…")
|
||||||
|
|
||||||
|
logger.debug("正在启动 before_restart 的函数…")
|
||||||
|
self._run_coroutine(self.lifespan.before_restart())
|
||||||
|
logger.debug("正在启动 before_shutdown 的函数…")
|
||||||
|
self._run_coroutine(self.lifespan.before_shutdown())
|
||||||
|
|
||||||
|
ProcessingManager.restart()
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def init(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
初始化轻雪, 自动调用
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.init_config()
|
||||||
|
self.init_logger()
|
||||||
|
if not _MAIN_PROCESS:
|
||||||
|
nonebot.init(**kwargs)
|
||||||
|
asyncio.run(self.lifespan.after_nonebot_init())
|
||||||
|
|
||||||
|
def init_logger(self):
|
||||||
|
from src.utils.base.log import init_log
|
||||||
|
init_log()
|
||||||
|
|
||||||
|
def init_config(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_adapters(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_before_start(self, func: LIFESPAN_FUNC):
|
||||||
|
"""
|
||||||
|
注册启动前的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.lifespan.on_before_start(func)
|
||||||
|
|
||||||
|
def on_after_start(self, func: LIFESPAN_FUNC):
|
||||||
|
"""
|
||||||
|
注册启动后的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.lifespan.on_after_start(func)
|
||||||
|
|
||||||
|
def on_before_shutdown(self, func: LIFESPAN_FUNC):
|
||||||
|
"""
|
||||||
|
注册停止前的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.lifespan.on_before_shutdown(func)
|
||||||
|
|
||||||
|
def on_after_shutdown(self, func: LIFESPAN_FUNC):
|
||||||
|
"""
|
||||||
|
注册停止后的函数:未实现
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.lifespan.on_after_shutdown(func)
|
||||||
|
|
||||||
|
def on_before_restart(self, func: LIFESPAN_FUNC):
|
||||||
|
"""
|
||||||
|
注册重启前的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.lifespan.on_before_restart(func)
|
||||||
|
|
||||||
|
def on_after_restart(self, func: LIFESPAN_FUNC):
|
||||||
|
"""
|
||||||
|
注册重启后的函数:未实现
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.lifespan.on_after_restart(func)
|
||||||
|
|
||||||
|
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
|
||||||
|
"""
|
||||||
|
注册nonebot初始化后的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.lifespan.on_after_nonebot_init(func)
|
||||||
|
|
||||||
|
|
||||||
|
_BOT_INSTANCE: Optional[LiteyukiBot] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot() -> Optional[LiteyukiBot]:
|
||||||
|
"""
|
||||||
|
获取轻雪实例
|
||||||
|
Returns:
|
||||||
|
LiteyukiBot: 当前的轻雪实例
|
||||||
|
"""
|
||||||
|
return _BOT_INSTANCE
|
181
liteyuki/bot/lifespan.py
Normal file
181
liteyuki/bot/lifespan.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/7/23 下午8:24
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : lifespan.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from typing import Any, Awaitable, Callable, TypeAlias
|
||||||
|
|
||||||
|
from liteyuki.utils import is_coroutine_callable
|
||||||
|
|
||||||
|
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||||
|
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||||
|
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC
|
||||||
|
|
||||||
|
|
||||||
|
class Lifespan:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
轻雪生命周期管理,启动、停止、重启
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.life_flag: int = 0 # 0: 启动前,1: 启动后,2: 停止前,3: 停止后
|
||||||
|
|
||||||
|
self._before_start_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
self._after_start_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
|
self._before_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
|
self._before_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
|
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _run_funcs(funcs: list[LIFESPAN_FUNC]) -> None:
|
||||||
|
"""
|
||||||
|
运行函数
|
||||||
|
Args:
|
||||||
|
funcs:
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
for func in funcs:
|
||||||
|
if is_coroutine_callable(func):
|
||||||
|
await func()
|
||||||
|
else:
|
||||||
|
func()
|
||||||
|
|
||||||
|
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
注册启动时的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
Returns:
|
||||||
|
LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
self._before_start_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
注册启动时的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
Returns:
|
||||||
|
LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
self._after_start_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def on_before_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
注册停止前的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
Returns:
|
||||||
|
LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
self._before_shutdown_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
注册停止后的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LIFESPAN_FUNC:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._after_shutdown_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def on_before_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
注册重启时的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
Returns:
|
||||||
|
LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
self._before_restart_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
注册重启后的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
Returns:
|
||||||
|
LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
self._after_restart_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def on_after_nonebot_init(self, func):
|
||||||
|
"""
|
||||||
|
注册 NoneBot 初始化后的函数
|
||||||
|
Args:
|
||||||
|
func:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._after_nonebot_init_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
async def before_start(self) -> None:
|
||||||
|
"""
|
||||||
|
启动前
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
await self._run_funcs(self._before_start_funcs)
|
||||||
|
|
||||||
|
async def after_start(self) -> None:
|
||||||
|
"""
|
||||||
|
启动后
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
await self._run_funcs(self._after_start_funcs)
|
||||||
|
|
||||||
|
async def before_shutdown(self) -> None:
|
||||||
|
"""
|
||||||
|
停止前
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
await self._run_funcs(self._before_shutdown_funcs)
|
||||||
|
|
||||||
|
async def after_shutdown(self) -> None:
|
||||||
|
"""
|
||||||
|
停止后
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
await self._run_funcs(self._after_shutdown_funcs)
|
||||||
|
|
||||||
|
async def before_restart(self) -> None:
|
||||||
|
"""
|
||||||
|
重启前
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
await self._run_funcs(self._before_restart_funcs)
|
||||||
|
|
||||||
|
async def after_restart(self) -> None:
|
||||||
|
"""
|
||||||
|
重启后
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
await self._run_funcs(self._after_restart_funcs)
|
||||||
|
|
||||||
|
async def after_nonebot_init(self) -> None:
|
||||||
|
"""
|
||||||
|
NoneBot 初始化后
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
await self._run_funcs(self._after_nonebot_init_funcs)
|
3
liteyuki/core/__init__.py
Normal file
3
liteyuki/core/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .spawn_process import *
|
||||||
|
|
||||||
|
|
37
liteyuki/core/spawn_process.py
Normal file
37
liteyuki/core/spawn_process.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import threading
|
||||||
|
from multiprocessing import get_context, Event
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
from nonebot import logger
|
||||||
|
|
||||||
|
from liteyuki.plugin.load import load_plugins
|
||||||
|
|
||||||
|
timeout_limit: int = 20
|
||||||
|
__all__ = [
|
||||||
|
"ProcessingManager",
|
||||||
|
"nb_run",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessingManager:
|
||||||
|
event: Event = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def restart(cls, delay: int = 0):
|
||||||
|
"""
|
||||||
|
发送终止信号
|
||||||
|
Args:
|
||||||
|
delay: 延迟时间,默认为0,单位秒
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
if cls.event is None:
|
||||||
|
raise RuntimeError("ProcessingManager 未初始化。")
|
||||||
|
if delay > 0:
|
||||||
|
threading.Timer(delay, function=cls.event.set).start()
|
||||||
|
return
|
||||||
|
cls.event.set()
|
||||||
|
|
||||||
|
|
||||||
|
def nb_run(event, *args, **kwargs):
|
||||||
|
ProcessingManager.event = event
|
||||||
|
nonebot.run(*args, **kwargs)
|
17
liteyuki/plugin/__init__.py
Normal file
17
liteyuki/plugin/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from liteyuki.plugin.model import Plugin, PluginMetadata
|
||||||
|
from liteyuki.plugin.load import load_plugin, _plugins
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PluginMetadata",
|
||||||
|
"Plugin",
|
||||||
|
"load_plugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_loaded_plugins() -> dict[str, Plugin]:
|
||||||
|
"""
|
||||||
|
获取已加载的插件
|
||||||
|
Returns:
|
||||||
|
dict[str, Plugin]: 插件字典
|
||||||
|
"""
|
||||||
|
return _plugins
|
78
liteyuki/plugin/load.py
Normal file
78
liteyuki/plugin/load.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/7/23 下午11:59
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : load.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from nonebot import logger
|
||||||
|
|
||||||
|
from liteyuki.plugin.model import Plugin, PluginMetadata
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from liteyuki.utils import path_to_module_name
|
||||||
|
|
||||||
|
_plugins: dict[str, Plugin] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin(module_path: str | Path) -> Optional[Plugin]:
|
||||||
|
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
module_path: 插件名称 `path.to.your.plugin`
|
||||||
|
或插件路径 `pathlib.Path(path/to/your/plugin)`
|
||||||
|
"""
|
||||||
|
module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path
|
||||||
|
try:
|
||||||
|
module = import_module(module_path)
|
||||||
|
_plugins[module.__name__] = Plugin(
|
||||||
|
name=module.__name__,
|
||||||
|
module=module,
|
||||||
|
module_name=module_path,
|
||||||
|
metadata=module.__dict__.get("__plugin_metadata__", None)
|
||||||
|
)
|
||||||
|
logger.opt(colors=True).success(
|
||||||
|
f'成功加载 轻雪插件 "<y>{module.__name__.split(".")[-1]}</y>"'
|
||||||
|
)
|
||||||
|
return _plugins[module.__name__]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.opt(colors=True).success(
|
||||||
|
f'未能加载 轻雪插件 "<r>{module_path}</r>"'
|
||||||
|
)
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
||||||
|
"""导入文件夹下多个插件
|
||||||
|
|
||||||
|
参数:
|
||||||
|
plugin_dir: 文件夹路径
|
||||||
|
"""
|
||||||
|
plugins = set()
|
||||||
|
for dir_path in plugin_dir:
|
||||||
|
# 遍历每一个文件夹下的py文件和包含__init__.py的文件夹,不递归
|
||||||
|
for f in os.listdir(dir_path):
|
||||||
|
path = Path(os.path.join(dir_path, f))
|
||||||
|
|
||||||
|
module_name = None
|
||||||
|
if os.path.isfile(path) and f.endswith('.py') and f != '__init__.py':
|
||||||
|
module_name = f"{path_to_module_name(Path(dir_path))}.{f[:-3]}"
|
||||||
|
|
||||||
|
elif os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')):
|
||||||
|
module_name = path_to_module_name(path)
|
||||||
|
|
||||||
|
if module_name:
|
||||||
|
load_plugin(module_name)
|
||||||
|
if _plugins.get(module_name):
|
||||||
|
plugins.add(_plugins[module_name])
|
||||||
|
return plugins
|
10
liteyuki/plugin/manager.py
Normal file
10
liteyuki/plugin/manager.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/7/23 下午11:59
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : manager.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
45
liteyuki/plugin/model.py
Normal file
45
liteyuki/plugin/model.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/7/24 上午12:02
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : model.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PluginMetadata(BaseModel):
|
||||||
|
"""
|
||||||
|
轻雪插件元数据,由插件编写者提供
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
usage: str = ""
|
||||||
|
type: str = ""
|
||||||
|
homepage: str = ""
|
||||||
|
running_in_main: bool = True # 是否在主进程运行
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(BaseModel):
|
||||||
|
"""
|
||||||
|
存储插件信息
|
||||||
|
"""
|
||||||
|
model_config = {
|
||||||
|
'arbitrary_types_allowed': True
|
||||||
|
}
|
||||||
|
name: str
|
||||||
|
"""插件名称 例如plugin_loader"""
|
||||||
|
module: ModuleType
|
||||||
|
"""插件模块对象"""
|
||||||
|
module_name: str
|
||||||
|
"""点分割模块路径 例如a.b.c"""
|
||||||
|
metadata: Optional[PluginMetadata] = None
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.module_name)
|
41
liteyuki/plugins/plugin_loader/__init__.py
Normal file
41
liteyuki/plugins/plugin_loader/__init__.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import multiprocessing
|
||||||
|
import time
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
from nonebot import get_driver
|
||||||
|
|
||||||
|
from liteyuki.plugin import PluginMetadata
|
||||||
|
from liteyuki import get_bot
|
||||||
|
|
||||||
|
__plugin_metadata__ = PluginMetadata(
|
||||||
|
name="plugin_loader",
|
||||||
|
description="轻雪插件加载器",
|
||||||
|
usage="",
|
||||||
|
type="",
|
||||||
|
homepage=""
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.utils import TempConfig, common_db
|
||||||
|
|
||||||
|
liteyuki = get_bot()
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki.on_after_start
|
||||||
|
def _():
|
||||||
|
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) # 更新数据
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki.on_before_start
|
||||||
|
def _():
|
||||||
|
print("灵温正在启动")
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki.on_after_nonebot_init
|
||||||
|
async def _():
|
||||||
|
print("NoneBot初始化完成")
|
||||||
|
nonebot.load_plugin("src.liteyuki_main")
|
10
liteyuki/plugins/plugin_loader/data_source.py
Normal file
10
liteyuki/plugins/plugin_loader/data_source.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/7/23 下午11:21
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : data_source.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
38
liteyuki/utils.py
Normal file
38
liteyuki/utils.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
一些常用的工具类,部分来源于 nonebot 并遵循其许可进行修改
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
|
||||||
|
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
||||||
|
"""
|
||||||
|
判断是否为协程可调用对象
|
||||||
|
Args:
|
||||||
|
call: 可调用对象
|
||||||
|
Returns:
|
||||||
|
bool: 是否为协程可调用对象
|
||||||
|
"""
|
||||||
|
if inspect.isroutine(call):
|
||||||
|
return inspect.iscoroutinefunction(call)
|
||||||
|
if inspect.isclass(call):
|
||||||
|
return False
|
||||||
|
func_ = getattr(call, "__call__", None)
|
||||||
|
return inspect.iscoroutinefunction(func_)
|
||||||
|
|
||||||
|
|
||||||
|
def path_to_module_name(path: Path) -> str:
|
||||||
|
"""
|
||||||
|
转换路径为模块名
|
||||||
|
Args:
|
||||||
|
path: 路径a/b/c/d -> a.b.c.d
|
||||||
|
Returns:
|
||||||
|
str: 模块名
|
||||||
|
"""
|
||||||
|
rel_path = path.resolve().relative_to(Path.cwd().resolve())
|
||||||
|
if rel_path.stem == "__init__":
|
||||||
|
return ".".join(rel_path.parts[:-1])
|
||||||
|
else:
|
||||||
|
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
32
main.py
32
main.py
@ -1,28 +1,6 @@
|
|||||||
import nonebot
|
from liteyuki import LiteyukiBot
|
||||||
from src.utils import adapter_manager, driver_manager, init
|
from src.utils import load_from_yaml
|
||||||
from src.utils.base.config import load_from_yaml
|
|
||||||
from src.utils.base.data_manager import StoredConfig, common_db
|
|
||||||
from src.utils.base.ly_api import liteyuki_api
|
|
||||||
|
|
||||||
if __name__ == "__mp_main__":
|
if __name__ in ("__main__", "__mp_main__"):
|
||||||
# Start as multiprocessing
|
bot = LiteyukiBot(**load_from_yaml("config.yml"))
|
||||||
init()
|
bot.run()
|
||||||
store_config: dict = common_db.where_one(StoredConfig(), default=StoredConfig()).config
|
|
||||||
static_config = load_from_yaml("config.yml")
|
|
||||||
store_config.update(static_config)
|
|
||||||
driver_manager.init(config=store_config)
|
|
||||||
adapter_manager.init(store_config)
|
|
||||||
nonebot.init(**store_config)
|
|
||||||
adapter_manager.register()
|
|
||||||
try:
|
|
||||||
nonebot.load_plugin("src.liteyuki_main")
|
|
||||||
nonebot.load_from_toml("pyproject.toml")
|
|
||||||
except BaseException as e:
|
|
||||||
if not isinstance(e, KeyboardInterrupt):
|
|
||||||
nonebot.logger.error(f"An error occurred: {e}, Bug will be reported automatically.")
|
|
||||||
liteyuki_api.bug_report(str(e.__repr__()))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Start as __main__
|
|
||||||
from src.utils.base.reloader import Reloader
|
|
||||||
nonebot.run()
|
|
||||||
|
58
pyproject.toml
Normal file
58
pyproject.toml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# PEP 621 project metadata
|
||||||
|
# See https://www.python.org/dev/peps/pep-0621/
|
||||||
|
# This file is for project use, but don`t use with nb-cli
|
||||||
|
# 此文件为项目所用,请不要和nb-cli一起使用以防被修改
|
||||||
|
[tool.poetry]
|
||||||
|
name = "ryoun-trim"
|
||||||
|
version = "0"
|
||||||
|
description = "based on liteyuki6"
|
||||||
|
authors = ["金羿Eilles"]
|
||||||
|
license = "MIT & LSO"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
aiofiles = "~23.2.1"
|
||||||
|
aiohttp = "~3.9.3"
|
||||||
|
aiosqlite3 = "~0.3.0"
|
||||||
|
colored = "~2.2.4"
|
||||||
|
fastapi = "~0.110.0"
|
||||||
|
GitPython = "~3.1.42"
|
||||||
|
httpx = "~0.27.0"
|
||||||
|
importlib_metadata = "~7.0.2"
|
||||||
|
jieba = "~0.42.1"
|
||||||
|
loguru = "~0.7.2"
|
||||||
|
nb-cli = "~1.4.1"
|
||||||
|
nonebot-adapter-onebot = "~2.4.3"
|
||||||
|
nonebot-adapter-satori = "~0.11.5"
|
||||||
|
nonebot-plugin-alconna = "~0.46.3"
|
||||||
|
nonebot-plugin-apscheduler = "~0.4.0"
|
||||||
|
nonebot-plugin-htmlrender = "~0.3.1"
|
||||||
|
nonebot2 = { version = "~2.3.0", extras = ["fastapi", "httpx", "websockets"] }
|
||||||
|
numpy = "<2.0.0"
|
||||||
|
packaging = "~23.1"
|
||||||
|
psutil = "~5.9.8"
|
||||||
|
py-cpuinfo = "~9.0.0"
|
||||||
|
pydantic = "~2.7.0"
|
||||||
|
Pygments = "~2.17.2"
|
||||||
|
python-dotenv = "~1.0.1"
|
||||||
|
pytest = "~8.3.1"
|
||||||
|
pytz = "~2024.1"
|
||||||
|
PyYAML = "~6.0.1"
|
||||||
|
requests = "~2.31.0"
|
||||||
|
starlette = "~0.36.3"
|
||||||
|
watchdog = "~4.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
[[tool.poetry.source]]
|
||||||
|
name = "tuna"
|
||||||
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
|
||||||
|
[tool.nonebot]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
homepage = "https://bot.liteyuki.icu"
|
||||||
|
repository = "https://gitee.com/TriM-Organization/LiteyukiBot-TriM"
|
||||||
|
documentation = "https://bot.liteyuki.icu"
|
||||||
|
|
@ -23,9 +23,9 @@ loguru~=0.7.2
|
|||||||
importlib_metadata~=7.0.2
|
importlib_metadata~=7.0.2
|
||||||
requests~=2.31.0
|
requests~=2.31.0
|
||||||
watchdog~=4.0.0
|
watchdog~=4.0.0
|
||||||
pillow~=10.2.0
|
pillow~=10.0.0
|
||||||
jieba~=0.42.1
|
jieba~=0.42.1
|
||||||
pip~=23.2.1
|
aiosqlite3~=0.3.0
|
||||||
fastapi~=0.110.0
|
fastapi~=0.110.0
|
||||||
python-dotenv~=1.0.1
|
python-dotenv~=1.0.1
|
||||||
nonebot_plugin_session
|
nonebot_plugin_session
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import abc
|
|
||||||
|
|
||||||
|
|
||||||
class Bot(abc.ABC):
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
@ -11,24 +11,15 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
usage="",
|
usage="",
|
||||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||||
extra={
|
extra={
|
||||||
"liteyuki" : True,
|
"liteyuki": True,
|
||||||
"toggleable": False,
|
"toggleable": False,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utils.base.language import Language, get_default_lang_code
|
from ..utils.base.language import Language, get_default_lang_code
|
||||||
|
|
||||||
print("\033[34m" + r"""
|
|
||||||
__ ______ ________ ________ __ __ __ __ __ __ ______
|
|
||||||
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
|
|
||||||
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
|
|
||||||
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
|
|
||||||
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
|
|
||||||
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
|
|
||||||
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
|
|
||||||
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
|
|
||||||
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
|
||||||
""" + "\033[0m")
|
|
||||||
|
|
||||||
sys_lang = 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")))
|
nonebot.logger.info(
|
||||||
|
sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))
|
||||||
|
)
|
||||||
|
@ -16,9 +16,12 @@ from src.utils.base.data_manager import StoredConfig, TempConfig, common_db
|
|||||||
from src.utils.base.language import get_user_lang
|
from src.utils.base.language import get_user_lang
|
||||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||||
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
|
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
|
||||||
from src.utils.base.reloader import Reloader
|
|
||||||
|
# from src.liteyuki.core import Reloader
|
||||||
from src.utils import event as event_utils, satori_utils
|
from src.utils import event as event_utils, satori_utils
|
||||||
|
from liteyuki.core import ProcessingManager
|
||||||
from .api import update_liteyuki
|
from .api import update_liteyuki
|
||||||
|
from liteyuki.bot import get_bot
|
||||||
from ..utils.base.ly_function import get_function
|
from ..utils.base.ly_function import get_function
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
require("nonebot_plugin_alconna")
|
||||||
@ -115,7 +118,9 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
|
|||||||
)
|
)
|
||||||
|
|
||||||
common_db.save(temp_data)
|
common_db.save(temp_data)
|
||||||
Reloader.reload(0)
|
# Reloader.reload(0)
|
||||||
|
bot = get_bot()
|
||||||
|
bot.restart()
|
||||||
|
|
||||||
|
|
||||||
@on_alconna(
|
@on_alconna(
|
||||||
@ -314,7 +319,6 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
|||||||
result = str(e)
|
result = str(e)
|
||||||
|
|
||||||
args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items())
|
args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items())
|
||||||
print(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
|
|
||||||
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
|
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
|
||||||
|
|
||||||
|
|
||||||
@ -376,12 +380,19 @@ async def test_for_md_image(bot: T_Bot, api: str, data: dict):
|
|||||||
|
|
||||||
@driver.on_startup
|
@driver.on_startup
|
||||||
async def on_startup():
|
async def on_startup():
|
||||||
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
|
# temp_data = common_db.where_one(TempConfig(), default=TempConfig())
|
||||||
# 储存重启信息
|
# # 储存重启信息
|
||||||
if temp_data.data.get("reload", False):
|
# if temp_data.data.get("reload", False):
|
||||||
delta_time = time.time() - temp_data.data.get("reload_time", 0)
|
# delta_time = time.time() - temp_data.data.get("reload_time", 0)
|
||||||
temp_data.data["delta_time"] = delta_time
|
# temp_data.data["delta_time"] = delta_time
|
||||||
common_db.save(temp_data) # 更新数据
|
# common_db.save(temp_data) # 更新数据
|
||||||
|
"""
|
||||||
|
该部分迁移至轻雪生命周期
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@driver.on_shutdown
|
@driver.on_shutdown
|
||||||
@ -407,7 +418,7 @@ async def _(bot: T_Bot):
|
|||||||
if isinstance(bot, satori.Bot):
|
if isinstance(bot, satori.Bot):
|
||||||
await bot.send_message(
|
await bot.send_message(
|
||||||
channel_id=reload_session_id,
|
channel_id=reload_session_id,
|
||||||
message="灵温 重载耗时 %.2f 秒" % delta_time,
|
message="轻雪核心 重载耗时 {:.2f} 秒\n*此数据仅作参考,具体计时请以实际为准\n灵温 预计体感重载耗时 {:.2f} 秒".format(delta_time,time.time() - temp_data.data.get("reload_time", 0)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bot.call_api(
|
await bot.call_api(
|
||||||
@ -415,7 +426,7 @@ async def _(bot: T_Bot):
|
|||||||
message_type=reload_session_type,
|
message_type=reload_session_type,
|
||||||
user_id=reload_session_id,
|
user_id=reload_session_id,
|
||||||
group_id=reload_session_id,
|
group_id=reload_session_id,
|
||||||
message="灵温 重载耗时 %.2f 秒" % delta_time,
|
message="轻雪核心 重载耗时 {:.2f} 秒\n*此数据仅作参考,具体计时请以实际为准\n灵温 预计体感重载耗时 {:.2f} 秒".format(delta_time,time.time() - temp_data.data.get("reload_time", 0)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -426,9 +437,9 @@ async def every_day_update():
|
|||||||
result, logs = update_liteyuki()
|
result, logs = update_liteyuki()
|
||||||
pip.main(["install", "-r", "requirements.txt"])
|
pip.main(["install", "-r", "requirements.txt"])
|
||||||
if result:
|
if result:
|
||||||
await broadcast_to_superusers(f"灵温已更新: ```\n{logs}\n```")
|
await broadcast_to_superusers(f"灵温已更新:```\n{logs}\n```")
|
||||||
nonebot.logger.info(f"灵温已更新: {logs}")
|
nonebot.logger.info(f"灵温已更新:{logs}")
|
||||||
Reloader.reload(5)
|
ProcessingManager.restart(3)
|
||||||
else:
|
else:
|
||||||
nonebot.logger.info(logs)
|
nonebot.logger.info(logs)
|
||||||
|
|
||||||
|
@ -2,28 +2,28 @@ import nonebot
|
|||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
from liteyuki.bot import get_bot
|
||||||
from src.utils.base.config import get_config
|
from src.utils.base.config import get_config
|
||||||
from src.utils.base.reloader import Reloader
|
from liteyuki.core import ProcessingManager
|
||||||
from src.utils.base.resource import load_resources
|
from src.utils.base.resource import load_resources
|
||||||
|
|
||||||
if get_config("debug", False):
|
if get_config("debug", False):
|
||||||
|
|
||||||
|
liteyuki_bot = get_bot()
|
||||||
|
|
||||||
src_directories = (
|
src_directories = (
|
||||||
"src/liteyuki_main",
|
"src/liteyuki_main",
|
||||||
"src/plugins",
|
"src/plugins",
|
||||||
"src/utils",
|
"src/utils",
|
||||||
)
|
)
|
||||||
src_excludes_extensions = (
|
src_excludes_extensions = ("pyc",)
|
||||||
"pyc",
|
|
||||||
)
|
|
||||||
|
|
||||||
res_directories = (
|
res_directories = (
|
||||||
"src/resources",
|
"src/resources",
|
||||||
"resources",
|
"resources",
|
||||||
)
|
)
|
||||||
|
|
||||||
nonebot.logger.info("Liteyuki Reload enabled, watching for file changes...")
|
nonebot.logger.info("已启用 Liteyuki Reload ,正在监测文件变动。")
|
||||||
|
|
||||||
|
|
||||||
class CodeModifiedHandler(FileSystemEventHandler):
|
class CodeModifiedHandler(FileSystemEventHandler):
|
||||||
"""
|
"""
|
||||||
@ -31,12 +31,14 @@ if get_config("debug", False):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
if event.src_path.endswith(
|
if (
|
||||||
src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path:
|
event.src_path.endswith(src_excludes_extensions)
|
||||||
|
or event.is_directory
|
||||||
|
or "__pycache__" in event.src_path
|
||||||
|
):
|
||||||
return
|
return
|
||||||
nonebot.logger.info(f"{event.src_path} modified, reloading bot...")
|
nonebot.logger.info(f"文件 {event.src_path} 变更,正在重载…")
|
||||||
Reloader.reload()
|
liteyuki_bot.restart()
|
||||||
|
|
||||||
|
|
||||||
class ResourceModifiedHandler(FileSystemEventHandler):
|
class ResourceModifiedHandler(FileSystemEventHandler):
|
||||||
"""
|
"""
|
||||||
@ -44,10 +46,9 @@ if get_config("debug", False):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
nonebot.logger.info(f"{event.src_path} modified, reloading resource...")
|
nonebot.logger.info(f"资源 {event.src_path} 变更,重载资源包…")
|
||||||
load_resources()
|
load_resources()
|
||||||
|
|
||||||
|
|
||||||
code_modified_handler = CodeModifiedHandler()
|
code_modified_handler = CodeModifiedHandler()
|
||||||
resource_modified_handle = ResourceModifiedHandler()
|
resource_modified_handle = ResourceModifiedHandler()
|
||||||
|
|
||||||
|
@ -6,10 +6,13 @@ from src.utils.base.data_manager import InstalledPlugin, plugin_db
|
|||||||
from src.utils.base.resource import load_resources
|
from src.utils.base.resource import load_resources
|
||||||
from src.utils.message.tools import check_for_package
|
from src.utils.message.tools import check_for_package
|
||||||
|
|
||||||
|
from liteyuki import get_bot
|
||||||
|
|
||||||
load_resources()
|
load_resources()
|
||||||
init_log()
|
init_log()
|
||||||
|
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
|
liteyuki_bot = get_bot()
|
||||||
|
|
||||||
|
|
||||||
@driver.on_startup
|
@driver.on_startup
|
||||||
@ -18,14 +21,47 @@ async def load_plugins():
|
|||||||
# 从数据库读取已安装的插件
|
# 从数据库读取已安装的插件
|
||||||
if not get_config("safe_mode", False):
|
if not get_config("safe_mode", False):
|
||||||
# 安全模式下,不加载插件
|
# 安全模式下,不加载插件
|
||||||
installed_plugins: list[InstalledPlugin] = plugin_db.where_all(InstalledPlugin())
|
installed_plugins: list[InstalledPlugin] = plugin_db.where_all(
|
||||||
|
InstalledPlugin()
|
||||||
|
)
|
||||||
if installed_plugins:
|
if installed_plugins:
|
||||||
for installed_plugin in installed_plugins:
|
for installed_plugin in installed_plugins:
|
||||||
if not check_for_package(installed_plugin.module_name):
|
if not check_for_package(installed_plugin.module_name):
|
||||||
nonebot.logger.error(
|
nonebot.logger.error(
|
||||||
f"{installed_plugin.module_name} not installed, but in loading database. please run `npm fixup` in chat to reinstall it.")
|
f"插件 {installed_plugin.module_name} 在加载列表中但未安装。请使用超管账户对机器人发送 `npm fixup` 以重新安装。"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
nonebot.load_plugin(installed_plugin.module_name)
|
nonebot.load_plugin(installed_plugin.module_name)
|
||||||
nonebot.plugin.load_plugins("plugins")
|
nonebot.plugin.load_plugins("plugins")
|
||||||
else:
|
else:
|
||||||
nonebot.logger.info("Safe mode is on, no plugin loaded.")
|
nonebot.logger.info("安全模式已启动,未加载任何插件。")
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki_bot.on_before_start
|
||||||
|
async def _():
|
||||||
|
print("启动前")
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki_bot.on_after_start
|
||||||
|
async def _():
|
||||||
|
print("启动后")
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki_bot.on_before_shutdown
|
||||||
|
async def _():
|
||||||
|
print("停止前")
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki_bot.on_after_shutdown
|
||||||
|
async def _():
|
||||||
|
print("停止后")
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki_bot.on_before_restart
|
||||||
|
async def _():
|
||||||
|
print("重启前")
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki_bot.on_after_restart
|
||||||
|
async def _():
|
||||||
|
print("重启后")
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import multiprocessing
|
||||||
|
|
||||||
from nonebot.plugin import PluginMetadata
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from liteyuki.plugin import get_loaded_plugins
|
||||||
from .rt_guide import *
|
from .rt_guide import *
|
||||||
from .crt_matchers import *
|
from .crt_matchers import *
|
||||||
|
|
||||||
@ -14,3 +17,5 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
"default_enable": True,
|
"default_enable": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("已加载插件:", len(get_loaded_plugins()))
|
@ -554,7 +554,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
Subcommand(
|
Subcommand(
|
||||||
disable,
|
disable,
|
||||||
Args["group_id", str, None],
|
Args["group_id", str, None],
|
||||||
alias=["d", "停用"],
|
alias=["d", "停用","禁用"],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN,
|
permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN,
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import nonebot
|
|
||||||
from src.utils import adapter_manager, driver_manager, init
|
|
||||||
from src.utils.base.config import load_from_yaml
|
|
||||||
from src.utils.base.data_manager import StoredConfig, common_db
|
|
||||||
from src.utils.base.ly_api import liteyuki_api
|
|
||||||
|
|
||||||
if __name__ == "__mp_main__":
|
|
||||||
# Start as multiprocessing
|
|
||||||
init()
|
|
||||||
store_config: dict = common_db.where_one(StoredConfig(), default=StoredConfig()).config
|
|
||||||
static_config = load_from_yaml("config.yml")
|
|
||||||
store_config.update(static_config)
|
|
||||||
driver_manager.init(config=store_config)
|
|
||||||
adapter_manager.init(store_config)
|
|
||||||
nonebot.init(**store_config)
|
|
||||||
adapter_manager.register()
|
|
||||||
try:
|
|
||||||
nonebot.load_plugin("liteyuki.liteyuki_main")
|
|
||||||
nonebot.load_from_toml("pyproject.toml")
|
|
||||||
except BaseException as e:
|
|
||||||
if not isinstance(e, KeyboardInterrupt):
|
|
||||||
nonebot.logger.error(f"An error occurred: {e}, Bug will be reported automatically.")
|
|
||||||
liteyuki_api.bug_report(str(e.__repr__()))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Start as __main__
|
|
||||||
from src.utils.base.reloader import Reloader
|
|
||||||
|
|
||||||
nonebot.run()
|
|
@ -6,8 +6,8 @@ import time
|
|||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
|
|
||||||
__NAME__ = "LiteyukiBot-TriMO"
|
__NAME__ = "尹灵温|轻雪-睿乐"
|
||||||
__VERSION__ = "6.3.3" # 60201
|
__VERSION__ = "6.3.4" # 60201
|
||||||
# __VERSION_I__ = 99060303
|
# __VERSION_I__ = 99060303
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -7,14 +7,14 @@ from nonebot.adapters import satori
|
|||||||
|
|
||||||
def init(config: dict):
|
def init(config: dict):
|
||||||
if config.get("satori", None) is None:
|
if config.get("satori", None) is None:
|
||||||
nonebot.logger.info("Satori config not found, skip Satori init.")
|
nonebot.logger.info("未查见 Satori 的配置文档,将跳过 Satori 初始化")
|
||||||
return None
|
return None
|
||||||
satori_config = config.get("satori")
|
satori_config = config.get("satori")
|
||||||
if not satori_config.get("enable", False):
|
if not satori_config.get("enable", False):
|
||||||
nonebot.logger.info("Satori not enabled, skip Satori init.")
|
nonebot.logger.info("未启用 Satori ,将跳过 Satori 初始化")
|
||||||
return None
|
return None
|
||||||
if os.getenv("SATORI_CLIENTS", None) is not None:
|
if os.getenv("SATORI_CLIENTS", None) is not None:
|
||||||
nonebot.logger.info("Satori clients already set in environment variable, skip.")
|
nonebot.logger.info("Satori 客户端已设入环境变量,跳过此步。")
|
||||||
os.environ["SATORI_CLIENTS"] = json.dumps(satori_config.get("hosts", []), ensure_ascii=False)
|
os.environ["SATORI_CLIENTS"] = json.dumps(satori_config.get("hosts", []), ensure_ascii=False)
|
||||||
config['satori_clients'] = satori_config.get("hosts", [])
|
config['satori_clients'] = satori_config.get("hosts", [])
|
||||||
return
|
return
|
||||||
|
@ -20,25 +20,26 @@ class SatoriNodeConfig(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SatoriConfig(BaseModel):
|
class SatoriConfig(BaseModel):
|
||||||
comment: str = "These features are still in development. Do not enable in production environment."
|
comment: str = "此皆正处于开发之中,切勿在生产环境中启用。"
|
||||||
enable: bool = False
|
enable: bool = False
|
||||||
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
|
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
|
||||||
|
|
||||||
|
|
||||||
class BasicConfig(BaseModel):
|
class BasicConfig(BaseModel):
|
||||||
host: str = "127.0.0.1"
|
host: str = "127.0.0.1"
|
||||||
port: int = 20216
|
port: int = 20247
|
||||||
superusers: list[str] = []
|
superusers: list[str] = []
|
||||||
command_start: list[str] = ["/", ""]
|
command_start: list[str] = ["/", ""]
|
||||||
nickname: list[str] = [f"LiteyukiBot-{random_hex_string(6)}"]
|
nickname: list[str] = [f"灵温-{random_hex_string(6)}"]
|
||||||
satori: SatoriConfig = SatoriConfig()
|
satori: SatoriConfig = SatoriConfig()
|
||||||
|
data_path: str = "data/liteyuki"
|
||||||
|
|
||||||
|
|
||||||
def load_from_yaml(file: str) -> dict:
|
def load_from_yaml(file: str) -> dict:
|
||||||
global config
|
global config
|
||||||
nonebot.logger.debug("Loading config from %s" % file)
|
nonebot.logger.debug("Loading config from %s" % file)
|
||||||
if not os.path.exists(file):
|
if not os.path.exists(file):
|
||||||
nonebot.logger.warning(f"Config file {file} not found, created default config, please modify it and restart")
|
nonebot.logger.warning(f"未找到配置文件 {file} ,已创建默认配置,请修改后重启。")
|
||||||
with open(file, "w", encoding="utf-8") as f:
|
with open(file, "w", encoding="utf-8") as f:
|
||||||
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
|
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ def load_from_yaml(file: str) -> dict:
|
|||||||
conf = init_conf(yaml.load(f, Loader=yaml.FullLoader))
|
conf = init_conf(yaml.load(f, Loader=yaml.FullLoader))
|
||||||
config = conf
|
config = conf
|
||||||
if conf is None:
|
if conf is None:
|
||||||
nonebot.logger.warning(f"Config file {file} is empty, use default config. please modify it and restart")
|
nonebot.logger.warning(f"配置文件 {file} 为空,已创建默认配置,请修改后重启。")
|
||||||
conf = BasicConfig().dict()
|
conf = BasicConfig().dict()
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
@ -95,6 +96,8 @@ def init_conf(conf: dict) -> dict:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# 若command_start中无"",则添加必要命令头,开启alconna_use_command_start防止冲突
|
# 若command_start中无"",则添加必要命令头,开启alconna_use_command_start防止冲突
|
||||||
if "" not in conf.get("command_start", []):
|
# 以下内容由于issue #53 被注释
|
||||||
conf["alconna_use_command_start"] = True
|
# if "" not in conf.get("command_start", []):
|
||||||
|
# conf["alconna_use_command_start"] = True
|
||||||
return conf
|
return conf
|
||||||
|
pass
|
||||||
|
@ -20,8 +20,9 @@ class LiteyukiAPI:
|
|||||||
self.data = json.loads(f.read())
|
self.data = json.loads(f.read())
|
||||||
self.liteyuki_id = self.data.get("liteyuki_id")
|
self.liteyuki_id = self.data.get("liteyuki_id")
|
||||||
self.report = load_from_yaml("config.yml").get("auto_report", True)
|
self.report = load_from_yaml("config.yml").get("auto_report", True)
|
||||||
|
|
||||||
if self.report:
|
if self.report:
|
||||||
nonebot.logger.info("Auto bug report is enabled")
|
nonebot.logger.info("已启用自动上报")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self) -> dict:
|
def device_info(self) -> dict:
|
||||||
@ -37,10 +38,10 @@ class LiteyukiAPI:
|
|||||||
"python" : f"{platform.python_implementation()} {platform.python_version()}",
|
"python" : f"{platform.python_implementation()} {platform.python_version()}",
|
||||||
"os" : f"{platform.system()} {platform.version()} {platform.machine()}",
|
"os" : f"{platform.system()} {platform.version()} {platform.machine()}",
|
||||||
"cpu" : f"{psutil.cpu_count(logical=False)}c{psutil.cpu_count()}t{psutil.cpu_freq().current}MHz",
|
"cpu" : f"{psutil.cpu_count(logical=False)}c{psutil.cpu_count()}t{psutil.cpu_freq().current}MHz",
|
||||||
"memory_total": f"{psutil.virtual_memory().total / 1024 / 1024 / 1024:.2f}GB",
|
"memory_total": f"{psutil.virtual_memory().total / 1024 ** 3:.2f}吉字节",
|
||||||
"memory_used" : f"{psutil.virtual_memory().used / 1024 / 1024 / 1024:.2f}GB",
|
"memory_used" : f"{psutil.virtual_memory().used / 1024 ** 3:.2f}吉字节",
|
||||||
"memory_bot" : f"{psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024:.2f}MB",
|
"memory_bot" : f"{psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2:.2f}兆字节",
|
||||||
"disk" : f"{psutil.disk_usage('/').total / 1024 / 1024 / 1024:.2f}GB"
|
"disk" : f"{psutil.disk_usage('/').total / 1024 ** 3:.2f}吉字节"
|
||||||
}
|
}
|
||||||
|
|
||||||
def bug_report(self, content: str):
|
def bug_report(self, content: str):
|
||||||
@ -53,7 +54,7 @@ class LiteyukiAPI:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if self.report:
|
if self.report:
|
||||||
nonebot.logger.warning(f"Reporting bug...: {content}")
|
nonebot.logger.warning(f"正在上报查误:{content}")
|
||||||
url = "https://api.liteyuki.icu/bug_report"
|
url = "https://api.liteyuki.icu/bug_report"
|
||||||
data = {
|
data = {
|
||||||
"liteyuki_id": self.liteyuki_id,
|
"liteyuki_id": self.liteyuki_id,
|
||||||
@ -62,11 +63,11 @@ class LiteyukiAPI:
|
|||||||
}
|
}
|
||||||
resp = requests.post(url, json=data)
|
resp = requests.post(url, json=data)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
nonebot.logger.success(f"Bug report sent successfully, report_id: {resp.json().get('report_id')}")
|
nonebot.logger.success(f"成功上报差误信息,报文ID为:{resp.json().get('report_id')}")
|
||||||
else:
|
else:
|
||||||
nonebot.logger.error(f"Bug report failed: {resp.text}")
|
nonebot.logger.error(f"差误上报错误:{resp.text}")
|
||||||
else:
|
else:
|
||||||
nonebot.logger.warning(f"Bug report is disabled: {content}")
|
nonebot.logger.warning(f"已禁用自动上报:{content}")
|
||||||
|
|
||||||
async def heartbeat_report(self):
|
async def heartbeat_report(self):
|
||||||
"""
|
"""
|
||||||
@ -77,14 +78,11 @@ class LiteyukiAPI:
|
|||||||
url = "https://api.liteyuki.icu/heartbeat"
|
url = "https://api.liteyuki.icu/heartbeat"
|
||||||
data = {
|
data = {
|
||||||
"liteyuki_id": self.liteyuki_id,
|
"liteyuki_id": self.liteyuki_id,
|
||||||
"version": __VERSION__,
|
"version" : __VERSION__,
|
||||||
}
|
}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(url, json=data) as resp:
|
async with session.post(url, json=data) as resp:
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
nonebot.logger.success("Heartbeat sent successfully")
|
nonebot.logger.success("心跳成功送达。")
|
||||||
else:
|
else:
|
||||||
nonebot.logger.error(f"Heartbeat failed: {await resp.text()}")
|
nonebot.logger.error(f"休克:{await resp.text()}")
|
||||||
|
|
||||||
|
|
||||||
liteyuki_api = LiteyukiAPI()
|
|
@ -9,13 +9,12 @@ from .defines import *
|
|||||||
def auto_set_env(config: dict):
|
def auto_set_env(config: dict):
|
||||||
dotenv.load_dotenv(".env")
|
dotenv.load_dotenv(".env")
|
||||||
if os.getenv("DRIVER", None) is not None:
|
if os.getenv("DRIVER", None) is not None:
|
||||||
print(os.getenv("DRIVER"))
|
nonebot.logger.info("Driver 已设入环境变量中,将跳过自动配置环节。")
|
||||||
nonebot.logger.info("Driver already set in environment variable, skip auto configure.")
|
|
||||||
return
|
return
|
||||||
if config.get("satori", {'enable': False}).get("enable", False):
|
if config.get("satori", {'enable': False}).get("enable", False):
|
||||||
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER, HTTPX_DRIVER, WEBSOCKETS_DRIVER)
|
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER, HTTPX_DRIVER, WEBSOCKETS_DRIVER)
|
||||||
nonebot.logger.info("Enable Satori, set driver to ASGI+HTTPX+WEBSOCKETS")
|
nonebot.logger.info("已启用 Satori,将 driver 设为 ASGI+HTTPX+WEBSOCKETS")
|
||||||
else:
|
else:
|
||||||
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER)
|
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER)
|
||||||
nonebot.logger.info("Disable Satori, set driver to ASGI")
|
nonebot.logger.info("已禁用 Satori,将 driver 设为 ASGI")
|
||||||
return
|
return
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
|
from .net import *
|
||||||
|
from .file import *
|
||||||
|
|
||||||
|
|
||||||
async def simple_get(url: str) -> str:
|
async def simple_get(url: str) -> str:
|
||||||
"""
|
"""
|
||||||
@ -8,7 +11,6 @@ async def simple_get(url: str) -> str:
|
|||||||
url:
|
url:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url) as resp:
|
29
src/utils/io/file.py
Normal file
29
src/utils/io/file.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import aiofiles
|
||||||
|
|
||||||
|
|
||||||
|
async def write_file(
|
||||||
|
file_path: str,
|
||||||
|
content: str | bytes,
|
||||||
|
mode: str = "w"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
写入文件
|
||||||
|
Args:
|
||||||
|
mode: 写入模式
|
||||||
|
file_path: 文件路径
|
||||||
|
content: 内容
|
||||||
|
"""
|
||||||
|
async with aiofiles.open(file_path, mode) as f:
|
||||||
|
await f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_file(file_path: str, mode: str = "r") -> str:
|
||||||
|
"""
|
||||||
|
读取文件
|
||||||
|
Args:
|
||||||
|
file_path: 文件路径
|
||||||
|
mode: 读取模式
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
async with aiofiles.open(file_path, mode) as f:
|
||||||
|
return await f.read()
|
12
src/utils/io/net.py
Normal file
12
src/utils/io/net.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
async def fetch(url: str) -> str:
|
||||||
|
"""
|
||||||
|
异步get请求
|
||||||
|
Args:
|
||||||
|
url:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with ClientSession() as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
return await resp.text()
|
5
test/test_core.py
Normal file
5
test/test_core.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from src.liteyuki import LiteyukiBot
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
lyb = LiteyukiBot()
|
||||||
|
lyb.run()
|
Loading…
Reference in New Issue
Block a user