🐛 fix 通道类回调函数在进程间传递时无法序列号的问题

This commit is contained in:
远野千束 2024-08-10 22:25:41 +08:00
parent 3bd40e7271
commit 7107d03b72
66 changed files with 5112 additions and 4916 deletions

View File

@ -1,25 +1,26 @@
import asyncio
import os
import platform
import sys
import threading import threading
import time import time
import asyncio
from typing import Any, Optional from typing import Any, Optional
from multiprocessing import freeze_support from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from liteyuki.bot.lifespan import (LIFESPAN_FUNC, Lifespan) from liteyuki.bot.lifespan import (LIFESPAN_FUNC, Lifespan)
from liteyuki.comm.channel import Channel from liteyuki.comm.channel import Channel, set_channel
from liteyuki.core import IS_MAIN_PROCESS from liteyuki.core import IS_MAIN_PROCESS
from liteyuki.core.manager import ProcessManager from liteyuki.core.manager import ProcessManager
from liteyuki.core.spawn_process import mb_run, nb_run from liteyuki.core.spawn_process import mb_run, nb_run
from liteyuki.log import init_log, logger from liteyuki.log import init_log, logger
from liteyuki.plugin import load_plugins from liteyuki.plugin import load_plugins
from liteyuki.utils import run_coroutine
__all__ = [ __all__ = [
"LiteyukiBot", "LiteyukiBot",
"get_bot" "get_bot"
] ]
"""是否为主进程"""
class LiteyukiBot: class LiteyukiBot:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -29,11 +30,12 @@ class LiteyukiBot:
self.init(**self.config) # 初始化 self.init(**self.config) # 初始化
self.lifespan: Lifespan = Lifespan() self.lifespan: Lifespan = Lifespan()
self.chan = Channel() # 进程通信通道
self.pm: ProcessManager = ProcessManager(bot=self, chan=self.chan) self.process_manager: ProcessManager = ProcessManager(bot=self)
self.loop = asyncio.new_event_loop() self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
self.call_restart_count = 0
print("\033[34m" + r""" print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______ __ ______ ________ ________ __ __ __ __ __ __ ______
@ -53,15 +55,83 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
self.loop_thread.start() # 启动事件循环 self.loop_thread.start() # 启动事件循环
asyncio.run(self.lifespan.before_start()) # 启动前钩子 asyncio.run(self.lifespan.before_start()) # 启动前钩子
self.pm.add_target("nonebot", nb_run, **self.config) self.process_manager.add_target("nonebot", nb_run, **self.config)
self.pm.start("nonebot") self.process_manager.start("nonebot")
self.pm.add_target("melobot", mb_run, **self.config) self.process_manager.add_target("melobot", mb_run, **self.config)
self.pm.start("melobot") self.process_manager.start("melobot")
asyncio.run(self.lifespan.after_start()) # 启动后钩子 asyncio.run(self.lifespan.after_start()) # 启动后钩子
def restart(self, name: Optional[str] = None): self.start_watcher() # 启动文件监视器
def start_watcher(self):
if self.config.get("debug", False):
code_directories = {}
src_directories = (
"liteyuki",
"src/liteyuki_main",
"src/liteyuki_plugins",
"src/nonebot_plugins",
"src/utils",
)
src_excludes_extensions = (
"pyc",
)
logger.debug("Liteyuki Reload enabled, watching for file changes...")
restart = self.restart_process
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
logger.info(f"{event.src_path} modified, reloading bot...")
restart()
code_modified_handler = CodeModifiedHandler()
observer = Observer()
for directory in src_directories:
observer.schedule(code_modified_handler, directory, recursive=True)
observer.start()
def restart(self, delay: int = 0):
"""
重启轻雪本体
Returns:
"""
if self.call_restart_count < 1:
executable = sys.executable
args = sys.argv
logger.info("Restarting LiteyukiBot...")
time.sleep(delay)
if platform.system() == "Windows":
cmd = "start"
elif platform.system() == "Linux":
cmd = "nohup"
elif platform.system() == "Darwin":
cmd = "open"
else:
cmd = "nohup"
self.process_manager.terminate_all()
# 等待所有进程退出
self.process_manager.chan_active.receive("main")
# 进程退出后重启
threading.Thread(target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)).start()
sys.exit(0)
self.call_restart_count += 1
def restart_process(self, name: Optional[str] = None):
""" """
停止轻雪 停止轻雪
Args: Args:
@ -75,10 +145,10 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子 self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子
if name: if name:
self.chan.send(1, name) self.chan_active.send(1, name)
else: else:
for name in self.pm.targets: for name in self.process_manager.targets:
self.chan.send(1, name) self.chan_active.send(1, name)
def init(self, *args, **kwargs): def init(self, *args, **kwargs):
""" """

View File

@ -9,11 +9,22 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Software: PyCharm @Software: PyCharm
该模块用于轻雪主进程和Nonebot子进程之间的通信 该模块用于轻雪主进程和Nonebot子进程之间的通信
""" """
from liteyuki.comm.channel import Channel, chan from liteyuki.comm.channel import (
Channel,
chan,
get_channel,
set_channel,
set_channels,
get_channels
)
from liteyuki.comm.event import Event from liteyuki.comm.event import Event
__all__ = [ __all__ = [
"Channel", "Channel",
"chan", "chan",
"Event", "Event",
"get_channel",
"set_channel",
"set_channels",
"get_channels"
] ]

View File

@ -10,8 +10,12 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
本模块定义了一个通用的通道类用于进程间通信 本模块定义了一个通用的通道类用于进程间通信
""" """
import functools
import multiprocessing
import threading
from multiprocessing import Pipe from multiprocessing import Pipe
from typing import Any, Optional, Callable, Awaitable, List, TypeAlias from typing import Any, Optional, Callable, Awaitable, List, TypeAlias
from uuid import uuid4
from liteyuki.utils import is_coroutine_callable, run_coroutine from liteyuki.utils import is_coroutine_callable, run_coroutine
@ -23,76 +27,89 @@ SYNC_FILTER_FUNC: TypeAlias = Callable[[Any], bool]
ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]] ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]]
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
_channel: dict[str, "Channel"] = {}
_callback_funcs: dict[str, ON_RECEIVE_FUNC] = {}
class Channel: class Channel:
""" """
通道类用于进程间通信 通道类用于进程间通信进程内不可用仅限主进程和子进程之间通信
有两种接收工作方式但是只能选择一种主动接收和被动接收主动接收使用 `receive` 方法被动接收使用 `on_receive` 装饰器 有两种接收工作方式但是只能选择一种主动接收和被动接收主动接收使用 `receive` 方法被动接收使用 `on_receive` 装饰器
""" """
def __init__(self): def __init__(self, _id: str):
self.receive_conn, self.send_conn = Pipe() self.main_send_conn, self.sub_receive_conn = Pipe()
self.sub_send_conn, self.main_receive_conn = Pipe()
self._closed = False self._closed = False
self._on_receive_funcs: List[ON_RECEIVE_FUNC] = [] self._on_main_receive_funcs: list[str] = []
self._on_receive_funcs_with_receiver: dict[str, List[ON_RECEIVE_FUNC]] = {} self._on_sub_receive_funcs: list[str] = []
self.name: str = _id
def send(self, data: Any, receiver: Optional[str] = None): self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
def __str__(self):
return f"Channel({self.name})"
def send(self, data: Any):
""" """
发送数据 发送数据
Args: Args:
data: 数据 data: 数据
receiver: 接收者如果为None则广播
""" """
if self._closed: if self._closed:
raise RuntimeError("Cannot send to a closed channel") raise RuntimeError("Cannot send to a closed channel")
self.send_conn.send((data, receiver)) if IS_MAIN_PROCESS:
print("主进程发送数据:", data)
self.main_send_conn.send(data)
else:
print("子进程发送数据:", data)
self.sub_send_conn.send(data)
def receive(self, receiver: str = None) -> Any: def receive(self) -> Any:
""" """
接收数据 接收数据
Args: Args:
receiver: 接收者如果为None则接收任意数据
""" """
if self._closed: if self._closed:
raise RuntimeError("Cannot receive from a closed channel") raise RuntimeError("Cannot receive from a closed channel")
while True: while True:
# 判断receiver是否为None或者receiver是否等于接收者是则接收数据否则不动数据 # 判断receiver是否为None或者receiver是否等于接收者是则接收数据否则不动数据
data, receiver_ = self.receive_conn.recv() if IS_MAIN_PROCESS:
if receiver is None or receiver == receiver_: data = self.main_receive_conn.recv()
self._run_on_receive_funcs(data, receiver_) print("主进程接收数据:", data)
return data else:
self.send_conn.send((data, receiver_)) data = self.sub_receive_conn.recv()
print("子进程接收数据:", data)
def peek(self) -> Optional[Any]:
"""
查看管道中的数据不移除
Returns:
"""
if self._closed:
raise RuntimeError("Cannot peek from a closed channel")
if self.receive_conn.poll():
data, receiver = self.receive_conn.recv()
self.receive_conn.send((data, receiver))
return data return data
return None
def close(self): def close(self):
""" """
关闭通道 关闭通道
""" """
self._closed = True self._closed = True
self.receive_conn.close() self.sub_receive_conn.close()
self.send_conn.close() self.main_send_conn.close()
self.sub_send_conn.close()
self.main_receive_conn.close()
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None, receiver: Optional[str] = None) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]: def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
""" """
接收数据并执行函数 接收数据并执行函数
Args: Args:
filter_func: 过滤函数为None则不过滤 filter_func: 过滤函数为None则不过滤
receiver: 接收者, 为None则接收任意数据
Returns: Returns:
装饰器装饰一个函数在接收到数据后执行 装饰器装饰一个函数在接收到数据后执行
""" """
if (not self.is_sub_receive_loop_running) and not IS_MAIN_PROCESS:
threading.Thread(target=self._start_sub_receive_loop).start()
if (not self.is_main_receive_loop_running) and IS_MAIN_PROCESS:
threading.Thread(target=self._start_main_receive_loop).start()
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC: def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any) -> Any: async def wrapper(data: Any) -> Any:
@ -105,28 +122,53 @@ class Channel:
return return
return await func(data) return await func(data)
if receiver is None: function_id = str(uuid4())
self._on_receive_funcs.append(wrapper) _callback_funcs[function_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(function_id)
else: else:
if receiver not in self._on_receive_funcs_with_receiver: self._on_sub_receive_funcs.append(function_id)
self._on_receive_funcs_with_receiver[receiver] = []
self._on_receive_funcs_with_receiver[receiver].append(wrapper)
return func return func
return decorator return decorator
def _run_on_receive_funcs(self, data: Any, receiver: Optional[str] = None): def _run_on_main_receive_funcs(self, data: Any):
""" """
运行接收函数 运行接收函数
Args: Args:
data: 数据 data: 数据
""" """
if receiver is None: for func_id in self._on_main_receive_funcs:
for func in self._on_receive_funcs: func = _callback_funcs[func_id]
run_coroutine(func(data)) run_coroutine(func(data))
else:
for func in self._on_receive_funcs_with_receiver.get(receiver, []): def _run_on_sub_receive_funcs(self, data: Any):
run_coroutine(func(data)) """
运行接收函数
Args:
data: 数据
"""
for func_id in self._on_sub_receive_funcs:
func = _callback_funcs[func_id]
run_coroutine(func(data))
def _start_main_receive_loop(self):
"""
开始接收数据
"""
self.is_main_receive_loop_running = True
while not self._closed:
data = self.main_receive_conn.recv()
self._run_on_main_receive_funcs(data)
def _start_sub_receive_loop(self):
"""
开始接收数据
"""
self.is_sub_receive_loop_running = True
while not self._closed:
data = self.sub_receive_conn.recv()
self._run_on_sub_receive_funcs(data)
def __iter__(self): def __iter__(self):
return self return self
@ -136,4 +178,42 @@ class Channel:
"""默认通道实例,可直接从模块导入使用""" """默认通道实例,可直接从模块导入使用"""
chan = Channel() chan = Channel("default")
def set_channel(name: str, channel: Channel):
"""
设置通道实例
Args:
name: 通道名称
channel: 通道实例
"""
_channel[name] = channel
def set_channels(channels: dict[str, Channel]):
"""
设置通道实例
Args:
channels: 通道名称
"""
for name, channel in channels.items():
_channel[name] = channel
def get_channel(name: str) -> Optional[Channel]:
"""
获取通道实例
Args:
name: 通道名称
Returns:
"""
return _channel.get(name, None)
def get_channels() -> dict[str, Channel]:
"""
获取通道实例
Returns:
"""
return _channel

View File

@ -13,7 +13,7 @@ import threading
from multiprocessing import Process from multiprocessing import Process
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from liteyuki.comm import Channel from liteyuki.comm import Channel, get_channel, set_channels
from liteyuki.log import logger from liteyuki.log import logger
if TYPE_CHECKING: if TYPE_CHECKING:
@ -31,12 +31,18 @@ class ProcessManager:
在主进程中被调用 在主进程中被调用
""" """
def __init__(self, bot: "LiteyukiBot", chan: Channel): def __init__(self, bot: "LiteyukiBot"):
self.bot = bot self.bot = bot
self.chan = chan
self.targets: dict[str, tuple[callable, tuple, dict]] = {} self.targets: dict[str, tuple[callable, tuple, dict]] = {}
self.processes: dict[str, Process] = {} self.processes: dict[str, Process] = {}
set_channels({
"nonebot-active" : Channel(_id="nonebot-active"),
"melobot-active" : Channel(_id="melobot-active"),
"nonebot-passive": Channel(_id="nonebot-passive"),
"melobot-passive": Channel(_id="melobot-passive"),
})
def start(self, name: str, delay: int = 0): def start(self, name: str, delay: int = 0):
""" """
开启后自动监控进程并添加到进程字典中 开启后自动监控进程并添加到进程字典中
@ -47,19 +53,21 @@ class ProcessManager:
Returns: Returns:
""" """
if name not in self.targets: if name not in self.targets:
raise KeyError(f"Process {name} not found.") raise KeyError(f"Process {name} not found.")
def _start(): def _start():
should_exit = False should_exit = False
while not should_exit: while not should_exit:
process = Process(target=self.targets[name][0], args=(self.chan, *self.targets[name][1]), kwargs=self.targets[name][2]) chan_active = get_channel(f"{name}-active")
chan_passive = get_channel(f"{name}-passive")
process = Process(target=self.targets[name][0], args=(chan_active, chan_passive, *self.targets[name][1]),
kwargs=self.targets[name][2])
self.processes[name] = process self.processes[name] = process
process.start() process.start()
while not should_exit: while not should_exit:
# 0退出 1重启 # 0退出 1重启
data = self.chan.receive(name) data = chan_active.receive()
if data == 1: if data == 1:
logger.info(f"Restarting process {name}") logger.info(f"Restarting process {name}")
asyncio.run(self.bot.lifespan.before_shutdown()) asyncio.run(self.bot.lifespan.before_shutdown())
@ -103,3 +111,7 @@ class ProcessManager:
process.join(TIMEOUT) process.join(TIMEOUT)
if process.is_alive(): if process.is_alive():
process.kill() process.kill()
def terminate_all(self):
for name in self.targets:
self.terminate(name)

View File

@ -3,6 +3,7 @@ from typing import Optional, TYPE_CHECKING
import nonebot import nonebot
from liteyuki.core.nb import adapter_manager, driver_manager from liteyuki.core.nb import adapter_manager, driver_manager
from liteyuki.comm.channel import set_channel
if TYPE_CHECKING: if TYPE_CHECKING:
from liteyuki.comm.channel import Channel from liteyuki.comm.channel import Channel
@ -10,23 +11,23 @@ if TYPE_CHECKING:
timeout_limit: int = 20 timeout_limit: int = 20
"""导出对象用于主进程与nonebot通信""" """导出对象用于主进程与nonebot通信"""
chan_in_spawn_nb: Optional["Channel"] = None _channels = {}
def nb_run(chan, *args, **kwargs): def nb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs):
""" """
初始化NoneBot并运行在子进程 初始化NoneBot并运行在子进程
Args: Args:
chan: chan_active:
*args: chan_passive:
**kwargs: **kwargs:
Returns: Returns:
""" """
global chan_in_spawn_nb set_channel("nonebot-active", chan_active)
chan_in_spawn_nb = chan set_channel("nonebot-passive", chan_passive)
nonebot.init(**kwargs) nonebot.init(**kwargs)
driver_manager.init(config=kwargs) driver_manager.init(config=kwargs)
adapter_manager.init(kwargs) adapter_manager.init(kwargs)
@ -35,17 +36,21 @@ def nb_run(chan, *args, **kwargs):
nonebot.run() nonebot.run()
def mb_run(chan, *args, **kwargs): def mb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs):
""" """
初始化MeloBot并运行在子进程 初始化MeloBot并运行在子进程
Args: Args:
chan chan_active
chan_passive
*args: *args:
**kwargs: **kwargs:
Returns: Returns:
""" """
set_channel("melobot-active", chan_active)
set_channel("melobot-passive", chan_passive)
# bot = MeloBot(__name__) # bot = MeloBot(__name__)
# bot.init(AbstractConnector(cd_time=0)) # bot.init(AbstractConnector(cd_time=0))
# bot.run() # bot.run()

View File

@ -7,14 +7,19 @@
# @Email : snowykami@outlook.com # @Email : snowykami@outlook.com
# @File : asa.py # @File : asa.py
# @Software: PyCharm # @Software: PyCharm
import asyncio
from liteyuki.plugin import PluginMetadata from liteyuki.plugin import PluginMetadata
from liteyuki import get_bot, logger from liteyuki import get_bot, logger
from liteyuki.comm.channel import get_channel
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="lifespan_monitor", name="lifespan_monitor",
) )
bot = get_bot() bot = get_bot()
nbp_chan = get_channel("nonebot-passive")
mbp_chan = get_channel("melobot-passive")
@bot.on_before_start @bot.on_before_start
@ -24,6 +29,7 @@ def _():
@bot.on_before_shutdown @bot.on_before_shutdown
def _(): def _():
print(get_channel("main"))
logger.info("生命周期监控器:准备停止") logger.info("生命周期监控器:准备停止")
@ -35,3 +41,17 @@ def _():
@bot.on_after_start @bot.on_after_start
def _(): def _():
logger.info("生命周期监控器:启动完成") logger.info("生命周期监控器:启动完成")
@bot.on_after_start
async def _():
logger.info("生命周期监控器:启动完成")
while True:
await asyncio.sleep(3)
nbp_chan.send("send by main")
@mbp_chan.on_receive()
@nbp_chan.on_receive()
async def _(data):
print("主进程收到数据", data)

View File

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

Binary file not shown.

View File

@ -6,29 +6,25 @@ import nonebot
import pip import pip
from nonebot import Bot, get_driver, require from nonebot import Bot, get_driver, require
from nonebot.adapters import onebot, satori from nonebot.adapters import onebot, satori
from nonebot.adapters.onebot.v11 import Message, escape, unescape from nonebot.adapters.onebot.v11 import Message, unescape
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
from nonebot.internal.matcher import Matcher from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from liteyuki import Channel # from src.liteyuki.core import Reloader
from src.utils import event as event_utils, satori_utils
from src.utils.base.config import get_config, load_from_yaml 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.data_manager import StoredConfig, TempConfig, common_db
from src.utils.base.language import get_user_lang from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
# from src.liteyuki.core import Reloader
from src.utils import event as event_utils, satori_utils
from liteyuki.core.spawn_process import chan_in_spawn_nb
from .api import update_liteyuki from .api import update_liteyuki
from liteyuki.bot import get_bot
from ..utils.base import reload from ..utils.base import reload
from ..utils.base.ly_function import get_function from ..utils.base.ly_function import get_function
require("nonebot_plugin_alconna") require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler") require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
driver = get_driver() driver = get_driver()
@ -81,7 +77,6 @@ async def _(bot: T_Bot, event: T_MessageEvent):
).handle() ).handle()
# Satori OK # Satori OK
async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent): async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
global channel_in_spawn_process
await matcher.send("Liteyuki reloading") await matcher.send("Liteyuki reloading")
temp_data = common_db.where_one(TempConfig(), default=TempConfig()) temp_data = common_db.where_one(TempConfig(), default=TempConfig())
@ -91,8 +86,8 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
"reload_time" : time.time(), "reload_time" : time.time(),
"reload_bot_id" : bot.self_id, "reload_bot_id" : bot.self_id,
"reload_session_type": event_utils.get_message_type(event), "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, "reload_session_id" : (event.group_id if event.message_type == "group" else event.user_id)
satori.event.Event) else event.chan.id, if not isinstance(event, satori.event.Event) else event.chan_active.id,
"delta_time" : 0 "delta_time" : 0
} }
) )
@ -358,7 +353,7 @@ async def _(bot: T_Bot):
delta_time = temp_data.data.get("delta_time", 0) delta_time = temp_data.data.get("delta_time", 0)
common_db.save(temp_data) # 更新数据 common_db.save(temp_data) # 更新数据
if delta_time <= 20.0: # 启动时间太长就别发了,丢人 if delta_time <= 20.0: # 启动时间太长就别发了,丢人
if isinstance(bot, satori.Bot): if isinstance(bot, satori.Bot):
await bot.send_message( await bot.send_message(
channel_id=reload_session_id, channel_id=reload_session_id,

View File

@ -11,35 +11,12 @@ if get_config("debug", False):
liteyuki_bot = get_bot() liteyuki_bot = get_bot()
src_directories = (
"src/liteyuki_main",
"src/plugins",
"src/utils",
)
src_excludes_extensions = (
"pyc",
)
res_directories = ( res_directories = (
"src/resources", "src/resources",
"resources", "resources",
) )
nonebot.logger.info("Liteyuki Reload enabled, 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...")
reload()
class ResourceModifiedHandler(FileSystemEventHandler): class ResourceModifiedHandler(FileSystemEventHandler):
""" """
@ -51,12 +28,9 @@ if get_config("debug", False):
load_resources() load_resources()
code_modified_handler = CodeModifiedHandler()
resource_modified_handle = ResourceModifiedHandler() resource_modified_handle = ResourceModifiedHandler()
observer = Observer() observer = Observer()
for directory in src_directories:
observer.schedule(code_modified_handler, directory, recursive=True)
for directory in res_directories: for directory in res_directories:
observer.schedule(resource_modified_handle, directory, recursive=True) observer.schedule(resource_modified_handle, directory, recursive=True)
observer.start() observer.start()

View File

@ -21,7 +21,7 @@ liteyuki_bot = get_bot()
@driver.on_startup @driver.on_startup
async def load_plugins(): async def load_plugins():
nonebot.plugin.load_plugins("src/plugins") nonebot.plugin.load_plugins("src/nonebot_plugins")
# 从数据库读取已安装的插件 # 从数据库读取已安装的插件
if not get_config("safe_mode", False): if not get_config("safe_mode", False):
# 安全模式下,不加载插件 # 安全模式下,不加载插件

View File

@ -0,0 +1,3 @@
# 说明
此目录为**轻雪插件**目录,非其他插件目录。

View File

@ -1,18 +1,27 @@
import multiprocessing import multiprocessing
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from .rt_guide import * from liteyuki.comm import get_channel
from .crt_matchers import * from .rt_guide import *
from .crt_matchers import *
__plugin_meta__ = PluginMetadata(
name="CRT生成工具", __plugin_meta__ = PluginMetadata(
description="一些CRT牌子生成器", name="CRT生成工具",
usage="我觉得你应该会用", description="一些CRT牌子生成器",
type="application", usage="我觉得你应该会用",
homepage="https://github.com/snowykami/LiteyukiBot", type="application",
extra={ homepage="https://github.com/snowykami/LiteyukiBot",
"liteyuki" : True, extra={
"toggleable" : True, "liteyuki" : True,
"default_enable": True, "toggleable" : True,
} "default_enable": True,
) }
)
chan = get_channel("nonebot-passive")
@chan.on_receive()
async def _(d):
print("CRT子进程接收到数据", d)
chan.send("CRT子进程已接收到数据")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from .minesweeper import * from .minesweeper import *
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="轻雪小游戏", name="轻雪小游戏",
description="内置了一些小游戏", description="内置了一些小游戏",
usage="", usage="",
type="application", type="application",
homepage="https://github.com/snowykami/LiteyukiBot", homepage="https://github.com/snowykami/LiteyukiBot",
extra={ extra={
"liteyuki": True, "liteyuki": True,
"toggleable" : True, "toggleable" : True,
"default_enable" : True, "default_enable" : True,
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,18 @@
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from .monitors import * from .monitors import *
from .matchers import * from .matchers import *
__author__ = "snowykami" __author__ = "snowykami"
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="轻雪智障回复", name="轻雪智障回复",
description="", description="",
usage="", usage="",
type="application", type="application",
homepage="https://github.com/snowykami/LiteyukiBot", homepage="https://github.com/snowykami/LiteyukiBot",
extra={ extra={
"liteyuki": True, "liteyuki": True,
"toggleable" : True, "toggleable" : True,
"default_enable" : True, "default_enable" : True,
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,60 +1,60 @@
from src.utils import event as event_utils from src.utils import event as event_utils
from src.utils.base.language import get_user_lang from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from .api import * from .api import *
require("nonebot_plugin_alconna") require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, UniMessage from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, UniMessage
status_alc = on_alconna( status_alc = on_alconna(
aliases={"状态"}, aliases={"状态"},
command=Alconna( command=Alconna(
"status", "status",
Subcommand( Subcommand(
"memory", "memory",
alias={"mem", "m", "内存"}, alias={"mem", "m", "内存"},
), ),
Subcommand( Subcommand(
"process", "process",
alias={"proc", "p", "进程"}, alias={"proc", "p", "进程"},
), ),
Subcommand( Subcommand(
"refresh", "refresh",
alias={"refr", "r", "刷新"}, alias={"refr", "r", "刷新"},
), ),
), ),
) )
status_card_cache = {} # lang -> bytes status_card_cache = {} # lang -> bytes
@status_alc.handle() @status_alc.handle()
async def _(event: T_MessageEvent, bot: T_Bot): async def _(event: T_MessageEvent, bot: T_Bot):
ulang = get_user_lang(event_utils.get_user_id(event)) ulang = get_user_lang(event_utils.get_user_id(event))
global status_card_cache global status_card_cache
if ulang.lang_code not in status_card_cache.keys() or ( if ulang.lang_code not in status_card_cache.keys() or (
ulang.lang_code in status_card_cache.keys() ulang.lang_code in status_card_cache.keys()
and time.time() - status_card_cache[ulang.lang_code][1] > 60 and time.time() - status_card_cache[ulang.lang_code][1] > 60
): ):
status_card_cache[ulang.lang_code] = ( status_card_cache[ulang.lang_code] = (
await generate_status_card( await generate_status_card(
bot=await get_bots_data(), bot=await get_bots_data(),
hardware=await get_hardware_data(), hardware=await get_hardware_data(),
liteyuki=await get_liteyuki_data(), liteyuki=await get_liteyuki_data(),
lang=ulang.lang_code, lang=ulang.lang_code,
bot_id=bot.self_id, bot_id=bot.self_id,
), ),
time.time(), time.time(),
) )
image = status_card_cache[ulang.lang_code][0] image = status_card_cache[ulang.lang_code][0]
await status_alc.finish(UniMessage.image(raw=image)) await status_alc.finish(UniMessage.image(raw=image))
@status_alc.assign("memory") @status_alc.assign("memory")
async def _(): async def _():
pass pass
@status_alc.assign("process") @status_alc.assign("process")
async def _(): async def _():
pass pass

View File

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

View File

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

View File

@ -1,16 +1,16 @@
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from .profile_manager import * from .profile_manager import *
__author__ = "snowykami" __author__ = "snowykami"
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="轻雪用户管理", name="轻雪用户管理",
description="用户管理插件", description="用户管理插件",
usage="", usage="",
homepage="https://github.com/snowykami/LiteyukiBot", homepage="https://github.com/snowykami/LiteyukiBot",
extra={ extra={
"liteyuki" : True, "liteyuki" : True,
"toggleable" : False, "toggleable" : False,
"default_enable": True, "default_enable": True,
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import threading import threading
from nonebot import logger from nonebot import logger
from liteyuki.core.spawn_process import chan_in_spawn_nb from liteyuki.comm.channel import get_channel
def reload(delay: float = 0.0, receiver: str = "nonebot"): def reload(delay: float = 0.0, receiver: str = "nonebot"):
@ -14,6 +14,13 @@ def reload(delay: float = 0.0, receiver: str = "nonebot"):
Returns: Returns:
""" """
chan = get_channel(receiver + "-active")
if chan is None:
logger.error(f"Channel {receiver}-active not found, cannot reload.")
return
chan_in_spawn_nb.send(1, receiver) if delay > 0:
logger.info(f"Reloading LiteyukiBot({receiver})...") threading.Timer(delay, chan.send, args=(1,)).start()
return
else:
chan.send(1)