🔥 小型重构

This commit is contained in:
远野千束 2024-06-26 13:52:04 +08:00
parent 35823be13e
commit 8b01943d14
268 changed files with 25225 additions and 24996 deletions

2
.gitignore vendored
View File

@ -15,7 +15,7 @@ _config.yml
config.yml
config.example.yml
compile.bat
liteyuki/resources/templates/latest-debug.html
src/resources/templates/latest-debug.html
# vuepress
.github
pyproject.toml

View File

@ -1,6 +1,6 @@
import {defineClientConfig} from "vuepress/client";
import resourceStoreComp from "./components/res_store.vue";
import pluginStoreComp from "./components/plugin_store.vue";
import resourceStoreComp from "./components/ResStore.vue";
import pluginStoreComp from "./components/PluginStore.vue";
//导入element-plus
import ElementPlus from 'element-plus';
@ -9,6 +9,5 @@ export default defineClientConfig({
app.component("resourceStoreComp", resourceStoreComp);
app.component("pluginStoreComp", pluginStoreComp);
app.use(ElementPlus);
},
});

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import {ref} from 'vue'
import ItemCard from './plugin_item_card.vue'
import ItemCard from './PluginItemCard.vue'
// Nonebot
let items = ref([])

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import {ref} from 'vue'
import ItemCard from './res_item_card.vue'
import ItemCard from './ResItemCard.vue'
// public/assets/resources.json
let items = ref([])
fetch('https://bot.liteyuki.icu/assets/resources.json')
fetch('/assets/resources.json')
.then(response => response.json())
.then(data => {
items.value = data

View File

@ -10,6 +10,7 @@ export default defineUserConfig({
description: "LiteyukiBot | 轻雪机器人 | An OneBot Standard ChatBot | 一个OneBot标准的聊天机器人",
head: [
// 设置 favor.ico.vuepress/public 下
["script", {src: "/js/style.js", "type": "module"}],
['link', {rel: 'icon', href: 'https://cdn.liteyuki.icu/favicon.ico'},],
['link', {rel: 'stylesheet', href: 'https://cdn.bootcdn.net/ajax/libs/firacode/6.2.0/fira_code.min.css'}],

View File

@ -0,0 +1,22 @@
function applyStyle() {
let lineNumbers = document.body.querySelectorAll('[class^="language-"].line-numbers-mode')
lineNumbers.forEach((item) => {
// 插入现成的html文本
let title = item.getAttribute('data-title')
let tabStr =
"<div class='tab' style='display: flex; background-color: #d0e9ff'>" +
" <div class='tab-buttons'>" +
" <div class='tab-button' style='background-color: #FF5F57'></div>" +
" <div class='tab-button' style='background-color: #FFBD2E'></div>" +
" <div class='tab-button' style='background-color: #27C93F'></div>" +
" </div>" +
` <div class='tab-title'>${title}</div>` +
" <div style='flex: 1'></div>" +
"</div>"
// 在代码块前插入选项卡
item.insertAdjacentHTML('beforebegin', tabStr);
})
}
applyStyle()

View File

@ -1,5 +1,10 @@
// place your custom styles here
:root {
--code-window-border-radius: 10px;
--button-distance: 8px;
}
#main-title {
font-family: ColorTube, "Fira Code", serif;
color: #ff0000 !important; /* 你想要的颜色 */
@ -15,11 +20,62 @@ code {
font-family: "Fira Code", monospace !important;
}
.vp-hero-image{
.vp-hero-image {
overflow: hidden;
padding: -50px;
}
#main-title{
#main-title {
display: none;
}
.theme-hope-content pre {
overflow: auto;
margin: 0 0;
padding: 1rem;
border-radius: 6px;
line-height: 1.375;
}
// 移除该before
.theme-hope-content pre::before {
content: none;
}
.theme-hope-content > div[class*=language-] {
margin: 0 0 0 0;
// 仅下半部分有圆弧
border-radius: 0 0 var(--code-window-border-radius) var(--code-window-border-radius);
}
.tab {
display: flex;
height: 25px;
margin-bottom: 0;
justify-content: space-between;
align-items: center;
border-top-left-radius: var(--code-window-border-radius);
border-top-right-radius: var(--code-window-border-radius);
}
.tab-buttons {
padding: 7px;
flex: 1;
display: flex;
justify-content: flex-start;
height: 60%;
align-items: center;
}
.tab-button {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-right: var(--button-distance);
border-radius: 50%;
height: 100%;
aspect-ratio: 1/1;
}
.tab-title{
text-align: center;
justify-content: center;
flex: 1;
}

View File

@ -21,6 +21,7 @@
"dependencies": {
"clipboard": "^2.0.11",
"element-plus": "^2.7.0",
"element-ui": "^2.15.14"
"element-ui": "^2.15.14",
"vue-router": "^4.4.0"
}
}

View File

@ -14,6 +14,9 @@ dependencies:
element-ui:
specifier: ^2.15.14
version: 2.15.14(vue@3.4.21)
vue-router:
specifier: ^4.4.0
version: 4.4.0(vue@3.4.21)
devDependencies:
'@vuepress/bundler-vite':
@ -938,7 +941,6 @@ packages:
/@vue/devtools-api@6.6.1:
resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
dev: true
/@vue/reactivity@3.4.21:
resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==}
@ -985,7 +987,7 @@ packages:
rollup: 4.13.1
vite: 5.2.6
vue: 3.4.21
vue-router: 4.3.0(vue@3.4.21)
vue-router: 4.4.0(vue@3.4.21)
transitivePeerDependencies:
- '@types/node'
- jiti
@ -1021,7 +1023,7 @@ packages:
'@vue/devtools-api': 6.6.1
'@vuepress/shared': 2.0.0-rc.9
vue: 3.4.21
vue-router: 4.3.0(vue@3.4.21)
vue-router: 4.4.0(vue@3.4.21)
transitivePeerDependencies:
- typescript
dev: true
@ -2739,14 +2741,13 @@ packages:
dependencies:
vue: 3.4.21
/vue-router@4.3.0(vue@3.4.21):
resolution: {integrity: sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==}
/vue-router@4.4.0(vue@3.4.21):
resolution: {integrity: sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==}
peerDependencies:
vue: ^3.2.0
dependencies:
'@vue/devtools-api': 6.6.1
vue: 3.4.21
dev: true
/vue@3.4.21:
resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==}

12
main.py
View File

@ -1,8 +1,8 @@
import nonebot
from liteyuki.utils import adapter_manager, driver_manager, init
from liteyuki.utils.base.config import load_from_yaml
from liteyuki.utils.base.data_manager import StoredConfig, common_db
from liteyuki.utils.base.ly_api import liteyuki_api
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
@ -15,7 +15,7 @@ if __name__ == "__mp_main__":
nonebot.init(**store_config)
adapter_manager.register()
try:
nonebot.load_plugin("liteyuki.liteyuki_main")
nonebot.load_plugin("src.liteyuki_main")
nonebot.load_from_toml("pyproject.toml")
except BaseException as e:
if not isinstance(e, KeyboardInterrupt):
@ -24,6 +24,6 @@ if __name__ == "__mp_main__":
if __name__ == "__main__":
# Start as __main__
from liteyuki.utils.base.reloader import Reloader
from src.utils.base.reloader import Reloader
nonebot.run()

View File

@ -10,6 +10,7 @@ nonebot-adapter-onebot~=2.4.3
nonebot-plugin-alconna~=0.46.3
nonebot_plugin_apscheduler~=0.4.0
nonebot-adapter-satori~=0.11.5
numpy~=2.0.0
packaging~=23.1
psutil~=5.9.8
py-cpuinfo~=9.0.0

View File

@ -0,0 +1,6 @@
import abc
class Bot(abc.ABC):
def __init__(self):
pass

10
src/liteyuki/exception.py Normal file
View File

@ -0,0 +1,10 @@
"""exception模块包含了liteyuki运行中的所有错误
"""
from typing import Any, Optional
class LiteyukiException(BaseException):
"""Liteyuki的异常基类。"""
def __str__(self) -> str:
return self.__repr__()

View File

@ -1,34 +1,34 @@
from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
from .dev 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
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
sys_lang = Language(get_default_lang_code())
nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name")))
from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
from .dev 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
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
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 liteyuki.utils.base.config import get_config
remote_urls = [
"https://github.com/snowykami/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/snowykami/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,399 +1,399 @@
import base64
import time
from typing import Any, AnyStr
import nonebot
import pip
from nonebot import Bot, get_driver, require
from nonebot.adapters import satori
from nonebot.adapters.onebot.v11 import Message, escape, unescape
from nonebot.exception import MockApiException
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
from liteyuki.utils.base.config import get_config, load_from_yaml
from liteyuki.utils.base.data_manager import StoredConfig, TempConfig, common_db
from liteyuki.utils.base.language import get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from liteyuki.utils.base.reloader import Reloader
from liteyuki.utils import event as event_utils, satori_utils
from .api import update_liteyuki
from ..utils.base.ly_function import get_function
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler
driver = get_driver()
markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get("markdown_image", False)
@on_alconna(
command=Alconna(
"liteecho",
Args["text", str, ""],
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if result.main_args.get("text"):
await matcher.finish(Message(unescape(result.main_args.get("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):
# 使用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, event=event, at_sender=False)
@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.channel.id,
"delta_time" : 0
}
)
common_db.save(temp_data)
Reloader.reload(0)
@on_alconna(
aliases={"配置"},
command=Alconna(
"config",
Subcommand(
"set",
Args["key", str]["value", Any],
alias=["设置"],
),
Subcommand(
"get",
Args["key", str, None],
alias=["查询", "获取"]
),
Subcommand(
"remove",
Args["key", str],
alias=["删除"]
)
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
if result.subcommands.get("set"):
key, value = result.subcommands.get("set").args.get("key"), result.subcommands.get("set").args.get("value")
try:
value = eval(value)
except:
pass
stored_config.config[key] = value
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}")
elif result.subcommands.get("get"):
key = result.subcommands.get("get").args.get("key")
file_config = load_from_yaml("config.yml")
reply = f"{ulang.get('liteyuki.current_config')}"
if key:
reply += f"```dotenv\n{key}={file_config.get(key, stored_config.config.get(key))}\n```"
else:
reply = f"{ulang.get('liteyuki.current_config')}"
reply += f"\n{ulang.get('liteyuki.static_config')}\n```dotenv"
for k, v in file_config.items():
reply += f"\n{k}={v}"
reply += "\n```"
if len(stored_config.config) > 0:
reply += f"\n{ulang.get('liteyuki.stored_config')}\n```dotenv"
for k, v in stored_config.config.items():
reply += f"\n{k}={v} {type(v)}"
reply += "\n```"
await md.send_md(reply, bot, event=event)
elif result.subcommands.get("remove"):
key = result.subcommands.get("remove").args.get("key")
if key in stored_config.config:
stored_config.config.pop(key)
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_remove_success', KEY=key)}")
else:
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
@on_alconna(
aliases={"切换图片模式"},
command=Alconna(
"switch-image-mode"
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(event: T_MessageEvent, matcher: Matcher):
global markdown_image
# 切换图片模式False以图片形式发送True以markdown形式发送
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
stored_config.config["markdown_image"] = not stored_config.config.get("markdown_image", False)
markdown_image = stored_config.config["markdown_image"]
common_db.save(stored_config)
await matcher.finish(
ulang.get("liteyuki.image_mode_on" if stored_config.config["markdown_image"] else "liteyuki.image_mode_off"))
@on_alconna(
command=Alconna(
"liteyuki-docs",
),
aliases={"轻雪文档"},
).handle()
# Satori OK
async def _(matcher: Matcher):
await matcher.finish("https://bot.liteyuki.icu/usage")
@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())
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}")
# system hook
@Bot.on_calling_api # 图片模式检测
async def test_for_md_image(bot: T_Bot, api: str, data: dict):
# 截获大图发送转换为markdown发送
if api in ["send_msg", "send_private_msg", "send_group_msg"] and markdown_image and data.get(
"user_id") != bot.self_id:
if api == "send_msg" and data.get("message_type") == "private" or api == "send_private_msg":
session_type = "private"
session_id = data.get("user_id")
elif api == "send_msg" and data.get("message_type") == "group" or api == "send_group_msg":
session_type = "group"
session_id = data.get("group_id")
else:
return
if len(data.get("message", [])) == 1 and data["message"][0].get("type") == "image":
file: str = data["message"][0].data.get("file")
# file:// http:// base64://
if file.startswith("http"):
result = await md.send_md(await md.image_async(file), bot, message_type=session_type,
session_id=session_id)
elif file.startswith("file"):
file = file.replace("file://", "")
result = await md.send_image(open(file, "rb").read(), bot, message_type=session_type,
session_id=session_id)
elif file.startswith("base64"):
file_bytes = base64.b64decode(file.replace("base64://", ""))
result = await md.send_image(file_bytes, bot, message_type=session_type, session_id=session_id)
else:
return
raise MockApiException(result=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) # 更新数据
@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 isinstance(bot, satori.Bot):
await bot.send_message(
channel_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time
)
else:
await bot.call_api(
"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
)
# 每天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}")
Reloader.reload(5)
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 base64
import time
from typing import Any, AnyStr
import nonebot
import pip
from nonebot import Bot, get_driver, require
from nonebot.adapters import satori
from nonebot.adapters.onebot.v11 import Message, escape, unescape
from nonebot.exception import MockApiException
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
from src.utils.base.config import get_config, load_from_yaml
from src.utils.base.data_manager import StoredConfig, 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 src.utils.base.reloader import Reloader
from src.utils import event as event_utils, satori_utils
from .api import update_liteyuki
from ..utils.base.ly_function import get_function
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler
driver = get_driver()
markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get("markdown_image", False)
@on_alconna(
command=Alconna(
"liteecho",
Args["text", str, ""],
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if result.main_args.get("text"):
await matcher.finish(Message(unescape(result.main_args.get("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):
# 使用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, event=event, at_sender=False)
@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.channel.id,
"delta_time" : 0
}
)
common_db.save(temp_data)
Reloader.reload(0)
@on_alconna(
aliases={"配置"},
command=Alconna(
"config",
Subcommand(
"set",
Args["key", str]["value", Any],
alias=["设置"],
),
Subcommand(
"get",
Args["key", str, None],
alias=["查询", "获取"]
),
Subcommand(
"remove",
Args["key", str],
alias=["删除"]
)
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
if result.subcommands.get("set"):
key, value = result.subcommands.get("set").args.get("key"), result.subcommands.get("set").args.get("value")
try:
value = eval(value)
except:
pass
stored_config.config[key] = value
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}")
elif result.subcommands.get("get"):
key = result.subcommands.get("get").args.get("key")
file_config = load_from_yaml("config.yml")
reply = f"{ulang.get('liteyuki.current_config')}"
if key:
reply += f"```dotenv\n{key}={file_config.get(key, stored_config.config.get(key))}\n```"
else:
reply = f"{ulang.get('liteyuki.current_config')}"
reply += f"\n{ulang.get('liteyuki.static_config')}\n```dotenv"
for k, v in file_config.items():
reply += f"\n{k}={v}"
reply += "\n```"
if len(stored_config.config) > 0:
reply += f"\n{ulang.get('liteyuki.stored_config')}\n```dotenv"
for k, v in stored_config.config.items():
reply += f"\n{k}={v} {type(v)}"
reply += "\n```"
await md.send_md(reply, bot, event=event)
elif result.subcommands.get("remove"):
key = result.subcommands.get("remove").args.get("key")
if key in stored_config.config:
stored_config.config.pop(key)
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_remove_success', KEY=key)}")
else:
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
@on_alconna(
aliases={"切换图片模式"},
command=Alconna(
"switch-image-mode"
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(event: T_MessageEvent, matcher: Matcher):
global markdown_image
# 切换图片模式False以图片形式发送True以markdown形式发送
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
stored_config.config["markdown_image"] = not stored_config.config.get("markdown_image", False)
markdown_image = stored_config.config["markdown_image"]
common_db.save(stored_config)
await matcher.finish(
ulang.get("liteyuki.image_mode_on" if stored_config.config["markdown_image"] else "liteyuki.image_mode_off"))
@on_alconna(
command=Alconna(
"liteyuki-docs",
),
aliases={"轻雪文档"},
).handle()
# Satori OK
async def _(matcher: Matcher):
await matcher.finish("https://bot.liteyuki.icu/usage")
@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())
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}")
# system hook
@Bot.on_calling_api # 图片模式检测
async def test_for_md_image(bot: T_Bot, api: str, data: dict):
# 截获大图发送转换为markdown发送
if api in ["send_msg", "send_private_msg", "send_group_msg"] and markdown_image and data.get(
"user_id") != bot.self_id:
if api == "send_msg" and data.get("message_type") == "private" or api == "send_private_msg":
session_type = "private"
session_id = data.get("user_id")
elif api == "send_msg" and data.get("message_type") == "group" or api == "send_group_msg":
session_type = "group"
session_id = data.get("group_id")
else:
return
if len(data.get("message", [])) == 1 and data["message"][0].get("type") == "image":
file: str = data["message"][0].data.get("file")
# file:// http:// base64://
if file.startswith("http"):
result = await md.send_md(await md.image_async(file), bot, message_type=session_type,
session_id=session_id)
elif file.startswith("file"):
file = file.replace("file://", "")
result = await md.send_image(open(file, "rb").read(), bot, message_type=session_type,
session_id=session_id)
elif file.startswith("base64"):
file_bytes = base64.b64decode(file.replace("base64://", ""))
result = await md.send_image(file_bytes, bot, message_type=session_type, session_id=session_id)
else:
return
raise MockApiException(result=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) # 更新数据
@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 isinstance(bot, satori.Bot):
await bot.send_message(
channel_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time
)
else:
await bot.call_api(
"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
)
# 每天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}")
Reloader.reload(5)
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,55 +1,59 @@
import nonebot
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.reloader import Reloader
from liteyuki.utils.base.resource import load_resources
if get_config("debug", False):
src_directories = (
"liteyuki/liteyuki_main",
"liteyuki/plugins",
"liteyuki/utils",
)
src_excludes_extensions = (
"pyc",
)
res_directories = (
"liteyuki/resources",
"resources",
)
nonebot.logger.info("Liteyuki Reload is enable, watching for file changes...")
class CodeModifiedHandler(FileSystemEventHandler):
"""
Handler for code file changes
"""
def on_modified(self, event):
if event.src_path.endswith(src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path:
return
nonebot.logger.info(f"{event.src_path} modified, reloading bot...")
Reloader.reload()
class ResourceModifiedHandler(FileSystemEventHandler):
"""
Handler for resource file changes
"""
def on_modified(self, event):
nonebot.logger.info(f"{event.src_path} modified, reloading resource...")
load_resources()
code_modified_handler = CodeModifiedHandler()
resource_modified_handle = ResourceModifiedHandler()
observer = Observer()
for directory in src_directories:
observer.schedule(code_modified_handler, directory, recursive=True)
for directory in res_directories:
observer.schedule(resource_modified_handle, directory, recursive=True)
observer.start()
import nonebot
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from src.utils.base.config import get_config
from src.utils.base.reloader import Reloader
from src.utils.base.resource import load_resources
if get_config("debug", False):
src_directories = (
"src/liteyuki_main",
"src/plugins",
"src/utils",
)
src_excludes_extensions = (
"pyc",
)
res_directories = (
"src/resources",
"resources",
)
nonebot.logger.info("Liteyuki Reload is enable, watching for file changes...")
class CodeModifiedHandler(FileSystemEventHandler):
"""
Handler for code file changes
"""
def on_modified(self, event):
if event.src_path.endswith(
src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path:
return
nonebot.logger.info(f"{event.src_path} modified, reloading bot...")
Reloader.reload()
class ResourceModifiedHandler(FileSystemEventHandler):
"""
Handler for resource file changes
"""
def on_modified(self, event):
nonebot.logger.info(f"{event.src_path} modified, reloading resource...")
load_resources()
code_modified_handler = CodeModifiedHandler()
resource_modified_handle = ResourceModifiedHandler()
observer = Observer()
for directory in src_directories:
observer.schedule(code_modified_handler, directory, recursive=True)
for directory in res_directories:
observer.schedule(resource_modified_handle, directory, recursive=True)
observer.start()

View File

@ -1,31 +1,31 @@
import nonebot.plugin
from nonebot import get_driver
from liteyuki.utils import init_log
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.data_manager import InstalledPlugin, plugin_db
from liteyuki.utils.base.resource import load_resources
from liteyuki.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("liteyuki/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 in loading database. please run `npm fixup` in chat to reinstall it.")
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 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("liteyuki/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 in loading database. please run `npm fixup` in chat to reinstall it.")
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 +1,16 @@
from nonebot.plugin import PluginMetadata
from .rt_guide import *
from .crt_matchers import *
__plugin_meta__ = PluginMetadata(
name="CRT生成工具",
description="一些CRT牌子生成器",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
from nonebot.plugin import PluginMetadata
from .rt_guide import *
from .crt_matchers import *
__plugin_meta__ = PluginMetadata(
name="CRT生成工具",
description="一些CRT牌子生成器",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)

View File

@ -1,78 +1,78 @@
from urllib.parse import quote
import aiohttp
from nonebot import require
from liteyuki.utils.event import get_user_id
from liteyuki.utils.base.language import Language
from liteyuki.utils.base.ly_typing import T_MessageEvent
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, Option
crt_cmd = on_alconna(
Alconna(
"crt",
Subcommand(
"route",
Args["start", str, "沙坪坝"]["end", str, "上新街"],
alias=("r",),
help_text="查询两地之间的地铁路线"
),
)
)
@crt_cmd.assign("route")
async def _(result: Arparma, event: T_MessageEvent):
# 获取语言
ulang = Language(get_user_id(event))
# 获取参数
# 你也别问我为什么要quote两次问就是CRT官网的锅只有这样才可以运行
start = quote(quote(result.other_args.get("start")))
end = quote(quote(result.other_args.get("end")))
# 判断参数语言
query_lang_code = ""
if start.isalpha() and end.isalpha():
query_lang_code = "Eng"
# 构造请求 URL
url = f"https://www.cqmetro.cn/Front/html/TakeLine!queryYs{query_lang_code}TakeLine.action?entity.startStaName={start}&entity.endStaName={end}"
# 请求数据
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
result = await resp.json()
# 检查结果/无则终止
if not result.get("result"):
await crt_cmd.send(ulang.get("crt.no_result"))
return
# 模板传参定义
templates = {
"data" : {
"result": result["result"],
},
"localization": ulang.get_many(
"crt.station",
"crt.hour",
"crt.minute",
)
}
# 生成图片
image = await template2image(
template=get_path("templates/crt_route.html"),
templates=templates,
debug=True
)
# 发送图片
await crt_cmd.send(UniMessage.image(raw=image))
from urllib.parse import quote
import aiohttp
from nonebot import require
from src.utils.event import get_user_id
from src.utils.base.language import Language
from src.utils.base.ly_typing import T_MessageEvent
from src.utils.base.resource import get_path
from src.utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, Option
crt_cmd = on_alconna(
Alconna(
"crt",
Subcommand(
"route",
Args["start", str, "沙坪坝"]["end", str, "上新街"],
alias=("r",),
help_text="查询两地之间的地铁路线"
),
)
)
@crt_cmd.assign("route")
async def _(result: Arparma, event: T_MessageEvent):
# 获取语言
ulang = Language(get_user_id(event))
# 获取参数
# 你也别问我为什么要quote两次问就是CRT官网的锅只有这样才可以运行
start = quote(quote(result.other_args.get("start")))
end = quote(quote(result.other_args.get("end")))
# 判断参数语言
query_lang_code = ""
if start.isalpha() and end.isalpha():
query_lang_code = "Eng"
# 构造请求 URL
url = f"https://www.cqmetro.cn/Front/html/TakeLine!queryYs{query_lang_code}TakeLine.action?entity.startStaName={start}&entity.endStaName={end}"
# 请求数据
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
result = await resp.json()
# 检查结果/无则终止
if not result.get("result"):
await crt_cmd.send(ulang.get("crt.no_result"))
return
# 模板传参定义
templates = {
"data" : {
"result": result["result"],
},
"localization": ulang.get_many(
"crt.station",
"crt.hour",
"crt.minute",
)
}
# 生成图片
image = await template2image(
template=get_path("templates/crt_route.html"),
templates=templates,
debug=True
)
# 发送图片
await crt_cmd.send(UniMessage.image(raw=image))

View File

@ -1,419 +1,419 @@
import json
from typing import List, Any
from PIL import Image
from arclet.alconna import Alconna
from nb_cli import run_sync
from nonebot import on_command
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage
from pydantic import BaseModel
from .canvas import *
from ...utils.base.resource import get_path
resolution = 256
class Entrance(BaseModel):
identifier: str
size: tuple[int, int]
dest: List[str]
class Station(BaseModel):
identifier: str
chineseName: str
englishName: str
position: tuple[int, int]
class Line(BaseModel):
identifier: str
chineseName: str
englishName: str
color: Any
stations: List["Station"]
font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2")
font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2")
@run_sync
def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution):
"""
Generates an entrance sign for the ride.
"""
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE))
# 加黑色图框
baseCanvas.outline = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0),
point=(0, 0),
img=Shape.rectangle(
size=(width, height),
fillet=0,
fill=(0, 0, 0, 0),
width=15,
outline=Color.BLACK
)
)
baseCanvas.contentPanel = Panel(
uv_size=(width, height),
box_size=(width - 28, height - 28),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
linePanelHeight = 0.7 * ratio[1]
linePanelWidth = linePanelHeight * 1.3
# 画线路面板部分
for i, line in enumerate(lineInfo):
linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel(
uv_size=ratio,
box_size=(linePanelWidth, linePanelHeight),
parent_point=(i * linePanelWidth / ratio[0], 1),
point=(0, 1),
)
linePanel.colorCube = Img(
uv_size=(1, 1),
box_size=(0.15, 1),
parent_point=(0.125, 1),
point=(0, 1),
img=Shape.rectangle(
size=(100, 100),
fillet=0,
fill=line.color,
),
keep_ratio=False
)
textPanel = linePanel.TextPanel = Panel(
uv_size=(1, 1),
box_size=(0.625, 1),
parent_point=(1, 1),
point=(1, 1)
)
# 中文线路名
textPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 2 / 3),
parent_point=(0, 0),
point=(0, 0),
)
nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i))
textPanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.chineseName,
color=Color.BLACK,
font_size=int(nameSize[1] * 0.5),
force_size=True,
font=font_bold
)
# 英文线路名
textPanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 1 / 3),
parent_point=(0, 1),
point=(0, 1),
)
englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i))
textPanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.englishName,
color=Color.BLACK,
font_size=int(englishNameSize[1] * 0.6),
force_size=True,
font=font_light
)
# 画名称部分
namePanel = baseCanvas.contentPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.4),
parent_point=(0.5, 0),
point=(0.5, 0),
)
namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=name,
color=Color.BLACK,
font_size=int(height * 0.3),
force_size=True,
font=font_bold
)
aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(0.5, 1),
point=(0.5, 1),
)
for j, alias in enumerate(aliases):
aliasesPanel.__dict__[alias] = Text(
uv_size=(1, 1),
box_size=(0.35, 0.5),
parent_point=(0.5, 0.5 * j),
point=(0.5, 0),
text=alias,
color=Color.BLACK,
font_size=int(height * 0.15),
font=font_light
)
# 画入口标识
entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel(
uv_size=(1, 1),
box_size=(0.2, 1),
parent_point=(1, 0.5),
point=(1, 0.5),
)
# 中文文本
entrancePanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 0),
point=(1, 0),
)
entrancePanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"{entranceIdentifier}出入口",
color=Color.BLACK,
font_size=int(height * 0.2),
force_size=True,
font=font_bold
)
# 英文文本
entrancePanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 1),
point=(1, 1),
)
entrancePanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"Entrance {entranceIdentifier}",
color=Color.BLACK,
font_size=int(height * 0.15),
force_size=True,
font=font_light
)
return baseCanvas.base_img.tobytes()
crt_alc = on_alconna(
Alconna(
"crt",
Subcommand(
"entrance",
Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A
)
)
)
@crt_alc.assign("entrance")
async def _(result: Arparma):
args = result.subcommands.get("entrance").args
name = args["name"]
lines = args["lines"]
entrance = args["entrance"]
line_info = []
for line in lines.split(","):
line_args = line.split("&")
line_info.append(Line(
identifier=1,
chineseName=line_args[0],
englishName=line_args[1],
color=line_args[2],
stations=[]
))
img_bytes = await generate_entrance_sign(
name=name,
aliases=name.split("&"),
lineInfo=line_info,
entranceIdentifier=entrance,
ratio=(8, 1),
reso=256,
)
await crt_alc.finish(
UniMessage.image(raw=img_bytes)
)
def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution):
"""
生成站台线路图
:param line: 线路对象
:param station: 本站点对象
:param ratio: 比例
:param reso: 分辨率1reso
:return: 两个方向的站牌
"""
if ratio is None:
ratio = [4, 1]
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW))
# 加黑色图框
baseCanvas.linePanel = Panel(
uv_size=(1, 1),
box_size=(0.8, 0.15),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
# 直线块
baseCanvas.linePanel.recLine = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=line.color,
),
keep_ratio=False
)
# 灰色直线块
baseCanvas.linePanel.recLineGrey = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=Color.GREY,
),
keep_ratio=False
)
# 生成各站圆点
outline_width = 40
circleForward = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
)
circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0)))
circleThisPanel.circleOuter = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
),
)
circleThisPanel.circleOuter.circleInner = Img(
uv_size=(1, 1),
box_size=(0.7, 0.7),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=line.color,
width=0,
outline=line.color,
),
)
circleThisPanel.export("a.png", alpha=True)
circleThis = circleThisPanel.base_img
circlePassed = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=Color.GREY,
)
arrival = False
distance = 1 / (len(line.stations) - 1)
for i, sta in enumerate(line.stations):
box_size = (1.618, 1.618)
if sta.identifier == station.identifier:
arrival = True
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=(1.8, 1.8),
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleThis,
keep_ratio=True
)
continue
if arrival:
# 后方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleForward,
keep_ratio=True
)
else:
# 前方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circlePassed,
keep_ratio=True
)
return baseCanvas
def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution
):
pass
# def main():
# generate_entrance_sign(
# "璧山",
# aliases=["Bishan"],
# lineInfo=[
#
# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]),
# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]),
# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]),
# ],
# entranceIdentifier="1",
# ratio=(8, 1)
# )
#
#
# main()
import json
from typing import List, Any
from PIL import Image
from arclet.alconna import Alconna
from nb_cli import run_sync
from nonebot import on_command
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage
from pydantic import BaseModel
from .canvas import *
from ...utils.base.resource import get_path
resolution = 256
class Entrance(BaseModel):
identifier: str
size: tuple[int, int]
dest: List[str]
class Station(BaseModel):
identifier: str
chineseName: str
englishName: str
position: tuple[int, int]
class Line(BaseModel):
identifier: str
chineseName: str
englishName: str
color: Any
stations: List["Station"]
font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2")
font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2")
@run_sync
def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution):
"""
Generates an entrance sign for the ride.
"""
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE))
# 加黑色图框
baseCanvas.outline = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0),
point=(0, 0),
img=Shape.rectangle(
size=(width, height),
fillet=0,
fill=(0, 0, 0, 0),
width=15,
outline=Color.BLACK
)
)
baseCanvas.contentPanel = Panel(
uv_size=(width, height),
box_size=(width - 28, height - 28),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
linePanelHeight = 0.7 * ratio[1]
linePanelWidth = linePanelHeight * 1.3
# 画线路面板部分
for i, line in enumerate(lineInfo):
linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel(
uv_size=ratio,
box_size=(linePanelWidth, linePanelHeight),
parent_point=(i * linePanelWidth / ratio[0], 1),
point=(0, 1),
)
linePanel.colorCube = Img(
uv_size=(1, 1),
box_size=(0.15, 1),
parent_point=(0.125, 1),
point=(0, 1),
img=Shape.rectangle(
size=(100, 100),
fillet=0,
fill=line.color,
),
keep_ratio=False
)
textPanel = linePanel.TextPanel = Panel(
uv_size=(1, 1),
box_size=(0.625, 1),
parent_point=(1, 1),
point=(1, 1)
)
# 中文线路名
textPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 2 / 3),
parent_point=(0, 0),
point=(0, 0),
)
nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i))
textPanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.chineseName,
color=Color.BLACK,
font_size=int(nameSize[1] * 0.5),
force_size=True,
font=font_bold
)
# 英文线路名
textPanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 1 / 3),
parent_point=(0, 1),
point=(0, 1),
)
englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i))
textPanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.englishName,
color=Color.BLACK,
font_size=int(englishNameSize[1] * 0.6),
force_size=True,
font=font_light
)
# 画名称部分
namePanel = baseCanvas.contentPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.4),
parent_point=(0.5, 0),
point=(0.5, 0),
)
namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=name,
color=Color.BLACK,
font_size=int(height * 0.3),
force_size=True,
font=font_bold
)
aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(0.5, 1),
point=(0.5, 1),
)
for j, alias in enumerate(aliases):
aliasesPanel.__dict__[alias] = Text(
uv_size=(1, 1),
box_size=(0.35, 0.5),
parent_point=(0.5, 0.5 * j),
point=(0.5, 0),
text=alias,
color=Color.BLACK,
font_size=int(height * 0.15),
font=font_light
)
# 画入口标识
entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel(
uv_size=(1, 1),
box_size=(0.2, 1),
parent_point=(1, 0.5),
point=(1, 0.5),
)
# 中文文本
entrancePanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 0),
point=(1, 0),
)
entrancePanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"{entranceIdentifier}出入口",
color=Color.BLACK,
font_size=int(height * 0.2),
force_size=True,
font=font_bold
)
# 英文文本
entrancePanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 1),
point=(1, 1),
)
entrancePanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"Entrance {entranceIdentifier}",
color=Color.BLACK,
font_size=int(height * 0.15),
force_size=True,
font=font_light
)
return baseCanvas.base_img.tobytes()
crt_alc = on_alconna(
Alconna(
"crt",
Subcommand(
"entrance",
Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A
)
)
)
@crt_alc.assign("entrance")
async def _(result: Arparma):
args = result.subcommands.get("entrance").args
name = args["name"]
lines = args["lines"]
entrance = args["entrance"]
line_info = []
for line in lines.split(","):
line_args = line.split("&")
line_info.append(Line(
identifier=1,
chineseName=line_args[0],
englishName=line_args[1],
color=line_args[2],
stations=[]
))
img_bytes = await generate_entrance_sign(
name=name,
aliases=name.split("&"),
lineInfo=line_info,
entranceIdentifier=entrance,
ratio=(8, 1),
reso=256,
)
await crt_alc.finish(
UniMessage.image(raw=img_bytes)
)
def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution):
"""
生成站台线路图
:param line: 线路对象
:param station: 本站点对象
:param ratio: 比例
:param reso: 分辨率1reso
:return: 两个方向的站牌
"""
if ratio is None:
ratio = [4, 1]
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW))
# 加黑色图框
baseCanvas.linePanel = Panel(
uv_size=(1, 1),
box_size=(0.8, 0.15),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
# 直线块
baseCanvas.linePanel.recLine = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=line.color,
),
keep_ratio=False
)
# 灰色直线块
baseCanvas.linePanel.recLineGrey = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=Color.GREY,
),
keep_ratio=False
)
# 生成各站圆点
outline_width = 40
circleForward = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
)
circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0)))
circleThisPanel.circleOuter = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
),
)
circleThisPanel.circleOuter.circleInner = Img(
uv_size=(1, 1),
box_size=(0.7, 0.7),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=line.color,
width=0,
outline=line.color,
),
)
circleThisPanel.export("a.png", alpha=True)
circleThis = circleThisPanel.base_img
circlePassed = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=Color.GREY,
)
arrival = False
distance = 1 / (len(line.stations) - 1)
for i, sta in enumerate(line.stations):
box_size = (1.618, 1.618)
if sta.identifier == station.identifier:
arrival = True
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=(1.8, 1.8),
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleThis,
keep_ratio=True
)
continue
if arrival:
# 后方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleForward,
keep_ratio=True
)
else:
# 前方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circlePassed,
keep_ratio=True
)
return baseCanvas
def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution
):
pass
# def main():
# generate_entrance_sign(
# "璧山",
# aliases=["Bishan"],
# lineInfo=[
#
# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]),
# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]),
# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]),
# ],
# entranceIdentifier="1",
# ratio=(8, 1)
# )
#
#
# main()

View File

@ -1,125 +1,125 @@
import nonebot
from nonebot import on_message, require
from nonebot.plugin import PluginMetadata
from liteyuki.utils.base.data import Database, LiteModel
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna
from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand
class Node(LiteModel):
TABLE_NAME: str = "node"
bot_id: str = ""
session_type: str = ""
session_id: str = ""
def __str__(self):
return f"{self.bot_id}.{self.session_type}.{self.session_id}"
class Push(LiteModel):
TABLE_NAME: str = "push"
source: Node = Node()
target: Node = Node()
inde: int = 0
pushes_db = Database("data/pushes.ldb")
pushes_db.auto_migrate(Push(), Node())
alc = Alconna(
"lep",
Subcommand(
"add",
Args["source", str],
Args["target", str],
Option("bidirectional", Args["bidirectional", bool])
),
Subcommand(
"rm",
Args["index", int],
),
Subcommand(
"list",
)
)
add_push = on_alconna(alc)
@add_push.handle()
async def _(result: Arparma):
"""bot_id.session_type.session_id"""
if result.subcommands.get("add"):
source = result.subcommands["add"].args.get("source")
target = result.subcommands["add"].args.get("target")
if source and target:
source = source.split(".")
target = target.split(".")
push1 = Push(
source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
inde=len(pushes_db.where_all(Push(), default=[]))
)
pushes_db.save(push1)
if result.subcommands["add"].args.get("bidirectional"):
push2 = Push(
source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
inde=len(pushes_db.where_all(Push(), default=[]))
)
pushes_db.save(push2)
await add_push.finish("添加成功")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("rm"):
index = result.subcommands["rm"].args.get("index")
if index is not None:
try:
pushes_db.delete(Push(), "inde = ?", index)
await add_push.finish("删除成功")
except IndexError:
await add_push.finish("索引错误")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("list"):
await add_push.finish(
"\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> "
f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in
enumerate(pushes_db.where_all(Push(), default=[]))]))
else:
await add_push.finish("参数错误")
@on_message(block=False).handle()
async def _(event: T_MessageEvent, bot: T_Bot):
for push in pushes_db.where_all(Push(), default=[]):
if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}":
bot2 = nonebot.get_bot(push.target.bot_id)
msg_formatted = ""
for line in str(event.message).split("\n"):
msg_formatted += f"**{line.strip()}**\n"
push_message = (
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
f"{msg_formatted}")
await md.send_md(push_message, bot2, message_type=push.target.session_type,
session_id=push.target.session_id)
return
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪事件推送",
description="事件推送插件支持单向和双向推送支持跨Bot推送",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)
import nonebot
from nonebot import on_message, require
from nonebot.plugin import PluginMetadata
from src.utils.base.data import Database, LiteModel
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna
from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand
class Node(LiteModel):
TABLE_NAME: str = "node"
bot_id: str = ""
session_type: str = ""
session_id: str = ""
def __str__(self):
return f"{self.bot_id}.{self.session_type}.{self.session_id}"
class Push(LiteModel):
TABLE_NAME: str = "push"
source: Node = Node()
target: Node = Node()
inde: int = 0
pushes_db = Database("data/pushes.ldb")
pushes_db.auto_migrate(Push(), Node())
alc = Alconna(
"lep",
Subcommand(
"add",
Args["source", str],
Args["target", str],
Option("bidirectional", Args["bidirectional", bool])
),
Subcommand(
"rm",
Args["index", int],
),
Subcommand(
"list",
)
)
add_push = on_alconna(alc)
@add_push.handle()
async def _(result: Arparma):
"""bot_id.session_type.session_id"""
if result.subcommands.get("add"):
source = result.subcommands["add"].args.get("source")
target = result.subcommands["add"].args.get("target")
if source and target:
source = source.split(".")
target = target.split(".")
push1 = Push(
source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
inde=len(pushes_db.where_all(Push(), default=[]))
)
pushes_db.save(push1)
if result.subcommands["add"].args.get("bidirectional"):
push2 = Push(
source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
inde=len(pushes_db.where_all(Push(), default=[]))
)
pushes_db.save(push2)
await add_push.finish("添加成功")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("rm"):
index = result.subcommands["rm"].args.get("index")
if index is not None:
try:
pushes_db.delete(Push(), "inde = ?", index)
await add_push.finish("删除成功")
except IndexError:
await add_push.finish("索引错误")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("list"):
await add_push.finish(
"\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> "
f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in
enumerate(pushes_db.where_all(Push(), default=[]))]))
else:
await add_push.finish("参数错误")
@on_message(block=False).handle()
async def _(event: T_MessageEvent, bot: T_Bot):
for push in pushes_db.where_all(Push(), default=[]):
if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}":
bot2 = nonebot.get_bot(push.target.bot_id)
msg_formatted = ""
for line in str(event.message).split("\n"):
msg_formatted += f"**{line.strip()}**\n"
push_message = (
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
f"{msg_formatted}")
await md.send_md(push_message, bot2, message_type=push.target.session_type,
session_id=push.target.session_id)
return
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪事件推送",
description="事件推送插件支持单向和双向推送支持跨Bot推送",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -1,52 +1,52 @@
from nonebot import on_command, require
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent, v11
from liteyuki.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from liteyuki.utils.message.html_tool import *
md_test = on_command("mdts", permission=SUPERUSER)
btn_test = on_command("btnts", permission=SUPERUSER)
latex_test = on_command("latex", permission=SUPERUSER)
@md_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_md(
v11.utils.unescape(str(arg)),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@btn_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_btn(
str(arg),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@latex_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
latex_text = f"$${v11.utils.unescape(str(arg))}$$"
img = await md_to_pic(latex_text)
await bot.send(event=event, message=MessageSegment.image(img))
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪Markdown测试",
description="用于测试Markdown的插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)
from nonebot import on_command, require
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from src.utils.base.ly_typing import T_Bot, T_MessageEvent, v11
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from src.utils.message.html_tool import *
md_test = on_command("mdts", permission=SUPERUSER)
btn_test = on_command("btnts", permission=SUPERUSER)
latex_test = on_command("latex", permission=SUPERUSER)
@md_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_md(
v11.utils.unescape(str(arg)),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@btn_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_btn(
str(arg),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@latex_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
latex_text = f"$${v11.utils.unescape(str(arg))}$$"
img = await md_to_pic(latex_text)
await bot.send(event=event, message=MessageSegment.image(img))
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪Markdown测试",
description="用于测试Markdown的插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -1,15 +1,15 @@
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
__plugin_meta__ = PluginMetadata(
name="Minecraft工具箱",
description="一些Minecraft相关工具箱",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
__plugin_meta__ = PluginMetadata(
name="Minecraft工具箱",
description="一些Minecraft相关工具箱",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)

View File

@ -1,15 +1,15 @@
from nonebot.plugin import PluginMetadata
from .minesweeper import *
__plugin_meta__ = PluginMetadata(
name="轻雪小游戏",
description="内置了一些小游戏",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)
from nonebot.plugin import PluginMetadata
from .minesweeper import *
__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,169 +1,169 @@
import random
from pydantic import BaseModel
from liteyuki.utils.message.message import MarkdownMessage as md
class Dot(BaseModel):
row: int
col: int
mask: bool = True
value: int = 0
flagged: bool = False
class Minesweeper:
# 0-8: number of mines around, 9: mine, -1: undefined
NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
MASK = "🅜"
FLAG = "🅕"
MINE = "🅑"
def __init__(self, rows, cols, num_mines, session_type, session_id):
assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols
self.session_type = session_type
self.session_id = session_id
self.rows = rows
self.cols = cols
self.num_mines = num_mines
self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)]
self.is_first = True
def reveal(self, row, col) -> bool:
"""
展开
Args:
row:
col:
Returns:
游戏是否继续
"""
if self.is_first:
# 第一次展开,生成地雷
self.generate_board(self.board[row][col])
self.is_first = False
if self.board[row][col].value == 9:
self.board[row][col].mask = False
return False
if not self.board[row][col].mask:
return True
self.board[row][col].mask = False
if self.board[row][col].value == 0:
self.reveal_neighbors(row, col)
return True
def is_win(self) -> bool:
"""
是否胜利
Returns:
"""
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].mask and self.board[row][col].value != 9:
return False
return True
def generate_board(self, first_dot: Dot):
"""
避开第一个点生成地雷
Args:
first_dot: 第一个点
Returns:
"""
generate_count = 0
while generate_count < self.num_mines:
row = random.randint(0, self.rows - 1)
col = random.randint(0, self.cols - 1)
if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col):
continue
self.board[row][col] = Dot(row=row, col=col, mask=True, value=9)
generate_count += 1
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].value != 9:
self.board[row][col].value = self.count_adjacent_mines(row, col)
def count_adjacent_mines(self, row, col):
"""
计算周围地雷数量
Args:
row:
col:
Returns:
"""
count = 0
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].value == 9:
count += 1
return count
def reveal_neighbors(self, row, col):
"""
递归展开使用深度优先搜索
Args:
row:
col:
Returns:
"""
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].mask:
self.board[r][c].mask = False
if self.board[r][c].value == 0:
self.reveal_neighbors(r, c)
def mark(self, row, col) -> bool:
"""
标记
Args:
row:
col:
Returns:
是否标记成功如果已经展开则无法标记
"""
if self.board[row][col].mask:
self.board[row][col].flagged = not self.board[row][col].flagged
return self.board[row][col].flagged
def board_markdown(self) -> str:
"""
打印地雷板
Returns:
"""
dis = " "
start = "> " if self.cols >= 10 else ""
text = start + self.NUMS[0] + dis*2
# 横向两个雷之间的间隔字符
# 生成横向索引
for i in range(self.cols):
text += f"{self.NUMS[i]}" + dis
text += "\n\n"
for i, row in enumerate(self.board):
text += start + f"{self.NUMS[i]}" + dis*2
print([d.value for d in row])
for dot in row:
if dot.mask and not dot.flagged:
text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}")
elif dot.flagged:
text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}")
else:
text += self.NUMS[dot.value]
text += dis
text += "\n"
btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False)
btn_end = md.btn_cmd("结束", "minesweeper end", enter=True)
text += f" {btn_mark} {btn_end}"
return text
import random
from pydantic import BaseModel
from src.utils.message.message import MarkdownMessage as md
class Dot(BaseModel):
row: int
col: int
mask: bool = True
value: int = 0
flagged: bool = False
class Minesweeper:
# 0-8: number of mines around, 9: mine, -1: undefined
NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
MASK = "🅜"
FLAG = "🅕"
MINE = "🅑"
def __init__(self, rows, cols, num_mines, session_type, session_id):
assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols
self.session_type = session_type
self.session_id = session_id
self.rows = rows
self.cols = cols
self.num_mines = num_mines
self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)]
self.is_first = True
def reveal(self, row, col) -> bool:
"""
展开
Args:
row:
col:
Returns:
游戏是否继续
"""
if self.is_first:
# 第一次展开,生成地雷
self.generate_board(self.board[row][col])
self.is_first = False
if self.board[row][col].value == 9:
self.board[row][col].mask = False
return False
if not self.board[row][col].mask:
return True
self.board[row][col].mask = False
if self.board[row][col].value == 0:
self.reveal_neighbors(row, col)
return True
def is_win(self) -> bool:
"""
是否胜利
Returns:
"""
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].mask and self.board[row][col].value != 9:
return False
return True
def generate_board(self, first_dot: Dot):
"""
避开第一个点生成地雷
Args:
first_dot: 第一个点
Returns:
"""
generate_count = 0
while generate_count < self.num_mines:
row = random.randint(0, self.rows - 1)
col = random.randint(0, self.cols - 1)
if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col):
continue
self.board[row][col] = Dot(row=row, col=col, mask=True, value=9)
generate_count += 1
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].value != 9:
self.board[row][col].value = self.count_adjacent_mines(row, col)
def count_adjacent_mines(self, row, col):
"""
计算周围地雷数量
Args:
row:
col:
Returns:
"""
count = 0
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].value == 9:
count += 1
return count
def reveal_neighbors(self, row, col):
"""
递归展开使用深度优先搜索
Args:
row:
col:
Returns:
"""
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].mask:
self.board[r][c].mask = False
if self.board[r][c].value == 0:
self.reveal_neighbors(r, c)
def mark(self, row, col) -> bool:
"""
标记
Args:
row:
col:
Returns:
是否标记成功如果已经展开则无法标记
"""
if self.board[row][col].mask:
self.board[row][col].flagged = not self.board[row][col].flagged
return self.board[row][col].flagged
def board_markdown(self) -> str:
"""
打印地雷板
Returns:
"""
dis = " "
start = "> " if self.cols >= 10 else ""
text = start + self.NUMS[0] + dis*2
# 横向两个雷之间的间隔字符
# 生成横向索引
for i in range(self.cols):
text += f"{self.NUMS[i]}" + dis
text += "\n\n"
for i, row in enumerate(self.board):
text += start + f"{self.NUMS[i]}" + dis*2
print([d.value for d in row])
for dot in row:
if dot.mask and not dot.flagged:
text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}")
elif dot.flagged:
text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}")
else:
text += self.NUMS[dot.value]
text += dis
text += "\n"
btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False)
btn_end = md.btn_cmd("结束", "minesweeper end", enter=True)
text += f" {btn_mark} {btn_end}"
return text

View File

@ -1,103 +1,103 @@
from nonebot import require
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from .game import Minesweeper
from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma
minesweeper = on_alconna(
aliases={"扫雷"},
command=Alconna(
"minesweeper",
Subcommand(
"start",
Args["row", int, 8]["col", int, 8]["mines", int, 10],
alias=["开始"],
),
Subcommand(
"end",
alias=["结束"]
),
Subcommand(
"reveal",
Args["row", int]["col", int],
alias=["展开"]
),
Subcommand(
"mark",
Args["row", int]["col", int],
alias=["标记"]
),
),
)
minesweeper_cache: list[Minesweeper] = []
def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None:
for i in minesweeper_cache:
if i.session_type == event.message_type:
if i.session_id == event.user_id or i.session_id == event.group_id:
return i
return None
@minesweeper.handle()
async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
game = get_minesweeper_cache(event)
if result.subcommands.get("start"):
if game:
await minesweeper.finish("当前会话不能同时进行多个扫雷游戏")
else:
try:
new_game = Minesweeper(
rows=result.subcommands["start"].args["row"],
cols=result.subcommands["start"].args["col"],
num_mines=result.subcommands["start"].args["mines"],
session_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id,
)
minesweeper_cache.append(new_game)
await minesweeper.send("游戏开始")
await md.send_md(new_game.board_markdown(), bot, event=event)
except AssertionError:
await minesweeper.finish("参数错误")
elif result.subcommands.get("end"):
if game:
minesweeper_cache.remove(game)
await minesweeper.finish("游戏结束")
else:
await minesweeper.finish("当前没有扫雷游戏")
elif result.subcommands.get("reveal"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["reveal"].args["row"]
col = result.subcommands["reveal"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
if not game.reveal(row, col):
minesweeper_cache.remove(game)
await md.send_md(game.board_markdown(), bot, event=event)
await minesweeper.finish("游戏结束")
await md.send_md(game.board_markdown(), bot, event=event)
if game.is_win():
minesweeper_cache.remove(game)
await minesweeper.finish("游戏胜利")
elif result.subcommands.get("mark"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["mark"].args["row"]
col = result.subcommands["mark"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
game.board[row][col].flagged = not game.board[row][col].flagged
await md.send_md(game.board_markdown(), bot, event=event)
else:
await minesweeper.finish("参数错误")
from nonebot import require
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from .game import Minesweeper
from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma
minesweeper = on_alconna(
aliases={"扫雷"},
command=Alconna(
"minesweeper",
Subcommand(
"start",
Args["row", int, 8]["col", int, 8]["mines", int, 10],
alias=["开始"],
),
Subcommand(
"end",
alias=["结束"]
),
Subcommand(
"reveal",
Args["row", int]["col", int],
alias=["展开"]
),
Subcommand(
"mark",
Args["row", int]["col", int],
alias=["标记"]
),
),
)
minesweeper_cache: list[Minesweeper] = []
def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None:
for i in minesweeper_cache:
if i.session_type == event.message_type:
if i.session_id == event.user_id or i.session_id == event.group_id:
return i
return None
@minesweeper.handle()
async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
game = get_minesweeper_cache(event)
if result.subcommands.get("start"):
if game:
await minesweeper.finish("当前会话不能同时进行多个扫雷游戏")
else:
try:
new_game = Minesweeper(
rows=result.subcommands["start"].args["row"],
cols=result.subcommands["start"].args["col"],
num_mines=result.subcommands["start"].args["mines"],
session_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id,
)
minesweeper_cache.append(new_game)
await minesweeper.send("游戏开始")
await md.send_md(new_game.board_markdown(), bot, event=event)
except AssertionError:
await minesweeper.finish("参数错误")
elif result.subcommands.get("end"):
if game:
minesweeper_cache.remove(game)
await minesweeper.finish("游戏结束")
else:
await minesweeper.finish("当前没有扫雷游戏")
elif result.subcommands.get("reveal"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["reveal"].args["row"]
col = result.subcommands["reveal"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
if not game.reveal(row, col):
minesweeper_cache.remove(game)
await md.send_md(game.board_markdown(), bot, event=event)
await minesweeper.finish("游戏结束")
await md.send_md(game.board_markdown(), bot, event=event)
if game.is_win():
minesweeper_cache.remove(game)
await minesweeper.finish("游戏胜利")
elif result.subcommands.get("mark"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["mark"].args["row"]
col = result.subcommands["mark"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
game.board[row][col].flagged = not game.board[row][col].flagged
await md.send_md(game.board_markdown(), bot, event=event)
else:
await minesweeper.finish("参数错误")

View File

@ -1,20 +1,20 @@
from nonebot.plugin import PluginMetadata
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪包管理器v2",
description="详细看文档",
usage=(
"npm list\n"
"npm enable/disable <plugin_name>\n"
"npm search <keywords...>\n"
"npm install/uninstall <plugin_name>\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : False,
}
)
from nonebot.plugin import PluginMetadata
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪包管理器v2",
description="详细看文档",
usage=(
"npm list\n"
"npm enable/disable <plugin_name>\n"
"npm search <keywords...>\n"
"npm install/uninstall <plugin_name>\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : False,
}
)

View File

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

View File

@ -1,256 +1,256 @@
import json
from typing import Optional
import aiofiles
import nonebot.plugin
from nonebot.adapters import satori
from liteyuki.utils import event as event_utils
from liteyuki.utils.base.data import LiteModel
from liteyuki.utils.base.data_manager import GlobalPlugin, Group, User, group_db, plugin_db, user_db
from liteyuki.utils.base.ly_typing import T_MessageEvent
__group_data = {} # 群数据缓存, {group_id: Group}
__user_data = {} # 用户数据缓存, {user_id: User}
__default_enable = {} # 插件默认启用状态缓存, {plugin_name: bool} static
__global_enable = {} # 插件全局启用状态缓存, {plugin_name: bool} dynamic
class PluginTag(LiteModel):
label: str
color: str = '#000000'
class StorePlugin(LiteModel):
name: str
desc: str
module_name: str # 插件商店中的模块名不等于本地的模块名,前者是文件夹名,后者是点分割模块名
project_link: str = ""
homepage: str = ""
author: str = ""
type: str | None = None
version: str | None = ""
time: str = ""
tags: list[PluginTag] = []
is_official: bool = False
def get_plugin_exist(plugin_name: str) -> bool:
"""
获取插件是否存在于加载列表
Args:
plugin_name:
Returns:
"""
for plugin in nonebot.plugin.get_loaded_plugins():
if plugin.name == plugin_name:
return True
return False
async def get_store_plugin(plugin_name: str) -> Optional[StorePlugin]:
"""
获取插件信息
Args:
plugin_name (str): 插件模块名
Returns:
Optional[StorePlugin]: 插件信息
"""
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
for plugin in plugins:
if plugin.module_name == plugin_name:
return plugin
return None
def get_plugin_default_enable(plugin_name: str) -> bool:
"""
获取插件默认启用状态由插件定义不存在则默认为启用优先从缓存中获取
Args:
plugin_name (str): 插件模块名
Returns:
bool: 插件默认状态
"""
if plugin_name not in __default_enable:
plug = nonebot.plugin.get_plugin(plugin_name)
default_enable = (plug.metadata.extra.get("default_enable", True) if plug.metadata else True) if plug else True
__default_enable[plugin_name] = default_enable
return __default_enable[plugin_name]
def get_plugin_session_enable(event: T_MessageEvent, plugin_name: str) -> bool:
"""
获取插件当前会话启用状态
Args:
event: 会话事件
plugin_name (str): 插件模块名
Returns:
bool: 插件当前状态
"""
if isinstance(event, satori.event.Event):
if event.guild is not None:
message_type = "group"
else:
message_type = "private"
else:
message_type = event.message_type
if message_type == "group":
group_id = str(event.guild.id if isinstance(event, satori.event.Event) else event.group_id)
if group_id not in __group_data:
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
__group_data[str(group_id)] = group
session = __group_data[group_id]
else:
# session: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id)))
user_id = str(event.user.id if isinstance(event, satori.event.Event) else event.user_id)
if user_id not in __user_data:
user: User = user_db.where_one(User(), "user_id = ?", user_id, default=User(user_id=user_id))
__user_data[user_id] = user
session = __user_data[user_id]
# 默认停用插件在启用列表内表示启用
# 默认停用插件不在启用列表内表示停用
# 默认启用插件在停用列表内表示停用
# 默认启用插件不在停用列表内表示启用
default_enable = get_plugin_default_enable(plugin_name)
if default_enable:
return plugin_name not in session.disabled_plugins
else:
return plugin_name in session.enabled_plugins
def set_plugin_session_enable(event: T_MessageEvent, plugin_name: str, enable: bool):
"""
设置插件会话启用状态同时更新数据库和缓存
Args:
event:
plugin_name:
enable:
Returns:
"""
if event_utils.get_message_type(event) == "group":
session: Group = group_db.where_one(Group(), "group_id = ?", str(event_utils.get_group_id(event)),
default=Group(group_id=str(event_utils.get_group_id(event))))
else:
session: User = user_db.where_one(User(), "user_id = ?", str(event_utils.get_user_id(event)),
default=User(user_id=str(event_utils.get_user_id(event))))
print(session)
default_enable = get_plugin_default_enable(plugin_name)
if default_enable:
if enable:
session.disabled_plugins.remove(plugin_name)
else:
session.disabled_plugins.append(plugin_name)
else:
if enable:
session.enabled_plugins.append(plugin_name)
else:
session.enabled_plugins.remove(plugin_name)
if event_utils.get_message_type(event) == "group":
__group_data[str(event_utils.get_group_id(event))] = session
group_db.save(session)
else:
__user_data[str(event_utils.get_user_id(event))] = session
user_db.save(session)
def get_plugin_global_enable(plugin_name: str) -> bool:
"""
获取插件全局启用状态, 优先从缓存中获取
Args:
plugin_name:
Returns:
"""
if plugin_name not in __global_enable:
plugin = plugin_db.where_one(
GlobalPlugin(),
"module_name = ?",
plugin_name,
default=GlobalPlugin(module_name=plugin_name, enabled=True))
__global_enable[plugin_name] = plugin.enabled
return __global_enable[plugin_name]
def set_plugin_global_enable(plugin_name: str, enable: bool):
"""
设置插件全局启用状态同时更新数据库和缓存
Args:
plugin_name:
enable:
Returns:
"""
plugin = plugin_db.where_one(
GlobalPlugin(),
"module_name = ?",
plugin_name,
default=GlobalPlugin(module_name=plugin_name, enabled=True))
plugin.enabled = enable
plugin_db.save(plugin)
__global_enable[plugin_name] = enable
def get_plugin_can_be_toggle(plugin_name: str) -> bool:
"""
获取插件是否可以被启用/停用
Args:
plugin_name (str): 插件模块名
Returns:
bool: 插件是否可以被启用/停用
"""
plug = nonebot.plugin.get_plugin(plugin_name)
return plug.metadata.extra.get("toggleable", True) if plug and plug.metadata else True
def get_group_enable(group_id: str) -> bool:
"""
获取群组是否启用插机器人
Args:
group_id (str): 群组ID
Returns:
bool: 群组是否启用插件
"""
group_id = str(group_id)
if group_id not in __group_data:
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
__group_data[group_id] = group
return __group_data[group_id].enable
def set_group_enable(group_id: str, enable: bool):
"""
设置群组是否启用插机器人
Args:
group_id (str): 群组ID
enable (bool): 是否启用
"""
group_id = str(group_id)
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
group.enable = enable
__group_data[group_id] = group
group_db.save(group)
import json
from typing import Optional
import aiofiles
import nonebot.plugin
from nonebot.adapters import satori
from src.utils import event as event_utils
from src.utils.base.data import LiteModel
from src.utils.base.data_manager import GlobalPlugin, Group, User, group_db, plugin_db, user_db
from src.utils.base.ly_typing import T_MessageEvent
__group_data = {} # 群数据缓存, {group_id: Group}
__user_data = {} # 用户数据缓存, {user_id: User}
__default_enable = {} # 插件默认启用状态缓存, {plugin_name: bool} static
__global_enable = {} # 插件全局启用状态缓存, {plugin_name: bool} dynamic
class PluginTag(LiteModel):
label: str
color: str = '#000000'
class StorePlugin(LiteModel):
name: str
desc: str
module_name: str # 插件商店中的模块名不等于本地的模块名,前者是文件夹名,后者是点分割模块名
project_link: str = ""
homepage: str = ""
author: str = ""
type: str | None = None
version: str | None = ""
time: str = ""
tags: list[PluginTag] = []
is_official: bool = False
def get_plugin_exist(plugin_name: str) -> bool:
"""
获取插件是否存在于加载列表
Args:
plugin_name:
Returns:
"""
for plugin in nonebot.plugin.get_loaded_plugins():
if plugin.name == plugin_name:
return True
return False
async def get_store_plugin(plugin_name: str) -> Optional[StorePlugin]:
"""
获取插件信息
Args:
plugin_name (str): 插件模块名
Returns:
Optional[StorePlugin]: 插件信息
"""
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
for plugin in plugins:
if plugin.module_name == plugin_name:
return plugin
return None
def get_plugin_default_enable(plugin_name: str) -> bool:
"""
获取插件默认启用状态由插件定义不存在则默认为启用优先从缓存中获取
Args:
plugin_name (str): 插件模块名
Returns:
bool: 插件默认状态
"""
if plugin_name not in __default_enable:
plug = nonebot.plugin.get_plugin(plugin_name)
default_enable = (plug.metadata.extra.get("default_enable", True) if plug.metadata else True) if plug else True
__default_enable[plugin_name] = default_enable
return __default_enable[plugin_name]
def get_plugin_session_enable(event: T_MessageEvent, plugin_name: str) -> bool:
"""
获取插件当前会话启用状态
Args:
event: 会话事件
plugin_name (str): 插件模块名
Returns:
bool: 插件当前状态
"""
if isinstance(event, satori.event.Event):
if event.guild is not None:
message_type = "group"
else:
message_type = "private"
else:
message_type = event.message_type
if message_type == "group":
group_id = str(event.guild.id if isinstance(event, satori.event.Event) else event.group_id)
if group_id not in __group_data:
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
__group_data[str(group_id)] = group
session = __group_data[group_id]
else:
# session: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id)))
user_id = str(event.user.id if isinstance(event, satori.event.Event) else event.user_id)
if user_id not in __user_data:
user: User = user_db.where_one(User(), "user_id = ?", user_id, default=User(user_id=user_id))
__user_data[user_id] = user
session = __user_data[user_id]
# 默认停用插件在启用列表内表示启用
# 默认停用插件不在启用列表内表示停用
# 默认启用插件在停用列表内表示停用
# 默认启用插件不在停用列表内表示启用
default_enable = get_plugin_default_enable(plugin_name)
if default_enable:
return plugin_name not in session.disabled_plugins
else:
return plugin_name in session.enabled_plugins
def set_plugin_session_enable(event: T_MessageEvent, plugin_name: str, enable: bool):
"""
设置插件会话启用状态同时更新数据库和缓存
Args:
event:
plugin_name:
enable:
Returns:
"""
if event_utils.get_message_type(event) == "group":
session: Group = group_db.where_one(Group(), "group_id = ?", str(event_utils.get_group_id(event)),
default=Group(group_id=str(event_utils.get_group_id(event))))
else:
session: User = user_db.where_one(User(), "user_id = ?", str(event_utils.get_user_id(event)),
default=User(user_id=str(event_utils.get_user_id(event))))
print(session)
default_enable = get_plugin_default_enable(plugin_name)
if default_enable:
if enable:
session.disabled_plugins.remove(plugin_name)
else:
session.disabled_plugins.append(plugin_name)
else:
if enable:
session.enabled_plugins.append(plugin_name)
else:
session.enabled_plugins.remove(plugin_name)
if event_utils.get_message_type(event) == "group":
__group_data[str(event_utils.get_group_id(event))] = session
group_db.save(session)
else:
__user_data[str(event_utils.get_user_id(event))] = session
user_db.save(session)
def get_plugin_global_enable(plugin_name: str) -> bool:
"""
获取插件全局启用状态, 优先从缓存中获取
Args:
plugin_name:
Returns:
"""
if plugin_name not in __global_enable:
plugin = plugin_db.where_one(
GlobalPlugin(),
"module_name = ?",
plugin_name,
default=GlobalPlugin(module_name=plugin_name, enabled=True))
__global_enable[plugin_name] = plugin.enabled
return __global_enable[plugin_name]
def set_plugin_global_enable(plugin_name: str, enable: bool):
"""
设置插件全局启用状态同时更新数据库和缓存
Args:
plugin_name:
enable:
Returns:
"""
plugin = plugin_db.where_one(
GlobalPlugin(),
"module_name = ?",
plugin_name,
default=GlobalPlugin(module_name=plugin_name, enabled=True))
plugin.enabled = enable
plugin_db.save(plugin)
__global_enable[plugin_name] = enable
def get_plugin_can_be_toggle(plugin_name: str) -> bool:
"""
获取插件是否可以被启用/停用
Args:
plugin_name (str): 插件模块名
Returns:
bool: 插件是否可以被启用/停用
"""
plug = nonebot.plugin.get_plugin(plugin_name)
return plug.metadata.extra.get("toggleable", True) if plug and plug.metadata else True
def get_group_enable(group_id: str) -> bool:
"""
获取群组是否启用插机器人
Args:
group_id (str): 群组ID
Returns:
bool: 群组是否启用插件
"""
group_id = str(group_id)
if group_id not in __group_data:
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
__group_data[group_id] = group
return __group_data[group_id].enable
def set_group_enable(group_id: str, enable: bool):
"""
设置群组是否启用插机器人
Args:
group_id (str): 群组ID
enable (bool): 是否启用
"""
group_id = str(group_id)
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
group.enable = enable
__group_data[group_id] = group
group_db.save(group)

View File

@ -1,186 +1,186 @@
# 轻雪资源包管理器
import os
import zipfile
import yaml
from nonebot import require
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
from liteyuki.utils.base.language import get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
from liteyuki.utils.base.resource import (ResourceMetadata, add_resource_pack, change_priority, check_exist, check_status, get_loaded_resource_packs, get_resource_metadata, load_resources, remove_resource_pack)
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import Alconna, Args, on_alconna, Arparma, Subcommand
@on_alconna(
aliases={"资源包"},
command=Alconna(
"rpm",
Subcommand(
"list",
Args["page", int, 1]["num", int, 10],
alias=["ls", "列表", "列出"],
),
Subcommand(
"load",
Args["name", str],
alias=["安装"],
),
Subcommand(
"unload",
Args["name", str],
alias=["卸载"],
),
Subcommand(
"up",
Args["name", str],
alias=["上移"],
),
Subcommand(
"down",
Args["name", str],
alias=["下移"],
),
Subcommand(
"top",
Args["name", str],
alias=["置顶"],
),
Subcommand(
"reload",
alias=["重载"],
),
),
permission=SUPERUSER
).handle()
async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher):
ulang = get_user_lang(str(event.user_id))
reply = ""
send_as_md = False
if result.subcommands.get("list"):
send_as_md = True
loaded_rps = get_loaded_resource_packs()
reply += f"{ulang.get('liteyuki.loaded_resources', NUM=len(loaded_rps))}\n"
for rp in loaded_rps:
btn_unload = md.btn_cmd(
ulang.get("npm.uninstall"),
f"rpm unload {rp.folder}"
)
btn_move_up = md.btn_cmd(
ulang.get("rpm.move_up"),
f"rpm up {rp.folder}"
)
btn_move_down = md.btn_cmd(
ulang.get("rpm.move_down"),
f"rpm down {rp.folder}"
)
btn_move_top = md.btn_cmd(
ulang.get("rpm.move_top"),
f"rpm top {rp.folder}"
)
# 添加新行
reply += (f"\n**{md.escape(rp.name)}**({md.escape(rp.folder)})\n\n"
f"> {btn_move_up} {btn_move_down} {btn_move_top} {btn_unload}\n\n***")
reply += f"\n\n{ulang.get('liteyuki.unloaded_resources')}\n"
loaded_folders = [rp.folder for rp in get_loaded_resource_packs()]
# 遍历resources文件夹获取未加载的资源包
for folder in os.listdir("resources"):
if folder not in loaded_folders:
if os.path.exists(os.path.join("resources", folder, "metadata.yml")):
metadata = ResourceMetadata(
**yaml.load(
open(
os.path.join("resources", folder, "metadata.yml"),
encoding="utf-8"
),
Loader=yaml.FullLoader
)
)
metadata.folder = folder
metadata.path = os.path.join("resources", folder)
btn_load = md.btn_cmd(
ulang.get("npm.install"),
f"rpm load {metadata.folder}"
)
# 添加新行
reply += (f"\n**{md.escape(metadata.name)}**({md.escape(metadata.folder)})\n\n"
f"> {btn_load}\n\n***")
elif os.path.isfile(os.path.join("resources", folder)) and folder.endswith(".zip"):
# zip文件
# 临时解压并读取metadata.yml
with zipfile.ZipFile(os.path.join("resources", folder), "r") as zip_ref:
with zip_ref.open("metadata.yml") as f:
metadata = ResourceMetadata(
**yaml.load(f, Loader=yaml.FullLoader)
)
btn_load = md.btn_cmd(
ulang.get("npm.install"),
f"rpm load {folder}"
)
# 添加新行
reply += (f"\n**{md.escape(metadata.name)}**({md.escape(folder)})\n\n"
f"> {btn_load}\n\n***")
elif result.subcommands.get("load") or result.subcommands.get("unload"):
load = result.subcommands.get("load") is not None
rp_name = result.args.get("name")
r = False # 操作结果
if check_exist(rp_name):
if load != check_status(rp_name):
# 状态不同
if load:
r = add_resource_pack(rp_name)
else:
r = remove_resource_pack(rp_name)
rp_meta = get_resource_metadata(rp_name)
reply += ulang.get(
f"liteyuki.{'load' if load else 'unload'}_resource_{'success' if r else 'failed'}",
NAME=rp_meta.name
)
else:
# 重复操作
reply += ulang.get(f"liteyuki.resource_already_{'load' if load else 'unload'}ed", NAME=rp_name)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
if r:
btn_reload = md.btn_cmd(
ulang.get("liteyuki.reload_resources"),
f"rpm reload"
)
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
elif result.subcommands.get("up") or result.subcommands.get("down") or result.subcommands.get("top"):
rp_name = result.args.get("name")
if result.subcommands.get("up"):
delta = -1
elif result.subcommands.get("down"):
delta = 1
else:
delta = 0
if check_exist(rp_name):
if check_status(rp_name):
r = change_priority(rp_name, delta)
reply += ulang.get(f"liteyuki.change_priority_{'success' if r else 'failed'}", NAME=rp_name)
if r:
btn_reload = md.btn_cmd(
ulang.get("liteyuki.reload_resources"),
f"rpm reload"
)
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
elif result.subcommands.get("reload"):
load_resources()
reply = ulang.get(
"liteyuki.reload_resources_success",
NUM=len(get_loaded_resource_packs())
)
else:
pass
if send_as_md:
await md.send_md(reply, bot, event=event)
else:
await matcher.finish(reply)
# 轻雪资源包管理器
import os
import zipfile
import yaml
from nonebot import require
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
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
from src.utils.base.resource import (ResourceMetadata, add_resource_pack, change_priority, check_exist, check_status, get_loaded_resource_packs, get_resource_metadata, load_resources, remove_resource_pack)
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import Alconna, Args, on_alconna, Arparma, Subcommand
@on_alconna(
aliases={"资源包"},
command=Alconna(
"rpm",
Subcommand(
"list",
Args["page", int, 1]["num", int, 10],
alias=["ls", "列表", "列出"],
),
Subcommand(
"load",
Args["name", str],
alias=["安装"],
),
Subcommand(
"unload",
Args["name", str],
alias=["卸载"],
),
Subcommand(
"up",
Args["name", str],
alias=["上移"],
),
Subcommand(
"down",
Args["name", str],
alias=["下移"],
),
Subcommand(
"top",
Args["name", str],
alias=["置顶"],
),
Subcommand(
"reload",
alias=["重载"],
),
),
permission=SUPERUSER
).handle()
async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher):
ulang = get_user_lang(str(event.user_id))
reply = ""
send_as_md = False
if result.subcommands.get("list"):
send_as_md = True
loaded_rps = get_loaded_resource_packs()
reply += f"{ulang.get('liteyuki.loaded_resources', NUM=len(loaded_rps))}\n"
for rp in loaded_rps:
btn_unload = md.btn_cmd(
ulang.get("npm.uninstall"),
f"rpm unload {rp.folder}"
)
btn_move_up = md.btn_cmd(
ulang.get("rpm.move_up"),
f"rpm up {rp.folder}"
)
btn_move_down = md.btn_cmd(
ulang.get("rpm.move_down"),
f"rpm down {rp.folder}"
)
btn_move_top = md.btn_cmd(
ulang.get("rpm.move_top"),
f"rpm top {rp.folder}"
)
# 添加新行
reply += (f"\n**{md.escape(rp.name)}**({md.escape(rp.folder)})\n\n"
f"> {btn_move_up} {btn_move_down} {btn_move_top} {btn_unload}\n\n***")
reply += f"\n\n{ulang.get('liteyuki.unloaded_resources')}\n"
loaded_folders = [rp.folder for rp in get_loaded_resource_packs()]
# 遍历resources文件夹获取未加载的资源包
for folder in os.listdir("resources"):
if folder not in loaded_folders:
if os.path.exists(os.path.join("resources", folder, "metadata.yml")):
metadata = ResourceMetadata(
**yaml.load(
open(
os.path.join("resources", folder, "metadata.yml"),
encoding="utf-8"
),
Loader=yaml.FullLoader
)
)
metadata.folder = folder
metadata.path = os.path.join("resources", folder)
btn_load = md.btn_cmd(
ulang.get("npm.install"),
f"rpm load {metadata.folder}"
)
# 添加新行
reply += (f"\n**{md.escape(metadata.name)}**({md.escape(metadata.folder)})\n\n"
f"> {btn_load}\n\n***")
elif os.path.isfile(os.path.join("resources", folder)) and folder.endswith(".zip"):
# zip文件
# 临时解压并读取metadata.yml
with zipfile.ZipFile(os.path.join("resources", folder), "r") as zip_ref:
with zip_ref.open("metadata.yml") as f:
metadata = ResourceMetadata(
**yaml.load(f, Loader=yaml.FullLoader)
)
btn_load = md.btn_cmd(
ulang.get("npm.install"),
f"rpm load {folder}"
)
# 添加新行
reply += (f"\n**{md.escape(metadata.name)}**({md.escape(folder)})\n\n"
f"> {btn_load}\n\n***")
elif result.subcommands.get("load") or result.subcommands.get("unload"):
load = result.subcommands.get("load") is not None
rp_name = result.args.get("name")
r = False # 操作结果
if check_exist(rp_name):
if load != check_status(rp_name):
# 状态不同
if load:
r = add_resource_pack(rp_name)
else:
r = remove_resource_pack(rp_name)
rp_meta = get_resource_metadata(rp_name)
reply += ulang.get(
f"liteyuki.{'load' if load else 'unload'}_resource_{'success' if r else 'failed'}",
NAME=rp_meta.name
)
else:
# 重复操作
reply += ulang.get(f"liteyuki.resource_already_{'load' if load else 'unload'}ed", NAME=rp_name)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
if r:
btn_reload = md.btn_cmd(
ulang.get("liteyuki.reload_resources"),
f"rpm reload"
)
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
elif result.subcommands.get("up") or result.subcommands.get("down") or result.subcommands.get("top"):
rp_name = result.args.get("name")
if result.subcommands.get("up"):
delta = -1
elif result.subcommands.get("down"):
delta = 1
else:
delta = 0
if check_exist(rp_name):
if check_status(rp_name):
r = change_priority(rp_name, delta)
reply += ulang.get(f"liteyuki.change_priority_{'success' if r else 'failed'}", NAME=rp_name)
if r:
btn_reload = md.btn_cmd(
ulang.get("liteyuki.reload_resources"),
f"rpm reload"
)
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
elif result.subcommands.get("reload"):
load_resources()
reply = ulang.get(
"liteyuki.reload_resources_success",
NUM=len(get_loaded_resource_packs())
)
else:
pass
if send_as_md:
await md.send_md(reply, bot, event=event)
else:
await matcher.finish(reply)

View File

@ -1,16 +1,16 @@
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,
}
)
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,21 +1,21 @@
import nonebot
from nonebot.message import event_preprocessor
# from nonebot_plugin_alconna.typings import Event
from liteyuki.utils.base.ly_typing import T_MessageEvent
from liteyuki.utils import satori_utils
from nonebot.adapters import satori
from nonebot_plugin_alconna.typings import Event
from liteyuki.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")
import nonebot
from nonebot.message import event_preprocessor
# from nonebot_plugin_alconna.typings import Event
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.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,163 +1,163 @@
import datetime
import time
import aiohttp
from nonebot import require
from nonebot.plugin import PluginMetadata
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.data import Database, LiteModel
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_alconna import Alconna, AlconnaResult, CommandResult, Subcommand, UniMessage, on_alconna, Args
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="签名服务器状态",
description="适用于ntqq的签名状态查看",
usage=(
"sign count 查看当前签名数\n"
"sign data 查看签名数变化\n"
"sign chart [limit] 查看签名数变化图表\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
SIGN_COUNT_URLS: dict[str, str] = get_config("sign_count_urls", None)
SIGN_COUNT_DURATION = get_config("sign_count_duration", 10)
class SignCount(LiteModel):
TABLE_NAME: str = "sign_count"
time: float = 0.0
count: int = 0
sid: str = ""
sign_db = Database("data/liteyuki/ntqq_sign.ldb")
sign_db.auto_migrate(SignCount())
sign_status = on_alconna(Alconna(
"sign",
Subcommand(
"chart",
Args["limit", int, 10000]
),
Subcommand(
"count"
),
Subcommand(
"data"
)
))
cache_img: bytes = None
@sign_status.assign("count")
async def _():
reply = "Current sign count:"
for name, count in (await get_now_sign()).items():
reply += f"\n{name}: {count[1]}"
await sign_status.send(reply)
@sign_status.assign("data")
async def _():
query_stamp = [1, 5, 10, 15]
reply = "QPS from last " + ", ".join([str(i) for i in query_stamp]) + "mins"
for name, url in SIGN_COUNT_URLS.items():
count_data = []
for stamp in query_stamp:
count_rows = sign_db.where_all(SignCount(), "sid = ? and time > ?", url, time.time() - 60 * stamp)
if len(count_rows) < 2:
count_data.append(-1)
else:
count_data.append((count_rows[-1].count - count_rows[0].count)/(stamp*60))
reply += f"\n{name}: " + ", ".join([f"{i:.1f}" for i in count_data])
await sign_status.send(reply)
@sign_status.assign("chart")
async def _(arp: CommandResult = AlconnaResult()):
limit = arp.result.subcommands.get("chart").args.get("limit")
if limit == 10000:
if cache_img:
await sign_status.send(UniMessage.image(raw=cache_img))
return
img = await generate_chart(limit)
await sign_status.send(UniMessage.image(raw=img))
@scheduler.scheduled_job("interval", seconds=SIGN_COUNT_DURATION, next_run_time=datetime.datetime.now())
async def update_sign_count():
global cache_img
if not SIGN_COUNT_URLS:
return
data = await get_now_sign()
for name, count in data.items():
await save_sign_count(count[0], count[1], SIGN_COUNT_URLS[name])
cache_img = await generate_chart(10000)
async def get_now_sign() -> dict[str, tuple[float, int]]:
"""
Get the sign count and the time of the latest sign
Returns:
tuple[float, int] | None: (time, count)
"""
data = {}
now = time.time()
async with aiohttp.ClientSession() as client:
for name, url in SIGN_COUNT_URLS.items():
async with client.get(url) as resp:
count = (await resp.json())["count"]
data[name] = (now, count)
return data
async def save_sign_count(timestamp: float, count: int, sid: str):
"""
Save the sign count to the database
Args:
sid: the sign id use url as the id
count:
timestamp (float): the time of the sign count (int): the count of the sign
"""
sign_db.save(SignCount(time=timestamp, count=count, sid=sid))
async def generate_chart(limit):
data = []
for name, url in SIGN_COUNT_URLS.items():
count_rows = sign_db.where_all(SignCount(), "sid = ? ORDER BY id DESC LIMIT ?", url, limit)
count_rows.reverse()
data.append(
{
"name" : name,
# "data": [[row.time, row.count] for row in count_rows]
"times" : [row.time for row in count_rows],
"counts": [row.count for row in count_rows]
}
)
img = await template2image(
template=get_path("templates/sign_status.html"),
templates={
"data": data
},
)
return img
import datetime
import time
import aiohttp
from nonebot import require
from nonebot.plugin import PluginMetadata
from src.utils.base.config import get_config
from src.utils.base.data import Database, LiteModel
from src.utils.base.resource import get_path
from src.utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_alconna import Alconna, AlconnaResult, CommandResult, Subcommand, UniMessage, on_alconna, Args
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="签名服务器状态",
description="适用于ntqq的签名状态查看",
usage=(
"sign count 查看当前签名数\n"
"sign data 查看签名数变化\n"
"sign chart [limit] 查看签名数变化图表\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
SIGN_COUNT_URLS: dict[str, str] = get_config("sign_count_urls", None)
SIGN_COUNT_DURATION = get_config("sign_count_duration", 10)
class SignCount(LiteModel):
TABLE_NAME: str = "sign_count"
time: float = 0.0
count: int = 0
sid: str = ""
sign_db = Database("data/liteyuki/ntqq_sign.ldb")
sign_db.auto_migrate(SignCount())
sign_status = on_alconna(Alconna(
"sign",
Subcommand(
"chart",
Args["limit", int, 10000]
),
Subcommand(
"count"
),
Subcommand(
"data"
)
))
cache_img: bytes = None
@sign_status.assign("count")
async def _():
reply = "Current sign count:"
for name, count in (await get_now_sign()).items():
reply += f"\n{name}: {count[1]}"
await sign_status.send(reply)
@sign_status.assign("data")
async def _():
query_stamp = [1, 5, 10, 15]
reply = "QPS from last " + ", ".join([str(i) for i in query_stamp]) + "mins"
for name, url in SIGN_COUNT_URLS.items():
count_data = []
for stamp in query_stamp:
count_rows = sign_db.where_all(SignCount(), "sid = ? and time > ?", url, time.time() - 60 * stamp)
if len(count_rows) < 2:
count_data.append(-1)
else:
count_data.append((count_rows[-1].count - count_rows[0].count)/(stamp*60))
reply += f"\n{name}: " + ", ".join([f"{i:.1f}" for i in count_data])
await sign_status.send(reply)
@sign_status.assign("chart")
async def _(arp: CommandResult = AlconnaResult()):
limit = arp.result.subcommands.get("chart").args.get("limit")
if limit == 10000:
if cache_img:
await sign_status.send(UniMessage.image(raw=cache_img))
return
img = await generate_chart(limit)
await sign_status.send(UniMessage.image(raw=img))
@scheduler.scheduled_job("interval", seconds=SIGN_COUNT_DURATION, next_run_time=datetime.datetime.now())
async def update_sign_count():
global cache_img
if not SIGN_COUNT_URLS:
return
data = await get_now_sign()
for name, count in data.items():
await save_sign_count(count[0], count[1], SIGN_COUNT_URLS[name])
cache_img = await generate_chart(10000)
async def get_now_sign() -> dict[str, tuple[float, int]]:
"""
Get the sign count and the time of the latest sign
Returns:
tuple[float, int] | None: (time, count)
"""
data = {}
now = time.time()
async with aiohttp.ClientSession() as client:
for name, url in SIGN_COUNT_URLS.items():
async with client.get(url) as resp:
count = (await resp.json())["count"]
data[name] = (now, count)
return data
async def save_sign_count(timestamp: float, count: int, sid: str):
"""
Save the sign count to the database
Args:
sid: the sign id use url as the id
count:
timestamp (float): the time of the sign count (int): the count of the sign
"""
sign_db.save(SignCount(time=timestamp, count=count, sid=sid))
async def generate_chart(limit):
data = []
for name, url in SIGN_COUNT_URLS.items():
count_rows = sign_db.where_all(SignCount(), "sid = ? ORDER BY id DESC LIMIT ?", url, limit)
count_rows.reverse()
data.append(
{
"name" : name,
# "data": [[row.time, row.count] for row in count_rows]
"times" : [row.time for row in count_rows],
"counts": [row.count for row in count_rows]
}
)
img = await template2image(
template=get_path("templates/sign_status.html"),
templates={
"data": data
},
)
return img

View File

@ -1,18 +1,18 @@
from nonebot.plugin import PluginMetadata
from .monitors import *
from .matchers 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,
}
from nonebot.plugin import PluginMetadata
from .monitors import *
from .matchers 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,106 +1,106 @@
import asyncio
import random
import nonebot
from nonebot import Bot, on_message, get_driver, require
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
from nonebot.rule import to_me
from nonebot.typing import T_State
from liteyuki.utils.base.ly_typing import T_MessageEvent
from .utils import get_keywords
from liteyuki.utils.base.word_bank import get_reply
from liteyuki.utils.event import get_message_type
from liteyuki.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
from liteyuki.utils.base.data_manager import group_db, Group
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma
nicknames = set()
driver = get_driver()
group_reply_probability: dict[str, float] = {
}
default_reply_probability = 0.05
cut_probability = 0.4 # 分几句话的概率
@on_alconna(
Alconna(
"set-reply-probability",
Args["probability", float, default_reply_probability],
),
aliases={"设置回复概率"},
permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER,
).handle()
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
# 修改内存和数据库的概率值
if get_message_type(event) == "group":
group_id = event.group_id
probability = result.main_args.get("probability")
# 保存到数据库
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=str(group_id)))
group.config["reply_probability"] = probability
group_db.save(group)
await matcher.send(f"已将群组{group_id}的回复概率设置为{probability}")
return
@group_db.on_save
def _(model: Group):
"""
在数据库更新时更新内存中的回复概率
Args:
model:
Returns:
"""
group_reply_probability[model.group_id] = model.config.get("reply_probability", default_reply_probability)
@driver.on_bot_connect
async def _(bot: Bot):
global nicknames
nicknames.update(bot.config.nickname)
# 从数据库加载群组的回复概率
groups = group_db.where_all(Group(), default=[])
for group in groups:
group_reply_probability[group.group_id] = group.config.get("reply_probability", default_reply_probability)
@on_message(priority=100).handle()
async def _(event: T_MessageEvent, bot: Bot, state: T_State, matcher: Matcher):
kws = await get_keywords(event.message.extract_plain_text())
tome = False
if await to_me()(event=event, bot=bot, state=state):
tome = True
else:
for kw in kws:
if kw in nicknames:
tome = True
break
# 回复概率
message_type = get_message_type(event)
if tome or message_type == "private":
p = 1.0
else:
p = group_reply_probability.get(event.group_id, default_reply_probability)
if random.random() < p:
if reply := get_reply(kws):
if random.random() < cut_probability:
reply = reply.replace("", "||").replace("", "||").replace("", "||").replace("", "||")
replies = reply.split("||")
for r in replies:
if r: # 防止空字符串
await asyncio.sleep(random.random() * 2)
await matcher.send(r)
else:
await asyncio.sleep(random.random() * 3)
await matcher.send(reply)
return
import asyncio
import random
import nonebot
from nonebot import Bot, on_message, get_driver, require
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
from nonebot.rule import to_me
from nonebot.typing import T_State
from src.utils.base.ly_typing import T_MessageEvent
from .utils import get_keywords
from src.utils.base.word_bank import get_reply
from src.utils.event import get_message_type
from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
from src.utils.base.data_manager import group_db, Group
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma
nicknames = set()
driver = get_driver()
group_reply_probability: dict[str, float] = {
}
default_reply_probability = 0.05
cut_probability = 0.4 # 分几句话的概率
@on_alconna(
Alconna(
"set-reply-probability",
Args["probability", float, default_reply_probability],
),
aliases={"设置回复概率"},
permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER,
).handle()
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
# 修改内存和数据库的概率值
if get_message_type(event) == "group":
group_id = event.group_id
probability = result.main_args.get("probability")
# 保存到数据库
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=str(group_id)))
group.config["reply_probability"] = probability
group_db.save(group)
await matcher.send(f"已将群组{group_id}的回复概率设置为{probability}")
return
@group_db.on_save
def _(model: Group):
"""
在数据库更新时更新内存中的回复概率
Args:
model:
Returns:
"""
group_reply_probability[model.group_id] = model.config.get("reply_probability", default_reply_probability)
@driver.on_bot_connect
async def _(bot: Bot):
global nicknames
nicknames.update(bot.config.nickname)
# 从数据库加载群组的回复概率
groups = group_db.where_all(Group(), default=[])
for group in groups:
group_reply_probability[group.group_id] = group.config.get("reply_probability", default_reply_probability)
@on_message(priority=100).handle()
async def _(event: T_MessageEvent, bot: Bot, state: T_State, matcher: Matcher):
kws = await get_keywords(event.message.extract_plain_text())
tome = False
if await to_me()(event=event, bot=bot, state=state):
tome = True
else:
for kw in kws:
if kw in nicknames:
tome = True
break
# 回复概率
message_type = get_message_type(event)
if tome or message_type == "private":
p = 1.0
else:
p = group_reply_probability.get(event.group_id, default_reply_probability)
if random.random() < p:
if reply := get_reply(kws):
if random.random() < cut_probability:
reply = reply.replace("", "||").replace("", "||").replace("", "||").replace("", "||")
replies = reply.split("||")
for r in replies:
if r: # 防止空字符串
await asyncio.sleep(random.random() * 2)
await matcher.send(r)
else:
await asyncio.sleep(random.random() * 3)
await matcher.send(reply)
return

View File

@ -1,13 +1,13 @@
from jieba import lcut
from nonebot.utils import run_sync
@run_sync
def get_keywords(text: str) -> list[str, ...]:
"""
获取关键词
Args:
text: 文本
Returns:
"""
return lcut(text)
from jieba import lcut
from nonebot.utils import run_sync
@run_sync
def get_keywords(text: str) -> list[str, ...]:
"""
获取关键词
Args:
text: 文本
Returns:
"""
return lcut(text)

View File

@ -1,29 +1,29 @@
from nonebot.plugin import PluginMetadata
from .stat_matchers import *
from .stat_monitors import *
from .stat_restful_api import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="统计信息",
description="统计机器人的信息,包括消息、群聊等,支持排名、图表等功能",
usage=(
"```\nstatistic message 查看统计消息\n"
"可选参数:\n"
" -g|--group [group_id] 指定群聊\n"
" -u|--user [user_id] 指定用户\n"
" -d|--duration [duration] 指定时长\n"
" -p|--period [period] 指定次数统计周期\n"
" -b|--bot [bot_id] 指定机器人\n"
"命令别名:\n"
" statistic|stat message|msg|m\n"
"```"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : False,
"default_enable": True,
}
)
from nonebot.plugin import PluginMetadata
from .stat_matchers import *
from .stat_monitors import *
from .stat_restful_api import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="统计信息",
description="统计机器人的信息,包括消息、群聊等,支持排名、图表等功能",
usage=(
"```\nstatistic message 查看统计消息\n"
"可选参数:\n"
" -g|--group [group_id] 指定群聊\n"
" -u|--user [user_id] 指定用户\n"
" -d|--duration [duration] 指定时长\n"
" -p|--period [period] 指定次数统计周期\n"
" -b|--bot [bot_id] 指定机器人\n"
"命令别名:\n"
" statistic|stat message|msg|m\n"
"```"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : False,
"default_enable": True,
}
)

View File

@ -1,21 +1,21 @@
from liteyuki.utils.base.data import Database, LiteModel
class MessageEventModel(LiteModel):
TABLE_NAME: str = "message_event"
time: int = 0
bot_id: str = ""
adapter: str = ""
user_id: str = ""
group_id: str = ""
message_id: str = ""
message: list = []
message_text: str = ""
message_type: str = ""
msg_db = Database("data/liteyuki/msg.ldb")
from src.utils.base.data import Database, LiteModel
class MessageEventModel(LiteModel):
TABLE_NAME: str = "message_event"
time: int = 0
bot_id: str = ""
adapter: str = ""
user_id: str = ""
group_id: str = ""
message_id: str = ""
message: list = []
message_text: str = ""
message_type: str = ""
msg_db = Database("data/liteyuki/msg.ldb")
msg_db.auto_migrate(MessageEventModel())

View File

@ -1,172 +1,172 @@
import time
from typing import Any
from collections import Counter
from nonebot import Bot
from liteyuki.utils.message.html_tool import template2image
from .common import MessageEventModel, msg_db
from liteyuki.utils.base.language import Language
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.string_tool import convert_seconds_to_time
from ...utils.external.logo import get_group_icon, get_user_icon
async def count_msg_by_bot_id(bot_id: str) -> int:
condition = " AND bot_id = ?"
condition_args = [bot_id]
msg_rows = msg_db.where_all(
MessageEventModel(),
condition,
*condition_args
)
return len(msg_rows)
async def get_stat_msg_image(
duration: int,
period: int,
group_id: str = None,
bot_id: str = None,
user_id: str = None,
ulang: Language = Language()
) -> bytes:
"""
获取统计消息
Args:
user_id:
ulang:
bot_id:
group_id:
duration: 统计时间单位秒
period: 统计周期单位秒
Returns:
tuple: [int,], [int,] 两个列表分别为周期中心时间戳和消息数量
"""
now = int(time.time())
start_time = (now - duration)
condition = "time > ?"
condition_args = [start_time]
if group_id:
condition += " AND group_id = ?"
condition_args.append(group_id)
if bot_id:
condition += " AND bot_id = ?"
condition_args.append(bot_id)
if user_id:
condition += " AND user_id = ?"
condition_args.append(user_id)
msg_rows = msg_db.where_all(
MessageEventModel(),
condition,
*condition_args
)
timestamps = []
msg_count = []
msg_rows.sort(key=lambda x: x.time)
start_time = max(msg_rows[0].time, start_time)
for i in range(start_time, now, period):
timestamps.append(i + period // 2)
msg_count.append(0)
for msg in msg_rows:
period_start_time = start_time + (msg.time - start_time) // period * period
period_center_time = period_start_time + period // 2
index = timestamps.index(period_center_time)
msg_count[index] += 1
templates = {
"data": [
{
"name" : ulang.get("stat.message")
+ f" Period {convert_seconds_to_time(period)}" + f" Duration {convert_seconds_to_time(duration)}"
+ (f" Group {group_id}" if group_id else "") + (f" Bot {bot_id}" if bot_id else "") + (
f" User {user_id}" if user_id else ""),
"times" : timestamps,
"counts": msg_count
}
]
}
return await template2image(get_path("templates/stat_msg.html"), templates)
async def get_stat_rank_image(
rank_type: str,
limit: dict[str, Any],
ulang: Language = Language(),
bot: Bot = None,
) -> bytes:
if rank_type == "user":
condition = "user_id != ''"
condition_args = []
else:
condition = "group_id != ''"
condition_args = []
for k, v in limit.items():
match k:
case "user_id":
condition += " AND user_id = ?"
condition_args.append(v)
case "group_id":
condition += " AND group_id = ?"
condition_args.append(v)
case "bot_id":
condition += " AND bot_id = ?"
condition_args.append(v)
case "duration":
condition += " AND time > ?"
condition_args.append(v)
msg_rows = msg_db.where_all(
MessageEventModel(),
condition,
*condition_args
)
"""
{
name: string, # user name or group name
count: int, # message count
icon: string # icon url
}
"""
if rank_type == "user":
ranking_counter = Counter([msg.user_id for msg in msg_rows])
else:
ranking_counter = Counter([msg.group_id for msg in msg_rows])
sorted_data = sorted(ranking_counter.items(), key=lambda x: x[1], reverse=True)
ranking: list[dict[str, Any]] = [
{
"name" : _[0],
"count": _[1],
"icon" : await (get_group_icon(platform="qq", group_id=_[0]) if rank_type == "group" else get_user_icon(
platform="qq", user_id=_[0]
))
}
for _ in sorted_data[0:min(len(sorted_data), limit["rank"])]
]
templates = {
"data":
{
"name" : ulang.get("stat.rank") + f" Type {rank_type}" + f" Limit {limit}",
"ranking": ranking
}
}
return await template2image(get_path("templates/stat_rank.html"), templates, debug=True)
import time
from typing import Any
from collections import Counter
from nonebot import Bot
from src.utils.message.html_tool import template2image
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
async def count_msg_by_bot_id(bot_id: str) -> int:
condition = " AND bot_id = ?"
condition_args = [bot_id]
msg_rows = msg_db.where_all(
MessageEventModel(),
condition,
*condition_args
)
return len(msg_rows)
async def get_stat_msg_image(
duration: int,
period: int,
group_id: str = None,
bot_id: str = None,
user_id: str = None,
ulang: Language = Language()
) -> bytes:
"""
获取统计消息
Args:
user_id:
ulang:
bot_id:
group_id:
duration: 统计时间单位秒
period: 统计周期单位秒
Returns:
tuple: [int,], [int,] 两个列表分别为周期中心时间戳和消息数量
"""
now = int(time.time())
start_time = (now - duration)
condition = "time > ?"
condition_args = [start_time]
if group_id:
condition += " AND group_id = ?"
condition_args.append(group_id)
if bot_id:
condition += " AND bot_id = ?"
condition_args.append(bot_id)
if user_id:
condition += " AND user_id = ?"
condition_args.append(user_id)
msg_rows = msg_db.where_all(
MessageEventModel(),
condition,
*condition_args
)
timestamps = []
msg_count = []
msg_rows.sort(key=lambda x: x.time)
start_time = max(msg_rows[0].time, start_time)
for i in range(start_time, now, period):
timestamps.append(i + period // 2)
msg_count.append(0)
for msg in msg_rows:
period_start_time = start_time + (msg.time - start_time) // period * period
period_center_time = period_start_time + period // 2
index = timestamps.index(period_center_time)
msg_count[index] += 1
templates = {
"data": [
{
"name" : ulang.get("stat.message")
+ f" Period {convert_seconds_to_time(period)}" + f" Duration {convert_seconds_to_time(duration)}"
+ (f" Group {group_id}" if group_id else "") + (f" Bot {bot_id}" if bot_id else "") + (
f" User {user_id}" if user_id else ""),
"times" : timestamps,
"counts": msg_count
}
]
}
return await template2image(get_path("templates/stat_msg.html"), templates)
async def get_stat_rank_image(
rank_type: str,
limit: dict[str, Any],
ulang: Language = Language(),
bot: Bot = None,
) -> bytes:
if rank_type == "user":
condition = "user_id != ''"
condition_args = []
else:
condition = "group_id != ''"
condition_args = []
for k, v in limit.items():
match k:
case "user_id":
condition += " AND user_id = ?"
condition_args.append(v)
case "group_id":
condition += " AND group_id = ?"
condition_args.append(v)
case "bot_id":
condition += " AND bot_id = ?"
condition_args.append(v)
case "duration":
condition += " AND time > ?"
condition_args.append(v)
msg_rows = msg_db.where_all(
MessageEventModel(),
condition,
*condition_args
)
"""
{
name: string, # user name or group name
count: int, # message count
icon: string # icon url
}
"""
if rank_type == "user":
ranking_counter = Counter([msg.user_id for msg in msg_rows])
else:
ranking_counter = Counter([msg.group_id for msg in msg_rows])
sorted_data = sorted(ranking_counter.items(), key=lambda x: x[1], reverse=True)
ranking: list[dict[str, Any]] = [
{
"name" : _[0],
"count": _[1],
"icon" : await (get_group_icon(platform="qq", group_id=_[0]) if rank_type == "group" else get_user_icon(
platform="qq", user_id=_[0]
))
}
for _ in sorted_data[0:min(len(sorted_data), limit["rank"])]
]
templates = {
"data":
{
"name" : ulang.get("stat.rank") + f" Type {rank_type}" + f" Limit {limit}",
"ranking": ranking
}
}
return await template2image(get_path("templates/stat_rank.html"), templates, debug=True)

View File

@ -1,135 +1,135 @@
from nonebot import Bot, require
from liteyuki.utils.message.string_tool import convert_duration, convert_time_to_seconds
from .stat_api import *
from liteyuki.utils import event as event_utils
from liteyuki.utils.base.language import Language
from liteyuki.utils.base.ly_typing import T_MessageEvent
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import (
UniMessage,
on_alconna,
Alconna,
Args,
Subcommand,
Arparma,
Option,
MultiVar
)
stat_msg = on_alconna(
Alconna(
"statistic",
Subcommand(
"message",
# Args["duration", str, "2d"]["period", str, "60s"], # 默认为1天
Option(
"-d|--duration",
Args["duration", str, "2d"],
help_text="统计时间",
),
Option(
"-p|--period",
Args["period", str, "60s"],
help_text="统计周期",
),
Option(
"-b|--bot", # 生成图表
Args["bot_id", str, "current"],
help_text="是否指定机器人",
),
Option(
"-g|--group",
Args["group_id", str, "current"],
help_text="指定群组"
),
Option(
"-u|--user",
Args["user_id", str, "current"],
help_text="指定用户"
),
alias={"msg", "m"},
help_text="查看统计次数内的消息"
),
Subcommand(
"rank",
Option(
"-u|--user",
help_text="以用户为指标",
),
Option(
"-g|--group",
help_text="以群组为指标",
),
Option(
"-l|--limit",
Args["limit", MultiVar(str)],
help_text="限制参数使用key=val格式",
),
Option(
"-d|--duration",
Args["duration", str, "1d"],
help_text="统计时间",
),
Option(
"-r|--rank",
Args["rank", int, 20],
help_text="指定排名",
),
alias={"r"},
)
),
aliases={"stat"}
)
@stat_msg.assign("message")
async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
ulang = Language(event_utils.get_user_id(event))
try:
duration = convert_time_to_seconds(result.other_args.get("duration", "2d")) # 秒数
period = convert_time_to_seconds(result.other_args.get("period", "1m"))
except BaseException as e:
await stat_msg.send(ulang.get("liteyuki.invalid_command", TEXT=str(e.__str__())))
return
group_id = result.other_args.get("group_id")
bot_id = result.other_args.get("bot_id")
user_id = result.other_args.get("user_id")
if group_id in ["current", "c"]:
group_id = str(event_utils.get_group_id(event))
if group_id in ["all", "a"]:
group_id = "all"
if bot_id in ["current", "c"]:
bot_id = str(bot.self_id)
if user_id in ["current", "c"]:
user_id = str(event_utils.get_user_id(event))
img = await get_stat_msg_image(duration=duration, period=period, group_id=group_id, bot_id=bot_id, user_id=user_id, ulang=ulang)
await stat_msg.send(UniMessage.image(raw=img))
@stat_msg.assign("rank")
async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
ulang = Language(event_utils.get_user_id(event))
rank_type = "user"
duration = convert_time_to_seconds(result.other_args.get("duration", "1d"))
print(result)
if result.subcommands.get("rank").options.get("user"):
rank_type = "user"
elif result.subcommands.get("rank").options.get("group"):
rank_type = "group"
limit = result.other_args.get("limit", {})
if limit:
limit = dict([i.split("=") for i in limit])
limit["duration"] = time.time() - duration # 起始时间戳
limit["rank"] = result.other_args.get("rank", 20)
img = await get_stat_rank_image(rank_type=rank_type, limit=limit, ulang=ulang)
await stat_msg.send(UniMessage.image(raw=img))
from nonebot import Bot, require
from src.utils.message.string_tool import convert_duration, convert_time_to_seconds
from .data_source import *
from src.utils import event as event_utils
from src.utils.base.language import Language
from src.utils.base.ly_typing import T_MessageEvent
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import (
UniMessage,
on_alconna,
Alconna,
Args,
Subcommand,
Arparma,
Option,
MultiVar
)
stat_msg = on_alconna(
Alconna(
"statistic",
Subcommand(
"message",
# Args["duration", str, "2d"]["period", str, "60s"], # 默认为1天
Option(
"-d|--duration",
Args["duration", str, "2d"],
help_text="统计时间",
),
Option(
"-p|--period",
Args["period", str, "60s"],
help_text="统计周期",
),
Option(
"-b|--bot", # 生成图表
Args["bot_id", str, "current"],
help_text="是否指定机器人",
),
Option(
"-g|--group",
Args["group_id", str, "current"],
help_text="指定群组"
),
Option(
"-u|--user",
Args["user_id", str, "current"],
help_text="指定用户"
),
alias={"msg", "m"},
help_text="查看统计次数内的消息"
),
Subcommand(
"rank",
Option(
"-u|--user",
help_text="以用户为指标",
),
Option(
"-g|--group",
help_text="以群组为指标",
),
Option(
"-l|--limit",
Args["limit", MultiVar(str)],
help_text="限制参数使用key=val格式",
),
Option(
"-d|--duration",
Args["duration", str, "1d"],
help_text="统计时间",
),
Option(
"-r|--rank",
Args["rank", int, 20],
help_text="指定排名",
),
alias={"r"},
)
),
aliases={"stat"}
)
@stat_msg.assign("message")
async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
ulang = Language(event_utils.get_user_id(event))
try:
duration = convert_time_to_seconds(result.other_args.get("duration", "2d")) # 秒数
period = convert_time_to_seconds(result.other_args.get("period", "1m"))
except BaseException as e:
await stat_msg.send(ulang.get("liteyuki.invalid_command", TEXT=str(e.__str__())))
return
group_id = result.other_args.get("group_id")
bot_id = result.other_args.get("bot_id")
user_id = result.other_args.get("user_id")
if group_id in ["current", "c"]:
group_id = str(event_utils.get_group_id(event))
if group_id in ["all", "a"]:
group_id = "all"
if bot_id in ["current", "c"]:
bot_id = str(bot.self_id)
if user_id in ["current", "c"]:
user_id = str(event_utils.get_user_id(event))
img = await get_stat_msg_image(duration=duration, period=period, group_id=group_id, bot_id=bot_id, user_id=user_id, ulang=ulang)
await stat_msg.send(UniMessage.image(raw=img))
@stat_msg.assign("rank")
async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
ulang = Language(event_utils.get_user_id(event))
rank_type = "user"
duration = convert_time_to_seconds(result.other_args.get("duration", "1d"))
print(result)
if result.subcommands.get("rank").options.get("user"):
rank_type = "user"
elif result.subcommands.get("rank").options.get("group"):
rank_type = "group"
limit = result.other_args.get("limit", {})
if limit:
limit = dict([i.split("=") for i in limit])
limit["duration"] = time.time() - duration # 起始时间戳
limit["rank"] = result.other_args.get("rank", 20)
img = await get_stat_rank_image(rank_type=rank_type, limit=limit, ulang=ulang)
await stat_msg.send(UniMessage.image(raw=img))

View File

@ -1,92 +1,92 @@
import time
from nonebot import require
from nonebot.message import event_postprocessor
from liteyuki.utils.base.data import Database, LiteModel
from liteyuki.utils.base.ly_typing import v11, v12, satori
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from .common import MessageEventModel, msg_db
from liteyuki.utils import event as event_utils
require("nonebot_plugin_alconna")
async def general_event_monitor(bot: T_Bot, event: T_MessageEvent):
print("POST PROCESS")
# if isinstance(bot, satori.Bot):
# print("POST PROCESS SATORI EVENT")
# return await satori_event_monitor(bot, event)
# elif isinstance(bot, v11.Bot):
# print("POST PROCESS V11 EVENT")
# return await onebot_v11_event_monitor(bot, event)
@event_postprocessor
async def onebot_v11_event_monitor(bot: v11.Bot, event: v11.MessageEvent):
if event.message_type == "group":
event: v11.GroupMessageEvent
group_id = str(event.group_id)
else:
group_id = ""
mem = MessageEventModel(
time=int(time.time()),
bot_id=bot.self_id,
adapter="onebot.v11",
group_id=group_id,
user_id=str(event.user_id),
message_id=str(event.message_id),
message=[ms.__dict__ for ms in event.message],
message_text=event.raw_message,
message_type=event.message_type,
)
msg_db.save(mem)
@event_postprocessor
async def onebot_v12_event_monitor(bot: v12.Bot, event: v12.MessageEvent):
if event.message_type == "group":
event: v12.GroupMessageEvent
group_id = str(event.group_id)
else:
group_id = ""
mem = MessageEventModel(
time=int(time.time()),
bot_id=bot.self_id,
adapter="onebot.v12",
group_id=group_id,
user_id=str(event.user_id),
message_id=[ms.__dict__ for ms in event.message],
message=event.message,
message_text=event.raw_message,
message_type=event.message_type,
)
msg_db.save(mem)
@event_postprocessor
async def satori_event_monitor(bot: satori.Bot, event: satori.MessageEvent):
if event.guild is not None:
event: satori.MessageEvent
group_id = str(event.guild.id)
else:
group_id = ""
mem = MessageEventModel(
time=int(time.time()),
bot_id=bot.self_id,
adapter="satori",
group_id=group_id,
user_id=str(event.user.id),
message_id=[ms.__str__() for ms in event.message],
message=event.message,
message_text=event.message.content,
message_type=event_utils.get_message_type(event),
)
msg_db.save(mem)
import time
from nonebot import require
from nonebot.message import event_postprocessor
from src.utils.base.data import Database, LiteModel
from src.utils.base.ly_typing import v11, v12, satori
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from .common import MessageEventModel, msg_db
from src.utils import event as event_utils
require("nonebot_plugin_alconna")
async def general_event_monitor(bot: T_Bot, event: T_MessageEvent):
print("POST PROCESS")
# if isinstance(bot, satori.Bot):
# print("POST PROCESS SATORI EVENT")
# return await satori_event_monitor(bot, event)
# elif isinstance(bot, v11.Bot):
# print("POST PROCESS V11 EVENT")
# return await onebot_v11_event_monitor(bot, event)
@event_postprocessor
async def onebot_v11_event_monitor(bot: v11.Bot, event: v11.MessageEvent):
if event.message_type == "group":
event: v11.GroupMessageEvent
group_id = str(event.group_id)
else:
group_id = ""
mem = MessageEventModel(
time=int(time.time()),
bot_id=bot.self_id,
adapter="onebot.v11",
group_id=group_id,
user_id=str(event.user_id),
message_id=str(event.message_id),
message=[ms.__dict__ for ms in event.message],
message_text=event.raw_message,
message_type=event.message_type,
)
msg_db.save(mem)
@event_postprocessor
async def onebot_v12_event_monitor(bot: v12.Bot, event: v12.MessageEvent):
if event.message_type == "group":
event: v12.GroupMessageEvent
group_id = str(event.group_id)
else:
group_id = ""
mem = MessageEventModel(
time=int(time.time()),
bot_id=bot.self_id,
adapter="onebot.v12",
group_id=group_id,
user_id=str(event.user_id),
message_id=[ms.__dict__ for ms in event.message],
message=event.message,
message_text=event.raw_message,
message_type=event.message_type,
)
msg_db.save(mem)
@event_postprocessor
async def satori_event_monitor(bot: satori.Bot, event: satori.MessageEvent):
if event.guild is not None:
event: satori.MessageEvent
group_id = str(event.guild.id)
else:
group_id = ""
mem = MessageEventModel(
time=int(time.time()),
bot_id=bot.self_id,
adapter="satori",
group_id=group_id,
user_id=str(event.user.id),
message_id=[ms.__str__() for ms in event.message],
message=event.message,
message_text=event.message.content,
message_type=event_utils.get_message_type(event),
)
msg_db.save(mem)

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 hemengyang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,107 @@
import asyncio
import concurrent.futures
import contextlib
import re
from functools import partial
from io import BytesIO
from random import choice
from typing import Optional
import jieba
import jieba.analyse
import numpy as np
from emoji import replace_emoji
from PIL import Image
from wordcloud import WordCloud
from .config import global_config, plugin_config
def pre_precess(msg: str) -> str:
"""对消息进行预处理"""
# 去除网址
# https://stackoverflow.com/a/17773849/9212748
url_regex = re.compile(
r"(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]"
r"+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})"
)
msg = url_regex.sub("", msg)
# 去除 \u200b
msg = re.sub(r"\u200b", "", msg)
# 去除 emoji
# https://github.com/carpedm20/emoji
msg = replace_emoji(msg)
return msg
def analyse_message(msg: str) -> dict[str, float]:
"""分析消息
分词并统计词频
"""
# 设置停用词表
if plugin_config.wordcloud_stopwords_path:
jieba.analyse.set_stop_words(plugin_config.wordcloud_stopwords_path)
# 加载用户词典
if plugin_config.wordcloud_userdict_path:
jieba.load_userdict(str(plugin_config.wordcloud_userdict_path))
# 基于 TF-IDF 算法的关键词抽取
# 返回所有关键词,因为设置了数量其实也只是 tags[:topK],不如交给词云库处理
words = jieba.analyse.extract_tags(msg, topK=0, withWeight=True)
return dict(words)
def get_mask(key: str):
"""获取 mask"""
mask_path = plugin_config.get_mask_path(key)
if mask_path.exists():
return np.array(Image.open(mask_path))
# 如果指定 mask 文件不存在,则尝试默认 mask
default_mask_path = plugin_config.get_mask_path()
if default_mask_path.exists():
return np.array(Image.open(default_mask_path))
def _get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]:
# 过滤掉命令
command_start = tuple(i for i in global_config.command_start if i)
message = " ".join(m for m in messages if not m.startswith(command_start))
# 预处理
message = pre_precess(message)
# 分析消息。分词,并统计词频
frequency = analyse_message(message)
# 词云参数
wordcloud_options = {}
wordcloud_options.update(plugin_config.wordcloud_options)
wordcloud_options.setdefault("font_path", str(plugin_config.wordcloud_font_path))
wordcloud_options.setdefault("width", plugin_config.wordcloud_width)
wordcloud_options.setdefault("height", plugin_config.wordcloud_height)
wordcloud_options.setdefault(
"background_color", plugin_config.wordcloud_background_color
)
# 如果 colormap 是列表,则随机选择一个
colormap = (
plugin_config.wordcloud_colormap
if isinstance(plugin_config.wordcloud_colormap, str)
else choice(plugin_config.wordcloud_colormap)
)
wordcloud_options.setdefault("colormap", colormap)
wordcloud_options.setdefault("mask", get_mask(mask_key))
with contextlib.suppress(ValueError):
wordcloud = WordCloud(**wordcloud_options)
image = wordcloud.generate_from_frequencies(frequency).to_image()
image_bytes = BytesIO()
image.save(image_bytes, format="PNG")
return image_bytes.getvalue()
async def get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]:
loop = asyncio.get_running_loop()
pfunc = partial(_get_wordcloud, messages, mask_key)
# 虽然不知道具体是哪里泄漏了,但是通过每次关闭线程池可以避免这个问题
# https://github.com/he0119/nonebot-plugin-wordcloud/issues/99
with concurrent.futures.ThreadPoolExecutor() as pool:
return await loop.run_in_executor(pool, pfunc)

View File

@ -1,24 +1,24 @@
from nonebot.plugin import PluginMetadata
from .status import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="状态查看器",
description="",
usage=(
"MARKDOWN### 状态查看器\n"
"查看机器人的状态\n"
"### 用法\n"
"- `/status` 查看基本情况\n"
"- `/status memory` 查看内存使用情况\n"
"- `/status process` 查看进程情况\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : True,
}
)
from nonebot.plugin import PluginMetadata
from .status import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="状态查看器",
description="",
usage=(
"MARKDOWN### 状态查看器\n"
"查看机器人的状态\n"
"### 用法\n"
"- `/status` 查看基本情况\n"
"- `/status memory` 查看内存使用情况\n"
"- `/status process` 查看进程情况\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : True,
}
)

View File

@ -1,278 +1,278 @@
import platform
import time
import nonebot
import psutil
from cpuinfo import cpuinfo
from nonebot import require
from nonebot.adapters import satori
from liteyuki.utils import __NAME__, __VERSION__
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.data_manager import TempConfig, common_db
from liteyuki.utils.base.language import Language
from liteyuki.utils.base.resource import get_loaded_resource_packs, get_path
from liteyuki.utils.message.html_tool import template2image
from liteyuki.utils import satori_utils
from .counter_for_satori import satori_counter
from git import Repo
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
commit_hash = Repo(".").head.commit.hexsha
protocol_names = {
0: "iPad",
1: "Android Phone",
2: "Android Watch",
3: "Mac",
5: "iPad",
6: "Android Pad",
}
"""
Universal Interface
data
- bot
- name: str
icon: str
id: int
protocol_name: str
groups: int
friends: int
message_sent: int
message_received: int
app_name: str
- hardware
- cpu
- percent: float
- name: str
- mem
- percent: float
- total: int
- used: int
- free: int
- swap
- percent: float
- total: int
- used: int
- free: int
- disk: list
- name: str
- percent: float
- total: int
"""
status_card_cache = {} # lang -> bytes
# 60s刷新一次
@scheduler.scheduled_job("cron", second="*/40")
async def refresh_status_card():
nonebot.logger.debug("Refreshing status card cache...")
global status_card_cache
bot_data = await get_bots_data()
hardware_data = await get_hardware_data()
liteyuki_data = await get_liteyuki_data()
for lang in status_card_cache.keys():
status_card_cache[lang] = await generate_status_card(
bot_data,
hardware_data,
liteyuki_data,
lang=lang,
use_cache=False
)
async def generate_status_card(bot: dict, hardware: dict, liteyuki: dict, lang="zh-CN", bot_id="0",
use_cache=False
) -> bytes:
if not use_cache:
return await template2image(
get_path("templates/status.html", abs_path=True),
{
"data": {
"bot" : bot,
"hardware" : hardware,
"liteyuki" : liteyuki,
"localization": get_local_data(lang)
}
},
)
else:
if lang not in status_card_cache:
status_card_cache[lang] = await generate_status_card(bot, hardware, liteyuki, lang=lang, bot_id=bot_id)
return status_card_cache[lang]
def get_local_data(lang_code) -> dict:
lang = Language(lang_code)
return {
"friends" : lang.get("status.friends"),
"groups" : lang.get("status.groups"),
"plugins" : lang.get("status.plugins"),
"bots" : lang.get("status.bots"),
"message_sent" : lang.get("status.message_sent"),
"message_received": lang.get("status.message_received"),
"cpu" : lang.get("status.cpu"),
"memory" : lang.get("status.memory"),
"swap" : lang.get("status.swap"),
"disk" : lang.get("status.disk"),
"usage" : lang.get("status.usage"),
"total" : lang.get("status.total"),
"used" : lang.get("status.used"),
"free" : lang.get("status.free"),
"days" : lang.get("status.days"),
"hours" : lang.get("status.hours"),
"minutes" : lang.get("status.minutes"),
"seconds" : lang.get("status.seconds"),
"runtime" : lang.get("status.runtime"),
"threads" : lang.get("status.threads"),
"cores" : lang.get("status.cores"),
"process" : lang.get("status.process"),
"resources" : lang.get("status.resources"),
"description" : lang.get("status.description"),
}
async def get_bots_data(self_id: str = "0") -> dict:
"""获取当前所有机器人数据
Returns:
"""
result = {
"self_id": self_id,
"bots" : [],
}
for bot_id, bot in nonebot.get_bots().items():
groups = 0
friends = 0
status = {}
bot_name = bot_id
version_info = {}
if isinstance(bot, satori.Bot):
try:
bot_name = (await satori_utils.user_infos.get(bot.self_id)).name
groups = str(await satori_utils.count_groups(bot))
friends = str(await satori_utils.count_friends(bot))
status = {}
version_info = await bot.get_version_info()
except Exception:
pass
else:
try:
# API fetch
bot_name = (await bot.get_login_info())["nickname"]
groups = len(await bot.get_group_list())
friends = len(await bot.get_friend_list())
status = await bot.get_status()
version_info = await bot.get_version_info()
except Exception:
pass
statistics = status.get("stat", {})
app_name = version_info.get("app_name", "UnknownImplementation")
if app_name in ["Lagrange.OneBot", "LLOneBot", "Shamrock", "NapCat.Onebot"]:
icon = f"https://q.qlogo.cn/g?b=qq&nk={bot_id}&s=640"
elif isinstance(bot, satori.Bot):
app_name = "Satori"
icon = (await bot.login_get()).user.avatar
else:
icon = None
bot_data = {
"name" : bot_name,
"icon" : icon,
"id" : bot_id,
"protocol_name" : protocol_names.get(version_info.get("protocol_name"), "Online"),
"groups" : groups,
"friends" : friends,
"message_sent" : satori_counter.msg_sent if isinstance(bot, satori.Bot) else statistics.get("message_sent", 0),
"message_received": satori_counter.msg_received if isinstance(bot, satori.Bot) else statistics.get("message_received", 0),
"app_name" : app_name
}
result["bots"].append(bot_data)
return result
async def get_hardware_data() -> dict:
mem = psutil.virtual_memory()
all_processes = psutil.Process().children(recursive=True)
all_processes.append(psutil.Process())
mem_used_process = 0
process_mem = {}
for process in all_processes:
try:
ps_name = process.name().replace(".exe", "")
if ps_name not in process_mem:
process_mem[ps_name] = 0
process_mem[ps_name] += process.memory_info().rss
mem_used_process += process.memory_info().rss
except Exception:
pass
swap = psutil.swap_memory()
cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "Unknown")
if "AMD" in cpu_brand_raw:
brand = "AMD"
elif "Intel" in cpu_brand_raw:
brand = "Intel"
else:
brand = "Unknown"
result = {
"cpu" : {
"percent": psutil.cpu_percent(),
"name" : f"{brand} {cpuinfo.get_cpu_info().get('arch', 'Unknown')}",
"cores" : psutil.cpu_count(logical=False),
"threads": psutil.cpu_count(logical=True),
"freq" : psutil.cpu_freq().current # MHz
},
"memory": {
"percent" : mem.percent,
"total" : mem.total,
"used" : mem.used,
"free" : mem.free,
"usedProcess": mem_used_process,
},
"swap" : {
"percent": swap.percent,
"total" : swap.total,
"used" : swap.used,
"free" : swap.free
},
"disk" : [],
}
for disk in psutil.disk_partitions(all=True):
try:
disk_usage = psutil.disk_usage(disk.mountpoint)
if disk_usage.total == 0:
continue # 虚拟磁盘
result["disk"].append({
"name" : disk.mountpoint,
"percent": disk_usage.percent,
"total" : disk_usage.total,
"used" : disk_usage.used,
"free" : disk_usage.free
})
except:
pass
return result
async def get_liteyuki_data() -> dict:
temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
result = {
"name" : list(get_config("nickname", [__NAME__]))[0],
"version" : f"{__VERSION__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}",
"plugins" : len(nonebot.get_loaded_plugins()),
"resources": len(get_loaded_resource_packs()),
"nonebot" : f"{nonebot.__version__}",
"python" : f"{platform.python_implementation()} {platform.python_version()}",
"system" : f"{platform.system()} {platform.release()}",
"runtime" : time.time() - temp_data.data.get("start_time", time.time()), # 运行时间秒数
"bots" : len(nonebot.get_bots())
}
return result
import platform
import time
import nonebot
import psutil
from cpuinfo import cpuinfo
from nonebot import require
from nonebot.adapters import satori
from src.utils import __NAME__, __VERSION__
from src.utils.base.config import get_config
from src.utils.base.data_manager import TempConfig, common_db
from src.utils.base.language import Language
from src.utils.base.resource import get_loaded_resource_packs, get_path
from src.utils.message.html_tool import template2image
from src.utils import satori_utils
from .counter_for_satori import satori_counter
from git import Repo
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
commit_hash = Repo(".").head.commit.hexsha
protocol_names = {
0: "iPad",
1: "Android Phone",
2: "Android Watch",
3: "Mac",
5: "iPad",
6: "Android Pad",
}
"""
Universal Interface
data
- bot
- name: str
icon: str
id: int
protocol_name: str
groups: int
friends: int
message_sent: int
message_received: int
app_name: str
- hardware
- cpu
- percent: float
- name: str
- mem
- percent: float
- total: int
- used: int
- free: int
- swap
- percent: float
- total: int
- used: int
- free: int
- disk: list
- name: str
- percent: float
- total: int
"""
status_card_cache = {} # lang -> bytes
# 60s刷新一次
@scheduler.scheduled_job("cron", second="*/40")
async def refresh_status_card():
nonebot.logger.debug("Refreshing status card cache...")
global status_card_cache
bot_data = await get_bots_data()
hardware_data = await get_hardware_data()
liteyuki_data = await get_liteyuki_data()
for lang in status_card_cache.keys():
status_card_cache[lang] = await generate_status_card(
bot_data,
hardware_data,
liteyuki_data,
lang=lang,
use_cache=False
)
async def generate_status_card(bot: dict, hardware: dict, liteyuki: dict, lang="zh-CN", bot_id="0",
use_cache=False
) -> bytes:
if not use_cache:
return await template2image(
get_path("templates/status.html", abs_path=True),
{
"data": {
"bot" : bot,
"hardware" : hardware,
"liteyuki" : liteyuki,
"localization": get_local_data(lang)
}
},
)
else:
if lang not in status_card_cache:
status_card_cache[lang] = await generate_status_card(bot, hardware, liteyuki, lang=lang, bot_id=bot_id)
return status_card_cache[lang]
def get_local_data(lang_code) -> dict:
lang = Language(lang_code)
return {
"friends" : lang.get("status.friends"),
"groups" : lang.get("status.groups"),
"plugins" : lang.get("status.plugins"),
"bots" : lang.get("status.bots"),
"message_sent" : lang.get("status.message_sent"),
"message_received": lang.get("status.message_received"),
"cpu" : lang.get("status.cpu"),
"memory" : lang.get("status.memory"),
"swap" : lang.get("status.swap"),
"disk" : lang.get("status.disk"),
"usage" : lang.get("status.usage"),
"total" : lang.get("status.total"),
"used" : lang.get("status.used"),
"free" : lang.get("status.free"),
"days" : lang.get("status.days"),
"hours" : lang.get("status.hours"),
"minutes" : lang.get("status.minutes"),
"seconds" : lang.get("status.seconds"),
"runtime" : lang.get("status.runtime"),
"threads" : lang.get("status.threads"),
"cores" : lang.get("status.cores"),
"process" : lang.get("status.process"),
"resources" : lang.get("status.resources"),
"description" : lang.get("status.description"),
}
async def get_bots_data(self_id: str = "0") -> dict:
"""获取当前所有机器人数据
Returns:
"""
result = {
"self_id": self_id,
"bots" : [],
}
for bot_id, bot in nonebot.get_bots().items():
groups = 0
friends = 0
status = {}
bot_name = bot_id
version_info = {}
if isinstance(bot, satori.Bot):
try:
bot_name = (await satori_utils.user_infos.get(bot.self_id)).name
groups = str(await satori_utils.count_groups(bot))
friends = str(await satori_utils.count_friends(bot))
status = {}
version_info = await bot.get_version_info()
except Exception:
pass
else:
try:
# API fetch
bot_name = (await bot.get_login_info())["nickname"]
groups = len(await bot.get_group_list())
friends = len(await bot.get_friend_list())
status = await bot.get_status()
version_info = await bot.get_version_info()
except Exception:
pass
statistics = status.get("stat", {})
app_name = version_info.get("app_name", "UnknownImplementation")
if app_name in ["Lagrange.OneBot", "LLOneBot", "Shamrock", "NapCat.Onebot"]:
icon = f"https://q.qlogo.cn/g?b=qq&nk={bot_id}&s=640"
elif isinstance(bot, satori.Bot):
app_name = "Satori"
icon = (await bot.login_get()).user.avatar
else:
icon = None
bot_data = {
"name" : bot_name,
"icon" : icon,
"id" : bot_id,
"protocol_name" : protocol_names.get(version_info.get("protocol_name"), "Online"),
"groups" : groups,
"friends" : friends,
"message_sent" : satori_counter.msg_sent if isinstance(bot, satori.Bot) else statistics.get("message_sent", 0),
"message_received": satori_counter.msg_received if isinstance(bot, satori.Bot) else statistics.get("message_received", 0),
"app_name" : app_name
}
result["bots"].append(bot_data)
return result
async def get_hardware_data() -> dict:
mem = psutil.virtual_memory()
all_processes = psutil.Process().children(recursive=True)
all_processes.append(psutil.Process())
mem_used_process = 0
process_mem = {}
for process in all_processes:
try:
ps_name = process.name().replace(".exe", "")
if ps_name not in process_mem:
process_mem[ps_name] = 0
process_mem[ps_name] += process.memory_info().rss
mem_used_process += process.memory_info().rss
except Exception:
pass
swap = psutil.swap_memory()
cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "Unknown")
if "AMD" in cpu_brand_raw:
brand = "AMD"
elif "Intel" in cpu_brand_raw:
brand = "Intel"
else:
brand = "Unknown"
result = {
"cpu" : {
"percent": psutil.cpu_percent(),
"name" : f"{brand} {cpuinfo.get_cpu_info().get('arch', 'Unknown')}",
"cores" : psutil.cpu_count(logical=False),
"threads": psutil.cpu_count(logical=True),
"freq" : psutil.cpu_freq().current # MHz
},
"memory": {
"percent" : mem.percent,
"total" : mem.total,
"used" : mem.used,
"free" : mem.free,
"usedProcess": mem_used_process,
},
"swap" : {
"percent": swap.percent,
"total" : swap.total,
"used" : swap.used,
"free" : swap.free
},
"disk" : [],
}
for disk in psutil.disk_partitions(all=True):
try:
disk_usage = psutil.disk_usage(disk.mountpoint)
if disk_usage.total == 0:
continue # 虚拟磁盘
result["disk"].append({
"name" : disk.mountpoint,
"percent": disk_usage.percent,
"total" : disk_usage.total,
"used" : disk_usage.used,
"free" : disk_usage.free
})
except:
pass
return result
async def get_liteyuki_data() -> dict:
temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
result = {
"name" : list(get_config("nickname", [__NAME__]))[0],
"version" : f"{__VERSION__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}",
"plugins" : len(nonebot.get_loaded_plugins()),
"resources": len(get_loaded_resource_packs()),
"nonebot" : f"{nonebot.__version__}",
"python" : f"{platform.python_implementation()} {platform.python_version()}",
"system" : f"{platform.system()} {platform.release()}",
"runtime" : time.time() - temp_data.data.get("start_time", time.time()), # 运行时间秒数
"bots" : len(nonebot.get_bots())
}
return result

View File

@ -1,10 +1,10 @@
class SatoriCounter:
msg_sent: int
msg_received: int
def __init__(self):
self.msg_sent = 0
self.msg_received = 0
satori_counter = SatoriCounter()
class SatoriCounter:
msg_sent: int
msg_received: int
def __init__(self):
self.msg_sent = 0
self.msg_received = 0
satori_counter = SatoriCounter()

View File

@ -1,49 +1,49 @@
from liteyuki.utils import event as event_utils
from liteyuki.utils.base.language import get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from .api import *
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, UniMessage
status_alc = on_alconna(
aliases={"状态"},
command=Alconna(
"status",
Subcommand(
"memory",
alias={"mem", "m", "内存"},
),
Subcommand(
"process",
alias={"proc", "p", "进程"},
)
),
)
@status_alc.handle()
async def _(event: T_MessageEvent, bot: T_Bot):
ulang = get_user_lang(event_utils.get_user_id(event))
if ulang.lang_code in status_card_cache:
image = status_card_cache[ulang.lang_code]
else:
image = await generate_status_card(
bot=await get_bots_data(),
hardware=await get_hardware_data(),
liteyuki=await get_liteyuki_data(),
lang=ulang.lang_code,
bot_id=bot.self_id,
use_cache=True
)
await status_alc.finish(UniMessage.image(raw=image))
@status_alc.assign("memory")
async def _():
print("memory")
@status_alc.assign("process")
async def _():
print("process")
from src.utils import event as event_utils
from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from .api import *
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, UniMessage
status_alc = on_alconna(
aliases={"状态"},
command=Alconna(
"status",
Subcommand(
"memory",
alias={"mem", "m", "内存"},
),
Subcommand(
"process",
alias={"proc", "p", "进程"},
)
),
)
@status_alc.handle()
async def _(event: T_MessageEvent, bot: T_Bot):
ulang = get_user_lang(event_utils.get_user_id(event))
if ulang.lang_code in status_card_cache:
image = status_card_cache[ulang.lang_code]
else:
image = await generate_status_card(
bot=await get_bots_data(),
hardware=await get_hardware_data(),
liteyuki=await get_liteyuki_data(),
lang=ulang.lang_code,
bot_id=bot.self_id,
use_cache=True
)
await status_alc.finish(UniMessage.image(raw=image))
@status_alc.assign("memory")
async def _():
print("memory")
@status_alc.assign("process")
async def _():
print("process")

View File

@ -1,17 +1,17 @@
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,
}
)
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,59 +1,59 @@
import datetime
import aiohttp
import httpx
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")
import datetime
import aiohttp
import httpx
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,16 +1,16 @@
from nonebot.plugin import PluginMetadata
from .profile_manager import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪用户管理",
description="用户管理插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : False,
"default_enable": True,
}
)
from nonebot.plugin import PluginMetadata
from .profile_manager import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪用户管理",
description="用户管理插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : False,
"default_enable": True,
}
)

View File

@ -1,23 +1,23 @@
representative_timezones_list = [
"Etc/GMT+12", # 国际日期变更线西
"Pacific/Honolulu", # 夏威夷标准时间
"America/Anchorage", # 阿拉斯加标准时间
"America/Los_Angeles", # 美国太平洋标准时间
"America/Denver", # 美国山地标准时间
"America/Chicago", # 美国中部标准时间
"America/New_York", # 美国东部标准时间
"Europe/London", # 英国标准时间
"Europe/Paris", # 中欧标准时间
"Europe/Moscow", # 莫斯科标准时间
"Asia/Dubai", # 阿联酋标准时间
"Asia/Kolkata", # 印度标准时间
"Asia/Shanghai", # 中国标准时间
"Asia/Hong_Kong", # 中国香港标准时间
"Asia/Chongqing", # 中国重庆标准时间
"Asia/Macau", # 中国澳门标准时间
"Asia/Taipei", # 中国台湾标准时间
"Asia/Tokyo", # 日本标准时间
"Australia/Sydney", # 澳大利亚东部标准时间
"Pacific/Auckland" # 新西兰标准时间
]
representative_timezones_list = [
"Etc/GMT+12", # 国际日期变更线西
"Pacific/Honolulu", # 夏威夷标准时间
"America/Anchorage", # 阿拉斯加标准时间
"America/Los_Angeles", # 美国太平洋标准时间
"America/Denver", # 美国山地标准时间
"America/Chicago", # 美国中部标准时间
"America/New_York", # 美国东部标准时间
"Europe/London", # 英国标准时间
"Europe/Paris", # 中欧标准时间
"Europe/Moscow", # 莫斯科标准时间
"Asia/Dubai", # 阿联酋标准时间
"Asia/Kolkata", # 印度标准时间
"Asia/Shanghai", # 中国标准时间
"Asia/Hong_Kong", # 中国香港标准时间
"Asia/Chongqing", # 中国重庆标准时间
"Asia/Macau", # 中国澳门标准时间
"Asia/Taipei", # 中国台湾标准时间
"Asia/Tokyo", # 日本标准时间
"Australia/Sydney", # 澳大利亚东部标准时间
"Pacific/Auckland" # 新西兰标准时间
]
representative_timezones_list.sort()

View File

@ -1,150 +1,150 @@
from typing import Optional
import pytz
from nonebot import require
from liteyuki.utils.base.data import LiteModel, Database
from liteyuki.utils.base.data_manager import User, user_db, group_db
from liteyuki.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
from .const import representative_timezones_list
from liteyuki.utils import event as event_utils
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
profile_alc = on_alconna(
Alconna(
"profile",
Subcommand(
"set",
Args["key", str]["value", str, None],
alias=["s", "设置"],
),
Subcommand(
"get",
Args["key", str],
alias=["g", "查询"],
),
),
aliases={"用户信息"}
)
# json储存
class Profile(LiteModel):
lang: str = "zh-CN"
nickname: str = ""
timezone: str = "Asia/Shanghai"
location: str = ""
@profile_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event),
default=User(user_id=str(event_utils.get_user_id(event))))
ulang = get_user_lang(str(event_utils.get_user_id(event)))
if result.subcommands.get("set"):
if result.subcommands["set"].args.get("value"):
# 对合法性进行校验后设置
r = set_profile(result.args["key"], result.args["value"], str(event_utils.get_user_id(event)))
if r:
user.profile[result.args["key"]] = result.args["value"]
user_db.save(user) # 数据库保存
await profile_alc.finish(
ulang.get(
"user.profile.set_success",
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
VALUE=result.args["value"]
)
)
else:
await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
else:
# 未输入值,尝试呼出菜单
menu = get_profile_menu(result.args["key"], ulang)
if menu:
await md.send_md(menu, bot, event=event)
else:
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
user.profile[result.args["key"]] = result.args["value"]
elif result.subcommands.get("get"):
if result.args["key"] in user.profile:
await profile_alc.finish(user.profile[result.args["key"]])
else:
await profile_alc.finish("无此键值")
else:
profile = Profile(**user.profile)
for k, v in user.profile.items():
profile.__setattr__(k, v)
reply = f"# {ulang.get('user.profile.info')}\n***\n"
hidden_attr = ["id", "TABLE_NAME"]
enter_attr = ["lang", "timezone"]
for key in sorted(profile.dict().keys()):
if key in hidden_attr:
continue
val = profile.dict()[key]
key_text = ulang.get(f"user.profile.{key}")
btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}",
enter=True if key in enter_attr else False)
reply += (f"\n**{key_text}** **{val}**\n"
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
f"\n> {btn_set} \n\n***\n")
await md.send_md(reply, bot, event=event)
def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
"""获取属性的markdown菜单
Args:
ulang: 用户语言
key: 属性键
Returns:
"""
setting_name = ulang.get(f"user.profile.{key}")
no_menu = ["id", "nickname", "location"]
if key in no_menu:
return None
reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n"
if key == "lang":
for lang_code, lang_name in get_all_lang().items():
btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}")
reply += f"\n{btn_set_lang}\n***\n"
elif key == "timezone":
for tz in representative_timezones_list:
btn_set_tz = md.btn_cmd(tz, f"profile set {key} {tz}")
reply += f"{btn_set_tz}\n***\n"
return reply
def set_profile(key: str, value: str, user_id: str) -> bool:
"""设置属性使用if分支对每一个合法性进行检查
Args:
user_id:
key:
value:
Returns:
是否成功设置输入合法性不通过返回False
"""
if key == "lang":
if value in get_all_lang():
change_user_lang(user_id, value)
return True
elif key == "timezone":
if value in pytz.all_timezones:
return True
elif key == "nickname":
return True
from typing import Optional
import pytz
from nonebot import require
from src.utils.base.data import LiteModel, Database
from src.utils.base.data_manager import User, user_db, group_db
from src.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md
from .const import representative_timezones_list
from src.utils import event as event_utils
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
profile_alc = on_alconna(
Alconna(
"profile",
Subcommand(
"set",
Args["key", str]["value", str, None],
alias=["s", "设置"],
),
Subcommand(
"get",
Args["key", str],
alias=["g", "查询"],
),
),
aliases={"用户信息"}
)
# json储存
class Profile(LiteModel):
lang: str = "zh-CN"
nickname: str = ""
timezone: str = "Asia/Shanghai"
location: str = ""
@profile_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event),
default=User(user_id=str(event_utils.get_user_id(event))))
ulang = get_user_lang(str(event_utils.get_user_id(event)))
if result.subcommands.get("set"):
if result.subcommands["set"].args.get("value"):
# 对合法性进行校验后设置
r = set_profile(result.args["key"], result.args["value"], str(event_utils.get_user_id(event)))
if r:
user.profile[result.args["key"]] = result.args["value"]
user_db.save(user) # 数据库保存
await profile_alc.finish(
ulang.get(
"user.profile.set_success",
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
VALUE=result.args["value"]
)
)
else:
await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
else:
# 未输入值,尝试呼出菜单
menu = get_profile_menu(result.args["key"], ulang)
if menu:
await md.send_md(menu, bot, event=event)
else:
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
user.profile[result.args["key"]] = result.args["value"]
elif result.subcommands.get("get"):
if result.args["key"] in user.profile:
await profile_alc.finish(user.profile[result.args["key"]])
else:
await profile_alc.finish("无此键值")
else:
profile = Profile(**user.profile)
for k, v in user.profile.items():
profile.__setattr__(k, v)
reply = f"# {ulang.get('user.profile.info')}\n***\n"
hidden_attr = ["id", "TABLE_NAME"]
enter_attr = ["lang", "timezone"]
for key in sorted(profile.dict().keys()):
if key in hidden_attr:
continue
val = profile.dict()[key]
key_text = ulang.get(f"user.profile.{key}")
btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}",
enter=True if key in enter_attr else False)
reply += (f"\n**{key_text}** **{val}**\n"
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
f"\n> {btn_set} \n\n***\n")
await md.send_md(reply, bot, event=event)
def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
"""获取属性的markdown菜单
Args:
ulang: 用户语言
key: 属性键
Returns:
"""
setting_name = ulang.get(f"user.profile.{key}")
no_menu = ["id", "nickname", "location"]
if key in no_menu:
return None
reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n"
if key == "lang":
for lang_code, lang_name in get_all_lang().items():
btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}")
reply += f"\n{btn_set_lang}\n***\n"
elif key == "timezone":
for tz in representative_timezones_list:
btn_set_tz = md.btn_cmd(tz, f"profile set {key} {tz}")
reply += f"{btn_set_tz}\n***\n"
return reply
def set_profile(key: str, value: str, user_id: str) -> bool:
"""设置属性使用if分支对每一个合法性进行检查
Args:
user_id:
key:
value:
Returns:
是否成功设置输入合法性不通过返回False
"""
if key == "lang":
if value in get_all_lang():
change_user_lang(user_id, value)
return True
elif key == "timezone":
if value in pytz.all_timezones:
return True
elif key == "nickname":
return True

View File

@ -1,27 +1,27 @@
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
from .qweather import *
__plugin_meta__ = PluginMetadata(
name="轻雪天气",
description="基于和风天气api的天气插件",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
from ...utils.base.data_manager import set_memory_data
driver = get_driver()
@driver.on_startup
async def _():
# 检查是否为开发者模式
is_dev = await check_key_dev(get_config("weather_key", ""))
set_memory_data("weather.is_dev", is_dev)
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
from .qweather import *
__plugin_meta__ = PluginMetadata(
name="轻雪天气",
description="基于和风天气api的天气插件",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
from ...utils.base.data_manager import set_memory_data
driver = get_driver()
@driver.on_startup
async def _():
# 检查是否为开发者模式
is_dev = await check_key_dev(get_config("weather_key", ""))
set_memory_data("weather.is_dev", is_dev)

View File

@ -1,171 +1,171 @@
import aiohttp
from .qw_models import *
import httpx
from ...utils.base.data_manager import get_memory_data
from ...utils.base.language import Language
dev_url = "https://devapi.qweather.com/" # 开发HBa
com_url = "https://api.qweather.com/" # 正式环境
def get_qw_lang(lang: str) -> str:
if lang in ["zh-HK", "zh-TW"]:
return "zh-hant"
elif lang.startswith("zh"):
return "zh"
elif lang.startswith("en"):
return "en"
else:
return lang
async def check_key_dev(key: str) -> bool:
url = "https://api.qweather.com/v7/weather/now?"
params = {
"location": "101010100",
"key" : key,
}
async with aiohttp.ClientSession() as client:
resp = await client.get(url, params=params)
return (await resp.json()).get("code") != "200" # 查询不到付费数据为开发版
def get_local_data(ulang_code: str) -> dict:
"""
获取本地化数据
Args:
ulang_code:
Returns:
"""
ulang = Language(ulang_code)
return {
"monday" : ulang.get("weather.monday"),
"tuesday" : ulang.get("weather.tuesday"),
"wednesday": ulang.get("weather.wednesday"),
"thursday" : ulang.get("weather.thursday"),
"friday" : ulang.get("weather.friday"),
"saturday" : ulang.get("weather.saturday"),
"sunday" : ulang.get("weather.sunday"),
"today" : ulang.get("weather.today"),
"tomorrow" : ulang.get("weather.tomorrow"),
"day" : ulang.get("weather.day"),
"night" : ulang.get("weather.night"),
"no_aqi" : ulang.get("weather.no_aqi"),
}
async def city_lookup(
location: str,
key: str,
adm: str = "",
number: int = 20,
lang: str = "zh",
) -> CityLookup:
"""
通过关键字搜索城市信息
Args:
location:
key:
adm:
number:
lang: 可传入标准i18n语言代码如zh-CNen-US等
Returns:
"""
url = "https://geoapi.qweather.com/v2/city/lookup?"
params = {
"location": location,
"adm" : adm,
"number" : number,
"key" : key,
"lang" : lang,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return CityLookup.parse_obj(resp.json())
async def get_weather_now(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/now?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_daily(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dd?" % (7 if dev else 30)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_hourly(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dh?" % (24 if dev else 168)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_airquality(
key: str,
location: str,
lang: str,
pollutant: bool = False,
station: bool = False,
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = f"airquality/v1/now/{location}?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"key" : key,
"lang" : lang,
"pollutant": pollutant,
"station" : station,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
import aiohttp
from .qw_models import *
import httpx
from ...utils.base.data_manager import get_memory_data
from ...utils.base.language import Language
dev_url = "https://devapi.qweather.com/" # 开发HBa
com_url = "https://api.qweather.com/" # 正式环境
def get_qw_lang(lang: str) -> str:
if lang in ["zh-HK", "zh-TW"]:
return "zh-hant"
elif lang.startswith("zh"):
return "zh"
elif lang.startswith("en"):
return "en"
else:
return lang
async def check_key_dev(key: str) -> bool:
url = "https://api.qweather.com/v7/weather/now?"
params = {
"location": "101010100",
"key" : key,
}
async with aiohttp.ClientSession() as client:
resp = await client.get(url, params=params)
return (await resp.json()).get("code") != "200" # 查询不到付费数据为开发版
def get_local_data(ulang_code: str) -> dict:
"""
获取本地化数据
Args:
ulang_code:
Returns:
"""
ulang = Language(ulang_code)
return {
"monday" : ulang.get("weather.monday"),
"tuesday" : ulang.get("weather.tuesday"),
"wednesday": ulang.get("weather.wednesday"),
"thursday" : ulang.get("weather.thursday"),
"friday" : ulang.get("weather.friday"),
"saturday" : ulang.get("weather.saturday"),
"sunday" : ulang.get("weather.sunday"),
"today" : ulang.get("weather.today"),
"tomorrow" : ulang.get("weather.tomorrow"),
"day" : ulang.get("weather.day"),
"night" : ulang.get("weather.night"),
"no_aqi" : ulang.get("weather.no_aqi"),
}
async def city_lookup(
location: str,
key: str,
adm: str = "",
number: int = 20,
lang: str = "zh",
) -> CityLookup:
"""
通过关键字搜索城市信息
Args:
location:
key:
adm:
number:
lang: 可传入标准i18n语言代码如zh-CNen-US等
Returns:
"""
url = "https://geoapi.qweather.com/v2/city/lookup?"
params = {
"location": location,
"adm" : adm,
"number" : number,
"key" : key,
"lang" : lang,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return CityLookup.parse_obj(resp.json())
async def get_weather_now(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/now?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_daily(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dd?" % (7 if dev else 30)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_hourly(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dh?" % (24 if dev else 168)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_airquality(
key: str,
location: str,
lang: str,
pollutant: bool = False,
station: bool = False,
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = f"airquality/v1/now/{location}?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"key" : key,
"lang" : lang,
"pollutant": pollutant,
"station" : station,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()

View File

@ -1,62 +1,62 @@
from liteyuki.utils.base.data import LiteModel
class Location(LiteModel):
name: str = ""
id: str = ""
lat: str = ""
lon: str = ""
adm2: str = ""
adm1: str = ""
country: str = ""
tz: str = ""
utcOffset: str = ""
isDst: str = ""
type: str = ""
rank: str = ""
fxLink: str = ""
sources: str = ""
license: str = ""
class CityLookup(LiteModel):
code: str = ""
location: list[Location] = [Location()]
class Now(LiteModel):
obsTime: str = ""
temp: str = ""
feelsLike: str = ""
icon: str = ""
text: str = ""
wind360: str = ""
windDir: str = ""
windScale: str = ""
windSpeed: str = ""
humidity: str = ""
precip: str = ""
pressure: str = ""
vis: str = ""
cloud: str = ""
dew: str = ""
sources: str = ""
license: str = ""
class WeatherNow(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
now: Now = Now()
class Daily(LiteModel):
pass
class WeatherDaily(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
daily: list[str] = []
from src.utils.base.data import LiteModel
class Location(LiteModel):
name: str = ""
id: str = ""
lat: str = ""
lon: str = ""
adm2: str = ""
adm1: str = ""
country: str = ""
tz: str = ""
utcOffset: str = ""
isDst: str = ""
type: str = ""
rank: str = ""
fxLink: str = ""
sources: str = ""
license: str = ""
class CityLookup(LiteModel):
code: str = ""
location: list[Location] = [Location()]
class Now(LiteModel):
obsTime: str = ""
temp: str = ""
feelsLike: str = ""
icon: str = ""
text: str = ""
wind360: str = ""
windDir: str = ""
windScale: str = ""
windSpeed: str = ""
humidity: str = ""
precip: str = ""
pressure: str = ""
vis: str = ""
cloud: str = ""
dew: str = ""
sources: str = ""
license: str = ""
class WeatherNow(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
now: Now = Now()
class Daily(LiteModel):
pass
class WeatherDaily(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
daily: list[str] = []

View File

@ -1,101 +1,101 @@
from nonebot import require, on_endswith
from nonebot.adapters import satori
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.internal.matcher import Matcher
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.ly_typing import T_MessageEvent
from .qw_api import *
from liteyuki.utils.base.data_manager import User, user_db
from liteyuki.utils.base.language import Language, get_user_lang
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.html_tool import template2image
from liteyuki.utils import event as event_utils
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma, UniMessage
wx_alc = on_alconna(
aliases={"天气"},
command=Alconna(
"weather",
Args["keywords", MultiVar(str), []],
),
)
@wx_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
kws = result.main_args.get("keywords")
image = await get_weather_now_card(matcher, event, kws)
await wx_alc.finish(UniMessage.image(raw=image))
@on_endswith(("天气", "weather")).handle()
async def _(event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
# kws = event.message.extract_plain_text()
kws = event.get_plaintext()
image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False)
if isinstance(event, satori.event.Event):
await matcher.finish(satori.MessageSegment.image(raw=image, mime="image/png"))
else:
await matcher.finish(MessageSegment.image(image))
async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True):
ulang = get_user_lang(event_utils.get_user_id(event))
qw_lang = get_qw_lang(ulang.lang_code)
key = get_config("weather_key")
is_dev = get_memory_data("weather.is_dev", True)
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), default=User())
# params
unit = user.profile.get("unit", "m")
stored_location = user.profile.get("location", None)
if not key:
await matcher.finish(ulang.get("weather.no_key") if tip else None)
if keyword:
if len(keyword) >= 2:
adm = keyword[0]
city = keyword[-1]
else:
adm = ""
city = keyword[0]
city_info = await city_lookup(city, key, adm=adm, lang=qw_lang)
city_name = " ".join(keyword)
else:
if not stored_location:
await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None)
city_info = await city_lookup(stored_location, key, lang=qw_lang)
city_name = stored_location
if city_info.code == "200":
location_data = city_info.location[0]
else:
await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None)
weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev)
image = await template2image(
template=get_path("templates/weather_now.html", abs_path=True),
templates={
"data": {
"params" : {
"unit": unit,
"lang": ulang.lang_code,
},
"weatherNow" : weather_now,
"weatherDaily" : weather_daily,
"weatherHourly": weather_hourly,
"aqi" : aqi,
"location" : location_data.dump(),
"localization" : get_local_data(ulang.lang_code)
}
},
)
return image
from nonebot import require, on_endswith
from nonebot.adapters import satori
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.internal.matcher import Matcher
from src.utils.base.config import get_config
from src.utils.base.ly_typing import T_MessageEvent
from .qw_api import *
from src.utils.base.data_manager import User, user_db
from src.utils.base.language import Language, get_user_lang
from src.utils.base.resource import get_path
from src.utils.message.html_tool import template2image
from src.utils import event as event_utils
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma, UniMessage
wx_alc = on_alconna(
aliases={"天气"},
command=Alconna(
"weather",
Args["keywords", MultiVar(str), []],
),
)
@wx_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
kws = result.main_args.get("keywords")
image = await get_weather_now_card(matcher, event, kws)
await wx_alc.finish(UniMessage.image(raw=image))
@on_endswith(("天气", "weather")).handle()
async def _(event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
# kws = event.message.extract_plain_text()
kws = event.get_plaintext()
image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False)
if isinstance(event, satori.event.Event):
await matcher.finish(satori.MessageSegment.image(raw=image, mime="image/png"))
else:
await matcher.finish(MessageSegment.image(image))
async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True):
ulang = get_user_lang(event_utils.get_user_id(event))
qw_lang = get_qw_lang(ulang.lang_code)
key = get_config("weather_key")
is_dev = get_memory_data("weather.is_dev", True)
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), default=User())
# params
unit = user.profile.get("unit", "m")
stored_location = user.profile.get("location", None)
if not key:
await matcher.finish(ulang.get("weather.no_key") if tip else None)
if keyword:
if len(keyword) >= 2:
adm = keyword[0]
city = keyword[-1]
else:
adm = ""
city = keyword[0]
city_info = await city_lookup(city, key, adm=adm, lang=qw_lang)
city_name = " ".join(keyword)
else:
if not stored_location:
await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None)
city_info = await city_lookup(stored_location, key, lang=qw_lang)
city_name = stored_location
if city_info.code == "200":
location_data = city_info.location[0]
else:
await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None)
weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev)
image = await template2image(
template=get_path("templates/weather_now.html", abs_path=True),
templates={
"data": {
"params" : {
"unit": unit,
"lang": ulang.lang_code,
},
"weatherNow" : weather_now,
"weatherDaily" : weather_daily,
"weatherHourly": weather_hourly,
"aqi" : aqi,
"location" : location_data.dump(),
"localization" : get_local_data(ulang.lang_code)
}
},
)
return image

View File

@ -1,21 +1,21 @@
from nonebot.plugin import PluginMetadata
from .main import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="网页监控面板",
description="网页监控面板,用于查看机器人的状态和信息",
usage=(
"访问 127.0.0.1:port 查看机器人的状态信息\n"
"stat msg -g|--group [group_id] 查看群的统计信息,不带参数为全群\n"
"配置项custom_domain自定义域名通常对外用内网无需"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : False,
"default_enable": True,
}
)
from nonebot.plugin import PluginMetadata
from .main import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="网页监控面板",
description="网页监控面板,用于查看机器人的状态和信息",
usage=(
"访问 127.0.0.1:port 查看机器人的状态信息\n"
"stat msg -g|--group [group_id] 查看群的统计信息,不带参数为全群\n"
"配置项custom_domain自定义域名通常对外用内网无需"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : False,
"default_enable": True,
}
)

View File

@ -1,4 +1,4 @@
from fastapi import FastAPI
from nonebot import get_app
from fastapi import FastAPI
from nonebot import get_app
app: FastAPI = get_app()

View File

@ -1,10 +1,10 @@
from fastapi import FastAPI
from nonebot import get_app
from .restful_api import *
@app.get("/ping")
async def root():
return {
"message": "pong"
}
from fastapi import FastAPI
from nonebot import get_app
from .restful_api import *
@app.get("/ping")
async def root():
return {
"message": "pong"
}

View File

@ -1,24 +1,24 @@
from fastapi import FastAPI, APIRouter
from .common import *
device_info_router = APIRouter(prefix="/api/device-info")
bot_info_router = APIRouter(prefix="/api/bot-info")
@device_info_router.get("/")
async def device_info():
print("Hello Device Info")
return {
"message": "Hello Device Info"
}
@bot_info_router.get("/")
async def bot_info():
return {
"message": "Hello Bot Info"
}
app.include_router(device_info_router)
app.include_router(bot_info_router)
from fastapi import FastAPI, APIRouter
from .common import *
device_info_router = APIRouter(prefix="/api/device-info")
bot_info_router = APIRouter(prefix="/api/bot-info")
@device_info_router.get("/")
async def device_info():
print("Hello Device Info")
return {
"message": "Hello Device Info"
}
@bot_info_router.get("/")
async def bot_info():
return {
"message": "Hello Bot Info"
}
app.include_router(device_info_router)
app.include_router(bot_info_router)

View File

@ -1,3 +1,3 @@
name: Sign Status
description: for Lagrange
name: Sign Status
description: for Lagrange
version: 2024.4.26

View File

@ -1,4 +1,4 @@
.sign-chart {
height: 400px;
background-color: rgba(255, 255, 255, 0.7);
.sign-chart {
height: 400px;
background-color: rgba(255, 255, 255, 0.7);
}

View File

@ -1,75 +1,75 @@
// 数据类型声明
// import * as echarts from 'echarts';
let data = JSON.parse(document.getElementById("data").innerText) // object
const signChartDivTemplate = document.importNode(document.getElementById("sign-chart-template").content, true)
data.forEach((item) => {
let signChartDiv = signChartDivTemplate.cloneNode(true)
let chartID = item["name"]
// 初始化ECharts实例
// 设置id
signChartDiv.querySelector(".sign-chart").id = chartID
document.body.appendChild(signChartDiv)
let signChart = echarts.init(document.getElementById(chartID))
let timeCount = []
item["counts"].forEach((count, index) => {
// 计算平均值index - 1的count + index的count + index + 1的count /3
if (index > 0) {
timeCount.push((item["counts"][index] - item["counts"][index - 1]) / (60*(item["times"][index] - item["times"][index - 1])))
}
})
console.log(timeCount)
signChart.setOption(
{
animation: false,
title: {
text: item["name"],
textStyle: {
color: '#000000' // 设置标题文本颜色为红色
}
},
xAxis: {
type: 'category',
data: item["times"].map(timestampToTime),
},
yAxis: [
{
type: 'value',
min: Math.min(...item["counts"]),
},
{
type: 'value',
min: Math.min(...timeCount),
}
],
series: [
{
data: item["counts"],
type: 'line',
yAxisIndex: 0
},
{
data: timeCount,
type: 'line',
yAxisIndex: 1
}
]
}
)
})
function timestampToTime(timestamp) {
let date = new Date(timestamp * 1000)
let Y = date.getFullYear() + '-'
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
let D = date.getDate() + ' '
let h = date.getHours() + ':'
let m = date.getMinutes() + ':'
let s = date.getSeconds()
return M + D + h + m + s
// 数据类型声明
// import * as echarts from 'echarts';
let data = JSON.parse(document.getElementById("data").innerText) // object
const signChartDivTemplate = document.importNode(document.getElementById("sign-chart-template").content, true)
data.forEach((item) => {
let signChartDiv = signChartDivTemplate.cloneNode(true)
let chartID = item["name"]
// 初始化ECharts实例
// 设置id
signChartDiv.querySelector(".sign-chart").id = chartID
document.body.appendChild(signChartDiv)
let signChart = echarts.init(document.getElementById(chartID))
let timeCount = []
item["counts"].forEach((count, index) => {
// 计算平均值index - 1的count + index的count + index + 1的count /3
if (index > 0) {
timeCount.push((item["counts"][index] - item["counts"][index - 1]) / (60*(item["times"][index] - item["times"][index - 1])))
}
})
console.log(timeCount)
signChart.setOption(
{
animation: false,
title: {
text: item["name"],
textStyle: {
color: '#000000' // 设置标题文本颜色为红色
}
},
xAxis: {
type: 'category',
data: item["times"].map(timestampToTime),
},
yAxis: [
{
type: 'value',
min: Math.min(...item["counts"]),
},
{
type: 'value',
min: Math.min(...timeCount),
}
],
series: [
{
data: item["counts"],
type: 'line',
yAxisIndex: 0
},
{
data: timeCount,
type: 'line',
yAxisIndex: 1
}
]
}
)
})
function timestampToTime(timestamp) {
let date = new Date(timestamp * 1000)
let Y = date.getFullYear() + '-'
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
let D = date.getDate() + ' '
let h = date.getHours() + ':'
let m = date.getMinutes() + ':'
let s = date.getSeconds()
return M + D + h + m + s
}

View File

@ -1,22 +1,22 @@
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Liteyuki Status</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/sign_status.css">
</head>
<body>
<template id="sign-chart-template">
<div class="info-box sign-chart">
</div>
</template>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
<script src="./js/sign_status.js"></script>
<script src="./js/card.js"></script>
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Liteyuki Status</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/sign_status.css">
</head>
<body>
<template id="sign-chart-template">
<div class="info-box sign-chart">
</div>
</template>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
<script src="./js/sign_status.js"></script>
<script src="./js/card.js"></script>
</body>

View File

@ -1,3 +1,3 @@
crt.station=Station(s)
crt.hour=Hour(s)
crt.station=Station(s)
crt.hour=Hour(s)
crt.minute=Min(s)

View File

@ -1,3 +1,3 @@
crt.station=站
crt.hour=小时
crt.station=站
crt.hour=小时
crt.minute=分钟

View File

@ -1,3 +1,3 @@
name: CRT相关资源包
description: For Liteyuki CRT utils
name: CRT相关资源包
description: For Liteyuki CRT utils
version: 2024.4.26

View File

@ -1,161 +1,161 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CRT 线路图</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
</head>
<style>
:root {
--color-primary: #f00;
--color-secondary: #fff;
--sub-text-color: #aaa;
}
.segment {
display: flex;
background-color: #0d1117;
margin-bottom: 10px;
}
.line-icon {
margin-right: 10px;
}
.vertical-bar {
display: flex;
flex-direction: column;
align-items: center;
width: 20px;
height: 100%;
border-radius: 50px;
background-color: #f00;
text-align: center;
}
.station-dot {
width: 15px;
height: 15px;
aspect-ratio: 1/1;
border-radius: 50%;
background-color: #fff;
}
/* 第一个点在bar顶端第二个在底部*/
.station-dot:first-child {
margin-top: 2px;
margin-bottom: auto;
}
.station-dot:last-child {
margin-top: auto;
margin-bottom: 2px;
}
.transfer-info {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.station-name {
font-size: 16px;
color: var(--color-secondary);
margin-right: 10px;
}
.end-station {
margin-top: auto;
}
.line-info {
display: flex;
align-items: center;
}
.line-name {
padding: 3px;
border-radius: 50px;
background-color: #f00;
color: var(--color-secondary);
font-size: 10px;
margin-right: 10px;
}
.line-direction {
font-size: 12px;
color: var(--sub-text-color);
}
.station-info {
align-items: center;
font-size: 10px;
color: var(--sub-text-color);
margin: 10px 0;
}
.start-station {
display: flex;
align-items: center;
}
.segment-index {
font-size: 12px;
color: var(--sub-text-color);
}
</style>
<body>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<!---->
<template id="segment-template">
<div class="segment">
<div class="line-icon">
<!-- 竖条-->
<div class="vertical-bar">
<div class="station-dot"></div>
<div class="station-dot"></div>
</div>
</div>
<div class="transfer-info">
<div class="start-station">
<div class="station-name start-station-name">
下北泽站
</div>
<div class="segment-index">
第1段
</div>
</div>
<div class="line-info">
<div class="line-name">
轨道交通环线外环
</div>
<div class="line-direction">
沙坪坝方向
</div>
</div>
<div class="station-info">
5站14分钟
</div>
<div class="end-station">
<div class="station-name end-station-name">
新桥站
</div>
</div>
</div>
</div>
</template>
<template id="route-template">
<div class="info-box route-info" id="route-info">
</div>
</template>
<script src="./js/card.js"></script>
<script src="./js/crt_route.js"></script>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CRT 线路图</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
</head>
<style>
:root {
--color-primary: #f00;
--color-secondary: #fff;
--sub-text-color: #aaa;
}
.segment {
display: flex;
background-color: #0d1117;
margin-bottom: 10px;
}
.line-icon {
margin-right: 10px;
}
.vertical-bar {
display: flex;
flex-direction: column;
align-items: center;
width: 20px;
height: 100%;
border-radius: 50px;
background-color: #f00;
text-align: center;
}
.station-dot {
width: 15px;
height: 15px;
aspect-ratio: 1/1;
border-radius: 50%;
background-color: #fff;
}
/* 第一个点在bar顶端第二个在底部*/
.station-dot:first-child {
margin-top: 2px;
margin-bottom: auto;
}
.station-dot:last-child {
margin-top: auto;
margin-bottom: 2px;
}
.transfer-info {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.station-name {
font-size: 16px;
color: var(--color-secondary);
margin-right: 10px;
}
.end-station {
margin-top: auto;
}
.line-info {
display: flex;
align-items: center;
}
.line-name {
padding: 3px;
border-radius: 50px;
background-color: #f00;
color: var(--color-secondary);
font-size: 10px;
margin-right: 10px;
}
.line-direction {
font-size: 12px;
color: var(--sub-text-color);
}
.station-info {
align-items: center;
font-size: 10px;
color: var(--sub-text-color);
margin: 10px 0;
}
.start-station {
display: flex;
align-items: center;
}
.segment-index {
font-size: 12px;
color: var(--sub-text-color);
}
</style>
<body>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<!---->
<template id="segment-template">
<div class="segment">
<div class="line-icon">
<!-- 竖条-->
<div class="vertical-bar">
<div class="station-dot"></div>
<div class="station-dot"></div>
</div>
</div>
<div class="transfer-info">
<div class="start-station">
<div class="station-name start-station-name">
下北泽站
</div>
<div class="segment-index">
第1段
</div>
</div>
<div class="line-info">
<div class="line-name">
轨道交通环线外环
</div>
<div class="line-direction">
沙坪坝方向
</div>
</div>
<div class="station-info">
5站14分钟
</div>
<div class="end-station">
<div class="station-name end-station-name">
新桥站
</div>
</div>
</div>
</div>
</template>
<template id="route-template">
<div class="info-box route-info" id="route-info">
</div>
</template>
<script src="./js/card.js"></script>
<script src="./js/crt_route.js"></script>
</body>
</html>

View File

@ -1,35 +1,35 @@
// Copyright (c) 2024 SnowyKami Liteyuki Studio All Rights Reserved.
/**
* @type {{
* results: Array<{
* abstracts: string,
* createdDt: string,
* endStaName: string,
* startStaName: string,
* isValid: boolean,
* needTimeScope: number,
* needTransferTimes: number,
* price: number,
* skipGenerateSequence: boolean,
* transferLines: string,
* transferLinesColor: string,
* transferStaDerict: string,
* transferStaNames: string,
* }>
* }}
*/
const data = JSON.parse(document.getElementById("data").innerText);
const results = data["result"];
const route_template = document.importNode(document.getElementById("route-template").content, true)
results.forEach(
(item, index) => {
}
)
// Copyright (c) 2024 SnowyKami Liteyuki Studio All Rights Reserved.
/**
* @type {{
* results: Array<{
* abstracts: string,
* createdDt: string,
* endStaName: string,
* startStaName: string,
* isValid: boolean,
* needTimeScope: number,
* needTransferTimes: number,
* price: number,
* skipGenerateSequence: boolean,
* transferLines: string,
* transferLinesColor: string,
* transferStaDerict: string,
* transferStaNames: string,
* }>
* }}
*/
const data = JSON.parse(document.getElementById("data").innerText);
const results = data["result"];
const route_template = document.importNode(document.getElementById("route-template").content, true)
results.forEach(
(item, index) => {
}
)

View File

@ -1,2 +1,2 @@
stat.message=统计消息
stat.message=统计消息
stat.rank=发言排名

View File

@ -1,3 +1,3 @@
name: 轻雪统计信息附件
description: For Liteyuki statistic
name: 轻雪统计信息附件
description: For Liteyuki statistic
version: 2024.4.26

View File

@ -1,4 +1,4 @@
.sign-chart {
height: 400px;
background-color: rgba(255, 255, 255, 0.7);
.sign-chart {
height: 400px;
background-color: rgba(255, 255, 255, 0.7);
}

View File

@ -1,54 +1,54 @@
// 数据类型声明
// import * as echarts from 'echarts';
let data = JSON.parse(document.getElementById("data").innerText) // object
const signChartDivTemplate = document.importNode(document.getElementById("sign-chart-template").content, true)
data.forEach((item) => {
let signChartDiv = signChartDivTemplate.cloneNode(true)
let chartID = item["name"]
// 初始化ECharts实例
// 设置id
signChartDiv.querySelector(".sign-chart").id = chartID
document.body.appendChild(signChartDiv)
let signChart = echarts.init(document.getElementById(chartID))
signChart.setOption(
{
animation: false,
title: {
text: item["name"],
textStyle: {
color: '#000000' // 设置标题文本颜色为红色
}
},
xAxis: {
type: 'category',
data: item["times"].map(timestampToTime),
},
yAxis: {
type: 'value',
min: Math.min(...item["counts"]),
},
series: [
{
data: item["counts"],
type: 'line',
}
]
}
)
})
function timestampToTime(timestamp) {
let date = new Date(timestamp * 1000)
let Y = date.getFullYear() + '-'
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
let D = date.getDate() + ' '
let h = date.getHours() + ':'
let m = date.getMinutes() + ':'
let s = date.getSeconds()
return M + D + h + m + s
// 数据类型声明
// import * as echarts from 'echarts';
let data = JSON.parse(document.getElementById("data").innerText) // object
const signChartDivTemplate = document.importNode(document.getElementById("sign-chart-template").content, true)
data.forEach((item) => {
let signChartDiv = signChartDivTemplate.cloneNode(true)
let chartID = item["name"]
// 初始化ECharts实例
// 设置id
signChartDiv.querySelector(".sign-chart").id = chartID
document.body.appendChild(signChartDiv)
let signChart = echarts.init(document.getElementById(chartID))
signChart.setOption(
{
animation: false,
title: {
text: item["name"],
textStyle: {
color: '#000000' // 设置标题文本颜色为红色
}
},
xAxis: {
type: 'category',
data: item["times"].map(timestampToTime),
},
yAxis: {
type: 'value',
min: Math.min(...item["counts"]),
},
series: [
{
data: item["counts"],
type: 'line',
}
]
}
)
})
function timestampToTime(timestamp) {
let date = new Date(timestamp * 1000)
let Y = date.getFullYear() + '-'
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
let D = date.getDate() + ' '
let h = date.getHours() + ':'
let m = date.getMinutes() + ':'
let s = date.getSeconds()
return M + D + h + m + s
}

View File

@ -1,25 +1,25 @@
let data = JSON.parse(document.getElementById("data").innerText) // object
const rowDiv = document.importNode(document.getElementById("row-template").content, true)
function randomHideChar(str) {
// 随机隐藏6位以上字符串的中间连续四位字符用*代替
if (str.length <= 6) {
return str
}
let start = Math.floor(str.length / 2) - 2
return str.slice(0, start) + "****" + str.slice(start + 4)
}
data["ranking"].forEach((item) => {
let row = rowDiv.cloneNode(true)
let rowID = item["name"]
let rowIconSrc = item["icon"]
let rowCount = item["count"]
row.querySelector(".row-name").innerText = randomHideChar(rowID)
row.querySelector(".row-icon").src = rowIconSrc
row.querySelector(".row-count").innerText = rowCount
document.body.appendChild(row)
})
let data = JSON.parse(document.getElementById("data").innerText) // object
const rowDiv = document.importNode(document.getElementById("row-template").content, true)
function randomHideChar(str) {
// 随机隐藏6位以上字符串的中间连续四位字符用*代替
if (str.length <= 6) {
return str
}
let start = Math.floor(str.length / 2) - 2
return str.slice(0, start) + "****" + str.slice(start + 4)
}
data["ranking"].forEach((item) => {
let row = rowDiv.cloneNode(true)
let rowID = item["name"]
let rowIconSrc = item["icon"]
let rowCount = item["count"]
row.querySelector(".row-name").innerText = randomHideChar(rowID)
row.querySelector(".row-icon").src = rowIconSrc
row.querySelector(".row-count").innerText = rowCount
document.body.appendChild(row)
})

View File

@ -1,22 +1,22 @@
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Liteyuki Stats Message</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/stat_msg.css">
</head>
<body>
<template id="sign-chart-template">
<div class="info-box sign-chart">
</div>
</template>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
<script src="./js/stat_msg.js"></script>
<script src="./js/card.js"></script>
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Liteyuki Stats Message</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/stat_msg.css">
</head>
<body>
<template id="sign-chart-template">
<div class="info-box sign-chart">
</div>
</template>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
<script src="./js/stat_msg.js"></script>
<script src="./js/card.js"></script>
</body>

View File

@ -1,54 +1,54 @@
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Liteyuki Stats Message</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/stat_rank.css">
<style>
.row {
height: 100px;
display: flex;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 100px;
margin-bottom: 10px;
padding-right: 10px;
}
.row-name {
font-size: 40px;
align-content: center;
width: 100px;
text-align: left;
}
.row-icon {
border-radius: 50%;
margin-right: auto;
}
.row-count {
align-content: center;
font-size: 40px;
/* 靠右*/
margin-left: auto;
}
</style>
</head>
<body>
<template id="row-template">
<div class="row">
<img src="./img/arrow-up.svg" alt="up" class="row-icon">
<div class="row-name"></div>
<div class="row-count"></div>
</div>
</template>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
<script src="./js/stat_rank.js"></script>
<script src="./js/card.js"></script>
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Liteyuki Stats Message</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/stat_rank.css">
<style>
.row {
height: 100px;
display: flex;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 100px;
margin-bottom: 10px;
padding-right: 10px;
}
.row-name {
font-size: 40px;
align-content: center;
width: 100px;
text-align: left;
}
.row-icon {
border-radius: 50%;
margin-right: auto;
}
.row-count {
align-content: center;
font-size: 40px;
/* 靠右*/
margin-left: auto;
}
</style>
</head>
<body>
<template id="row-template">
<div class="row">
<img src="./img/arrow-up.svg" alt="up" class="row-icon">
<div class="row-name"></div>
<div class="row-count"></div>
</div>
</template>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
<script src="./js/stat_rank.js"></script>
<script src="./js/card.js"></script>
</body>

View File

@ -1,12 +1,12 @@
weather.monday=Mon
weather.tuesday=Tue
weather.wednesday=Wed
weather.thursday=Thu
weather.friday=Fri
weather.saturday=Sat
weather.sunday=Sun
weather.day=Day
weather.night=Night
weather.today=Today
weather.tomorrow=Tomorrow
weather.monday=Mon
weather.tuesday=Tue
weather.wednesday=Wed
weather.thursday=Thu
weather.friday=Fri
weather.saturday=Sat
weather.sunday=Sun
weather.day=Day
weather.night=Night
weather.today=Today
weather.tomorrow=Tomorrow
weather.no_aqi=No AQI data

View File

@ -1,12 +1,12 @@
weather.monday=月
weather.tuesday=火
weather.wednesday=水
weather.thursday=木
weather.friday=金
weather.saturday=土
weather.sunday=日
weather.day=昼
weather.night=夜
weather.today=今日
weather.tomorrow=明日
weather.monday=月
weather.tuesday=火
weather.wednesday=水
weather.thursday=木
weather.friday=金
weather.saturday=土
weather.sunday=日
weather.day=昼
weather.night=夜
weather.today=今日
weather.tomorrow=明日
weather.no_aqi=空気質データなし

View File

@ -1,12 +1,12 @@
weather.monday=周一
weather.tuesday=周二
weather.wednesday=周三
weather.thursday=周四
weather.friday=周五
weather.saturday=周六
weather.sunday=周日
weather.day=白天
weather.night=夜晚
weather.today=今天
weather.tomorrow=明天
weather.monday=周一
weather.tuesday=周二
weather.wednesday=周三
weather.thursday=周四
weather.friday=周五
weather.saturday=周六
weather.sunday=周日
weather.day=白天
weather.night=夜晚
weather.today=今天
weather.tomorrow=明天
weather.no_aqi=暂无AQI数据

View File

@ -1,3 +1,3 @@
name: 轻雪天气资源包
description: For Liteyuki Weather
name: 轻雪天气资源包
description: For Liteyuki Weather
version: 2024.4.26

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