forked from bot/app
first commit
This commit is contained in:
parent
9e9f6e4ad6
commit
51cb1a87b8
19
.gitignore
vendored
19
.gitignore
vendored
@ -1,16 +1,7 @@
|
|||||||
# idea
|
.venv/
|
||||||
plugin/
|
|
||||||
|
.cache/
|
||||||
|
data/
|
||||||
|
|
||||||
# config
|
|
||||||
config.yml
|
config.yml
|
||||||
|
config.example.yml
|
||||||
# external plugins
|
|
||||||
/plugins/
|
|
||||||
|
|
||||||
# pyc/pyo
|
|
||||||
**/*.pyc
|
|
||||||
**/*.pyo
|
|
||||||
|
|
||||||
# data
|
|
||||||
/data/
|
|
||||||
```
|
|
@ -19,11 +19,10 @@
|
|||||||
|
|
||||||
## 手动安装和部署
|
## 手动安装和部署
|
||||||
|
|
||||||
1. 安装`Git`和`Python3.10+`后,使用命令`git clone https://github.com/snowykami/LiteyukiBot` 克隆项目至本地。
|
1. 前置:`Git`和`Python3.10+`
|
||||||
一定要安装Git,Bot自带功能需要git支持
|
2. 使用命令`git clone https://github.com/snowykami/LiteyukiBot`
|
||||||
2. 切换到轻雪目录,使用`pip install -r requirements.txt`安装依赖
|
3. 切换到轻雪目录,使用`pip install -r requirements.txt`安装依赖
|
||||||
|
4. `python main.py`启动!
|
||||||
3. `python main.py`启动!
|
|
||||||
|
|
||||||
## 一键部署脚本(复制到本地保存执行)
|
## 一键部署脚本(复制到本地保存执行)
|
||||||
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Liteyuki Default",
|
|
||||||
"version": "1.0"
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Liteyuki Language Pack",
|
|
||||||
"version": "1.0"
|
|
||||||
}
|
|
19
main.py
19
main.py
@ -1,7 +1,16 @@
|
|||||||
from src.liteyuki import *
|
import nonebot
|
||||||
|
|
||||||
|
from nonebot.adapters.onebot import v11, v12
|
||||||
|
from src.utils.config import load_from_yaml
|
||||||
|
|
||||||
if __name__ == '__main__':
|
nonebot.init(**load_from_yaml("config.yml").get("nonebot", {}))
|
||||||
liteyuki = Liteyuki()
|
|
||||||
app = liteyuki.get_asgi()
|
adapters = [v11.Adapter, v12.Adapter]
|
||||||
liteyuki.run(app="main:app")
|
driver = nonebot.get_driver()
|
||||||
|
for adapter in adapters:
|
||||||
|
driver.register_adapter(adapter)
|
||||||
|
|
||||||
|
nonebot.load_plugin("src.plugins.liteyuki_plugin_main")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
nonebot.run()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
nonebot2[fastapi]
|
arclet-alconna==2.0.0a1
|
||||||
nonebot-adapter-onebot==2.4.1
|
nonebot2[fastapi]==2.2.1
|
||||||
pydantic==1.10.8
|
nonebot-adapter-onebot==2.4.3
|
||||||
yaml==0.2.5
|
nonebot-plugin-alconna==0.41.0
|
||||||
pyyaml==6.0
|
pydantic==2.6.4
|
@ -1,33 +0,0 @@
|
|||||||
import os
|
|
||||||
import yaml
|
|
||||||
from nonebot import logger
|
|
||||||
|
|
||||||
|
|
||||||
def load_config() -> dict[str, any]:
|
|
||||||
"""
|
|
||||||
Load config from config.yml
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
config = {
|
|
||||||
'host': '0.0.0.0',
|
|
||||||
'port': 20216,
|
|
||||||
'nickname': ['Liteyuki'],
|
|
||||||
'command_start': [''],
|
|
||||||
}
|
|
||||||
|
|
||||||
if not os.path.exists('config.yml'):
|
|
||||||
logger.warning('warn.config_file_not_found')
|
|
||||||
with open('config.yml', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(yaml.dump(config, indent=4))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open('config.yml', 'r', encoding='utf-8') as f:
|
|
||||||
config.update(yaml.load(f, Loader=yaml.FullLoader))
|
|
||||||
logger.success('success.config_loaded')
|
|
||||||
# 格式化后写入
|
|
||||||
with open('config.yml', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(yaml.dump(config, indent=4))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f'error.load_config: {e}')
|
|
||||||
|
|
||||||
return config
|
|
0
src/assets/lang/zh_cn.lang
Normal file
0
src/assets/lang/zh_cn.lang
Normal file
@ -1,45 +0,0 @@
|
|||||||
from typing import Any, Optional
|
|
||||||
import nonebot
|
|
||||||
from nonebot import DOTENV_TYPE
|
|
||||||
from nonebot.adapters.onebot.v11 import Adapter as OnebotV11Adapter
|
|
||||||
from nonebot.adapters.onebot.v12 import Adapter as OnebotV12Adapter
|
|
||||||
|
|
||||||
from src.api.utils import load_config
|
|
||||||
|
|
||||||
app = None
|
|
||||||
adapters = [
|
|
||||||
OnebotV11Adapter,
|
|
||||||
OnebotV12Adapter
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Liteyuki:
|
|
||||||
def __init__(self, *, _env_file: Optional[DOTENV_TYPE] = None, **kwargs: Any):
|
|
||||||
print(
|
|
||||||
'\033[34m' + r''' __ ______ ________ ________ __ __ __ __ __ __ ______
|
|
||||||
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
|
|
||||||
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
|
|
||||||
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
|
|
||||||
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
|
|
||||||
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
|
|
||||||
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
|
|
||||||
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
|
|
||||||
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ ''' + '\033[0m'
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs = load_config()
|
|
||||||
self.nonebot = nonebot
|
|
||||||
self.nonebot.init(_env_file=_env_file, **kwargs)
|
|
||||||
self.driver = self.nonebot.get_driver()
|
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
|
||||||
for adapter in adapters:
|
|
||||||
self.driver.register_adapter(adapter)
|
|
||||||
self.nonebot.load_plugin('src.liteyuki_main') # Load main plugin
|
|
||||||
self.nonebot.load_plugins('src/builtin') # Load builtin plugins
|
|
||||||
self.nonebot.load_plugins('plugins') # Load custom plugins
|
|
||||||
# Todo: load from database
|
|
||||||
self.nonebot.run(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_asgi(self):
|
|
||||||
return self.nonebot.get_asgi()
|
|
@ -1,16 +0,0 @@
|
|||||||
from nonebot import on_command
|
|
||||||
from nonebot.adapters.onebot.v11.event import MessageEvent
|
|
||||||
from nonebot.permission import SUPERUSER
|
|
||||||
import os
|
|
||||||
folders = ['plugins']
|
|
||||||
for folder in folders:
|
|
||||||
if not os.path.exists(folder):
|
|
||||||
os.makedirs(folder)
|
|
||||||
|
|
||||||
echo = on_command('echo', permission=SUPERUSER, priority=5)
|
|
||||||
|
|
||||||
|
|
||||||
@echo.handle()
|
|
||||||
async def _(event: MessageEvent):
|
|
||||||
print(event.get_message())
|
|
||||||
await echo.finish(event.get_message())
|
|
19
src/plugins/liteyuki_plugin_main/__init__.py
Normal file
19
src/plugins/liteyuki_plugin_main/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import nonebot
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
||||||
|
__author__ = "snowykami"
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="轻雪主程序",
|
||||||
|
description="轻雪主程序插件,包含了许多初始化的功能",
|
||||||
|
usage="",
|
||||||
|
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||||
|
)
|
||||||
|
|
||||||
|
fastapi_app = nonebot.get_app()
|
||||||
|
|
||||||
|
|
||||||
|
@fastapi_app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {
|
||||||
|
"message": "Hello LiteyukiBot!",
|
||||||
|
}
|
0
src/plugins/liteyuki_plugin_main/load.py
Normal file
0
src/plugins/liteyuki_plugin_main/load.py
Normal file
18
src/plugins/liteyuki_plugin_npm/__init__.py
Normal file
18
src/plugins/liteyuki_plugin_npm/__init__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from .manager import *
|
||||||
|
from .installer import *
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = "snowykami"
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="轻雪插件管理",
|
||||||
|
description="本地插件管理和插件商店支持,支持启用/停用,安装/卸载插件",
|
||||||
|
usage=(
|
||||||
|
"lnpm list\n"
|
||||||
|
"lnpm enable/disable <plugin_name>\n"
|
||||||
|
"lnpm search <keywords...>\n"
|
||||||
|
"lnpm install/uninstall <plugin_name>\n"
|
||||||
|
),
|
||||||
|
type="application",
|
||||||
|
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||||
|
)
|
1
src/plugins/liteyuki_plugin_npm/common.py
Normal file
1
src/plugins/liteyuki_plugin_npm/common.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
LNPM_COMMAND_START = "lnpm"
|
0
src/plugins/liteyuki_plugin_npm/helper.py
Normal file
0
src/plugins/liteyuki_plugin_npm/helper.py
Normal file
0
src/plugins/liteyuki_plugin_npm/installer.py
Normal file
0
src/plugins/liteyuki_plugin_npm/installer.py
Normal file
16
src/plugins/liteyuki_plugin_npm/manager.py
Normal file
16
src/plugins/liteyuki_plugin_npm/manager.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import nonebot.plugin
|
||||||
|
from nonebot import on_command
|
||||||
|
from src.utils.adapter import MessageEvent
|
||||||
|
from src.utils.language import get_user_lang
|
||||||
|
|
||||||
|
list_plugins = on_command("list-plugin", aliases={"列出插件"}, priority=0)
|
||||||
|
toggle_plugin = on_command("enable-plugin", aliases={"启用插件", "禁用插件", "disable-plugin"}, priority=0)
|
||||||
|
|
||||||
|
|
||||||
|
@list_plugins.handle()
|
||||||
|
async def _(event: MessageEvent):
|
||||||
|
lang = get_user_lang(event.user_id)
|
||||||
|
reply = lang.get("npm.current_plugins")
|
||||||
|
for plugin in nonebot.get_loaded_plugins():
|
||||||
|
reply += f"\n- {plugin.name}"
|
||||||
|
await list_plugins.finish(reply)
|
7
src/utils/adapter.py
Normal file
7
src/utils/adapter.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from nonebot.adapters.onebot import v11, v12
|
||||||
|
|
||||||
|
|
||||||
|
Bot = v11.Bot | v12.Bot
|
||||||
|
GroupMessageEvent = v11.GroupMessageEvent | v12.GroupMessageEvent
|
||||||
|
PrivateMessageEvent = v11.PrivateMessageEvent | v12.PrivateMessageEvent
|
||||||
|
MessageEvent = v11.MessageEvent | v12.MessageEvent
|
6
src/utils/config.py
Normal file
6
src/utils/config.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from yaml import load, FullLoader
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_yaml(file: str) -> dict:
|
||||||
|
with open(file, 'r', encoding='utf-8') as f:
|
||||||
|
return load(f, Loader=FullLoader)
|
@ -69,8 +69,8 @@ class BaseORMAdapter(ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class SqliteORMAdapter(BaseORMAdapter):
|
class SqliteORMDatabase(BaseORMAdapter):
|
||||||
"""SQLiteORM适配器,严禁使用FORIEGNID和JSON作为主键前缀,严禁使用$ID:作为字符串值前缀
|
"""SQLiteORM适配器,严禁使用`FORIEGNID`和`JSON`作为主键前缀,严禁使用`$ID:`作为字符串值前缀
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ class SqliteORMAdapter(BaseORMAdapter):
|
|||||||
value_list.append(f'{self.ID}:{value.__class__.__name__}:{self.save(value)}')
|
value_list.append(f'{self.ID}:{value.__class__.__name__}:{self.save(value)}')
|
||||||
elif isinstance(value, BaseIterable):
|
elif isinstance(value, BaseIterable):
|
||||||
key_list.append(f'{self.JSON}{field}')
|
key_list.append(f'{self.JSON}{field}')
|
||||||
value_list.append(self.flat(value))
|
value_list.append(self._flat(value))
|
||||||
else:
|
else:
|
||||||
key_list.append(field)
|
key_list.append(field)
|
||||||
value_list.append(value)
|
value_list.append(value)
|
||||||
@ -178,7 +178,7 @@ class SqliteORMAdapter(BaseORMAdapter):
|
|||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
return ids[0] if len(ids) == 1 else tuple(ids)
|
return ids[0] if len(ids) == 1 else tuple(ids)
|
||||||
|
|
||||||
def flat(self, data: Iterable) -> str:
|
def _flat(self, data: Iterable) -> str:
|
||||||
"""扁平化数据,返回扁平化对象
|
"""扁平化数据,返回扁平化对象
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -192,7 +192,7 @@ class SqliteORMAdapter(BaseORMAdapter):
|
|||||||
if isinstance(v, LiteModel):
|
if isinstance(v, LiteModel):
|
||||||
return_data[f'{self.FOREIGNID}{k}'] = f'{self.ID}:{v.__class__.__name__}:{self.save(v)}'
|
return_data[f'{self.FOREIGNID}{k}'] = f'{self.ID}:{v.__class__.__name__}:{self.save(v)}'
|
||||||
elif isinstance(v, BaseIterable):
|
elif isinstance(v, BaseIterable):
|
||||||
return_data[f'{self.JSON}{k}'] = self.flat(v)
|
return_data[f'{self.JSON}{k}'] = self._flat(v)
|
||||||
else:
|
else:
|
||||||
return_data[k] = v
|
return_data[k] = v
|
||||||
|
|
||||||
@ -202,7 +202,7 @@ class SqliteORMAdapter(BaseORMAdapter):
|
|||||||
if isinstance(v, LiteModel):
|
if isinstance(v, LiteModel):
|
||||||
return_data.append(f'{self.ID}:{v.__class__.__name__}:{self.save(v)}')
|
return_data.append(f'{self.ID}:{v.__class__.__name__}:{self.save(v)}')
|
||||||
elif isinstance(v, BaseIterable):
|
elif isinstance(v, BaseIterable):
|
||||||
return_data.append(self.flat(v))
|
return_data.append(self._flat(v))
|
||||||
else:
|
else:
|
||||||
return_data.append(v)
|
return_data.append(v)
|
||||||
else:
|
else:
|
16
src/utils/data_manager.py
Normal file
16
src/utils/data_manager.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from src.utils.data import LiteModel, SqliteORMDatabase as DB
|
||||||
|
|
||||||
|
DATA_PATH = "data/liteyuki"
|
||||||
|
|
||||||
|
user_db = DB(os.path.join(DATA_PATH, 'users.ldb'))
|
||||||
|
|
||||||
|
|
||||||
|
class UserModel(LiteModel):
|
||||||
|
id: str
|
||||||
|
username: str
|
||||||
|
lang: str
|
||||||
|
|
||||||
|
|
||||||
|
user_db.auto_migrate(UserModel)
|
111
src/utils/language.py
Normal file
111
src/utils/language.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
语言模块,添加对多语言的支持
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
|
||||||
|
from src.utils.data_manager import UserModel, user_db
|
||||||
|
|
||||||
|
_language_data = {
|
||||||
|
"en": {
|
||||||
|
"name": "English",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_lang(file_path: str, lang_code: str = None):
|
||||||
|
"""
|
||||||
|
从lang文件中加载语言数据,用于简单的文本键值对
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: lang文件路径
|
||||||
|
lang_code: 语言代码,如果为None则从文件名中获取
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if lang_code is None:
|
||||||
|
lang_code = os.path.basename(file_path).split('.')[0]
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
data = {}
|
||||||
|
for line in file:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'): # 空行或注释
|
||||||
|
continue
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
data[key.strip()] = value.strip()
|
||||||
|
if lang_code not in _language_data:
|
||||||
|
_language_data[lang_code] = {}
|
||||||
|
_language_data[lang_code].update(data)
|
||||||
|
except Exception as e:
|
||||||
|
nonebot.logger.error(f"Failed to load language data from {file_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_json(file_path: str, lang_code: str = None):
|
||||||
|
"""
|
||||||
|
从json文件中加载语言数据,可以定义一些变量
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lang_code: 语言代码,如果为None则从文件名中获取
|
||||||
|
file_path: json文件路径
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if lang_code is None:
|
||||||
|
lang_code = os.path.basename(file_path).split('.')[0]
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
data = json.load(file)
|
||||||
|
if lang_code not in _language_data:
|
||||||
|
_language_data[lang_code] = {}
|
||||||
|
_language_data[lang_code].update(data)
|
||||||
|
except Exception as e:
|
||||||
|
nonebot.logger.error(f"Failed to load language data from {file_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_dict(data: dict, lang_code: str):
|
||||||
|
"""
|
||||||
|
从字典中加载语言数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lang_code: 语言代码
|
||||||
|
data: 字典数据
|
||||||
|
"""
|
||||||
|
if lang_code not in _language_data:
|
||||||
|
_language_data[lang_code] = {}
|
||||||
|
_language_data[lang_code].update(data)
|
||||||
|
|
||||||
|
|
||||||
|
class Language:
|
||||||
|
def __init__(self, lang_code: str = "en", fallback_lang_code: str = "en"):
|
||||||
|
self.lang_code = lang_code
|
||||||
|
self.fallback_lang_code = fallback_lang_code
|
||||||
|
|
||||||
|
def get(self, item: str, *args) -> str | Any:
|
||||||
|
"""
|
||||||
|
获取当前语言文本
|
||||||
|
Args:
|
||||||
|
item: 文本键
|
||||||
|
*args: 格式化参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 当前语言的文本
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.lang_code in _language_data and item in _language_data[self.lang_code]:
|
||||||
|
return _language_data[self.lang_code][item].format(*args)
|
||||||
|
if self.fallback_lang_code in _language_data and item in _language_data[self.fallback_lang_code]:
|
||||||
|
return _language_data[self.fallback_lang_code][item].format(*args)
|
||||||
|
return item
|
||||||
|
except Exception as e:
|
||||||
|
nonebot.logger.error(f"Failed to get language text or format: {e}")
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_lang(user_id: str) -> Language:
|
||||||
|
"""
|
||||||
|
获取用户的语言代码
|
||||||
|
"""
|
||||||
|
user = user_db.first(UserModel, "id = ?", user_id, default=UserModel(id=user_id, username="Unknown", lang="en"))
|
||||||
|
return Language(user.lang)
|
Loading…
Reference in New Issue
Block a user