diff --git a/liteyuki/bot/__init__.py b/liteyuki/bot/__init__.py index 55163a8e..94bad37d 100644 --- a/liteyuki/bot/__init__.py +++ b/liteyuki/bot/__init__.py @@ -1,25 +1,26 @@ +import asyncio +import os +import platform +import sys import threading import time -import asyncio 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.comm.channel import Channel +from liteyuki.comm.channel import Channel, set_channel from liteyuki.core import IS_MAIN_PROCESS from liteyuki.core.manager import ProcessManager from liteyuki.core.spawn_process import mb_run, nb_run from liteyuki.log import init_log, logger from liteyuki.plugin import load_plugins -from liteyuki.utils import run_coroutine __all__ = [ "LiteyukiBot", "get_bot" ] -"""是否为主进程""" - class LiteyukiBot: def __init__(self, *args, **kwargs): @@ -29,11 +30,12 @@ class LiteyukiBot: self.init(**self.config) # 初始化 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() asyncio.set_event_loop(self.loop) self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) + self.call_restart_count = 0 print("\033[34m" + r""" __ ______ ________ ________ __ __ __ __ __ __ ______ @@ -53,15 +55,83 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ self.loop_thread.start() # 启动事件循环 asyncio.run(self.lifespan.before_start()) # 启动前钩子 - self.pm.add_target("nonebot", nb_run, **self.config) - self.pm.start("nonebot") + self.process_manager.add_target("nonebot", nb_run, **self.config) + self.process_manager.start("nonebot") - self.pm.add_target("melobot", mb_run, **self.config) - self.pm.start("melobot") + self.process_manager.add_target("melobot", mb_run, **self.config) + self.process_manager.start("melobot") 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: @@ -75,10 +145,10 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子 if name: - self.chan.send(1, name) + self.chan_active.send(1, name) else: - for name in self.pm.targets: - self.chan.send(1, name) + for name in self.process_manager.targets: + self.chan_active.send(1, name) def init(self, *args, **kwargs): """ diff --git a/liteyuki/comm/__init__.py b/liteyuki/comm/__init__.py index fd19662a..ac4c6a78 100644 --- a/liteyuki/comm/__init__.py +++ b/liteyuki/comm/__init__.py @@ -9,11 +9,22 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved @Software: PyCharm 该模块用于轻雪主进程和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 __all__ = [ "Channel", "chan", "Event", + "get_channel", + "set_channel", + "set_channels", + "get_channels" ] diff --git a/liteyuki/comm/channel.py b/liteyuki/comm/channel.py index 581cffba..ec22014c 100644 --- a/liteyuki/comm/channel.py +++ b/liteyuki/comm/channel.py @@ -10,8 +10,12 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved 本模块定义了一个通用的通道类,用于进程间通信 """ +import functools +import multiprocessing +import threading from multiprocessing import Pipe from typing import Any, Optional, Callable, Awaitable, List, TypeAlias +from uuid import uuid4 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]] 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: """ - 通道类,用于进程间通信 + 通道类,用于进程间通信,进程内不可用,仅限主进程和子进程之间通信 有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器 """ - def __init__(self): - self.receive_conn, self.send_conn = Pipe() + def __init__(self, _id: str): + self.main_send_conn, self.sub_receive_conn = Pipe() + self.sub_send_conn, self.main_receive_conn = Pipe() self._closed = False - self._on_receive_funcs: List[ON_RECEIVE_FUNC] = [] - self._on_receive_funcs_with_receiver: dict[str, List[ON_RECEIVE_FUNC]] = {} + self._on_main_receive_funcs: list[str] = [] + 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: data: 数据 - receiver: 接收者,如果为None则广播 """ if self._closed: 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: - receiver: 接收者,如果为None则接收任意数据 """ if self._closed: raise RuntimeError("Cannot receive from a closed channel") + while True: # 判断receiver是否为None或者receiver是否等于接收者,是则接收数据,否则不动数据 - data, receiver_ = self.receive_conn.recv() - if receiver is None or receiver == receiver_: - self._run_on_receive_funcs(data, receiver_) - return data - self.send_conn.send((data, receiver_)) + if IS_MAIN_PROCESS: + data = self.main_receive_conn.recv() + print("主进程接收数据:", data) + else: + 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 None def close(self): """ 关闭通道 """ self._closed = True - self.receive_conn.close() - self.send_conn.close() + self.sub_receive_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: filter_func: 过滤函数,为None则不过滤 - receiver: 接收者, 为None则接收任意数据 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: async def wrapper(data: Any) -> Any: @@ -105,28 +122,53 @@ class Channel: return return await func(data) - if receiver is None: - self._on_receive_funcs.append(wrapper) + function_id = str(uuid4()) + _callback_funcs[function_id] = wrapper + if IS_MAIN_PROCESS: + self._on_main_receive_funcs.append(function_id) else: - if receiver not in self._on_receive_funcs_with_receiver: - self._on_receive_funcs_with_receiver[receiver] = [] - self._on_receive_funcs_with_receiver[receiver].append(wrapper) + self._on_sub_receive_funcs.append(function_id) return func return decorator - def _run_on_receive_funcs(self, data: Any, receiver: Optional[str] = None): + def _run_on_main_receive_funcs(self, data: Any): """ 运行接收函数 Args: data: 数据 """ - if receiver is None: - for func in self._on_receive_funcs: - run_coroutine(func(data)) - else: - for func in self._on_receive_funcs_with_receiver.get(receiver, []): - run_coroutine(func(data)) + for func_id in self._on_main_receive_funcs: + func = _callback_funcs[func_id] + run_coroutine(func(data)) + + def _run_on_sub_receive_funcs(self, data: Any): + """ + 运行接收函数 + 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): 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 diff --git a/liteyuki/core/manager.py b/liteyuki/core/manager.py index f88ac555..06d62eee 100644 --- a/liteyuki/core/manager.py +++ b/liteyuki/core/manager.py @@ -13,7 +13,7 @@ import threading from multiprocessing import Process from typing import TYPE_CHECKING -from liteyuki.comm import Channel +from liteyuki.comm import Channel, get_channel, set_channels from liteyuki.log import logger 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.chan = chan self.targets: dict[str, tuple[callable, tuple, dict]] = {} 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): """ 开启后自动监控进程,并添加到进程字典中 @@ -47,19 +53,21 @@ class ProcessManager: Returns: """ - if name not in self.targets: raise KeyError(f"Process {name} not found.") def _start(): should_exit = False 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 process.start() while not should_exit: # 0退出 1重启 - data = self.chan.receive(name) + data = chan_active.receive() if data == 1: logger.info(f"Restarting process {name}") asyncio.run(self.bot.lifespan.before_shutdown()) @@ -103,3 +111,7 @@ class ProcessManager: process.join(TIMEOUT) if process.is_alive(): process.kill() + + def terminate_all(self): + for name in self.targets: + self.terminate(name) diff --git a/liteyuki/core/spawn_process.py b/liteyuki/core/spawn_process.py index 089a770e..040392aa 100644 --- a/liteyuki/core/spawn_process.py +++ b/liteyuki/core/spawn_process.py @@ -3,6 +3,7 @@ from typing import Optional, TYPE_CHECKING import nonebot from liteyuki.core.nb import adapter_manager, driver_manager +from liteyuki.comm.channel import set_channel if TYPE_CHECKING: from liteyuki.comm.channel import Channel @@ -10,23 +11,23 @@ if TYPE_CHECKING: timeout_limit: int = 20 """导出对象,用于主进程与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并运行在子进程 Args: - chan: - *args: + chan_active: + chan_passive: **kwargs: Returns: """ - global chan_in_spawn_nb - chan_in_spawn_nb = chan + set_channel("nonebot-active", chan_active) + set_channel("nonebot-passive", chan_passive) nonebot.init(**kwargs) driver_manager.init(config=kwargs) adapter_manager.init(kwargs) @@ -35,17 +36,21 @@ def nb_run(chan, *args, **kwargs): nonebot.run() -def mb_run(chan, *args, **kwargs): +def mb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs): """ 初始化MeloBot并运行在子进程 Args: - chan + chan_active + chan_passive *args: **kwargs: Returns: """ + set_channel("melobot-active", chan_active) + set_channel("melobot-passive", chan_passive) + # bot = MeloBot(__name__) # bot.init(AbstractConnector(cd_time=0)) # bot.run() diff --git a/liteyuki/plugins/lifespan_monitor.py b/liteyuki/plugins/lifespan_monitor.py index c17fd7d3..56127862 100644 --- a/liteyuki/plugins/lifespan_monitor.py +++ b/liteyuki/plugins/lifespan_monitor.py @@ -7,14 +7,19 @@ # @Email : snowykami@outlook.com # @File : asa.py # @Software: PyCharm +import asyncio + from liteyuki.plugin import PluginMetadata from liteyuki import get_bot, logger +from liteyuki.comm.channel import get_channel __plugin_meta__ = PluginMetadata( name="lifespan_monitor", ) bot = get_bot() +nbp_chan = get_channel("nonebot-passive") +mbp_chan = get_channel("melobot-passive") @bot.on_before_start @@ -24,6 +29,7 @@ def _(): @bot.on_before_shutdown def _(): + print(get_channel("main")) logger.info("生命周期监控器:准备停止") @@ -35,3 +41,17 @@ def _(): @bot.on_after_start def _(): 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) diff --git a/liteyuki/plugins/reloader_monitor.py b/liteyuki/plugins/reloader_monitor.py new file mode 100644 index 00000000..4900e215 --- /dev/null +++ b/liteyuki/plugins/reloader_monitor.py @@ -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 +""" \ No newline at end of file diff --git a/src/libs/ly_api_windows_amd64.dll b/src/libs/ly_api_windows_amd64.dll deleted file mode 100644 index 94fef90d..00000000 Binary files a/src/libs/ly_api_windows_amd64.dll and /dev/null differ diff --git a/src/liteyuki_main/core.py b/src/liteyuki_main/core.py index 1904be49..d34cb9dd 100644 --- a/src/liteyuki_main/core.py +++ b/src/liteyuki_main/core.py @@ -6,29 +6,25 @@ import nonebot import pip from nonebot import Bot, get_driver, require 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.internal.matcher import Matcher 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.data_manager import StoredConfig, TempConfig, common_db from src.utils.base.language import get_user_lang from src.utils.base.ly_typing import T_Bot, T_MessageEvent from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers -# from src.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 liteyuki.bot import get_bot from ..utils.base import reload from ..utils.base.ly_function import get_function require("nonebot_plugin_alconna") require("nonebot_plugin_apscheduler") -from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar +from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar from nonebot_plugin_apscheduler import scheduler driver = get_driver() @@ -81,7 +77,6 @@ async def _(bot: T_Bot, event: T_MessageEvent): ).handle() # Satori OK async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent): - global channel_in_spawn_process await matcher.send("Liteyuki reloading") 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_bot_id" : bot.self_id, "reload_session_type": event_utils.get_message_type(event), - "reload_session_id" : (event.group_id if event.message_type == "group" else event.user_id) if not isinstance(event, - satori.event.Event) else event.chan.id, + "reload_session_id" : (event.group_id if event.message_type == "group" else event.user_id) + if not isinstance(event, satori.event.Event) else event.chan_active.id, "delta_time" : 0 } ) @@ -358,7 +353,7 @@ async def _(bot: T_Bot): delta_time = temp_data.data.get("delta_time", 0) common_db.save(temp_data) # 更新数据 - if delta_time <= 20.0: # 启动时间太长就别发了,丢人 + if delta_time <= 20.0: # 启动时间太长就别发了,丢人 if isinstance(bot, satori.Bot): await bot.send_message( channel_id=reload_session_id, diff --git a/src/liteyuki_main/dev.py b/src/liteyuki_main/dev.py index 172af36a..e4a24f87 100644 --- a/src/liteyuki_main/dev.py +++ b/src/liteyuki_main/dev.py @@ -11,35 +11,12 @@ if get_config("debug", False): liteyuki_bot = get_bot() - src_directories = ( - "src/liteyuki_main", - "src/plugins", - "src/utils", - ) - src_excludes_extensions = ( - "pyc", - ) - res_directories = ( "src/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): """ @@ -51,12 +28,9 @@ if get_config("debug", False): load_resources() - code_modified_handler = CodeModifiedHandler() resource_modified_handle = ResourceModifiedHandler() observer = Observer() - for directory in src_directories: - observer.schedule(code_modified_handler, directory, recursive=True) for directory in res_directories: observer.schedule(resource_modified_handle, directory, recursive=True) observer.start() diff --git a/src/liteyuki_main/loader.py b/src/liteyuki_main/loader.py index 020b3ade..40883080 100644 --- a/src/liteyuki_main/loader.py +++ b/src/liteyuki_main/loader.py @@ -21,7 +21,7 @@ liteyuki_bot = get_bot() @driver.on_startup async def load_plugins(): - nonebot.plugin.load_plugins("src/plugins") + nonebot.plugin.load_plugins("src/nonebot_plugins") # 从数据库读取已安装的插件 if not get_config("safe_mode", False): # 安全模式下,不加载插件 diff --git a/src/liteyuki_plugins/README.md b/src/liteyuki_plugins/README.md new file mode 100644 index 00000000..aba0376d --- /dev/null +++ b/src/liteyuki_plugins/README.md @@ -0,0 +1,3 @@ +# 说明 + +此目录为**轻雪插件**目录,非其他插件目录。 \ No newline at end of file diff --git a/src/plugins/liteyuki_crt_utils/__init__.py b/src/nonebot_plugins/liteyuki_crt_utils/__init__.py similarity index 69% rename from src/plugins/liteyuki_crt_utils/__init__.py rename to src/nonebot_plugins/liteyuki_crt_utils/__init__.py index 7c6196a9..3ffdae2b 100644 --- a/src/plugins/liteyuki_crt_utils/__init__.py +++ b/src/nonebot_plugins/liteyuki_crt_utils/__init__.py @@ -1,18 +1,27 @@ -import multiprocessing - -from nonebot.plugin import PluginMetadata -from .rt_guide import * -from .crt_matchers import * - -__plugin_meta__ = PluginMetadata( - name="CRT生成工具", - description="一些CRT牌子生成器", - usage="我觉得你应该会用", - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki" : True, - "toggleable" : True, - "default_enable": True, - } -) \ No newline at end of file +import multiprocessing + +from nonebot.plugin import PluginMetadata +from liteyuki.comm import get_channel +from .rt_guide import * +from .crt_matchers import * + +__plugin_meta__ = PluginMetadata( + name="CRT生成工具", + description="一些CRT牌子生成器", + usage="我觉得你应该会用", + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki" : True, + "toggleable" : True, + "default_enable": True, + } +) + +chan = get_channel("nonebot-passive") + + +@chan.on_receive() +async def _(d): + print("CRT子进程接收到数据:", d) + chan.send("CRT子进程已接收到数据") diff --git a/src/plugins/liteyuki_crt_utils/canvas.py b/src/nonebot_plugins/liteyuki_crt_utils/canvas.py similarity index 97% rename from src/plugins/liteyuki_crt_utils/canvas.py rename to src/nonebot_plugins/liteyuki_crt_utils/canvas.py index 1e913ffc..02e6e3df 100644 --- a/src/plugins/liteyuki_crt_utils/canvas.py +++ b/src/nonebot_plugins/liteyuki_crt_utils/canvas.py @@ -1,575 +1,575 @@ -import os -import uuid -from typing import Tuple, Union, List - -import nonebot -from PIL import Image, ImageFont, ImageDraw - -default_color = (255, 255, 255, 255) -default_font = "resources/fonts/MiSans-Semibold.ttf" - - -def render_canvas_from_json(file: str, background: Image) -> "Canvas": - pass - - -class BasePanel: - def __init__(self, - uv_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0), - box_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0), - parent_point: Tuple[float, float] = (0.5, 0.5), - point: Tuple[float, float] = (0.5, 0.5)): - """ - :param uv_size: 底面板大小 - :param box_size: 子(自身)面板大小 - :param parent_point: 底面板锚点 - :param point: 子(自身)面板锚点 - """ - self.canvas: Canvas | None = None - self.uv_size = uv_size - self.box_size = box_size - self.parent_point = parent_point - self.point = point - self.parent: BasePanel | None = None - self.canvas_box: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0) - # 此节点在父节点上的盒子 - self.box = ( - self.parent_point[0] - self.point[0] * self.box_size[0] / self.uv_size[0], - self.parent_point[1] - self.point[1] * self.box_size[1] / self.uv_size[1], - self.parent_point[0] + (1 - self.point[0]) * self.box_size[0] / self.uv_size[0], - self.parent_point[1] + (1 - self.point[1]) * self.box_size[1] / self.uv_size[1] - ) - - def load(self, only_calculate=False): - """ - 将对象写入画布 - 此处仅作声明 - 由各子类重写 - - :return: - """ - self.actual_pos = self.canvas_box - - def save_as(self, canvas_box, only_calculate=False): - """ - 此函数执行时间较长,建议异步运行 - :param only_calculate: - :param canvas_box 此节点在画布上的盒子,并不是在父节点上的盒子 - :return: - """ - for name, child in self.__dict__.items(): - # 此节点在画布上的盒子 - if isinstance(child, BasePanel) and name not in ["canvas", "parent"]: - child.parent = self - if isinstance(self, Canvas): - child.canvas = self - else: - child.canvas = self.canvas - dxc = canvas_box[2] - canvas_box[0] - dyc = canvas_box[3] - canvas_box[1] - child.canvas_box = ( - canvas_box[0] + dxc * child.box[0], - canvas_box[1] + dyc * child.box[1], - canvas_box[0] + dxc * child.box[2], - canvas_box[1] + dyc * child.box[3] - ) - child.load(only_calculate) - child.save_as(child.canvas_box, only_calculate) - - -class Canvas(BasePanel): - def __init__(self, base_img: Image.Image): - self.base_img = base_img - self.canvas = self - super(Canvas, self).__init__() - self.draw_line_list = [] - - def export(self, file, alpha=False): - self.base_img = self.base_img.convert("RGBA") - self.save_as((0, 0, 1, 1)) - draw = ImageDraw.Draw(self.base_img) - for line in self.draw_line_list: - draw.line(*line) - if not alpha: - self.base_img = self.base_img.convert("RGB") - self.base_img.save(file) - - def delete(self): - os.remove(self.file) - - def get_actual_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]: - """ - 获取控件实际相对大小 - 函数执行时间较长 - - :param path: 控件路径 - :return: - """ - sub_obj = self - self.save_as((0, 0, 1, 1), True) - control_path = "" - for i, seq in enumerate(path.split(".")): - if seq not in sub_obj.__dict__: - raise KeyError(f"在{control_path}中找不到控件:{seq}") - control_path += f".{seq}" - sub_obj = sub_obj.__dict__[seq] - return sub_obj.actual_pos - - def get_actual_pixel_size(self, path: str) -> Union[None, Tuple[int, int]]: - """ - 获取控件实际像素长宽 - 函数执行时间较长 - :param path: 控件路径 - :return: - """ - sub_obj = self - self.save_as((0, 0, 1, 1), True) - control_path = "" - for i, seq in enumerate(path.split(".")): - if seq not in sub_obj.__dict__: - raise KeyError(f"在{control_path}中找不到控件:{seq}") - control_path += f".{seq}" - sub_obj = sub_obj.__dict__[seq] - dx = int(sub_obj.canvas.base_img.size[0] * (sub_obj.actual_pos[2] - sub_obj.actual_pos[0])) - dy = int(sub_obj.canvas.base_img.size[1] * (sub_obj.actual_pos[3] - sub_obj.actual_pos[1])) - return dx, dy - - def get_actual_pixel_box(self, path: str) -> Union[None, Tuple[int, int, int, int]]: - """ - 获取控件实际像素大小盒子 - 函数执行时间较长 - :param path: 控件路径 - :return: - """ - sub_obj = self - self.save_as((0, 0, 1, 1), True) - control_path = "" - for i, seq in enumerate(path.split(".")): - if seq not in sub_obj.__dict__: - raise KeyError(f"在{control_path}中找不到控件:{seq}") - control_path += f".{seq}" - sub_obj = sub_obj.__dict__[seq] - x1 = int(sub_obj.canvas.base_img.size[0] * sub_obj.actual_pos[0]) - y1 = int(sub_obj.canvas.base_img.size[1] * sub_obj.actual_pos[1]) - x2 = int(sub_obj.canvas.base_img.size[2] * sub_obj.actual_pos[2]) - y2 = int(sub_obj.canvas.base_img.size[3] * sub_obj.actual_pos[3]) - return x1, y1, x2, y2 - - def get_parent_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]: - """ - 获取控件在父节点的大小 - 函数执行时间较长 - - :param path: 控件路径 - :return: - """ - sub_obj = self.get_control_by_path(path) - on_parent_pos = ( - (sub_obj.actual_pos[0] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]), - (sub_obj.actual_pos[1] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]), - (sub_obj.actual_pos[2] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]), - (sub_obj.actual_pos[3] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]) - ) - return on_parent_pos - - def get_control_by_path(self, path: str) -> Union[BasePanel, "Img", "Rectangle", "Text"]: - sub_obj = self - self.save_as((0, 0, 1, 1), True) - control_path = "" - for i, seq in enumerate(path.split(".")): - if seq not in sub_obj.__dict__: - raise KeyError(f"在{control_path}中找不到控件:{seq}") - control_path += f".{seq}" - sub_obj = sub_obj.__dict__[seq] - return sub_obj - - def draw_line(self, path: str, p1: Tuple[float, float], p2: Tuple[float, float], color, width): - """ - 画线 - - :param color: - :param width: - :param path: - :param p1: - :param p2: - :return: - """ - ac_pos = self.get_actual_box(path) - control = self.get_control_by_path(path) - dx = ac_pos[2] - ac_pos[0] - dy = ac_pos[3] - ac_pos[1] - xy_box = int((ac_pos[0] + dx * p1[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p1[1]) * control.canvas.base_img.size[1]), int( - (ac_pos[0] + dx * p2[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p2[1]) * control.canvas.base_img.size[1]) - self.draw_line_list.append((xy_box, color, width)) - - -class Panel(BasePanel): - def __init__(self, uv_size, box_size, parent_point, point): - super(Panel, self).__init__(uv_size, box_size, parent_point, point) - - -class TextSegment: - def __init__(self, text, **kwargs): - if not isinstance(text, str): - raise TypeError("请输入字符串") - self.text = text - self.color = kwargs.get("color", None) - self.font = kwargs.get("font", None) - - @staticmethod - def text2text_segment_list(text: str): - """ - 暂时没写好 - - :param text: %FFFFFFFF%1123%FFFFFFFF%21323 - :return: - """ - pass - - -class Text(BasePanel): - def __init__(self, uv_size, box_size, parent_point, point, text: Union[str, list], font=default_font, color=(255, 255, 255, 255), vertical=False, - line_feed=False, force_size=False, fill=(0, 0, 0, 0), fillet=0, outline=(0, 0, 0, 0), outline_width=0, rectangle_side=0, font_size=None, dp: int = 5, - anchor: str = "la"): - """ - :param uv_size: - :param box_size: - :param parent_point: - :param point: - :param text: list[TextSegment] | str - :param font: - :param color: - :param vertical: 是否竖直 - :param line_feed: 是否换行 - :param force_size: 强制大小 - :param dp: 字体大小递减精度 - :param anchor : https://www.zhihu.com/question/474216280 - :param fill: 底部填充颜色 - :param fillet: 填充圆角 - :param rectangle_side: 边框宽度 - :param outline: 填充矩形边框颜色 - :param outline_width: 填充矩形边框宽度 - """ - self.actual_pos = None - self.outline_width = outline_width - self.outline = outline - self.fill = fill - self.fillet = fillet - self.font = font - self.text = text - self.color = color - self.force_size = force_size - self.vertical = vertical - self.line_feed = line_feed - self.dp = dp - self.font_size = font_size - self.rectangle_side = rectangle_side - self.anchor = anchor - super(Text, self).__init__(uv_size, box_size, parent_point, point) - - def load(self, only_calculate=False): - """限制区域像素大小""" - if isinstance(self.text, str): - self.text = [ - TextSegment(text=self.text, color=self.color, font=self.font) - ] - all_text = str() - for text in self.text: - all_text += text.text - limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1]) - font_size = limited_size[1] if self.font_size is None else self.font_size - image_font = ImageFont.truetype(self.font, font_size) - actual_size = image_font.getsize(all_text) - while (actual_size[0] > limited_size[0] or actual_size[1] > limited_size[1]) and not self.force_size: - font_size -= self.dp - image_font = ImageFont.truetype(self.font, font_size) - actual_size = image_font.getsize(all_text) - draw = ImageDraw.Draw(self.canvas.base_img) - if isinstance(self.parent, Img) or isinstance(self.parent, Text): - self.parent.canvas_box = self.parent.actual_pos - dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0] - dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1] - dx1 = actual_size[0] / self.canvas.base_img.size[0] - dy1 = actual_size[1] / self.canvas.base_img.size[1] - start_point = [ - int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]), - int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1]) - ] - self.actual_pos = ( - start_point[0] / self.canvas.base_img.size[0], - start_point[1] / self.canvas.base_img.size[1], - (start_point[0] + actual_size[0]) / self.canvas.base_img.size[0], - (start_point[1] + actual_size[1]) / self.canvas.base_img.size[1], - ) - self.font_size = font_size - if not only_calculate: - for text_segment in self.text: - if text_segment.color is None: - text_segment.color = self.color - if text_segment.font is None: - text_segment.font = self.font - image_font = ImageFont.truetype(font=text_segment.font, size=font_size) - if self.fill[-1] > 0: - rectangle = Shape.rectangle(size=(actual_size[0] + 2 * self.rectangle_side, actual_size[1] + 2 * self.rectangle_side), fillet=self.fillet, fill=self.fill, - width=self.outline_width, outline=self.outline) - self.canvas.base_img.paste(im=rectangle, box=(start_point[0] - self.rectangle_side, - start_point[1] - self.rectangle_side, - start_point[0] + actual_size[0] + self.rectangle_side, - start_point[1] + actual_size[1] + self.rectangle_side), - mask=rectangle.split()[-1]) - draw.text((start_point[0] - self.rectangle_side, start_point[1] - self.rectangle_side), - text_segment.text, text_segment.color, font=image_font, anchor=self.anchor) - text_width = image_font.getsize(text_segment.text) - start_point[0] += text_width[0] - - -class Img(BasePanel): - def __init__(self, uv_size, box_size, parent_point, point, img: Image.Image, keep_ratio=True): - self.img_base_img = img - self.keep_ratio = keep_ratio - super(Img, self).__init__(uv_size, box_size, parent_point, point) - - def load(self, only_calculate=False): - self.preprocess() - self.img_base_img = self.img_base_img.convert("RGBA") - limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), \ - int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1]) - - if self.keep_ratio: - """保持比例""" - actual_ratio = self.img_base_img.size[0] / self.img_base_img.size[1] - limited_ratio = limited_size[0] / limited_size[1] - if actual_ratio >= limited_ratio: - # 图片过长 - self.img_base_img = self.img_base_img.resize( - (int(self.img_base_img.size[0] * limited_size[0] / self.img_base_img.size[0]), - int(self.img_base_img.size[1] * limited_size[0] / self.img_base_img.size[0])) - ) - else: - self.img_base_img = self.img_base_img.resize( - (int(self.img_base_img.size[0] * limited_size[1] / self.img_base_img.size[1]), - int(self.img_base_img.size[1] * limited_size[1] / self.img_base_img.size[1])) - ) - - else: - """不保持比例""" - self.img_base_img = self.img_base_img.resize(limited_size) - - # 占比长度 - if isinstance(self.parent, Img) or isinstance(self.parent, Text): - self.parent.canvas_box = self.parent.actual_pos - - dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0] - dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1] - - dx1 = self.img_base_img.size[0] / self.canvas.base_img.size[0] - dy1 = self.img_base_img.size[1] / self.canvas.base_img.size[1] - start_point = ( - int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]), - int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1]) - ) - alpha = self.img_base_img.split()[3] - self.actual_pos = ( - start_point[0] / self.canvas.base_img.size[0], - start_point[1] / self.canvas.base_img.size[1], - (start_point[0] + self.img_base_img.size[0]) / self.canvas.base_img.size[0], - (start_point[1] + self.img_base_img.size[1]) / self.canvas.base_img.size[1], - ) - if not only_calculate: - self.canvas.base_img.paste(self.img_base_img, start_point, alpha) - - def preprocess(self): - pass - - -class Rectangle(Img): - def __init__(self, uv_size, box_size, parent_point, point, fillet: Union[int, float] = 0, img: Union[Image.Image] = None, keep_ratio=True, - color=default_color, outline_width=0, outline_color=default_color): - """ - 圆角图 - :param uv_size: - :param box_size: - :param parent_point: - :param point: - :param fillet: 圆角半径浮点或整数 - :param img: - :param keep_ratio: - """ - self.fillet = fillet - self.color = color - self.outline_width = outline_width - self.outline_color = outline_color - super(Rectangle, self).__init__(uv_size, box_size, parent_point, point, img, keep_ratio) - - def preprocess(self): - limited_size = (int(self.canvas.base_img.size[0] * (self.canvas_box[2] - self.canvas_box[0])), - int(self.canvas.base_img.size[1] * (self.canvas_box[3] - self.canvas_box[1]))) - if not self.keep_ratio and self.img_base_img is not None and self.img_base_img.size[0] / self.img_base_img.size[1] != limited_size[0] / limited_size[1]: - self.img_base_img = self.img_base_img.resize(limited_size) - self.img_base_img = Shape.rectangle(size=limited_size, fillet=self.fillet, fill=self.color, width=self.outline_width, outline=self.outline_color) - - -class Color: - GREY = (128, 128, 128, 255) - RED = (255, 0, 0, 255) - GREEN = (0, 255, 0, 255) - BLUE = (0, 0, 255, 255) - YELLOW = (255, 255, 0, 255) - PURPLE = (255, 0, 255, 255) - CYAN = (0, 255, 255, 255) - WHITE = (255, 255, 255, 255) - BLACK = (0, 0, 0, 255) - - @staticmethod - def hex2dec(colorHex: str) -> Tuple[int, int, int, int]: - """ - :param colorHex: FFFFFFFF (ARGB)-> (R, G, B, A) - :return: - """ - return int(colorHex[2:4], 16), int(colorHex[4:6], 16), int(colorHex[6:8], 16), int(colorHex[0:2], 16) - - -class Shape: - @staticmethod - def circular(radius: int, fill: tuple, width: int = 0, outline: tuple = Color.BLACK) -> Image.Image: - """ - :param radius: 半径(像素) - :param fill: 填充颜色 - :param width: 轮廓粗细(像素) - :param outline: 轮廓颜色 - :return: 圆形Image对象 - """ - img = Image.new("RGBA", (radius * 2, radius * 2), color=radius) - draw = ImageDraw.Draw(img) - draw.ellipse(xy=(0, 0, radius * 2, radius * 2), fill=fill, outline=outline, width=width) - return img - - @staticmethod - def rectangle(size: Tuple[int, int], fill: tuple, width: int = 0, outline: tuple = Color.BLACK, fillet: int = 0) -> Image.Image: - """ - :param fillet: 圆角半径(像素) - :param size: 长宽(像素) - :param fill: 填充颜色 - :param width: 轮廓粗细(像素) - :param outline: 轮廓颜色 - :return: 矩形Image对象 - """ - img = Image.new("RGBA", size, color=fill) - draw = ImageDraw.Draw(img) - draw.rounded_rectangle(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline, width=width, radius=fillet) - return img - - @staticmethod - def ellipse(size: Tuple[int, int], fill: tuple, outline: int = 0, outline_color: tuple = Color.BLACK) -> Image.Image: - """ - :param size: 长宽(像素) - :param fill: 填充颜色 - :param outline: 轮廓粗细(像素) - :param outline_color: 轮廓颜色 - :return: 椭圆Image对象 - """ - img = Image.new("RGBA", size, color=fill) - draw = ImageDraw.Draw(img) - draw.ellipse(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline_color, width=outline) - return img - - @staticmethod - def polygon(points: List[Tuple[int, int]], fill: tuple, outline: int, outline_color: tuple) -> Image.Image: - """ - :param points: 多边形顶点列表 - :param fill: 填充颜色 - :param outline: 轮廓粗细(像素) - :param outline_color: 轮廓颜色 - :return: 多边形Image对象 - """ - img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill) - draw = ImageDraw.Draw(img) - draw.polygon(xy=points, fill=fill, outline=outline_color, width=outline) - return img - - @staticmethod - def line(points: List[Tuple[int, int]], fill: tuple, width: int) -> Image: - """ - :param points: 线段顶点列表 - :param fill: 填充颜色 - :param width: 线段粗细(像素) - :return: 线段Image对象 - """ - img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill) - draw = ImageDraw.Draw(img) - draw.line(xy=points, fill=fill, width=width) - return img - - -class Utils: - - @staticmethod - def central_clip_by_ratio(img: Image.Image, size: Tuple, use_cache=True): - """ - :param use_cache: 是否使用缓存,剪切过一次后默认生成缓存 - :param img: - :param size: 仅为比例,满填充裁剪 - :return: - """ - cache_file_path = str() - if use_cache: - filename_without_end = ".".join(os.path.basename(img.fp.name).split(".")[0:-1]) + f"_{size[0]}x{size[1]}" + ".png" - cache_file_path = os.path.join(".cache", filename_without_end) - if os.path.exists(cache_file_path): - nonebot.logger.info("本次使用缓存加载图片,不裁剪") - return Image.open(os.path.join(".cache", filename_without_end)) - img_ratio = img.size[0] / img.size[1] - limited_ratio = size[0] / size[1] - if limited_ratio > img_ratio: - actual_size = ( - img.size[0], - img.size[0] / size[0] * size[1] - ) - box = ( - 0, (img.size[1] - actual_size[1]) // 2, - img.size[0], img.size[1] - (img.size[1] - actual_size[1]) // 2 - ) - else: - actual_size = ( - img.size[1] / size[1] * size[0], - img.size[1], - ) - box = ( - (img.size[0] - actual_size[0]) // 2, 0, - img.size[0] - (img.size[0] - actual_size[0]) // 2, img.size[1] - ) - img = img.crop(box).resize(size) - if use_cache: - img.save(cache_file_path) - return img - - @staticmethod - def circular_clip(img: Image.Image): - """ - 裁剪为alpha圆形 - - :param img: - :return: - """ - length = min(img.size) - alpha_cover = Image.new("RGBA", (length, length), color=(0, 0, 0, 0)) - if img.size[0] > img.size[1]: - box = ( - (img.size[0] - img[1]) // 2, 0, - (img.size[0] - img[1]) // 2 + img.size[1], img.size[1] - ) - else: - box = ( - 0, (img.size[1] - img.size[0]) // 2, - img.size[0], (img.size[1] - img.size[0]) // 2 + img.size[0] - ) - img = img.crop(box).resize((length, length)) - draw = ImageDraw.Draw(alpha_cover) - draw.ellipse(xy=(0, 0, length, length), fill=(255, 255, 255, 255)) - alpha = alpha_cover.split()[-1] - img.putalpha(alpha) - return img - - @staticmethod - def open_img(path) -> Image.Image: - return Image.open(path, "RGBA") +import os +import uuid +from typing import Tuple, Union, List + +import nonebot +from PIL import Image, ImageFont, ImageDraw + +default_color = (255, 255, 255, 255) +default_font = "resources/fonts/MiSans-Semibold.ttf" + + +def render_canvas_from_json(file: str, background: Image) -> "Canvas": + pass + + +class BasePanel: + def __init__(self, + uv_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0), + box_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0), + parent_point: Tuple[float, float] = (0.5, 0.5), + point: Tuple[float, float] = (0.5, 0.5)): + """ + :param uv_size: 底面板大小 + :param box_size: 子(自身)面板大小 + :param parent_point: 底面板锚点 + :param point: 子(自身)面板锚点 + """ + self.canvas: Canvas | None = None + self.uv_size = uv_size + self.box_size = box_size + self.parent_point = parent_point + self.point = point + self.parent: BasePanel | None = None + self.canvas_box: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0) + # 此节点在父节点上的盒子 + self.box = ( + self.parent_point[0] - self.point[0] * self.box_size[0] / self.uv_size[0], + self.parent_point[1] - self.point[1] * self.box_size[1] / self.uv_size[1], + self.parent_point[0] + (1 - self.point[0]) * self.box_size[0] / self.uv_size[0], + self.parent_point[1] + (1 - self.point[1]) * self.box_size[1] / self.uv_size[1] + ) + + def load(self, only_calculate=False): + """ + 将对象写入画布 + 此处仅作声明 + 由各子类重写 + + :return: + """ + self.actual_pos = self.canvas_box + + def save_as(self, canvas_box, only_calculate=False): + """ + 此函数执行时间较长,建议异步运行 + :param only_calculate: + :param canvas_box 此节点在画布上的盒子,并不是在父节点上的盒子 + :return: + """ + for name, child in self.__dict__.items(): + # 此节点在画布上的盒子 + if isinstance(child, BasePanel) and name not in ["canvas", "parent"]: + child.parent = self + if isinstance(self, Canvas): + child.canvas = self + else: + child.canvas = self.canvas + dxc = canvas_box[2] - canvas_box[0] + dyc = canvas_box[3] - canvas_box[1] + child.canvas_box = ( + canvas_box[0] + dxc * child.box[0], + canvas_box[1] + dyc * child.box[1], + canvas_box[0] + dxc * child.box[2], + canvas_box[1] + dyc * child.box[3] + ) + child.load(only_calculate) + child.save_as(child.canvas_box, only_calculate) + + +class Canvas(BasePanel): + def __init__(self, base_img: Image.Image): + self.base_img = base_img + self.canvas = self + super(Canvas, self).__init__() + self.draw_line_list = [] + + def export(self, file, alpha=False): + self.base_img = self.base_img.convert("RGBA") + self.save_as((0, 0, 1, 1)) + draw = ImageDraw.Draw(self.base_img) + for line in self.draw_line_list: + draw.line(*line) + if not alpha: + self.base_img = self.base_img.convert("RGB") + self.base_img.save(file) + + def delete(self): + os.remove(self.file) + + def get_actual_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]: + """ + 获取控件实际相对大小 + 函数执行时间较长 + + :param path: 控件路径 + :return: + """ + sub_obj = self + self.save_as((0, 0, 1, 1), True) + control_path = "" + for i, seq in enumerate(path.split(".")): + if seq not in sub_obj.__dict__: + raise KeyError(f"在{control_path}中找不到控件:{seq}") + control_path += f".{seq}" + sub_obj = sub_obj.__dict__[seq] + return sub_obj.actual_pos + + def get_actual_pixel_size(self, path: str) -> Union[None, Tuple[int, int]]: + """ + 获取控件实际像素长宽 + 函数执行时间较长 + :param path: 控件路径 + :return: + """ + sub_obj = self + self.save_as((0, 0, 1, 1), True) + control_path = "" + for i, seq in enumerate(path.split(".")): + if seq not in sub_obj.__dict__: + raise KeyError(f"在{control_path}中找不到控件:{seq}") + control_path += f".{seq}" + sub_obj = sub_obj.__dict__[seq] + dx = int(sub_obj.canvas.base_img.size[0] * (sub_obj.actual_pos[2] - sub_obj.actual_pos[0])) + dy = int(sub_obj.canvas.base_img.size[1] * (sub_obj.actual_pos[3] - sub_obj.actual_pos[1])) + return dx, dy + + def get_actual_pixel_box(self, path: str) -> Union[None, Tuple[int, int, int, int]]: + """ + 获取控件实际像素大小盒子 + 函数执行时间较长 + :param path: 控件路径 + :return: + """ + sub_obj = self + self.save_as((0, 0, 1, 1), True) + control_path = "" + for i, seq in enumerate(path.split(".")): + if seq not in sub_obj.__dict__: + raise KeyError(f"在{control_path}中找不到控件:{seq}") + control_path += f".{seq}" + sub_obj = sub_obj.__dict__[seq] + x1 = int(sub_obj.canvas.base_img.size[0] * sub_obj.actual_pos[0]) + y1 = int(sub_obj.canvas.base_img.size[1] * sub_obj.actual_pos[1]) + x2 = int(sub_obj.canvas.base_img.size[2] * sub_obj.actual_pos[2]) + y2 = int(sub_obj.canvas.base_img.size[3] * sub_obj.actual_pos[3]) + return x1, y1, x2, y2 + + def get_parent_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]: + """ + 获取控件在父节点的大小 + 函数执行时间较长 + + :param path: 控件路径 + :return: + """ + sub_obj = self.get_control_by_path(path) + on_parent_pos = ( + (sub_obj.actual_pos[0] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]), + (sub_obj.actual_pos[1] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]), + (sub_obj.actual_pos[2] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]), + (sub_obj.actual_pos[3] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]) + ) + return on_parent_pos + + def get_control_by_path(self, path: str) -> Union[BasePanel, "Img", "Rectangle", "Text"]: + sub_obj = self + self.save_as((0, 0, 1, 1), True) + control_path = "" + for i, seq in enumerate(path.split(".")): + if seq not in sub_obj.__dict__: + raise KeyError(f"在{control_path}中找不到控件:{seq}") + control_path += f".{seq}" + sub_obj = sub_obj.__dict__[seq] + return sub_obj + + def draw_line(self, path: str, p1: Tuple[float, float], p2: Tuple[float, float], color, width): + """ + 画线 + + :param color: + :param width: + :param path: + :param p1: + :param p2: + :return: + """ + ac_pos = self.get_actual_box(path) + control = self.get_control_by_path(path) + dx = ac_pos[2] - ac_pos[0] + dy = ac_pos[3] - ac_pos[1] + xy_box = int((ac_pos[0] + dx * p1[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p1[1]) * control.canvas.base_img.size[1]), int( + (ac_pos[0] + dx * p2[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p2[1]) * control.canvas.base_img.size[1]) + self.draw_line_list.append((xy_box, color, width)) + + +class Panel(BasePanel): + def __init__(self, uv_size, box_size, parent_point, point): + super(Panel, self).__init__(uv_size, box_size, parent_point, point) + + +class TextSegment: + def __init__(self, text, **kwargs): + if not isinstance(text, str): + raise TypeError("请输入字符串") + self.text = text + self.color = kwargs.get("color", None) + self.font = kwargs.get("font", None) + + @staticmethod + def text2text_segment_list(text: str): + """ + 暂时没写好 + + :param text: %FFFFFFFF%1123%FFFFFFFF%21323 + :return: + """ + pass + + +class Text(BasePanel): + def __init__(self, uv_size, box_size, parent_point, point, text: Union[str, list], font=default_font, color=(255, 255, 255, 255), vertical=False, + line_feed=False, force_size=False, fill=(0, 0, 0, 0), fillet=0, outline=(0, 0, 0, 0), outline_width=0, rectangle_side=0, font_size=None, dp: int = 5, + anchor: str = "la"): + """ + :param uv_size: + :param box_size: + :param parent_point: + :param point: + :param text: list[TextSegment] | str + :param font: + :param color: + :param vertical: 是否竖直 + :param line_feed: 是否换行 + :param force_size: 强制大小 + :param dp: 字体大小递减精度 + :param anchor : https://www.zhihu.com/question/474216280 + :param fill: 底部填充颜色 + :param fillet: 填充圆角 + :param rectangle_side: 边框宽度 + :param outline: 填充矩形边框颜色 + :param outline_width: 填充矩形边框宽度 + """ + self.actual_pos = None + self.outline_width = outline_width + self.outline = outline + self.fill = fill + self.fillet = fillet + self.font = font + self.text = text + self.color = color + self.force_size = force_size + self.vertical = vertical + self.line_feed = line_feed + self.dp = dp + self.font_size = font_size + self.rectangle_side = rectangle_side + self.anchor = anchor + super(Text, self).__init__(uv_size, box_size, parent_point, point) + + def load(self, only_calculate=False): + """限制区域像素大小""" + if isinstance(self.text, str): + self.text = [ + TextSegment(text=self.text, color=self.color, font=self.font) + ] + all_text = str() + for text in self.text: + all_text += text.text + limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1]) + font_size = limited_size[1] if self.font_size is None else self.font_size + image_font = ImageFont.truetype(self.font, font_size) + actual_size = image_font.getsize(all_text) + while (actual_size[0] > limited_size[0] or actual_size[1] > limited_size[1]) and not self.force_size: + font_size -= self.dp + image_font = ImageFont.truetype(self.font, font_size) + actual_size = image_font.getsize(all_text) + draw = ImageDraw.Draw(self.canvas.base_img) + if isinstance(self.parent, Img) or isinstance(self.parent, Text): + self.parent.canvas_box = self.parent.actual_pos + dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0] + dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1] + dx1 = actual_size[0] / self.canvas.base_img.size[0] + dy1 = actual_size[1] / self.canvas.base_img.size[1] + start_point = [ + int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]), + int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1]) + ] + self.actual_pos = ( + start_point[0] / self.canvas.base_img.size[0], + start_point[1] / self.canvas.base_img.size[1], + (start_point[0] + actual_size[0]) / self.canvas.base_img.size[0], + (start_point[1] + actual_size[1]) / self.canvas.base_img.size[1], + ) + self.font_size = font_size + if not only_calculate: + for text_segment in self.text: + if text_segment.color is None: + text_segment.color = self.color + if text_segment.font is None: + text_segment.font = self.font + image_font = ImageFont.truetype(font=text_segment.font, size=font_size) + if self.fill[-1] > 0: + rectangle = Shape.rectangle(size=(actual_size[0] + 2 * self.rectangle_side, actual_size[1] + 2 * self.rectangle_side), fillet=self.fillet, fill=self.fill, + width=self.outline_width, outline=self.outline) + self.canvas.base_img.paste(im=rectangle, box=(start_point[0] - self.rectangle_side, + start_point[1] - self.rectangle_side, + start_point[0] + actual_size[0] + self.rectangle_side, + start_point[1] + actual_size[1] + self.rectangle_side), + mask=rectangle.split()[-1]) + draw.text((start_point[0] - self.rectangle_side, start_point[1] - self.rectangle_side), + text_segment.text, text_segment.color, font=image_font, anchor=self.anchor) + text_width = image_font.getsize(text_segment.text) + start_point[0] += text_width[0] + + +class Img(BasePanel): + def __init__(self, uv_size, box_size, parent_point, point, img: Image.Image, keep_ratio=True): + self.img_base_img = img + self.keep_ratio = keep_ratio + super(Img, self).__init__(uv_size, box_size, parent_point, point) + + def load(self, only_calculate=False): + self.preprocess() + self.img_base_img = self.img_base_img.convert("RGBA") + limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), \ + int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1]) + + if self.keep_ratio: + """保持比例""" + actual_ratio = self.img_base_img.size[0] / self.img_base_img.size[1] + limited_ratio = limited_size[0] / limited_size[1] + if actual_ratio >= limited_ratio: + # 图片过长 + self.img_base_img = self.img_base_img.resize( + (int(self.img_base_img.size[0] * limited_size[0] / self.img_base_img.size[0]), + int(self.img_base_img.size[1] * limited_size[0] / self.img_base_img.size[0])) + ) + else: + self.img_base_img = self.img_base_img.resize( + (int(self.img_base_img.size[0] * limited_size[1] / self.img_base_img.size[1]), + int(self.img_base_img.size[1] * limited_size[1] / self.img_base_img.size[1])) + ) + + else: + """不保持比例""" + self.img_base_img = self.img_base_img.resize(limited_size) + + # 占比长度 + if isinstance(self.parent, Img) or isinstance(self.parent, Text): + self.parent.canvas_box = self.parent.actual_pos + + dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0] + dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1] + + dx1 = self.img_base_img.size[0] / self.canvas.base_img.size[0] + dy1 = self.img_base_img.size[1] / self.canvas.base_img.size[1] + start_point = ( + int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]), + int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1]) + ) + alpha = self.img_base_img.split()[3] + self.actual_pos = ( + start_point[0] / self.canvas.base_img.size[0], + start_point[1] / self.canvas.base_img.size[1], + (start_point[0] + self.img_base_img.size[0]) / self.canvas.base_img.size[0], + (start_point[1] + self.img_base_img.size[1]) / self.canvas.base_img.size[1], + ) + if not only_calculate: + self.canvas.base_img.paste(self.img_base_img, start_point, alpha) + + def preprocess(self): + pass + + +class Rectangle(Img): + def __init__(self, uv_size, box_size, parent_point, point, fillet: Union[int, float] = 0, img: Union[Image.Image] = None, keep_ratio=True, + color=default_color, outline_width=0, outline_color=default_color): + """ + 圆角图 + :param uv_size: + :param box_size: + :param parent_point: + :param point: + :param fillet: 圆角半径浮点或整数 + :param img: + :param keep_ratio: + """ + self.fillet = fillet + self.color = color + self.outline_width = outline_width + self.outline_color = outline_color + super(Rectangle, self).__init__(uv_size, box_size, parent_point, point, img, keep_ratio) + + def preprocess(self): + limited_size = (int(self.canvas.base_img.size[0] * (self.canvas_box[2] - self.canvas_box[0])), + int(self.canvas.base_img.size[1] * (self.canvas_box[3] - self.canvas_box[1]))) + if not self.keep_ratio and self.img_base_img is not None and self.img_base_img.size[0] / self.img_base_img.size[1] != limited_size[0] / limited_size[1]: + self.img_base_img = self.img_base_img.resize(limited_size) + self.img_base_img = Shape.rectangle(size=limited_size, fillet=self.fillet, fill=self.color, width=self.outline_width, outline=self.outline_color) + + +class Color: + GREY = (128, 128, 128, 255) + RED = (255, 0, 0, 255) + GREEN = (0, 255, 0, 255) + BLUE = (0, 0, 255, 255) + YELLOW = (255, 255, 0, 255) + PURPLE = (255, 0, 255, 255) + CYAN = (0, 255, 255, 255) + WHITE = (255, 255, 255, 255) + BLACK = (0, 0, 0, 255) + + @staticmethod + def hex2dec(colorHex: str) -> Tuple[int, int, int, int]: + """ + :param colorHex: FFFFFFFF (ARGB)-> (R, G, B, A) + :return: + """ + return int(colorHex[2:4], 16), int(colorHex[4:6], 16), int(colorHex[6:8], 16), int(colorHex[0:2], 16) + + +class Shape: + @staticmethod + def circular(radius: int, fill: tuple, width: int = 0, outline: tuple = Color.BLACK) -> Image.Image: + """ + :param radius: 半径(像素) + :param fill: 填充颜色 + :param width: 轮廓粗细(像素) + :param outline: 轮廓颜色 + :return: 圆形Image对象 + """ + img = Image.new("RGBA", (radius * 2, radius * 2), color=radius) + draw = ImageDraw.Draw(img) + draw.ellipse(xy=(0, 0, radius * 2, radius * 2), fill=fill, outline=outline, width=width) + return img + + @staticmethod + def rectangle(size: Tuple[int, int], fill: tuple, width: int = 0, outline: tuple = Color.BLACK, fillet: int = 0) -> Image.Image: + """ + :param fillet: 圆角半径(像素) + :param size: 长宽(像素) + :param fill: 填充颜色 + :param width: 轮廓粗细(像素) + :param outline: 轮廓颜色 + :return: 矩形Image对象 + """ + img = Image.new("RGBA", size, color=fill) + draw = ImageDraw.Draw(img) + draw.rounded_rectangle(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline, width=width, radius=fillet) + return img + + @staticmethod + def ellipse(size: Tuple[int, int], fill: tuple, outline: int = 0, outline_color: tuple = Color.BLACK) -> Image.Image: + """ + :param size: 长宽(像素) + :param fill: 填充颜色 + :param outline: 轮廓粗细(像素) + :param outline_color: 轮廓颜色 + :return: 椭圆Image对象 + """ + img = Image.new("RGBA", size, color=fill) + draw = ImageDraw.Draw(img) + draw.ellipse(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline_color, width=outline) + return img + + @staticmethod + def polygon(points: List[Tuple[int, int]], fill: tuple, outline: int, outline_color: tuple) -> Image.Image: + """ + :param points: 多边形顶点列表 + :param fill: 填充颜色 + :param outline: 轮廓粗细(像素) + :param outline_color: 轮廓颜色 + :return: 多边形Image对象 + """ + img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill) + draw = ImageDraw.Draw(img) + draw.polygon(xy=points, fill=fill, outline=outline_color, width=outline) + return img + + @staticmethod + def line(points: List[Tuple[int, int]], fill: tuple, width: int) -> Image: + """ + :param points: 线段顶点列表 + :param fill: 填充颜色 + :param width: 线段粗细(像素) + :return: 线段Image对象 + """ + img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill) + draw = ImageDraw.Draw(img) + draw.line(xy=points, fill=fill, width=width) + return img + + +class Utils: + + @staticmethod + def central_clip_by_ratio(img: Image.Image, size: Tuple, use_cache=True): + """ + :param use_cache: 是否使用缓存,剪切过一次后默认生成缓存 + :param img: + :param size: 仅为比例,满填充裁剪 + :return: + """ + cache_file_path = str() + if use_cache: + filename_without_end = ".".join(os.path.basename(img.fp.name).split(".")[0:-1]) + f"_{size[0]}x{size[1]}" + ".png" + cache_file_path = os.path.join(".cache", filename_without_end) + if os.path.exists(cache_file_path): + nonebot.logger.info("本次使用缓存加载图片,不裁剪") + return Image.open(os.path.join(".cache", filename_without_end)) + img_ratio = img.size[0] / img.size[1] + limited_ratio = size[0] / size[1] + if limited_ratio > img_ratio: + actual_size = ( + img.size[0], + img.size[0] / size[0] * size[1] + ) + box = ( + 0, (img.size[1] - actual_size[1]) // 2, + img.size[0], img.size[1] - (img.size[1] - actual_size[1]) // 2 + ) + else: + actual_size = ( + img.size[1] / size[1] * size[0], + img.size[1], + ) + box = ( + (img.size[0] - actual_size[0]) // 2, 0, + img.size[0] - (img.size[0] - actual_size[0]) // 2, img.size[1] + ) + img = img.crop(box).resize(size) + if use_cache: + img.save(cache_file_path) + return img + + @staticmethod + def circular_clip(img: Image.Image): + """ + 裁剪为alpha圆形 + + :param img: + :return: + """ + length = min(img.size) + alpha_cover = Image.new("RGBA", (length, length), color=(0, 0, 0, 0)) + if img.size[0] > img.size[1]: + box = ( + (img.size[0] - img[1]) // 2, 0, + (img.size[0] - img[1]) // 2 + img.size[1], img.size[1] + ) + else: + box = ( + 0, (img.size[1] - img.size[0]) // 2, + img.size[0], (img.size[1] - img.size[0]) // 2 + img.size[0] + ) + img = img.crop(box).resize((length, length)) + draw = ImageDraw.Draw(alpha_cover) + draw.ellipse(xy=(0, 0, length, length), fill=(255, 255, 255, 255)) + alpha = alpha_cover.split()[-1] + img.putalpha(alpha) + return img + + @staticmethod + def open_img(path) -> Image.Image: + return Image.open(path, "RGBA") diff --git a/src/plugins/liteyuki_crt_utils/crt.py b/src/nonebot_plugins/liteyuki_crt_utils/crt.py similarity index 100% rename from src/plugins/liteyuki_crt_utils/crt.py rename to src/nonebot_plugins/liteyuki_crt_utils/crt.py diff --git a/src/plugins/liteyuki_crt_utils/crt_matchers.py b/src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py similarity index 96% rename from src/plugins/liteyuki_crt_utils/crt_matchers.py rename to src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py index 62402cca..0a4fa646 100644 --- a/src/plugins/liteyuki_crt_utils/crt_matchers.py +++ b/src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py @@ -1,78 +1,78 @@ -from urllib.parse import quote - -import aiohttp -from nonebot import require - -from src.utils.event import get_user_id -from src.utils.base.language import Language -from src.utils.base.ly_typing import T_MessageEvent -from src.utils.base.resource import get_path -from src.utils.message.html_tool import template2image - -require("nonebot_plugin_alconna") - -from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, Option - -crt_cmd = on_alconna( - Alconna( - "crt", - Subcommand( - "route", - Args["start", str, "沙坪坝"]["end", str, "上新街"], - alias=("r",), - help_text="查询两地之间的地铁路线" - ), - ) -) - - -@crt_cmd.assign("route") -async def _(result: Arparma, event: T_MessageEvent): - # 获取语言 - ulang = Language(get_user_id(event)) - - # 获取参数 - # 你也别问我为什么要quote两次,问就是CRT官网的锅,只有这样才可以运行 - start = quote(quote(result.other_args.get("start"))) - end = quote(quote(result.other_args.get("end"))) - - # 判断参数语言 - query_lang_code = "" - if start.isalpha() and end.isalpha(): - query_lang_code = "Eng" - - # 构造请求 URL - url = f"https://www.cqmetro.cn/Front/html/TakeLine!queryYs{query_lang_code}TakeLine.action?entity.startStaName={start}&entity.endStaName={end}" - - # 请求数据 - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - result = await resp.json() - - # 检查结果/无则终止 - if not result.get("result"): - await crt_cmd.send(ulang.get("crt.no_result")) - return - - # 模板传参定义 - templates = { - "data" : { - "result": result["result"], - }, - "localization": ulang.get_many( - "crt.station", - "crt.hour", - "crt.minute", - ) - - } - - # 生成图片 - image = await template2image( - template=get_path("templates/crt_route.html"), - templates=templates, - debug=True - ) - - # 发送图片 - await crt_cmd.send(UniMessage.image(raw=image)) +from urllib.parse import quote + +import aiohttp +from nonebot import require + +from src.utils.event import get_user_id +from src.utils.base.language import Language +from src.utils.base.ly_typing import T_MessageEvent +from src.utils.base.resource import get_path +from src.utils.message.html_tool import template2image + +require("nonebot_plugin_alconna") + +from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, Option + +crt_cmd = on_alconna( + Alconna( + "crt", + Subcommand( + "route", + Args["start", str, "沙坪坝"]["end", str, "上新街"], + alias=("r",), + help_text="查询两地之间的地铁路线" + ), + ) +) + + +@crt_cmd.assign("route") +async def _(result: Arparma, event: T_MessageEvent): + # 获取语言 + ulang = Language(get_user_id(event)) + + # 获取参数 + # 你也别问我为什么要quote两次,问就是CRT官网的锅,只有这样才可以运行 + start = quote(quote(result.other_args.get("start"))) + end = quote(quote(result.other_args.get("end"))) + + # 判断参数语言 + query_lang_code = "" + if start.isalpha() and end.isalpha(): + query_lang_code = "Eng" + + # 构造请求 URL + url = f"https://www.cqmetro.cn/Front/html/TakeLine!queryYs{query_lang_code}TakeLine.action?entity.startStaName={start}&entity.endStaName={end}" + + # 请求数据 + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + result = await resp.json() + + # 检查结果/无则终止 + if not result.get("result"): + await crt_cmd.send(ulang.get("crt.no_result")) + return + + # 模板传参定义 + templates = { + "data" : { + "result": result["result"], + }, + "localization": ulang.get_many( + "crt.station", + "crt.hour", + "crt.minute", + ) + + } + + # 生成图片 + image = await template2image( + template=get_path("templates/crt_route.html"), + templates=templates, + debug=True + ) + + # 发送图片 + await crt_cmd.send(UniMessage.image(raw=image)) diff --git a/src/plugins/liteyuki_crt_utils/rt_guide.py b/src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py similarity index 96% rename from src/plugins/liteyuki_crt_utils/rt_guide.py rename to src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py index 321f6c98..4167cbf7 100644 --- a/src/plugins/liteyuki_crt_utils/rt_guide.py +++ b/src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py @@ -1,419 +1,419 @@ -import json -from typing import List, Any - -from PIL import Image -from arclet.alconna import Alconna -from nb_cli import run_sync -from nonebot import on_command -from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage -from pydantic import BaseModel - -from .canvas import * -from ...utils.base.resource import get_path - -resolution = 256 - - -class Entrance(BaseModel): - identifier: str - size: tuple[int, int] - dest: List[str] - - -class Station(BaseModel): - identifier: str - chineseName: str - englishName: str - position: tuple[int, int] - - -class Line(BaseModel): - identifier: str - chineseName: str - englishName: str - color: Any - stations: List["Station"] - - -font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2") -font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2") - -@run_sync -def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float], - reso: int = resolution): - """ - Generates an entrance sign for the ride. - """ - width, height = ratio[0] * reso, ratio[1] * reso - baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE)) - # 加黑色图框 - baseCanvas.outline = Img( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0, 0), - point=(0, 0), - img=Shape.rectangle( - size=(width, height), - fillet=0, - fill=(0, 0, 0, 0), - width=15, - outline=Color.BLACK - ) - ) - - baseCanvas.contentPanel = Panel( - uv_size=(width, height), - box_size=(width - 28, height - 28), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - ) - - linePanelHeight = 0.7 * ratio[1] - linePanelWidth = linePanelHeight * 1.3 - - # 画线路面板部分 - - for i, line in enumerate(lineInfo): - linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel( - uv_size=ratio, - box_size=(linePanelWidth, linePanelHeight), - parent_point=(i * linePanelWidth / ratio[0], 1), - point=(0, 1), - ) - - linePanel.colorCube = Img( - uv_size=(1, 1), - box_size=(0.15, 1), - parent_point=(0.125, 1), - point=(0, 1), - img=Shape.rectangle( - size=(100, 100), - fillet=0, - fill=line.color, - ), - keep_ratio=False - ) - - textPanel = linePanel.TextPanel = Panel( - uv_size=(1, 1), - box_size=(0.625, 1), - parent_point=(1, 1), - point=(1, 1) - ) - - # 中文线路名 - textPanel.namePanel = Panel( - uv_size=(1, 1), - box_size=(1, 2 / 3), - parent_point=(0, 0), - point=(0, 0), - ) - nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i)) - textPanel.namePanel.text = Text( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - text=line.chineseName, - color=Color.BLACK, - font_size=int(nameSize[1] * 0.5), - force_size=True, - font=font_bold - - ) - - # 英文线路名 - textPanel.englishNamePanel = Panel( - uv_size=(1, 1), - box_size=(1, 1 / 3), - parent_point=(0, 1), - point=(0, 1), - ) - englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i)) - textPanel.englishNamePanel.text = Text( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - text=line.englishName, - color=Color.BLACK, - font_size=int(englishNameSize[1] * 0.6), - force_size=True, - font=font_light - ) - - # 画名称部分 - namePanel = baseCanvas.contentPanel.namePanel = Panel( - uv_size=(1, 1), - box_size=(1, 0.4), - parent_point=(0.5, 0), - point=(0.5, 0), - ) - - namePanel.text = Text( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - text=name, - color=Color.BLACK, - font_size=int(height * 0.3), - force_size=True, - font=font_bold - ) - - aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel( - uv_size=(1, 1), - box_size=(1, 0.5), - parent_point=(0.5, 1), - point=(0.5, 1), - - ) - for j, alias in enumerate(aliases): - aliasesPanel.__dict__[alias] = Text( - uv_size=(1, 1), - box_size=(0.35, 0.5), - parent_point=(0.5, 0.5 * j), - point=(0.5, 0), - text=alias, - color=Color.BLACK, - font_size=int(height * 0.15), - font=font_light - ) - - # 画入口标识 - entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel( - uv_size=(1, 1), - box_size=(0.2, 1), - parent_point=(1, 0.5), - point=(1, 0.5), - ) - # 中文文本 - entrancePanel.namePanel = Panel( - uv_size=(1, 1), - box_size=(1, 0.5), - parent_point=(1, 0), - point=(1, 0), - ) - entrancePanel.namePanel.text = Text( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0, 0.5), - point=(0, 0.5), - text=f"{entranceIdentifier}出入口", - color=Color.BLACK, - font_size=int(height * 0.2), - force_size=True, - font=font_bold - ) - # 英文文本 - entrancePanel.englishNamePanel = Panel( - uv_size=(1, 1), - box_size=(1, 0.5), - parent_point=(1, 1), - point=(1, 1), - ) - entrancePanel.englishNamePanel.text = Text( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0, 0.5), - point=(0, 0.5), - text=f"Entrance {entranceIdentifier}", - color=Color.BLACK, - font_size=int(height * 0.15), - force_size=True, - font=font_light - ) - - return baseCanvas.base_img.tobytes() - - -crt_alc = on_alconna( - Alconna( - "crt", - Subcommand( - "entrance", - Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A - ) - ) -) - - -@crt_alc.assign("entrance") -async def _(result: Arparma): - args = result.subcommands.get("entrance").args - name = args["name"] - lines = args["lines"] - entrance = args["entrance"] - line_info = [] - for line in lines.split(","): - line_args = line.split("&") - line_info.append(Line( - identifier=1, - chineseName=line_args[0], - englishName=line_args[1], - color=line_args[2], - stations=[] - )) - img_bytes = await generate_entrance_sign( - name=name, - aliases=name.split("&"), - lineInfo=line_info, - entranceIdentifier=entrance, - ratio=(8, 1), - reso=256, - ) - await crt_alc.finish( - UniMessage.image(raw=img_bytes) - ) - - -def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution): - """ - 生成站台线路图 - :param line: 线路对象 - :param station: 本站点对象 - :param ratio: 比例 - :param reso: 分辨率,1:reso - :return: 两个方向的站牌 - """ - if ratio is None: - ratio = [4, 1] - width, height = ratio[0] * reso, ratio[1] * reso - baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW)) - # 加黑色图框 - baseCanvas.linePanel = Panel( - uv_size=(1, 1), - box_size=(0.8, 0.15), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - ) - - # 直线块 - baseCanvas.linePanel.recLine = Img( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - img=Shape.rectangle( - size=(10, 10), - fill=line.color, - ), - keep_ratio=False - ) - # 灰色直线块 - baseCanvas.linePanel.recLineGrey = Img( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - img=Shape.rectangle( - size=(10, 10), - fill=Color.GREY, - ), - keep_ratio=False - ) - # 生成各站圆点 - outline_width = 40 - circleForward = Shape.circular( - radius=200, - fill=Color.WHITE, - width=outline_width, - outline=line.color, - ) - - circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0))) - circleThisPanel.circleOuter = Img( - uv_size=(1, 1), - box_size=(1, 1), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - img=Shape.circular( - radius=200, - fill=Color.WHITE, - width=outline_width, - outline=line.color, - ), - ) - circleThisPanel.circleOuter.circleInner = Img( - uv_size=(1, 1), - box_size=(0.7, 0.7), - parent_point=(0.5, 0.5), - point=(0.5, 0.5), - img=Shape.circular( - radius=200, - fill=line.color, - width=0, - outline=line.color, - ), - ) - - circleThisPanel.export("a.png", alpha=True) - circleThis = circleThisPanel.base_img - - circlePassed = Shape.circular( - radius=200, - fill=Color.WHITE, - width=outline_width, - outline=Color.GREY, - ) - - arrival = False - distance = 1 / (len(line.stations) - 1) - for i, sta in enumerate(line.stations): - box_size = (1.618, 1.618) - if sta.identifier == station.identifier: - arrival = True - baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img( - uv_size=(1, 1), - box_size=(1.8, 1.8), - parent_point=(distance * i, 0.5), - point=(0.5, 0.5), - img=circleThis, - keep_ratio=True - ) - continue - if arrival: - # 后方站绘制 - baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img( - uv_size=(1, 1), - box_size=box_size, - parent_point=(distance * i, 0.5), - point=(0.5, 0.5), - img=circleForward, - keep_ratio=True - ) - else: - # 前方站绘制 - baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img( - uv_size=(1, 1), - box_size=box_size, - parent_point=(distance * i, 0.5), - point=(0.5, 0.5), - img=circlePassed, - keep_ratio=True - ) - return baseCanvas - - -def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float], - reso: int = resolution - ): - pass - -# def main(): -# generate_entrance_sign( -# "璧山", -# aliases=["Bishan"], -# lineInfo=[ -# -# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]), -# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]), -# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]), -# ], -# entranceIdentifier="1", -# ratio=(8, 1) -# ) -# -# -# main() +import json +from typing import List, Any + +from PIL import Image +from arclet.alconna import Alconna +from nb_cli import run_sync +from nonebot import on_command +from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage +from pydantic import BaseModel + +from .canvas import * +from ...utils.base.resource import get_path + +resolution = 256 + + +class Entrance(BaseModel): + identifier: str + size: tuple[int, int] + dest: List[str] + + +class Station(BaseModel): + identifier: str + chineseName: str + englishName: str + position: tuple[int, int] + + +class Line(BaseModel): + identifier: str + chineseName: str + englishName: str + color: Any + stations: List["Station"] + + +font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2") +font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2") + +@run_sync +def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float], + reso: int = resolution): + """ + Generates an entrance sign for the ride. + """ + width, height = ratio[0] * reso, ratio[1] * reso + baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE)) + # 加黑色图框 + baseCanvas.outline = Img( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0, 0), + point=(0, 0), + img=Shape.rectangle( + size=(width, height), + fillet=0, + fill=(0, 0, 0, 0), + width=15, + outline=Color.BLACK + ) + ) + + baseCanvas.contentPanel = Panel( + uv_size=(width, height), + box_size=(width - 28, height - 28), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + ) + + linePanelHeight = 0.7 * ratio[1] + linePanelWidth = linePanelHeight * 1.3 + + # 画线路面板部分 + + for i, line in enumerate(lineInfo): + linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel( + uv_size=ratio, + box_size=(linePanelWidth, linePanelHeight), + parent_point=(i * linePanelWidth / ratio[0], 1), + point=(0, 1), + ) + + linePanel.colorCube = Img( + uv_size=(1, 1), + box_size=(0.15, 1), + parent_point=(0.125, 1), + point=(0, 1), + img=Shape.rectangle( + size=(100, 100), + fillet=0, + fill=line.color, + ), + keep_ratio=False + ) + + textPanel = linePanel.TextPanel = Panel( + uv_size=(1, 1), + box_size=(0.625, 1), + parent_point=(1, 1), + point=(1, 1) + ) + + # 中文线路名 + textPanel.namePanel = Panel( + uv_size=(1, 1), + box_size=(1, 2 / 3), + parent_point=(0, 0), + point=(0, 0), + ) + nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i)) + textPanel.namePanel.text = Text( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + text=line.chineseName, + color=Color.BLACK, + font_size=int(nameSize[1] * 0.5), + force_size=True, + font=font_bold + + ) + + # 英文线路名 + textPanel.englishNamePanel = Panel( + uv_size=(1, 1), + box_size=(1, 1 / 3), + parent_point=(0, 1), + point=(0, 1), + ) + englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i)) + textPanel.englishNamePanel.text = Text( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + text=line.englishName, + color=Color.BLACK, + font_size=int(englishNameSize[1] * 0.6), + force_size=True, + font=font_light + ) + + # 画名称部分 + namePanel = baseCanvas.contentPanel.namePanel = Panel( + uv_size=(1, 1), + box_size=(1, 0.4), + parent_point=(0.5, 0), + point=(0.5, 0), + ) + + namePanel.text = Text( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + text=name, + color=Color.BLACK, + font_size=int(height * 0.3), + force_size=True, + font=font_bold + ) + + aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel( + uv_size=(1, 1), + box_size=(1, 0.5), + parent_point=(0.5, 1), + point=(0.5, 1), + + ) + for j, alias in enumerate(aliases): + aliasesPanel.__dict__[alias] = Text( + uv_size=(1, 1), + box_size=(0.35, 0.5), + parent_point=(0.5, 0.5 * j), + point=(0.5, 0), + text=alias, + color=Color.BLACK, + font_size=int(height * 0.15), + font=font_light + ) + + # 画入口标识 + entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel( + uv_size=(1, 1), + box_size=(0.2, 1), + parent_point=(1, 0.5), + point=(1, 0.5), + ) + # 中文文本 + entrancePanel.namePanel = Panel( + uv_size=(1, 1), + box_size=(1, 0.5), + parent_point=(1, 0), + point=(1, 0), + ) + entrancePanel.namePanel.text = Text( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0, 0.5), + point=(0, 0.5), + text=f"{entranceIdentifier}出入口", + color=Color.BLACK, + font_size=int(height * 0.2), + force_size=True, + font=font_bold + ) + # 英文文本 + entrancePanel.englishNamePanel = Panel( + uv_size=(1, 1), + box_size=(1, 0.5), + parent_point=(1, 1), + point=(1, 1), + ) + entrancePanel.englishNamePanel.text = Text( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0, 0.5), + point=(0, 0.5), + text=f"Entrance {entranceIdentifier}", + color=Color.BLACK, + font_size=int(height * 0.15), + force_size=True, + font=font_light + ) + + return baseCanvas.base_img.tobytes() + + +crt_alc = on_alconna( + Alconna( + "crt", + Subcommand( + "entrance", + Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A + ) + ) +) + + +@crt_alc.assign("entrance") +async def _(result: Arparma): + args = result.subcommands.get("entrance").args + name = args["name"] + lines = args["lines"] + entrance = args["entrance"] + line_info = [] + for line in lines.split(","): + line_args = line.split("&") + line_info.append(Line( + identifier=1, + chineseName=line_args[0], + englishName=line_args[1], + color=line_args[2], + stations=[] + )) + img_bytes = await generate_entrance_sign( + name=name, + aliases=name.split("&"), + lineInfo=line_info, + entranceIdentifier=entrance, + ratio=(8, 1), + reso=256, + ) + await crt_alc.finish( + UniMessage.image(raw=img_bytes) + ) + + +def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution): + """ + 生成站台线路图 + :param line: 线路对象 + :param station: 本站点对象 + :param ratio: 比例 + :param reso: 分辨率,1:reso + :return: 两个方向的站牌 + """ + if ratio is None: + ratio = [4, 1] + width, height = ratio[0] * reso, ratio[1] * reso + baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW)) + # 加黑色图框 + baseCanvas.linePanel = Panel( + uv_size=(1, 1), + box_size=(0.8, 0.15), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + ) + + # 直线块 + baseCanvas.linePanel.recLine = Img( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + img=Shape.rectangle( + size=(10, 10), + fill=line.color, + ), + keep_ratio=False + ) + # 灰色直线块 + baseCanvas.linePanel.recLineGrey = Img( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + img=Shape.rectangle( + size=(10, 10), + fill=Color.GREY, + ), + keep_ratio=False + ) + # 生成各站圆点 + outline_width = 40 + circleForward = Shape.circular( + radius=200, + fill=Color.WHITE, + width=outline_width, + outline=line.color, + ) + + circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0))) + circleThisPanel.circleOuter = Img( + uv_size=(1, 1), + box_size=(1, 1), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + img=Shape.circular( + radius=200, + fill=Color.WHITE, + width=outline_width, + outline=line.color, + ), + ) + circleThisPanel.circleOuter.circleInner = Img( + uv_size=(1, 1), + box_size=(0.7, 0.7), + parent_point=(0.5, 0.5), + point=(0.5, 0.5), + img=Shape.circular( + radius=200, + fill=line.color, + width=0, + outline=line.color, + ), + ) + + circleThisPanel.export("a.png", alpha=True) + circleThis = circleThisPanel.base_img + + circlePassed = Shape.circular( + radius=200, + fill=Color.WHITE, + width=outline_width, + outline=Color.GREY, + ) + + arrival = False + distance = 1 / (len(line.stations) - 1) + for i, sta in enumerate(line.stations): + box_size = (1.618, 1.618) + if sta.identifier == station.identifier: + arrival = True + baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img( + uv_size=(1, 1), + box_size=(1.8, 1.8), + parent_point=(distance * i, 0.5), + point=(0.5, 0.5), + img=circleThis, + keep_ratio=True + ) + continue + if arrival: + # 后方站绘制 + baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img( + uv_size=(1, 1), + box_size=box_size, + parent_point=(distance * i, 0.5), + point=(0.5, 0.5), + img=circleForward, + keep_ratio=True + ) + else: + # 前方站绘制 + baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img( + uv_size=(1, 1), + box_size=box_size, + parent_point=(distance * i, 0.5), + point=(0.5, 0.5), + img=circlePassed, + keep_ratio=True + ) + return baseCanvas + + +def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float], + reso: int = resolution + ): + pass + +# def main(): +# generate_entrance_sign( +# "璧山", +# aliases=["Bishan"], +# lineInfo=[ +# +# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]), +# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]), +# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]), +# ], +# entranceIdentifier="1", +# ratio=(8, 1) +# ) +# +# +# main() diff --git a/src/plugins/liteyuki_eventpush.py b/src/nonebot_plugins/liteyuki_eventpush.py similarity index 97% rename from src/plugins/liteyuki_eventpush.py rename to src/nonebot_plugins/liteyuki_eventpush.py index a0fd912f..0afe018e 100644 --- a/src/plugins/liteyuki_eventpush.py +++ b/src/nonebot_plugins/liteyuki_eventpush.py @@ -1,125 +1,125 @@ -import nonebot -from nonebot import on_message, require -from nonebot.plugin import PluginMetadata - -from src.utils.base.data import Database, LiteModel -from src.utils.base.ly_typing import T_Bot, T_MessageEvent -from src.utils.message.message import MarkdownMessage as md - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import on_alconna -from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand - - -class Node(LiteModel): - TABLE_NAME: str = "node" - bot_id: str = "" - session_type: str = "" - session_id: str = "" - - def __str__(self): - return f"{self.bot_id}.{self.session_type}.{self.session_id}" - - -class Push(LiteModel): - TABLE_NAME: str = "push" - source: Node = Node() - target: Node = Node() - inde: int = 0 - - -pushes_db = Database("data/pushes.ldb") -pushes_db.auto_migrate(Push(), Node()) - -alc = Alconna( - "lep", - Subcommand( - "add", - Args["source", str], - Args["target", str], - Option("bidirectional", Args["bidirectional", bool]) - ), - Subcommand( - "rm", - Args["index", int], - - ), - Subcommand( - "list", - ) -) - -add_push = on_alconna(alc) - - -@add_push.handle() -async def _(result: Arparma): - """bot_id.session_type.session_id""" - if result.subcommands.get("add"): - source = result.subcommands["add"].args.get("source") - target = result.subcommands["add"].args.get("target") - if source and target: - source = source.split(".") - target = target.split(".") - push1 = Push( - source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]), - target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]), - inde=len(pushes_db.where_all(Push(), default=[])) - ) - pushes_db.save(push1) - - if result.subcommands["add"].args.get("bidirectional"): - push2 = Push( - source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]), - target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]), - inde=len(pushes_db.where_all(Push(), default=[])) - ) - pushes_db.save(push2) - await add_push.finish("添加成功") - else: - await add_push.finish("参数缺失") - elif result.subcommands.get("rm"): - index = result.subcommands["rm"].args.get("index") - if index is not None: - try: - pushes_db.delete(Push(), "inde = ?", index) - await add_push.finish("删除成功") - except IndexError: - await add_push.finish("索引错误") - else: - await add_push.finish("参数缺失") - elif result.subcommands.get("list"): - await add_push.finish( - "\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> " - f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in - enumerate(pushes_db.where_all(Push(), default=[]))])) - else: - await add_push.finish("参数错误") - - -@on_message(block=False).handle() -async def _(event: T_MessageEvent, bot: T_Bot): - for push in pushes_db.where_all(Push(), default=[]): - if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}": - bot2 = nonebot.get_bot(push.target.bot_id) - msg_formatted = "" - for line in str(event.message).split("\n"): - msg_formatted += f"**{line.strip()}**\n" - push_message = ( - f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n" - f"{msg_formatted}") - await md.send_md(push_message, bot2, message_type=push.target.session_type, - session_id=push.target.session_id) - return - - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="轻雪事件推送", - description="事件推送插件,支持单向和双向推送,支持跨Bot推送", - usage="", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - } -) +import nonebot +from nonebot import on_message, require +from nonebot.plugin import PluginMetadata + +from src.utils.base.data import Database, LiteModel +from src.utils.base.ly_typing import T_Bot, T_MessageEvent +from src.utils.message.message import MarkdownMessage as md + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import on_alconna +from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand + + +class Node(LiteModel): + TABLE_NAME: str = "node" + bot_id: str = "" + session_type: str = "" + session_id: str = "" + + def __str__(self): + return f"{self.bot_id}.{self.session_type}.{self.session_id}" + + +class Push(LiteModel): + TABLE_NAME: str = "push" + source: Node = Node() + target: Node = Node() + inde: int = 0 + + +pushes_db = Database("data/pushes.ldb") +pushes_db.auto_migrate(Push(), Node()) + +alc = Alconna( + "lep", + Subcommand( + "add", + Args["source", str], + Args["target", str], + Option("bidirectional", Args["bidirectional", bool]) + ), + Subcommand( + "rm", + Args["index", int], + + ), + Subcommand( + "list", + ) +) + +add_push = on_alconna(alc) + + +@add_push.handle() +async def _(result: Arparma): + """bot_id.session_type.session_id""" + if result.subcommands.get("add"): + source = result.subcommands["add"].args.get("source") + target = result.subcommands["add"].args.get("target") + if source and target: + source = source.split(".") + target = target.split(".") + push1 = Push( + source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]), + target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]), + inde=len(pushes_db.where_all(Push(), default=[])) + ) + pushes_db.save(push1) + + if result.subcommands["add"].args.get("bidirectional"): + push2 = Push( + source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]), + target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]), + inde=len(pushes_db.where_all(Push(), default=[])) + ) + pushes_db.save(push2) + await add_push.finish("添加成功") + else: + await add_push.finish("参数缺失") + elif result.subcommands.get("rm"): + index = result.subcommands["rm"].args.get("index") + if index is not None: + try: + pushes_db.delete(Push(), "inde = ?", index) + await add_push.finish("删除成功") + except IndexError: + await add_push.finish("索引错误") + else: + await add_push.finish("参数缺失") + elif result.subcommands.get("list"): + await add_push.finish( + "\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> " + f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in + enumerate(pushes_db.where_all(Push(), default=[]))])) + else: + await add_push.finish("参数错误") + + +@on_message(block=False).handle() +async def _(event: T_MessageEvent, bot: T_Bot): + for push in pushes_db.where_all(Push(), default=[]): + if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}": + bot2 = nonebot.get_bot(push.target.bot_id) + msg_formatted = "" + for line in str(event.message).split("\n"): + msg_formatted += f"**{line.strip()}**\n" + push_message = ( + f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n" + f"{msg_formatted}") + await md.send_md(push_message, bot2, message_type=push.target.session_type, + session_id=push.target.session_id) + return + + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="轻雪事件推送", + description="事件推送插件,支持单向和双向推送,支持跨Bot推送", + usage="", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + } +) diff --git a/src/plugins/liteyuki_markdowntest.py b/src/nonebot_plugins/liteyuki_markdowntest.py similarity index 97% rename from src/plugins/liteyuki_markdowntest.py rename to src/nonebot_plugins/liteyuki_markdowntest.py index 10cdd1a9..58e67350 100644 --- a/src/plugins/liteyuki_markdowntest.py +++ b/src/nonebot_plugins/liteyuki_markdowntest.py @@ -1,52 +1,52 @@ -from nonebot import on_command, require -from nonebot.adapters.onebot.v11 import MessageSegment -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER -from nonebot.plugin import PluginMetadata - -from src.utils.base.ly_typing import T_Bot, T_MessageEvent, v11 -from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers -from src.utils.message.html_tool import * - -md_test = on_command("mdts", permission=SUPERUSER) -btn_test = on_command("btnts", permission=SUPERUSER) -latex_test = on_command("latex", permission=SUPERUSER) - - -@md_test.handle() -async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()): - await md.send_md( - v11.utils.unescape(str(arg)), - bot, - message_type=event.message_type, - session_id=event.user_id if event.message_type == "private" else event.group_id - ) - - -@btn_test.handle() -async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()): - await md.send_btn( - str(arg), - bot, - message_type=event.message_type, - session_id=event.user_id if event.message_type == "private" else event.group_id - ) - - -@latex_test.handle() -async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()): - latex_text = f"$${v11.utils.unescape(str(arg))}$$" - img = await md_to_pic(latex_text) - await bot.send(event=event, message=MessageSegment.image(img)) - - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="轻雪Markdown测试", - description="用于测试Markdown的插件", - usage="", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - } -) +from nonebot import on_command, require +from nonebot.adapters.onebot.v11 import MessageSegment +from nonebot.params import CommandArg +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata + +from src.utils.base.ly_typing import T_Bot, T_MessageEvent, v11 +from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers +from src.utils.message.html_tool import * + +md_test = on_command("mdts", permission=SUPERUSER) +btn_test = on_command("btnts", permission=SUPERUSER) +latex_test = on_command("latex", permission=SUPERUSER) + + +@md_test.handle() +async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()): + await md.send_md( + v11.utils.unescape(str(arg)), + bot, + message_type=event.message_type, + session_id=event.user_id if event.message_type == "private" else event.group_id + ) + + +@btn_test.handle() +async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()): + await md.send_btn( + str(arg), + bot, + message_type=event.message_type, + session_id=event.user_id if event.message_type == "private" else event.group_id + ) + + +@latex_test.handle() +async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()): + latex_text = f"$${v11.utils.unescape(str(arg))}$$" + img = await md_to_pic(latex_text) + await bot.send(event=event, message=MessageSegment.image(img)) + + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="轻雪Markdown测试", + description="用于测试Markdown的插件", + usage="", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + } +) diff --git a/src/plugins/liteyuki_mctools/__init__.py b/src/nonebot_plugins/liteyuki_mctools/__init__.py similarity index 96% rename from src/plugins/liteyuki_mctools/__init__.py rename to src/nonebot_plugins/liteyuki_mctools/__init__.py index a1c51664..dece4725 100644 --- a/src/plugins/liteyuki_mctools/__init__.py +++ b/src/nonebot_plugins/liteyuki_mctools/__init__.py @@ -1,15 +1,15 @@ -from nonebot.plugin import PluginMetadata -from nonebot import get_driver - -__plugin_meta__ = PluginMetadata( - name="Minecraft工具箱", - description="一些Minecraft相关工具箱", - usage="我觉得你应该会用", - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki" : True, - "toggleable" : True, - "default_enable": True, - } +from nonebot.plugin import PluginMetadata +from nonebot import get_driver + +__plugin_meta__ = PluginMetadata( + name="Minecraft工具箱", + description="一些Minecraft相关工具箱", + usage="我觉得你应该会用", + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki" : True, + "toggleable" : True, + "default_enable": True, + } ) \ No newline at end of file diff --git a/src/plugins/liteyuki_minigame/__init__.py b/src/nonebot_plugins/liteyuki_minigame/__init__.py similarity index 96% rename from src/plugins/liteyuki_minigame/__init__.py rename to src/nonebot_plugins/liteyuki_minigame/__init__.py index 5483f05b..3dd85d99 100644 --- a/src/plugins/liteyuki_minigame/__init__.py +++ b/src/nonebot_plugins/liteyuki_minigame/__init__.py @@ -1,15 +1,15 @@ -from nonebot.plugin import PluginMetadata -from .minesweeper import * - -__plugin_meta__ = PluginMetadata( - name="轻雪小游戏", - description="内置了一些小游戏", - usage="", - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - "toggleable" : True, - "default_enable" : True, - } -) +from nonebot.plugin import PluginMetadata +from .minesweeper import * + +__plugin_meta__ = PluginMetadata( + name="轻雪小游戏", + description="内置了一些小游戏", + usage="", + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + "toggleable" : True, + "default_enable" : True, + } +) diff --git a/src/plugins/liteyuki_minigame/game.py b/src/nonebot_plugins/liteyuki_minigame/game.py similarity index 96% rename from src/plugins/liteyuki_minigame/game.py rename to src/nonebot_plugins/liteyuki_minigame/game.py index b6ff63b8..8ed838c6 100644 --- a/src/plugins/liteyuki_minigame/game.py +++ b/src/nonebot_plugins/liteyuki_minigame/game.py @@ -1,168 +1,168 @@ -import random -from pydantic import BaseModel -from src.utils.message.message import MarkdownMessage as md - -class Dot(BaseModel): - row: int - col: int - mask: bool = True - value: int = 0 - flagged: bool = False - - -class Minesweeper: - # 0-8: number of mines around, 9: mine, -1: undefined - NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" - MASK = "🅜" - FLAG = "🅕" - MINE = "🅑" - - def __init__(self, rows, cols, num_mines, session_type, session_id): - assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols - self.session_type = session_type - self.session_id = session_id - self.rows = rows - self.cols = cols - self.num_mines = num_mines - self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)] - self.is_first = True - - def reveal(self, row, col) -> bool: - """ - 展开 - Args: - row: - col: - - Returns: - 游戏是否继续 - - """ - - if self.is_first: - # 第一次展开,生成地雷 - self.generate_board(self.board[row][col]) - self.is_first = False - - if self.board[row][col].value == 9: - self.board[row][col].mask = False - return False - - if not self.board[row][col].mask: - return True - - self.board[row][col].mask = False - - if self.board[row][col].value == 0: - self.reveal_neighbors(row, col) - return True - - def is_win(self) -> bool: - """ - 是否胜利 - Returns: - """ - for row in range(self.rows): - for col in range(self.cols): - if self.board[row][col].mask and self.board[row][col].value != 9: - return False - return True - - def generate_board(self, first_dot: Dot): - """ - 避开第一个点,生成地雷 - Args: - first_dot: 第一个点 - - Returns: - - """ - generate_count = 0 - while generate_count < self.num_mines: - row = random.randint(0, self.rows - 1) - col = random.randint(0, self.cols - 1) - if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col): - continue - self.board[row][col] = Dot(row=row, col=col, mask=True, value=9) - generate_count += 1 - - for row in range(self.rows): - for col in range(self.cols): - if self.board[row][col].value != 9: - self.board[row][col].value = self.count_adjacent_mines(row, col) - - def count_adjacent_mines(self, row, col): - """ - 计算周围地雷数量 - Args: - row: - col: - - Returns: - - """ - count = 0 - for r in range(max(0, row - 1), min(self.rows, row + 2)): - for c in range(max(0, col - 1), min(self.cols, col + 2)): - if self.board[r][c].value == 9: - count += 1 - return count - - def reveal_neighbors(self, row, col): - """ - 递归展开,使用深度优先搜索 - Args: - row: - col: - - Returns: - - """ - for r in range(max(0, row - 1), min(self.rows, row + 2)): - for c in range(max(0, col - 1), min(self.cols, col + 2)): - if self.board[r][c].mask: - self.board[r][c].mask = False - if self.board[r][c].value == 0: - self.reveal_neighbors(r, c) - - def mark(self, row, col) -> bool: - """ - 标记 - Args: - row: - col: - Returns: - 是否标记成功,如果已经展开则无法标记 - """ - if self.board[row][col].mask: - self.board[row][col].flagged = not self.board[row][col].flagged - return self.board[row][col].flagged - - def board_markdown(self) -> str: - """ - 打印地雷板 - Returns: - """ - dis = " " - start = "> " if self.cols >= 10 else "" - text = start + self.NUMS[0] + dis*2 - # 横向两个雷之间的间隔字符 - # 生成横向索引 - for i in range(self.cols): - text += f"{self.NUMS[i]}" + dis - text += "\n\n" - for i, row in enumerate(self.board): - text += start + f"{self.NUMS[i]}" + dis*2 - for dot in row: - if dot.mask and not dot.flagged: - text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}") - elif dot.flagged: - text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}") - else: - text += self.NUMS[dot.value] - text += dis - text += "\n" - btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False) - btn_end = md.btn_cmd("结束", "minesweeper end", enter=True) - text += f" {btn_mark} {btn_end}" - return text +import random +from pydantic import BaseModel +from src.utils.message.message import MarkdownMessage as md + +class Dot(BaseModel): + row: int + col: int + mask: bool = True + value: int = 0 + flagged: bool = False + + +class Minesweeper: + # 0-8: number of mines around, 9: mine, -1: undefined + NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" + MASK = "🅜" + FLAG = "🅕" + MINE = "🅑" + + def __init__(self, rows, cols, num_mines, session_type, session_id): + assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols + self.session_type = session_type + self.session_id = session_id + self.rows = rows + self.cols = cols + self.num_mines = num_mines + self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)] + self.is_first = True + + def reveal(self, row, col) -> bool: + """ + 展开 + Args: + row: + col: + + Returns: + 游戏是否继续 + + """ + + if self.is_first: + # 第一次展开,生成地雷 + self.generate_board(self.board[row][col]) + self.is_first = False + + if self.board[row][col].value == 9: + self.board[row][col].mask = False + return False + + if not self.board[row][col].mask: + return True + + self.board[row][col].mask = False + + if self.board[row][col].value == 0: + self.reveal_neighbors(row, col) + return True + + def is_win(self) -> bool: + """ + 是否胜利 + Returns: + """ + for row in range(self.rows): + for col in range(self.cols): + if self.board[row][col].mask and self.board[row][col].value != 9: + return False + return True + + def generate_board(self, first_dot: Dot): + """ + 避开第一个点,生成地雷 + Args: + first_dot: 第一个点 + + Returns: + + """ + generate_count = 0 + while generate_count < self.num_mines: + row = random.randint(0, self.rows - 1) + col = random.randint(0, self.cols - 1) + if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col): + continue + self.board[row][col] = Dot(row=row, col=col, mask=True, value=9) + generate_count += 1 + + for row in range(self.rows): + for col in range(self.cols): + if self.board[row][col].value != 9: + self.board[row][col].value = self.count_adjacent_mines(row, col) + + def count_adjacent_mines(self, row, col): + """ + 计算周围地雷数量 + Args: + row: + col: + + Returns: + + """ + count = 0 + for r in range(max(0, row - 1), min(self.rows, row + 2)): + for c in range(max(0, col - 1), min(self.cols, col + 2)): + if self.board[r][c].value == 9: + count += 1 + return count + + def reveal_neighbors(self, row, col): + """ + 递归展开,使用深度优先搜索 + Args: + row: + col: + + Returns: + + """ + for r in range(max(0, row - 1), min(self.rows, row + 2)): + for c in range(max(0, col - 1), min(self.cols, col + 2)): + if self.board[r][c].mask: + self.board[r][c].mask = False + if self.board[r][c].value == 0: + self.reveal_neighbors(r, c) + + def mark(self, row, col) -> bool: + """ + 标记 + Args: + row: + col: + Returns: + 是否标记成功,如果已经展开则无法标记 + """ + if self.board[row][col].mask: + self.board[row][col].flagged = not self.board[row][col].flagged + return self.board[row][col].flagged + + def board_markdown(self) -> str: + """ + 打印地雷板 + Returns: + """ + dis = " " + start = "> " if self.cols >= 10 else "" + text = start + self.NUMS[0] + dis*2 + # 横向两个雷之间的间隔字符 + # 生成横向索引 + for i in range(self.cols): + text += f"{self.NUMS[i]}" + dis + text += "\n\n" + for i, row in enumerate(self.board): + text += start + f"{self.NUMS[i]}" + dis*2 + for dot in row: + if dot.mask and not dot.flagged: + text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}") + elif dot.flagged: + text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}") + else: + text += self.NUMS[dot.value] + text += dis + text += "\n" + btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False) + btn_end = md.btn_cmd("结束", "minesweeper end", enter=True) + text += f" {btn_mark} {btn_end}" + return text diff --git a/src/plugins/liteyuki_minigame/minesweeper.py b/src/nonebot_plugins/liteyuki_minigame/minesweeper.py similarity index 97% rename from src/plugins/liteyuki_minigame/minesweeper.py rename to src/nonebot_plugins/liteyuki_minigame/minesweeper.py index 01ca1948..715bb7a6 100644 --- a/src/plugins/liteyuki_minigame/minesweeper.py +++ b/src/nonebot_plugins/liteyuki_minigame/minesweeper.py @@ -1,103 +1,103 @@ -from nonebot import require - -from src.utils.base.ly_typing import T_Bot, T_MessageEvent -from src.utils.message.message import MarkdownMessage as md - -require("nonebot_plugin_alconna") -from .game import Minesweeper - -from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma - -minesweeper = on_alconna( - aliases={"扫雷"}, - command=Alconna( - "minesweeper", - Subcommand( - "start", - Args["row", int, 8]["col", int, 8]["mines", int, 10], - alias=["开始"], - - ), - Subcommand( - "end", - alias=["结束"] - ), - Subcommand( - "reveal", - Args["row", int]["col", int], - alias=["展开"] - - ), - Subcommand( - "mark", - Args["row", int]["col", int], - alias=["标记"] - ), - ), -) - -minesweeper_cache: list[Minesweeper] = [] - - -def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None: - for i in minesweeper_cache: - if i.session_type == event.message_type: - if i.session_id == event.user_id or i.session_id == event.group_id: - return i - return None - - -@minesweeper.handle() -async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot): - game = get_minesweeper_cache(event) - if result.subcommands.get("start"): - if game: - await minesweeper.finish("当前会话不能同时进行多个扫雷游戏") - else: - try: - new_game = Minesweeper( - rows=result.subcommands["start"].args["row"], - cols=result.subcommands["start"].args["col"], - num_mines=result.subcommands["start"].args["mines"], - session_type=event.message_type, - session_id=event.user_id if event.message_type == "private" else event.group_id, - ) - minesweeper_cache.append(new_game) - await minesweeper.send("游戏开始") - await md.send_md(new_game.board_markdown(), bot, event=event) - except AssertionError: - await minesweeper.finish("参数错误") - elif result.subcommands.get("end"): - if game: - minesweeper_cache.remove(game) - await minesweeper.finish("游戏结束") - else: - await minesweeper.finish("当前没有扫雷游戏") - elif result.subcommands.get("reveal"): - if not game: - await minesweeper.finish("当前没有扫雷游戏") - else: - row = result.subcommands["reveal"].args["row"] - col = result.subcommands["reveal"].args["col"] - if not (0 <= row < game.rows and 0 <= col < game.cols): - await minesweeper.finish("参数错误") - if not game.reveal(row, col): - minesweeper_cache.remove(game) - await md.send_md(game.board_markdown(), bot, event=event) - await minesweeper.finish("游戏结束") - await md.send_md(game.board_markdown(), bot, event=event) - if game.is_win(): - minesweeper_cache.remove(game) - await minesweeper.finish("游戏胜利") - elif result.subcommands.get("mark"): - if not game: - await minesweeper.finish("当前没有扫雷游戏") - else: - row = result.subcommands["mark"].args["row"] - col = result.subcommands["mark"].args["col"] - if not (0 <= row < game.rows and 0 <= col < game.cols): - await minesweeper.finish("参数错误") - game.board[row][col].flagged = not game.board[row][col].flagged - await md.send_md(game.board_markdown(), bot, event=event) - else: - await minesweeper.finish("参数错误") +from nonebot import require + +from src.utils.base.ly_typing import T_Bot, T_MessageEvent +from src.utils.message.message import MarkdownMessage as md + +require("nonebot_plugin_alconna") +from .game import Minesweeper + +from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma + +minesweeper = on_alconna( + aliases={"扫雷"}, + command=Alconna( + "minesweeper", + Subcommand( + "start", + Args["row", int, 8]["col", int, 8]["mines", int, 10], + alias=["开始"], + + ), + Subcommand( + "end", + alias=["结束"] + ), + Subcommand( + "reveal", + Args["row", int]["col", int], + alias=["展开"] + + ), + Subcommand( + "mark", + Args["row", int]["col", int], + alias=["标记"] + ), + ), +) + +minesweeper_cache: list[Minesweeper] = [] + + +def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None: + for i in minesweeper_cache: + if i.session_type == event.message_type: + if i.session_id == event.user_id or i.session_id == event.group_id: + return i + return None + + +@minesweeper.handle() +async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot): + game = get_minesweeper_cache(event) + if result.subcommands.get("start"): + if game: + await minesweeper.finish("当前会话不能同时进行多个扫雷游戏") + else: + try: + new_game = Minesweeper( + rows=result.subcommands["start"].args["row"], + cols=result.subcommands["start"].args["col"], + num_mines=result.subcommands["start"].args["mines"], + session_type=event.message_type, + session_id=event.user_id if event.message_type == "private" else event.group_id, + ) + minesweeper_cache.append(new_game) + await minesweeper.send("游戏开始") + await md.send_md(new_game.board_markdown(), bot, event=event) + except AssertionError: + await minesweeper.finish("参数错误") + elif result.subcommands.get("end"): + if game: + minesweeper_cache.remove(game) + await minesweeper.finish("游戏结束") + else: + await minesweeper.finish("当前没有扫雷游戏") + elif result.subcommands.get("reveal"): + if not game: + await minesweeper.finish("当前没有扫雷游戏") + else: + row = result.subcommands["reveal"].args["row"] + col = result.subcommands["reveal"].args["col"] + if not (0 <= row < game.rows and 0 <= col < game.cols): + await minesweeper.finish("参数错误") + if not game.reveal(row, col): + minesweeper_cache.remove(game) + await md.send_md(game.board_markdown(), bot, event=event) + await minesweeper.finish("游戏结束") + await md.send_md(game.board_markdown(), bot, event=event) + if game.is_win(): + minesweeper_cache.remove(game) + await minesweeper.finish("游戏胜利") + elif result.subcommands.get("mark"): + if not game: + await minesweeper.finish("当前没有扫雷游戏") + else: + row = result.subcommands["mark"].args["row"] + col = result.subcommands["mark"].args["col"] + if not (0 <= row < game.rows and 0 <= col < game.cols): + await minesweeper.finish("参数错误") + game.board[row][col].flagged = not game.board[row][col].flagged + await md.send_md(game.board_markdown(), bot, event=event) + else: + await minesweeper.finish("参数错误") diff --git a/src/plugins/liteyuki_pacman/__init__.py b/src/nonebot_plugins/liteyuki_pacman/__init__.py similarity index 96% rename from src/plugins/liteyuki_pacman/__init__.py rename to src/nonebot_plugins/liteyuki_pacman/__init__.py index 0886ea1f..a2ca5036 100644 --- a/src/plugins/liteyuki_pacman/__init__.py +++ b/src/nonebot_plugins/liteyuki_pacman/__init__.py @@ -1,22 +1,22 @@ -from nonebot.plugin import PluginMetadata -from .npm import * -from .rpm import * - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="轻雪包管理器", - description="本地插件管理和插件商店支持,资源包管理,支持启用/停用,安装/卸载插件", - usage=( - "npm list\n" - "npm enable/disable \n" - "npm search \n" - "npm install/uninstall \n" - ), - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - "toggleable" : False, - "default_enable" : True, - } -) +from nonebot.plugin import PluginMetadata +from .npm import * +from .rpm import * + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="轻雪包管理器", + description="本地插件管理和插件商店支持,资源包管理,支持启用/停用,安装/卸载插件", + usage=( + "npm list\n" + "npm enable/disable \n" + "npm search \n" + "npm install/uninstall \n" + ), + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + "toggleable" : False, + "default_enable" : True, + } +) diff --git a/src/plugins/liteyuki_pacman/common.py b/src/nonebot_plugins/liteyuki_pacman/common.py similarity index 96% rename from src/plugins/liteyuki_pacman/common.py rename to src/nonebot_plugins/liteyuki_pacman/common.py index 16bd0913..759d6aeb 100644 --- a/src/plugins/liteyuki_pacman/common.py +++ b/src/nonebot_plugins/liteyuki_pacman/common.py @@ -1,255 +1,255 @@ -import json -from typing import Optional - -import aiofiles -import nonebot.plugin -from nonebot.adapters import satori - -from src.utils import event as event_utils -from src.utils.base.data import LiteModel -from src.utils.base.data_manager import GlobalPlugin, Group, User, group_db, plugin_db, user_db -from src.utils.base.ly_typing import T_MessageEvent - -__group_data = {} # 群数据缓存, {group_id: Group} -__user_data = {} # 用户数据缓存, {user_id: User} -__default_enable = {} # 插件默认启用状态缓存, {plugin_name: bool} static -__global_enable = {} # 插件全局启用状态缓存, {plugin_name: bool} dynamic - - -class PluginTag(LiteModel): - label: str - color: str = '#000000' - - -class StorePlugin(LiteModel): - name: str - desc: str - module_name: str # 插件商店中的模块名不等于本地的模块名,前者是文件夹名,后者是点分割模块名 - project_link: str = "" - homepage: str = "" - author: str = "" - type: str | None = None - version: str | None = "" - time: str = "" - tags: list[PluginTag] = [] - is_official: bool = False - - -def get_plugin_exist(plugin_name: str) -> bool: - """ - 获取插件是否存在于加载列表 - Args: - plugin_name: - - Returns: - - """ - for plugin in nonebot.plugin.get_loaded_plugins(): - if plugin.name == plugin_name: - return True - return False - - -async def get_store_plugin(plugin_name: str) -> Optional[StorePlugin]: - """ - 获取插件信息 - - Args: - plugin_name (str): 插件模块名 - - Returns: - Optional[StorePlugin]: 插件信息 - """ - async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f: - plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())] - for plugin in plugins: - if plugin.module_name == plugin_name: - return plugin - return None - - -def get_plugin_default_enable(plugin_name: str) -> bool: - """ - 获取插件默认启用状态,由插件定义,不存在则默认为启用,优先从缓存中获取 - - Args: - plugin_name (str): 插件模块名 - - Returns: - bool: 插件默认状态 - """ - if plugin_name not in __default_enable: - plug = nonebot.plugin.get_plugin(plugin_name) - default_enable = (plug.metadata.extra.get("default_enable", True) if plug.metadata else True) if plug else True - __default_enable[plugin_name] = default_enable - - return __default_enable[plugin_name] - - -def get_plugin_session_enable(event: T_MessageEvent, plugin_name: str) -> bool: - """ - 获取插件当前会话启用状态 - - Args: - event: 会话事件 - plugin_name (str): 插件模块名 - - Returns: - bool: 插件当前状态 - """ - if isinstance(event, satori.event.Event): - if event.guild is not None: - message_type = "group" - else: - message_type = "private" - else: - message_type = event.message_type - if message_type == "group": - group_id = str(event.guild.id if isinstance(event, satori.event.Event) else event.group_id) - if group_id not in __group_data: - group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id)) - __group_data[str(group_id)] = group - - session = __group_data[group_id] - else: - # session: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id))) - user_id = str(event.user.id if isinstance(event, satori.event.Event) else event.user_id) - if user_id not in __user_data: - user: User = user_db.where_one(User(), "user_id = ?", user_id, default=User(user_id=user_id)) - __user_data[user_id] = user - session = __user_data[user_id] - # 默认停用插件在启用列表内表示启用 - # 默认停用插件不在启用列表内表示停用 - # 默认启用插件在停用列表内表示停用 - # 默认启用插件不在停用列表内表示启用 - default_enable = get_plugin_default_enable(plugin_name) - if default_enable: - return plugin_name not in session.disabled_plugins - else: - return plugin_name in session.enabled_plugins - - -def set_plugin_session_enable(event: T_MessageEvent, plugin_name: str, enable: bool): - """ - 设置插件会话启用状态,同时更新数据库和缓存 - Args: - event: - plugin_name: - enable: - - Returns: - - """ - if event_utils.get_message_type(event) == "group": - session: Group = group_db.where_one(Group(), "group_id = ?", str(event_utils.get_group_id(event)), - default=Group(group_id=str(event_utils.get_group_id(event)))) - else: - session: User = user_db.where_one(User(), "user_id = ?", str(event_utils.get_user_id(event)), - default=User(user_id=str(event_utils.get_user_id(event)))) - default_enable = get_plugin_default_enable(plugin_name) - if default_enable: - if enable: - session.disabled_plugins.remove(plugin_name) - else: - session.disabled_plugins.append(plugin_name) - else: - if enable: - session.enabled_plugins.append(plugin_name) - else: - session.enabled_plugins.remove(plugin_name) - - if event_utils.get_message_type(event) == "group": - __group_data[str(event_utils.get_group_id(event))] = session - group_db.save(session) - else: - __user_data[str(event_utils.get_user_id(event))] = session - user_db.save(session) - - -def get_plugin_global_enable(plugin_name: str) -> bool: - """ - 获取插件全局启用状态, 优先从缓存中获取 - Args: - plugin_name: - - Returns: - - """ - if plugin_name not in __global_enable: - plugin = plugin_db.where_one( - GlobalPlugin(), - "module_name = ?", - plugin_name, - default=GlobalPlugin(module_name=plugin_name, enabled=True)) - __global_enable[plugin_name] = plugin.enabled - - return __global_enable[plugin_name] - - -def set_plugin_global_enable(plugin_name: str, enable: bool): - """ - 设置插件全局启用状态,同时更新数据库和缓存 - Args: - plugin_name: - enable: - - Returns: - - """ - plugin = plugin_db.where_one( - GlobalPlugin(), - "module_name = ?", - plugin_name, - default=GlobalPlugin(module_name=plugin_name, enabled=True)) - plugin.enabled = enable - - plugin_db.save(plugin) - __global_enable[plugin_name] = enable - - -def get_plugin_can_be_toggle(plugin_name: str) -> bool: - """ - 获取插件是否可以被启用/停用 - - Args: - plugin_name (str): 插件模块名 - - Returns: - bool: 插件是否可以被启用/停用 - """ - plug = nonebot.plugin.get_plugin(plugin_name) - return plug.metadata.extra.get("toggleable", True) if plug and plug.metadata else True - - -def get_group_enable(group_id: str) -> bool: - """ - 获取群组是否启用插机器人 - - Args: - group_id (str): 群组ID - - Returns: - bool: 群组是否启用插件 - """ - group_id = str(group_id) - if group_id not in __group_data: - group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id)) - __group_data[group_id] = group - - return __group_data[group_id].enable - - -def set_group_enable(group_id: str, enable: bool): - """ - 设置群组是否启用插机器人 - - Args: - group_id (str): 群组ID - enable (bool): 是否启用 - """ - group_id = str(group_id) - group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id)) - group.enable = enable - - __group_data[group_id] = group - group_db.save(group) +import json +from typing import Optional + +import aiofiles +import nonebot.plugin +from nonebot.adapters import satori + +from src.utils import event as event_utils +from src.utils.base.data import LiteModel +from src.utils.base.data_manager import GlobalPlugin, Group, User, group_db, plugin_db, user_db +from src.utils.base.ly_typing import T_MessageEvent + +__group_data = {} # 群数据缓存, {group_id: Group} +__user_data = {} # 用户数据缓存, {user_id: User} +__default_enable = {} # 插件默认启用状态缓存, {plugin_name: bool} static +__global_enable = {} # 插件全局启用状态缓存, {plugin_name: bool} dynamic + + +class PluginTag(LiteModel): + label: str + color: str = '#000000' + + +class StorePlugin(LiteModel): + name: str + desc: str + module_name: str # 插件商店中的模块名不等于本地的模块名,前者是文件夹名,后者是点分割模块名 + project_link: str = "" + homepage: str = "" + author: str = "" + type: str | None = None + version: str | None = "" + time: str = "" + tags: list[PluginTag] = [] + is_official: bool = False + + +def get_plugin_exist(plugin_name: str) -> bool: + """ + 获取插件是否存在于加载列表 + Args: + plugin_name: + + Returns: + + """ + for plugin in nonebot.plugin.get_loaded_plugins(): + if plugin.name == plugin_name: + return True + return False + + +async def get_store_plugin(plugin_name: str) -> Optional[StorePlugin]: + """ + 获取插件信息 + + Args: + plugin_name (str): 插件模块名 + + Returns: + Optional[StorePlugin]: 插件信息 + """ + async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f: + plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())] + for plugin in plugins: + if plugin.module_name == plugin_name: + return plugin + return None + + +def get_plugin_default_enable(plugin_name: str) -> bool: + """ + 获取插件默认启用状态,由插件定义,不存在则默认为启用,优先从缓存中获取 + + Args: + plugin_name (str): 插件模块名 + + Returns: + bool: 插件默认状态 + """ + if plugin_name not in __default_enable: + plug = nonebot.plugin.get_plugin(plugin_name) + default_enable = (plug.metadata.extra.get("default_enable", True) if plug.metadata else True) if plug else True + __default_enable[plugin_name] = default_enable + + return __default_enable[plugin_name] + + +def get_plugin_session_enable(event: T_MessageEvent, plugin_name: str) -> bool: + """ + 获取插件当前会话启用状态 + + Args: + event: 会话事件 + plugin_name (str): 插件模块名 + + Returns: + bool: 插件当前状态 + """ + if isinstance(event, satori.event.Event): + if event.guild is not None: + message_type = "group" + else: + message_type = "private" + else: + message_type = event.message_type + if message_type == "group": + group_id = str(event.guild.id if isinstance(event, satori.event.Event) else event.group_id) + if group_id not in __group_data: + group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id)) + __group_data[str(group_id)] = group + + session = __group_data[group_id] + else: + # session: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id))) + user_id = str(event.user.id if isinstance(event, satori.event.Event) else event.user_id) + if user_id not in __user_data: + user: User = user_db.where_one(User(), "user_id = ?", user_id, default=User(user_id=user_id)) + __user_data[user_id] = user + session = __user_data[user_id] + # 默认停用插件在启用列表内表示启用 + # 默认停用插件不在启用列表内表示停用 + # 默认启用插件在停用列表内表示停用 + # 默认启用插件不在停用列表内表示启用 + default_enable = get_plugin_default_enable(plugin_name) + if default_enable: + return plugin_name not in session.disabled_plugins + else: + return plugin_name in session.enabled_plugins + + +def set_plugin_session_enable(event: T_MessageEvent, plugin_name: str, enable: bool): + """ + 设置插件会话启用状态,同时更新数据库和缓存 + Args: + event: + plugin_name: + enable: + + Returns: + + """ + if event_utils.get_message_type(event) == "group": + session: Group = group_db.where_one(Group(), "group_id = ?", str(event_utils.get_group_id(event)), + default=Group(group_id=str(event_utils.get_group_id(event)))) + else: + session: User = user_db.where_one(User(), "user_id = ?", str(event_utils.get_user_id(event)), + default=User(user_id=str(event_utils.get_user_id(event)))) + default_enable = get_plugin_default_enable(plugin_name) + if default_enable: + if enable: + session.disabled_plugins.remove(plugin_name) + else: + session.disabled_plugins.append(plugin_name) + else: + if enable: + session.enabled_plugins.append(plugin_name) + else: + session.enabled_plugins.remove(plugin_name) + + if event_utils.get_message_type(event) == "group": + __group_data[str(event_utils.get_group_id(event))] = session + group_db.save(session) + else: + __user_data[str(event_utils.get_user_id(event))] = session + user_db.save(session) + + +def get_plugin_global_enable(plugin_name: str) -> bool: + """ + 获取插件全局启用状态, 优先从缓存中获取 + Args: + plugin_name: + + Returns: + + """ + if plugin_name not in __global_enable: + plugin = plugin_db.where_one( + GlobalPlugin(), + "module_name = ?", + plugin_name, + default=GlobalPlugin(module_name=plugin_name, enabled=True)) + __global_enable[plugin_name] = plugin.enabled + + return __global_enable[plugin_name] + + +def set_plugin_global_enable(plugin_name: str, enable: bool): + """ + 设置插件全局启用状态,同时更新数据库和缓存 + Args: + plugin_name: + enable: + + Returns: + + """ + plugin = plugin_db.where_one( + GlobalPlugin(), + "module_name = ?", + plugin_name, + default=GlobalPlugin(module_name=plugin_name, enabled=True)) + plugin.enabled = enable + + plugin_db.save(plugin) + __global_enable[plugin_name] = enable + + +def get_plugin_can_be_toggle(plugin_name: str) -> bool: + """ + 获取插件是否可以被启用/停用 + + Args: + plugin_name (str): 插件模块名 + + Returns: + bool: 插件是否可以被启用/停用 + """ + plug = nonebot.plugin.get_plugin(plugin_name) + return plug.metadata.extra.get("toggleable", True) if plug and plug.metadata else True + + +def get_group_enable(group_id: str) -> bool: + """ + 获取群组是否启用插机器人 + + Args: + group_id (str): 群组ID + + Returns: + bool: 群组是否启用插件 + """ + group_id = str(group_id) + if group_id not in __group_data: + group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id)) + __group_data[group_id] = group + + return __group_data[group_id].enable + + +def set_group_enable(group_id: str, enable: bool): + """ + 设置群组是否启用插机器人 + + Args: + group_id (str): 群组ID + enable (bool): 是否启用 + """ + group_id = str(group_id) + group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=group_id)) + group.enable = enable + + __group_data[group_id] = group + group_db.save(group) diff --git a/src/plugins/liteyuki_pacman/npm.py b/src/nonebot_plugins/liteyuki_pacman/npm.py similarity index 97% rename from src/plugins/liteyuki_pacman/npm.py rename to src/nonebot_plugins/liteyuki_pacman/npm.py index eb251e24..24e0dfad 100644 --- a/src/plugins/liteyuki_pacman/npm.py +++ b/src/nonebot_plugins/liteyuki_pacman/npm.py @@ -1,846 +1,846 @@ -import os -import sys -import aiohttp -import nonebot.plugin -import pip -from io import StringIO -from arclet.alconna import MultiVar -from nonebot import Bot, require -from nonebot.exception import FinishedException, IgnoredException, MockApiException -from nonebot.internal.adapter import Event -from nonebot.internal.matcher import Matcher -from nonebot.message import run_preprocessor -from nonebot.permission import SUPERUSER -from nonebot.plugin import Plugin, PluginMetadata -from nonebot.utils import run_sync - -from src.utils.base.data_manager import InstalledPlugin -from src.utils.base.language import get_user_lang -from src.utils.base.ly_typing import T_Bot -from src.utils.message.message import MarkdownMessage as md -from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md -from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER -from src.utils.message.tools import clamp -from .common import * - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import ( - on_alconna, - Alconna, - Args, - Arparma, - Subcommand, - Option, - OptionResult, - SubcommandResult, -) - -# const -enable_global = "enable-global" -disable_global = "disable-global" -enable = "enable" -disable = "disable" - - -@on_alconna( - aliases={"插件"}, - command=Alconna( - "npm", - Subcommand( - "enable", - Args["plugin_name", str], - Option( - "-g|--group", - Args["group_id", str, None], - help_text="群号", - ), - alias=["e", "启用"], - ), - Subcommand( - "disable", - Args["plugin_name", str], - Option( - "-g|--group", - Args["group_id", str, None], - help_text="群号", - ), - alias=["d", "停用"], - ), - Subcommand( - enable_global, - Args["plugin_name", str], - alias=["eg", "全局启用"], - ), - Subcommand( - disable_global, - Args["plugin_name", str], - alias=["dg", "全局停用"], - ), - # 安装部分 - Subcommand( - "update", - alias=["u", "更新"], - ), - Subcommand( - "search", - Args["keywords", MultiVar(str)], - alias=["s", "搜索"], - ), - Subcommand( - "install", - Args["plugin_name", str], - alias=["i", "安装"], - ), - Subcommand( - "uninstall", - Args["plugin_name", str], - alias=["r", "rm", "卸载"], - ), - Subcommand( - "list", - Args["page", int, 1]["num", int, 10], - alias=["ls", "列表"], - ), - ), -).handle() -async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher): - if not os.path.exists("data/liteyuki/plugins.json"): - await npm_update() - # 判断会话类型 - ulang = get_user_lang(str(event.user_id)) - plugin_name = result.args.get("plugin_name") - sc = result.subcommands # 获取子命令 - perm_s = await SUPERUSER(bot, event) # 判断是否为超级用户 - # 支持对自定义command_start的判断 - if sc.get("enable") or sc.get("disable"): - - toggle = result.subcommands.get("enable") is not None - - plugin_exist = get_plugin_exist(plugin_name) - - # 判定会话类型 - # 输入群号 - if ( - group_id := ( - sc.get("enable", SubcommandResult()) - .options.get("group", OptionResult()) - .args.get("group_id") - or sc.get("disable", SubcommandResult()) - .options.get("group", OptionResult()) - .args.get("group_id") - ) - ) and await SUPERUSER(bot, event): - session_id = group_id - new_event = event.copy() - new_event.group_id = group_id - new_event.message_type = "group" - - elif event.message_type == "private": - session_id = event.user_id - new_event = event - else: - if ( - await GROUP_ADMIN(bot, event) - or await GROUP_OWNER(bot, event) - or await SUPERUSER(bot, event) - ): - session_id = event.group_id - new_event = event - else: - raise FinishedException(ulang.get("Permission Denied")) - - session_enable = get_plugin_session_enable( - new_event, plugin_name - ) # 获取插件当前状态 - - can_be_toggled = get_plugin_can_be_toggle( - plugin_name - ) # 获取插件是否可以被启用/停用 - - if not plugin_exist: - await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) - - if not can_be_toggled: - await npm.finish( - ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name) - ) - - if session_enable == toggle: - await npm.finish( - ulang.get( - "npm.plugin_already", - NAME=plugin_name, - STATUS=( - ulang.get("npm.enable") if toggle else ulang.get("npm.disable") - ), - ) - ) - - # 键入自定义群号的情况 - - try: - set_plugin_session_enable(new_event, plugin_name, toggle) - except Exception as e: - nonebot.logger.error(e) - await npm.finish( - ulang.get( - "npm.toggle_failed", - NAME=plugin_name, - STATUS=( - ulang.get("npm.enable") if toggle else ulang.get("npm.disable") - ), - ERROR=str(e), - ) - ) - - await npm.finish( - ulang.get( - "npm.toggle_success", - NAME=plugin_name, - STATUS=( - ulang.get("npm.enable") if toggle else ulang.get("npm.disable") - ), - ) # + str(session_id) 这里应该不需增加一个id,在任何语言文件里,这句话都不是这样翻的,你是不是调试的时候忘删了? - ) - - elif ( - sc.get(enable_global) - or result.subcommands.get(disable_global) - and await SUPERUSER(bot, event) - ): - plugin_exist = get_plugin_exist(plugin_name) - - toggle = result.subcommands.get(enable_global) is not None - - can_be_toggled = get_plugin_can_be_toggle(plugin_name) - - if not plugin_exist: - await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) - - if not can_be_toggled: - await npm.finish( - ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name) - ) - - global_enable = get_plugin_global_enable(plugin_name) - if global_enable == toggle: - await npm.finish( - ulang.get( - "npm.plugin_already", - NAME=plugin_name, - STATUS=( - ulang.get("npm.enable") if toggle else ulang.get("npm.disable") - ), - ) - ) - - try: - set_plugin_global_enable(plugin_name, toggle) - except Exception as e: - await npm.finish( - ulang.get( - "npm.toggle_failed", - NAME=plugin_name, - STATUS=( - ulang.get("npm.enable") if toggle else ulang.get("npm.disable") - ), - ERROR=str(e), - ) - ) - - await npm.finish( - ulang.get( - "npm.toggle_success", - NAME=plugin_name, - STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"), - ) - ) - - elif sc.get("update") and perm_s: - r = await npm_update() - if r: - await npm.finish(ulang.get("npm.store_update_success")) - else: - await npm.finish(ulang.get("npm.store_update_failed")) - - elif sc.get("search"): - keywords: list[str] = result.subcommands["search"].args.get("keywords") - rs = await npm_search(keywords) - max_show = 10 - if len(rs): - reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***" - for storePlugin in rs[: min(max_show, len(rs))]: - btn_install_or_update = md.btn_cmd( - ( - ulang.get("npm.update") - if get_plugin_exist(storePlugin.module_name) - else ulang.get("npm.install") - ), - "npm install %s" % storePlugin.module_name, - ) - link_page = md.btn_link(ulang.get("npm.homepage"), storePlugin.homepage) - link_pypi = md.btn_link(ulang.get("npm.pypi"), storePlugin.homepage) - - reply += ( - f"\n# **{storePlugin.name}**\n" - f"\n> **{storePlugin.desc}**\n" - f"\n> {ulang.get('npm.author')}: {storePlugin.author}" - f"\n> *{md.escape(storePlugin.module_name)}*" - f"\n> {btn_install_or_update} {link_page} {link_pypi}\n\n***\n" - ) - if len(rs) > max_show: - reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}" - else: - reply = ulang.get("npm.search_no_result") - await md.send_md(reply, bot, event=event) - - elif sc.get("install") and perm_s: - plugin_name: str = result.subcommands["install"].args.get("plugin_name") - store_plugin = await get_store_plugin(plugin_name) - await npm.send(ulang.get("npm.installing", NAME=plugin_name)) - - r, log = await npm_install(plugin_name) - log = log.replace("\\", "/") - - if not store_plugin: - await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) - - homepage_btn = md.btn_cmd(ulang.get("npm.homepage"), store_plugin.homepage) - if r: - r_load = nonebot.load_plugin(plugin_name) # 加载插件 - installed_plugin = InstalledPlugin( - module_name=plugin_name - ) # 构造插件信息模型 - found_in_db_plugin = plugin_db.where_one( - InstalledPlugin(), "module_name = ?", plugin_name - ) # 查询数据库中是否已经安装 - if r_load: - if found_in_db_plugin is None: - plugin_db.save(installed_plugin) - info = md.escape( - ulang.get("npm.install_success", NAME=store_plugin.name) - ) # markdown转义 - await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event) - else: - await npm.finish( - ulang.get( - "npm.plugin_already_installed", NAME=store_plugin.name - ) - ) - else: - info = ulang.get( - "npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn - ).replace("_", r"\\_") - await md.send_md(f"{info}\n\n" f"```\n{log}\n```\n", bot, event=event) - else: - info = ulang.get( - "npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn - ).replace("_", r"\\_") - await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event) - - elif sc.get("uninstall") and perm_s: - plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore - found_installed_plugin: InstalledPlugin = plugin_db.where_one( - InstalledPlugin(), "module_name = ?", plugin_name - ) - if found_installed_plugin: - plugin_db.delete(InstalledPlugin(), "module_name = ?", plugin_name) - reply = f"{ulang.get('npm.uninstall_success', NAME=found_installed_plugin.module_name)}" - await npm.finish(reply) - else: - await npm.finish(ulang.get("npm.plugin_not_installed", NAME=plugin_name)) - - elif sc.get("list"): - loaded_plugin_list = sorted(nonebot.get_loaded_plugins(), key=lambda x: x.name) - num_per_page = result.subcommands.get("list").args.get("num") - total = len(loaded_plugin_list) // num_per_page + ( - 1 if len(loaded_plugin_list) % num_per_page else 0 - ) - - page = clamp(result.subcommands.get("list").args.get("page"), 1, total) - - # 已加载插件 | 总计10 | 第1/3页 - reply = ( - f"# {ulang.get('npm.loaded_plugins')} | " - f"{ulang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} | " - f"{ulang.get('npm.page', PAGE=page, TOTAL=total)} \n***\n" - ) - - permission_oas = ( - await GROUP_ADMIN(bot, event) - or await GROUP_OWNER(bot, event) - or await SUPERUSER(bot, event) - ) - permission_s = await SUPERUSER(bot, event) - - for storePlugin in loaded_plugin_list[ - (page - 1) - * num_per_page : min(page * num_per_page, len(loaded_plugin_list)) - ]: - # 检查是否有 metadata 属性 - # 添加帮助按钮 - - btn_usage = md.btn_cmd( - ulang.get("npm.usage"), f"help {storePlugin.name}", False - ) - store_plugin = await get_store_plugin(storePlugin.name) - session_enable = get_plugin_session_enable(event, storePlugin.name) - if store_plugin: - # btn_homepage = md.btn_link(ulang.get("npm.homepage"), store_plugin.homepage) - show_name = store_plugin.name - elif storePlugin.metadata: - # if storePlugin.metadata.extra.get("liteyuki"): - # btn_homepage = md.btn_link(ulang.get("npm.homepage"), "https://github.com/snowykami/LiteyukiBot") - # else: - # btn_homepage = ulang.get("npm.homepage") - show_name = storePlugin.metadata.name - else: - # btn_homepage = ulang.get("npm.homepage") - show_name = storePlugin.name - ulang.get("npm.no_description") - - if storePlugin.metadata: - reply += f"\n**{md.escape(show_name)}**\n" - else: - reply += f"**{md.escape(show_name)}**\n" - - reply += f"\n > {btn_usage}" - - if permission_oas: - # 添加启用/停用插件按钮 - cmd_toggle = f"npm {'disable' if session_enable else 'enable'} {storePlugin.name}" - text_toggle = ulang.get( - "npm.disable" if session_enable else "npm.enable" - ) - can_be_toggle = get_plugin_can_be_toggle(storePlugin.name) - btn_toggle = ( - text_toggle - if not can_be_toggle - else md.btn_cmd(text_toggle, cmd_toggle) - ) - reply += f" {btn_toggle}" - - if permission_s: - plugin_in_database = plugin_db.where_one( - InstalledPlugin(), "module_name = ?", storePlugin.name - ) - # 添加移除插件和全局切换按钮 - global_enable = get_plugin_global_enable(storePlugin.name) - btn_uninstall = ( - ( - md.btn_cmd( - ulang.get("npm.uninstall"), - f"npm uninstall {storePlugin.name}", - ) - ) - if plugin_in_database - else ulang.get("npm.uninstall") - ) - btn_toggle_global_text = ulang.get( - "npm.disable_global" if global_enable else "npm.enable_global" - ) - cmd_toggle_global = f"npm {'disable' if global_enable else 'enable'}-global {storePlugin.name}" - btn_toggle_global = ( - btn_toggle_global_text - if not can_be_toggle - else md.btn_cmd(btn_toggle_global_text, cmd_toggle_global) - ) - - reply += f" {btn_uninstall} {btn_toggle_global}" - reply += "\n\n***\n" - # 根据页数添加翻页按钮。第一页显示上一页文本而不是按钮,最后一页显示下一页文本而不是按钮 - btn_prev = ( - md.btn_cmd( - ulang.get("npm.prev_page"), f"npm list {page - 1} {num_per_page}" - ) - if page > 1 - else ulang.get("npm.prev_page") - ) - btn_next = ( - md.btn_cmd( - ulang.get("npm.next_page"), f"npm list {page + 1} {num_per_page}" - ) - if page < total - else ulang.get("npm.next_page") - ) - reply += f"\n{btn_prev} {page}/{total} {btn_next}" - await md.send_md(reply, bot, event=event) - - else: - if await SUPERUSER(bot, event): - btn_enable_global = md.btn_cmd( - ulang.get("npm.enable_global"), "npm enable-global", False, False - ) - btn_disable_global = md.btn_cmd( - ulang.get("npm.disable_global"), "npm disable-global", False, False - ) - btn_search = md.btn_cmd( - ulang.get("npm.search"), "npm search ", False, False - ) - btn_uninstall_ = md.btn_cmd( - ulang.get("npm.uninstall"), "npm uninstall ", False, False - ) - btn_install_ = md.btn_cmd( - ulang.get("npm.install"), "npm install ", False, False - ) - btn_update = md.btn_cmd( - ulang.get("npm.update_index"), "npm update", False, True - ) - btn_list = md.btn_cmd( - ulang.get("npm.list_plugins"), "npm list ", False, False - ) - btn_disable = md.btn_cmd( - ulang.get("npm.disable_session"), "npm disable ", False, False - ) - btn_enable = md.btn_cmd( - ulang.get("npm.enable_session"), "npm enable ", False, False - ) - reply = ( - f"\n# **{ulang.get('npm.help')}**" - f"\n{btn_update}" - f"\n\n>*{md.escape('npm update')}*\n" - f"\n{btn_install_}" - f"\n\n>*{md.escape('npm install \n" - f"\n{btn_uninstall_}" - f"\n\n>*{md.escape('npm uninstall \n" - f"\n{btn_search}" - f"\n\n>*{md.escape('npm search \n" - f"\n{btn_disable_global}" - f"\n\n>*{md.escape('npm disable-global \n" - f"\n{btn_enable_global}" - f"\n\n>*{md.escape('npm enable-global \n" - f"\n{btn_disable}" - f"\n\n>*{md.escape('npm disable \n" - f"\n{btn_enable}" - f"\n\n>*{md.escape('npm enable \n" - f"\n{btn_list}" - f"\n\n>page为页数,num为每页显示数量" - f"\n\n>*{md.escape('npm list [page] [num]')}*" - ) - await md.send_md(reply, bot, event=event) - else: - - btn_list = md.btn_cmd( - ulang.get("npm.list_plugins"), "npm list ", False, False - ) - btn_disable = md.btn_cmd( - ulang.get("npm.disable_session"), "npm disable ", False, False - ) - btn_enable = md.btn_cmd( - ulang.get("npm.enable_session"), "npm enable ", False, False - ) - reply = ( - f"\n# **{ulang.get('npm.help')}**" - f"\n{btn_disable}" - f"\n\n>*{md.escape('npm disable \n" - f"\n{btn_enable}" - f"\n\n>*{md.escape('npm enable \n" - f"\n{btn_list}" - f"\n\n>page为页数,num为每页显示数量" - f"\n\n>*{md.escape('npm list [page] [num]')}*" - ) - await md.send_md(reply, bot, event=event) - - -@on_alconna( - aliases={"群聊"}, - command=Alconna( - "gm", - Subcommand( - enable, - Args["group_id", str, None], - alias=["e", "启用"], - ), - Subcommand( - disable, - Args["group_id", str, None], - alias=["d", "停用"], - ), - ), - permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN, -).handle() -async def _(bot: T_Bot, event: T_MessageEvent, gm: Matcher, result: Arparma): - ulang = get_user_lang(str(event.user_id)) - to_enable = result.subcommands.get(enable) is not None - - group_id = None - if await SUPERUSER(bot, event): - # 仅超级用户可以自定义群号 - group_id = result.subcommands.get( - enable, result.subcommands.get(disable) - ).args.get("group_id") - if group_id is None and event.message_type == "group": - group_id = str(event.group_id) - - if group_id is None: - await gm.finish(ulang.get("liteyuki.invalid_command"), liteyuki_pass=True) - - enabled = get_group_enable(group_id) - if enabled == to_enable: - await gm.finish( - ulang.get( - "liteyuki.group_already", - STATUS=( - ulang.get("npm.enable") if to_enable else ulang.get("npm.disable") - ), - GROUP=group_id, - ), - liteyuki_pass=True, - ) - else: - set_group_enable(group_id, to_enable) - await gm.finish( - ulang.get( - "liteyuki.group_success", - STATUS=( - ulang.get("npm.enable") if to_enable else ulang.get("npm.disable") - ), - GROUP=group_id, - ), - liteyuki_pass=True, - ) - - -@on_alconna( - aliases={"帮助"}, - command=Alconna( - "help", - Args["plugin_name", str, None], - ), -).handle() -async def _(result: Arparma, matcher: Matcher, event: T_MessageEvent, bot: T_Bot): - ulang = get_user_lang(str(event.user_id)) - plugin_name = result.main_args.get("plugin_name") - if plugin_name: - searched_plugins = search_loaded_plugin(plugin_name) - if searched_plugins: - loaded_plugin = searched_plugins[0] - else: - await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) - - if loaded_plugin: - if loaded_plugin.metadata is None: - loaded_plugin.metadata = PluginMetadata( - name=plugin_name, description="", usage="" - ) - # 从商店获取详细信息 - store_plugin = await get_store_plugin(plugin_name) - if loaded_plugin.metadata.extra.get("liteyuki"): - store_plugin = StorePlugin( - name=loaded_plugin.metadata.name, - desc=loaded_plugin.metadata.description, - author="SnowyKami", - module_name=plugin_name, - homepage="https://github.com/snowykami/LiteyukiBot", - ) - elif store_plugin is None: - store_plugin = StorePlugin( - name=loaded_plugin.metadata.name, - desc=loaded_plugin.metadata.description, - author="", - module_name=plugin_name, - homepage="", - ) - - if store_plugin: - link = store_plugin.homepage - elif loaded_plugin.metadata.extra.get("liteyuki"): - link = "https://github.com/snowykami/LiteyukiBot" - else: - link = None - - reply = [ - mdc.heading(escape_md(store_plugin.name)), - mdc.quote(store_plugin.module_name), - mdc.quote( - mdc.bold(ulang.get("npm.author")) - + " " - + ( - mdc.link( - store_plugin.author, - f"https://github.com/{store_plugin.author}", - ) - if store_plugin.author - else "Unknown" - ) - ), - mdc.quote( - mdc.bold(ulang.get("npm.description")) - + " " - + mdc.paragraph( - max(loaded_plugin.metadata.description, store_plugin.desc) - ) - ), - mdc.heading(ulang.get("npm.usage"), 2), - mdc.paragraph(loaded_plugin.metadata.usage.replace("\n", "\n\n")), - ( - mdc.link(ulang.get("npm.homepage"), link) - if link - else mdc.paragraph(ulang.get("npm.homepage")) - ), - ] - await md.send_md(compile_md(reply), bot, event=event) - else: - await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) - else: - pass - - -# 传入事件阻断hook -@run_preprocessor -async def pre_handle(event: Event, matcher: Matcher): - plugin: Plugin = matcher.plugin - plugin_global_enable = get_plugin_global_enable(plugin.name) - if not plugin_global_enable: - raise IgnoredException("Plugin disabled globally") - if event.get_type() == "message": - plugin_session_enable = get_plugin_session_enable(event, plugin.name) - if not plugin_session_enable: - raise IgnoredException("Plugin disabled in session") - - -# 群聊开关阻断hook -@Bot.on_calling_api -async def block_disable_session(bot: Bot, api: str, args: dict): - if "group_id" in args and not args.get("liteyuki_pass", False): - group_id = args["group_id"] - if not get_group_enable(group_id): - nonebot.logger.debug(f"Group {group_id} disabled") - raise MockApiException(f"Group {group_id} disabled") - - -async def npm_update() -> bool: - """ - 更新本地插件json缓存 - - Returns: - bool: 是否成功更新 - """ - url_list = [ - "https://registry.nonebot.dev/plugins.json", - ] - for url in url_list: - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - if resp.status == 200: - async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f: - data = await resp.read() - await f.write(data) - return True - return False - - -async def npm_search(keywords: list[str]) -> list[StorePlugin]: - """ - 在本地缓存商店数据中搜索插件 - - Args: - keywords (list[str]): 关键词列表 - - Returns: - list[StorePlugin]: 插件列表 - """ - plugin_blacklist = [ - "nonebot_plugin_xiuxian_2", - "nonebot_plugin_htmlrender", - "nonebot_plugin_alconna", - ] - - results = [] - async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f: - plugins: list[StorePlugin] = [ - StorePlugin(**pobj) for pobj in json.loads(await f.read()) - ] - for plugin in plugins: - if plugin.module_name in plugin_blacklist: - continue - plugin_text = " ".join( - [ - plugin.name, - plugin.desc, - plugin.author, - plugin.module_name, - " ".join([tag.label for tag in plugin.tags]), - ] - ) - if all([keyword in plugin_text for keyword in keywords]): - results.append(plugin) - return results - - -@run_sync -def npm_install(plugin_package_name) -> tuple[bool, str]: - """ - 异步安装插件,使用pip安装 - Args: - plugin_package_name: - - Returns: - tuple[bool, str]: 是否成功,输出信息 - - """ - # 重定向标准输出 - buffer = StringIO() - sys.stdout = buffer - sys.stderr = buffer - - update = False - if get_plugin_exist(plugin_package_name): - update = True - - mirrors = [ - "https://pypi.tuna.tsinghua.edu.cn/simple", # 清华大学 - "https://pypi.org/simple", # 官方源 - ] - - # 使用pip安装包,对每个镜像尝试一次,成功后返回值 - success = False - for mirror in mirrors: - try: - nonebot.logger.info(f"pip install try mirror: {mirror}") - if update: - result = pip.main( - ["install", "--upgrade", plugin_package_name, "-i", mirror] - ) - else: - result = pip.main(["install", plugin_package_name, "-i", mirror]) - success = result == 0 - if success: - break - else: - nonebot.logger.warning(f"pip install failed, try next mirror.") - except Exception as e: - success = False - continue - - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - - return success, buffer.getvalue() - - -def search_loaded_plugin(keyword: str) -> list[Plugin]: - """ - 搜索已加载插件 - - Args: - keyword (str): 关键词 - - Returns: - list[Plugin]: 插件列表 - """ - if nonebot.get_plugin(keyword) is not None: - return [nonebot.get_plugin(keyword)] - else: - results = [] - for plugin in nonebot.get_loaded_plugins(): - if plugin.metadata is None: - plugin.metadata = PluginMetadata( - name=plugin.name, description="", usage="" - ) - if ( - keyword - in plugin.name + plugin.metadata.name + plugin.metadata.description - ): - results.append(plugin) - return results +import os +import sys +import aiohttp +import nonebot.plugin +import pip +from io import StringIO +from arclet.alconna import MultiVar +from nonebot import Bot, require +from nonebot.exception import FinishedException, IgnoredException, MockApiException +from nonebot.internal.adapter import Event +from nonebot.internal.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.permission import SUPERUSER +from nonebot.plugin import Plugin, PluginMetadata +from nonebot.utils import run_sync + +from src.utils.base.data_manager import InstalledPlugin +from src.utils.base.language import get_user_lang +from src.utils.base.ly_typing import T_Bot +from src.utils.message.message import MarkdownMessage as md +from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md +from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER +from src.utils.message.tools import clamp +from .common import * + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import ( + on_alconna, + Alconna, + Args, + Arparma, + Subcommand, + Option, + OptionResult, + SubcommandResult, +) + +# const +enable_global = "enable-global" +disable_global = "disable-global" +enable = "enable" +disable = "disable" + + +@on_alconna( + aliases={"插件"}, + command=Alconna( + "npm", + Subcommand( + "enable", + Args["plugin_name", str], + Option( + "-g|--group", + Args["group_id", str, None], + help_text="群号", + ), + alias=["e", "启用"], + ), + Subcommand( + "disable", + Args["plugin_name", str], + Option( + "-g|--group", + Args["group_id", str, None], + help_text="群号", + ), + alias=["d", "停用"], + ), + Subcommand( + enable_global, + Args["plugin_name", str], + alias=["eg", "全局启用"], + ), + Subcommand( + disable_global, + Args["plugin_name", str], + alias=["dg", "全局停用"], + ), + # 安装部分 + Subcommand( + "update", + alias=["u", "更新"], + ), + Subcommand( + "search", + Args["keywords", MultiVar(str)], + alias=["s", "搜索"], + ), + Subcommand( + "install", + Args["plugin_name", str], + alias=["i", "安装"], + ), + Subcommand( + "uninstall", + Args["plugin_name", str], + alias=["r", "rm", "卸载"], + ), + Subcommand( + "list", + Args["page", int, 1]["num", int, 10], + alias=["ls", "列表"], + ), + ), +).handle() +async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher): + if not os.path.exists("data/liteyuki/plugins.json"): + await npm_update() + # 判断会话类型 + ulang = get_user_lang(str(event.user_id)) + plugin_name = result.args.get("plugin_name") + sc = result.subcommands # 获取子命令 + perm_s = await SUPERUSER(bot, event) # 判断是否为超级用户 + # 支持对自定义command_start的判断 + if sc.get("enable") or sc.get("disable"): + + toggle = result.subcommands.get("enable") is not None + + plugin_exist = get_plugin_exist(plugin_name) + + # 判定会话类型 + # 输入群号 + if ( + group_id := ( + sc.get("enable", SubcommandResult()) + .options.get("group", OptionResult()) + .args.get("group_id") + or sc.get("disable", SubcommandResult()) + .options.get("group", OptionResult()) + .args.get("group_id") + ) + ) and await SUPERUSER(bot, event): + session_id = group_id + new_event = event.copy() + new_event.group_id = group_id + new_event.message_type = "group" + + elif event.message_type == "private": + session_id = event.user_id + new_event = event + else: + if ( + await GROUP_ADMIN(bot, event) + or await GROUP_OWNER(bot, event) + or await SUPERUSER(bot, event) + ): + session_id = event.group_id + new_event = event + else: + raise FinishedException(ulang.get("Permission Denied")) + + session_enable = get_plugin_session_enable( + new_event, plugin_name + ) # 获取插件当前状态 + + can_be_toggled = get_plugin_can_be_toggle( + plugin_name + ) # 获取插件是否可以被启用/停用 + + if not plugin_exist: + await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + + if not can_be_toggled: + await npm.finish( + ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name) + ) + + if session_enable == toggle: + await npm.finish( + ulang.get( + "npm.plugin_already", + NAME=plugin_name, + STATUS=( + ulang.get("npm.enable") if toggle else ulang.get("npm.disable") + ), + ) + ) + + # 键入自定义群号的情况 + + try: + set_plugin_session_enable(new_event, plugin_name, toggle) + except Exception as e: + nonebot.logger.error(e) + await npm.finish( + ulang.get( + "npm.toggle_failed", + NAME=plugin_name, + STATUS=( + ulang.get("npm.enable") if toggle else ulang.get("npm.disable") + ), + ERROR=str(e), + ) + ) + + await npm.finish( + ulang.get( + "npm.toggle_success", + NAME=plugin_name, + STATUS=( + ulang.get("npm.enable") if toggle else ulang.get("npm.disable") + ), + ) # + str(session_id) 这里应该不需增加一个id,在任何语言文件里,这句话都不是这样翻的,你是不是调试的时候忘删了? + ) + + elif ( + sc.get(enable_global) + or result.subcommands.get(disable_global) + and await SUPERUSER(bot, event) + ): + plugin_exist = get_plugin_exist(plugin_name) + + toggle = result.subcommands.get(enable_global) is not None + + can_be_toggled = get_plugin_can_be_toggle(plugin_name) + + if not plugin_exist: + await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + + if not can_be_toggled: + await npm.finish( + ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name) + ) + + global_enable = get_plugin_global_enable(plugin_name) + if global_enable == toggle: + await npm.finish( + ulang.get( + "npm.plugin_already", + NAME=plugin_name, + STATUS=( + ulang.get("npm.enable") if toggle else ulang.get("npm.disable") + ), + ) + ) + + try: + set_plugin_global_enable(plugin_name, toggle) + except Exception as e: + await npm.finish( + ulang.get( + "npm.toggle_failed", + NAME=plugin_name, + STATUS=( + ulang.get("npm.enable") if toggle else ulang.get("npm.disable") + ), + ERROR=str(e), + ) + ) + + await npm.finish( + ulang.get( + "npm.toggle_success", + NAME=plugin_name, + STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"), + ) + ) + + elif sc.get("update") and perm_s: + r = await npm_update() + if r: + await npm.finish(ulang.get("npm.store_update_success")) + else: + await npm.finish(ulang.get("npm.store_update_failed")) + + elif sc.get("search"): + keywords: list[str] = result.subcommands["search"].args.get("keywords") + rs = await npm_search(keywords) + max_show = 10 + if len(rs): + reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***" + for storePlugin in rs[: min(max_show, len(rs))]: + btn_install_or_update = md.btn_cmd( + ( + ulang.get("npm.update") + if get_plugin_exist(storePlugin.module_name) + else ulang.get("npm.install") + ), + "npm install %s" % storePlugin.module_name, + ) + link_page = md.btn_link(ulang.get("npm.homepage"), storePlugin.homepage) + link_pypi = md.btn_link(ulang.get("npm.pypi"), storePlugin.homepage) + + reply += ( + f"\n# **{storePlugin.name}**\n" + f"\n> **{storePlugin.desc}**\n" + f"\n> {ulang.get('npm.author')}: {storePlugin.author}" + f"\n> *{md.escape(storePlugin.module_name)}*" + f"\n> {btn_install_or_update} {link_page} {link_pypi}\n\n***\n" + ) + if len(rs) > max_show: + reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}" + else: + reply = ulang.get("npm.search_no_result") + await md.send_md(reply, bot, event=event) + + elif sc.get("install") and perm_s: + plugin_name: str = result.subcommands["install"].args.get("plugin_name") + store_plugin = await get_store_plugin(plugin_name) + await npm.send(ulang.get("npm.installing", NAME=plugin_name)) + + r, log = await npm_install(plugin_name) + log = log.replace("\\", "/") + + if not store_plugin: + await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + + homepage_btn = md.btn_cmd(ulang.get("npm.homepage"), store_plugin.homepage) + if r: + r_load = nonebot.load_plugin(plugin_name) # 加载插件 + installed_plugin = InstalledPlugin( + module_name=plugin_name + ) # 构造插件信息模型 + found_in_db_plugin = plugin_db.where_one( + InstalledPlugin(), "module_name = ?", plugin_name + ) # 查询数据库中是否已经安装 + if r_load: + if found_in_db_plugin is None: + plugin_db.save(installed_plugin) + info = md.escape( + ulang.get("npm.install_success", NAME=store_plugin.name) + ) # markdown转义 + await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event) + else: + await npm.finish( + ulang.get( + "npm.plugin_already_installed", NAME=store_plugin.name + ) + ) + else: + info = ulang.get( + "npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn + ).replace("_", r"\\_") + await md.send_md(f"{info}\n\n" f"```\n{log}\n```\n", bot, event=event) + else: + info = ulang.get( + "npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn + ).replace("_", r"\\_") + await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event) + + elif sc.get("uninstall") and perm_s: + plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore + found_installed_plugin: InstalledPlugin = plugin_db.where_one( + InstalledPlugin(), "module_name = ?", plugin_name + ) + if found_installed_plugin: + plugin_db.delete(InstalledPlugin(), "module_name = ?", plugin_name) + reply = f"{ulang.get('npm.uninstall_success', NAME=found_installed_plugin.module_name)}" + await npm.finish(reply) + else: + await npm.finish(ulang.get("npm.plugin_not_installed", NAME=plugin_name)) + + elif sc.get("list"): + loaded_plugin_list = sorted(nonebot.get_loaded_plugins(), key=lambda x: x.name) + num_per_page = result.subcommands.get("list").args.get("num") + total = len(loaded_plugin_list) // num_per_page + ( + 1 if len(loaded_plugin_list) % num_per_page else 0 + ) + + page = clamp(result.subcommands.get("list").args.get("page"), 1, total) + + # 已加载插件 | 总计10 | 第1/3页 + reply = ( + f"# {ulang.get('npm.loaded_plugins')} | " + f"{ulang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} | " + f"{ulang.get('npm.page', PAGE=page, TOTAL=total)} \n***\n" + ) + + permission_oas = ( + await GROUP_ADMIN(bot, event) + or await GROUP_OWNER(bot, event) + or await SUPERUSER(bot, event) + ) + permission_s = await SUPERUSER(bot, event) + + for storePlugin in loaded_plugin_list[ + (page - 1) + * num_per_page : min(page * num_per_page, len(loaded_plugin_list)) + ]: + # 检查是否有 metadata 属性 + # 添加帮助按钮 + + btn_usage = md.btn_cmd( + ulang.get("npm.usage"), f"help {storePlugin.name}", False + ) + store_plugin = await get_store_plugin(storePlugin.name) + session_enable = get_plugin_session_enable(event, storePlugin.name) + if store_plugin: + # btn_homepage = md.btn_link(ulang.get("npm.homepage"), store_plugin.homepage) + show_name = store_plugin.name + elif storePlugin.metadata: + # if storePlugin.metadata.extra.get("liteyuki"): + # btn_homepage = md.btn_link(ulang.get("npm.homepage"), "https://github.com/snowykami/LiteyukiBot") + # else: + # btn_homepage = ulang.get("npm.homepage") + show_name = storePlugin.metadata.name + else: + # btn_homepage = ulang.get("npm.homepage") + show_name = storePlugin.name + ulang.get("npm.no_description") + + if storePlugin.metadata: + reply += f"\n**{md.escape(show_name)}**\n" + else: + reply += f"**{md.escape(show_name)}**\n" + + reply += f"\n > {btn_usage}" + + if permission_oas: + # 添加启用/停用插件按钮 + cmd_toggle = f"npm {'disable' if session_enable else 'enable'} {storePlugin.name}" + text_toggle = ulang.get( + "npm.disable" if session_enable else "npm.enable" + ) + can_be_toggle = get_plugin_can_be_toggle(storePlugin.name) + btn_toggle = ( + text_toggle + if not can_be_toggle + else md.btn_cmd(text_toggle, cmd_toggle) + ) + reply += f" {btn_toggle}" + + if permission_s: + plugin_in_database = plugin_db.where_one( + InstalledPlugin(), "module_name = ?", storePlugin.name + ) + # 添加移除插件和全局切换按钮 + global_enable = get_plugin_global_enable(storePlugin.name) + btn_uninstall = ( + ( + md.btn_cmd( + ulang.get("npm.uninstall"), + f"npm uninstall {storePlugin.name}", + ) + ) + if plugin_in_database + else ulang.get("npm.uninstall") + ) + btn_toggle_global_text = ulang.get( + "npm.disable_global" if global_enable else "npm.enable_global" + ) + cmd_toggle_global = f"npm {'disable' if global_enable else 'enable'}-global {storePlugin.name}" + btn_toggle_global = ( + btn_toggle_global_text + if not can_be_toggle + else md.btn_cmd(btn_toggle_global_text, cmd_toggle_global) + ) + + reply += f" {btn_uninstall} {btn_toggle_global}" + reply += "\n\n***\n" + # 根据页数添加翻页按钮。第一页显示上一页文本而不是按钮,最后一页显示下一页文本而不是按钮 + btn_prev = ( + md.btn_cmd( + ulang.get("npm.prev_page"), f"npm list {page - 1} {num_per_page}" + ) + if page > 1 + else ulang.get("npm.prev_page") + ) + btn_next = ( + md.btn_cmd( + ulang.get("npm.next_page"), f"npm list {page + 1} {num_per_page}" + ) + if page < total + else ulang.get("npm.next_page") + ) + reply += f"\n{btn_prev} {page}/{total} {btn_next}" + await md.send_md(reply, bot, event=event) + + else: + if await SUPERUSER(bot, event): + btn_enable_global = md.btn_cmd( + ulang.get("npm.enable_global"), "npm enable-global", False, False + ) + btn_disable_global = md.btn_cmd( + ulang.get("npm.disable_global"), "npm disable-global", False, False + ) + btn_search = md.btn_cmd( + ulang.get("npm.search"), "npm search ", False, False + ) + btn_uninstall_ = md.btn_cmd( + ulang.get("npm.uninstall"), "npm uninstall ", False, False + ) + btn_install_ = md.btn_cmd( + ulang.get("npm.install"), "npm install ", False, False + ) + btn_update = md.btn_cmd( + ulang.get("npm.update_index"), "npm update", False, True + ) + btn_list = md.btn_cmd( + ulang.get("npm.list_plugins"), "npm list ", False, False + ) + btn_disable = md.btn_cmd( + ulang.get("npm.disable_session"), "npm disable ", False, False + ) + btn_enable = md.btn_cmd( + ulang.get("npm.enable_session"), "npm enable ", False, False + ) + reply = ( + f"\n# **{ulang.get('npm.help')}**" + f"\n{btn_update}" + f"\n\n>*{md.escape('npm update')}*\n" + f"\n{btn_install_}" + f"\n\n>*{md.escape('npm install \n" + f"\n{btn_uninstall_}" + f"\n\n>*{md.escape('npm uninstall \n" + f"\n{btn_search}" + f"\n\n>*{md.escape('npm search \n" + f"\n{btn_disable_global}" + f"\n\n>*{md.escape('npm disable-global \n" + f"\n{btn_enable_global}" + f"\n\n>*{md.escape('npm enable-global \n" + f"\n{btn_disable}" + f"\n\n>*{md.escape('npm disable \n" + f"\n{btn_enable}" + f"\n\n>*{md.escape('npm enable \n" + f"\n{btn_list}" + f"\n\n>page为页数,num为每页显示数量" + f"\n\n>*{md.escape('npm list [page] [num]')}*" + ) + await md.send_md(reply, bot, event=event) + else: + + btn_list = md.btn_cmd( + ulang.get("npm.list_plugins"), "npm list ", False, False + ) + btn_disable = md.btn_cmd( + ulang.get("npm.disable_session"), "npm disable ", False, False + ) + btn_enable = md.btn_cmd( + ulang.get("npm.enable_session"), "npm enable ", False, False + ) + reply = ( + f"\n# **{ulang.get('npm.help')}**" + f"\n{btn_disable}" + f"\n\n>*{md.escape('npm disable \n" + f"\n{btn_enable}" + f"\n\n>*{md.escape('npm enable \n" + f"\n{btn_list}" + f"\n\n>page为页数,num为每页显示数量" + f"\n\n>*{md.escape('npm list [page] [num]')}*" + ) + await md.send_md(reply, bot, event=event) + + +@on_alconna( + aliases={"群聊"}, + command=Alconna( + "gm", + Subcommand( + enable, + Args["group_id", str, None], + alias=["e", "启用"], + ), + Subcommand( + disable, + Args["group_id", str, None], + alias=["d", "停用"], + ), + ), + permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN, +).handle() +async def _(bot: T_Bot, event: T_MessageEvent, gm: Matcher, result: Arparma): + ulang = get_user_lang(str(event.user_id)) + to_enable = result.subcommands.get(enable) is not None + + group_id = None + if await SUPERUSER(bot, event): + # 仅超级用户可以自定义群号 + group_id = result.subcommands.get( + enable, result.subcommands.get(disable) + ).args.get("group_id") + if group_id is None and event.message_type == "group": + group_id = str(event.group_id) + + if group_id is None: + await gm.finish(ulang.get("liteyuki.invalid_command"), liteyuki_pass=True) + + enabled = get_group_enable(group_id) + if enabled == to_enable: + await gm.finish( + ulang.get( + "liteyuki.group_already", + STATUS=( + ulang.get("npm.enable") if to_enable else ulang.get("npm.disable") + ), + GROUP=group_id, + ), + liteyuki_pass=True, + ) + else: + set_group_enable(group_id, to_enable) + await gm.finish( + ulang.get( + "liteyuki.group_success", + STATUS=( + ulang.get("npm.enable") if to_enable else ulang.get("npm.disable") + ), + GROUP=group_id, + ), + liteyuki_pass=True, + ) + + +@on_alconna( + aliases={"帮助"}, + command=Alconna( + "help", + Args["plugin_name", str, None], + ), +).handle() +async def _(result: Arparma, matcher: Matcher, event: T_MessageEvent, bot: T_Bot): + ulang = get_user_lang(str(event.user_id)) + plugin_name = result.main_args.get("plugin_name") + if plugin_name: + searched_plugins = search_loaded_plugin(plugin_name) + if searched_plugins: + loaded_plugin = searched_plugins[0] + else: + await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + + if loaded_plugin: + if loaded_plugin.metadata is None: + loaded_plugin.metadata = PluginMetadata( + name=plugin_name, description="", usage="" + ) + # 从商店获取详细信息 + store_plugin = await get_store_plugin(plugin_name) + if loaded_plugin.metadata.extra.get("liteyuki"): + store_plugin = StorePlugin( + name=loaded_plugin.metadata.name, + desc=loaded_plugin.metadata.description, + author="SnowyKami", + module_name=plugin_name, + homepage="https://github.com/snowykami/LiteyukiBot", + ) + elif store_plugin is None: + store_plugin = StorePlugin( + name=loaded_plugin.metadata.name, + desc=loaded_plugin.metadata.description, + author="", + module_name=plugin_name, + homepage="", + ) + + if store_plugin: + link = store_plugin.homepage + elif loaded_plugin.metadata.extra.get("liteyuki"): + link = "https://github.com/snowykami/LiteyukiBot" + else: + link = None + + reply = [ + mdc.heading(escape_md(store_plugin.name)), + mdc.quote(store_plugin.module_name), + mdc.quote( + mdc.bold(ulang.get("npm.author")) + + " " + + ( + mdc.link( + store_plugin.author, + f"https://github.com/{store_plugin.author}", + ) + if store_plugin.author + else "Unknown" + ) + ), + mdc.quote( + mdc.bold(ulang.get("npm.description")) + + " " + + mdc.paragraph( + max(loaded_plugin.metadata.description, store_plugin.desc) + ) + ), + mdc.heading(ulang.get("npm.usage"), 2), + mdc.paragraph(loaded_plugin.metadata.usage.replace("\n", "\n\n")), + ( + mdc.link(ulang.get("npm.homepage"), link) + if link + else mdc.paragraph(ulang.get("npm.homepage")) + ), + ] + await md.send_md(compile_md(reply), bot, event=event) + else: + await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name)) + else: + pass + + +# 传入事件阻断hook +@run_preprocessor +async def pre_handle(event: Event, matcher: Matcher): + plugin: Plugin = matcher.plugin + plugin_global_enable = get_plugin_global_enable(plugin.name) + if not plugin_global_enable: + raise IgnoredException("Plugin disabled globally") + if event.get_type() == "message": + plugin_session_enable = get_plugin_session_enable(event, plugin.name) + if not plugin_session_enable: + raise IgnoredException("Plugin disabled in session") + + +# 群聊开关阻断hook +@Bot.on_calling_api +async def block_disable_session(bot: Bot, api: str, args: dict): + if "group_id" in args and not args.get("liteyuki_pass", False): + group_id = args["group_id"] + if not get_group_enable(group_id): + nonebot.logger.debug(f"Group {group_id} disabled") + raise MockApiException(f"Group {group_id} disabled") + + +async def npm_update() -> bool: + """ + 更新本地插件json缓存 + + Returns: + bool: 是否成功更新 + """ + url_list = [ + "https://registry.nonebot.dev/plugins.json", + ] + for url in url_list: + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status == 200: + async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f: + data = await resp.read() + await f.write(data) + return True + return False + + +async def npm_search(keywords: list[str]) -> list[StorePlugin]: + """ + 在本地缓存商店数据中搜索插件 + + Args: + keywords (list[str]): 关键词列表 + + Returns: + list[StorePlugin]: 插件列表 + """ + plugin_blacklist = [ + "nonebot_plugin_xiuxian_2", + "nonebot_plugin_htmlrender", + "nonebot_plugin_alconna", + ] + + results = [] + async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f: + plugins: list[StorePlugin] = [ + StorePlugin(**pobj) for pobj in json.loads(await f.read()) + ] + for plugin in plugins: + if plugin.module_name in plugin_blacklist: + continue + plugin_text = " ".join( + [ + plugin.name, + plugin.desc, + plugin.author, + plugin.module_name, + " ".join([tag.label for tag in plugin.tags]), + ] + ) + if all([keyword in plugin_text for keyword in keywords]): + results.append(plugin) + return results + + +@run_sync +def npm_install(plugin_package_name) -> tuple[bool, str]: + """ + 异步安装插件,使用pip安装 + Args: + plugin_package_name: + + Returns: + tuple[bool, str]: 是否成功,输出信息 + + """ + # 重定向标准输出 + buffer = StringIO() + sys.stdout = buffer + sys.stderr = buffer + + update = False + if get_plugin_exist(plugin_package_name): + update = True + + mirrors = [ + "https://pypi.tuna.tsinghua.edu.cn/simple", # 清华大学 + "https://pypi.org/simple", # 官方源 + ] + + # 使用pip安装包,对每个镜像尝试一次,成功后返回值 + success = False + for mirror in mirrors: + try: + nonebot.logger.info(f"pip install try mirror: {mirror}") + if update: + result = pip.main( + ["install", "--upgrade", plugin_package_name, "-i", mirror] + ) + else: + result = pip.main(["install", plugin_package_name, "-i", mirror]) + success = result == 0 + if success: + break + else: + nonebot.logger.warning(f"pip install failed, try next mirror.") + except Exception as e: + success = False + continue + + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + + return success, buffer.getvalue() + + +def search_loaded_plugin(keyword: str) -> list[Plugin]: + """ + 搜索已加载插件 + + Args: + keyword (str): 关键词 + + Returns: + list[Plugin]: 插件列表 + """ + if nonebot.get_plugin(keyword) is not None: + return [nonebot.get_plugin(keyword)] + else: + results = [] + for plugin in nonebot.get_loaded_plugins(): + if plugin.metadata is None: + plugin.metadata = PluginMetadata( + name=plugin.name, description="", usage="" + ) + if ( + keyword + in plugin.name + plugin.metadata.name + plugin.metadata.description + ): + results.append(plugin) + return results diff --git a/src/plugins/liteyuki_pacman/rpm.py b/src/nonebot_plugins/liteyuki_pacman/rpm.py similarity index 97% rename from src/plugins/liteyuki_pacman/rpm.py rename to src/nonebot_plugins/liteyuki_pacman/rpm.py index ab0f7753..35a47747 100644 --- a/src/plugins/liteyuki_pacman/rpm.py +++ b/src/nonebot_plugins/liteyuki_pacman/rpm.py @@ -1,186 +1,186 @@ -# 轻雪资源包管理器 -import os -import zipfile -import yaml -from nonebot import require -from nonebot.internal.matcher import Matcher -from nonebot.permission import SUPERUSER - -from src.utils.base.language import get_user_lang -from src.utils.base.ly_typing import T_Bot, T_MessageEvent -from src.utils.message.message import MarkdownMessage as md -from src.utils.base.resource import (ResourceMetadata, add_resource_pack, change_priority, check_exist, check_status, get_loaded_resource_packs, get_resource_metadata, load_resources, remove_resource_pack) - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import Alconna, Args, on_alconna, Arparma, Subcommand - - -@on_alconna( - aliases={"资源包"}, - command=Alconna( - "rpm", - Subcommand( - "list", - Args["page", int, 1]["num", int, 10], - alias=["ls", "列表", "列出"], - ), - Subcommand( - "load", - Args["name", str], - alias=["安装"], - ), - Subcommand( - "unload", - Args["name", str], - alias=["卸载"], - ), - Subcommand( - "up", - Args["name", str], - alias=["上移"], - ), - Subcommand( - "down", - Args["name", str], - alias=["下移"], - ), - Subcommand( - "top", - Args["name", str], - alias=["置顶"], - ), - Subcommand( - "reload", - alias=["重载"], - ), - ), - permission=SUPERUSER -).handle() -async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher): - ulang = get_user_lang(str(event.user_id)) - reply = "" - send_as_md = False - if result.subcommands.get("list"): - send_as_md = True - loaded_rps = get_loaded_resource_packs() - reply += f"{ulang.get('liteyuki.loaded_resources', NUM=len(loaded_rps))}\n" - for rp in loaded_rps: - btn_unload = md.btn_cmd( - ulang.get("npm.uninstall"), - f"rpm unload {rp.folder}" - ) - btn_move_up = md.btn_cmd( - ulang.get("rpm.move_up"), - f"rpm up {rp.folder}" - ) - btn_move_down = md.btn_cmd( - ulang.get("rpm.move_down"), - f"rpm down {rp.folder}" - ) - btn_move_top = md.btn_cmd( - ulang.get("rpm.move_top"), - f"rpm top {rp.folder}" - ) - # 添加新行 - reply += (f"\n**{md.escape(rp.name)}**({md.escape(rp.folder)})\n\n" - f"> {btn_move_up} {btn_move_down} {btn_move_top} {btn_unload}\n\n***") - reply += f"\n\n{ulang.get('liteyuki.unloaded_resources')}\n" - loaded_folders = [rp.folder for rp in get_loaded_resource_packs()] - # 遍历resources文件夹,获取未加载的资源包 - for folder in os.listdir("resources"): - if folder not in loaded_folders: - if os.path.exists(os.path.join("resources", folder, "metadata.yml")): - metadata = ResourceMetadata( - **yaml.load( - open( - os.path.join("resources", folder, "metadata.yml"), - encoding="utf-8" - ), - Loader=yaml.FullLoader - ) - ) - metadata.folder = folder - metadata.path = os.path.join("resources", folder) - btn_load = md.btn_cmd( - ulang.get("npm.install"), - f"rpm load {metadata.folder}" - ) - # 添加新行 - reply += (f"\n**{md.escape(metadata.name)}**({md.escape(metadata.folder)})\n\n" - f"> {btn_load}\n\n***") - elif os.path.isfile(os.path.join("resources", folder)) and folder.endswith(".zip"): - # zip文件 - # 临时解压并读取metadata.yml - with zipfile.ZipFile(os.path.join("resources", folder), "r") as zip_ref: - with zip_ref.open("metadata.yml") as f: - metadata = ResourceMetadata( - **yaml.load(f, Loader=yaml.FullLoader) - ) - btn_load = md.btn_cmd( - ulang.get("npm.install"), - f"rpm load {folder}" - ) - # 添加新行 - reply += (f"\n**{md.escape(metadata.name)}**({md.escape(folder)})\n\n" - f"> {btn_load}\n\n***") - elif result.subcommands.get("load") or result.subcommands.get("unload"): - load = result.subcommands.get("load") is not None - rp_name = result.args.get("name") - r = False # 操作结果 - if check_exist(rp_name): - if load != check_status(rp_name): - # 状态不同 - if load: - r = add_resource_pack(rp_name) - else: - r = remove_resource_pack(rp_name) - rp_meta = get_resource_metadata(rp_name) - reply += ulang.get( - f"liteyuki.{'load' if load else 'unload'}_resource_{'success' if r else 'failed'}", - NAME=rp_meta.name - ) - else: - # 重复操作 - reply += ulang.get(f"liteyuki.resource_already_{'load' if load else 'unload'}ed", NAME=rp_name) - else: - reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name) - if r: - btn_reload = md.btn_cmd( - ulang.get("liteyuki.reload_resources"), - f"rpm reload" - ) - reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload) - elif result.subcommands.get("up") or result.subcommands.get("down") or result.subcommands.get("top"): - rp_name = result.args.get("name") - if result.subcommands.get("up"): - delta = -1 - elif result.subcommands.get("down"): - delta = 1 - else: - delta = 0 - if check_exist(rp_name): - if check_status(rp_name): - r = change_priority(rp_name, delta) - reply += ulang.get(f"liteyuki.change_priority_{'success' if r else 'failed'}", NAME=rp_name) - if r: - btn_reload = md.btn_cmd( - ulang.get("liteyuki.reload_resources"), - f"rpm reload" - ) - reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload) - else: - reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name) - else: - reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name) - elif result.subcommands.get("reload"): - load_resources() - reply = ulang.get( - "liteyuki.reload_resources_success", - NUM=len(get_loaded_resource_packs()) - ) - else: - pass - if send_as_md: - await md.send_md(reply, bot, event=event) - else: - await matcher.finish(reply) +# 轻雪资源包管理器 +import os +import zipfile +import yaml +from nonebot import require +from nonebot.internal.matcher import Matcher +from nonebot.permission import SUPERUSER + +from src.utils.base.language import get_user_lang +from src.utils.base.ly_typing import T_Bot, T_MessageEvent +from src.utils.message.message import MarkdownMessage as md +from src.utils.base.resource import (ResourceMetadata, add_resource_pack, change_priority, check_exist, check_status, get_loaded_resource_packs, get_resource_metadata, load_resources, remove_resource_pack) + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import Alconna, Args, on_alconna, Arparma, Subcommand + + +@on_alconna( + aliases={"资源包"}, + command=Alconna( + "rpm", + Subcommand( + "list", + Args["page", int, 1]["num", int, 10], + alias=["ls", "列表", "列出"], + ), + Subcommand( + "load", + Args["name", str], + alias=["安装"], + ), + Subcommand( + "unload", + Args["name", str], + alias=["卸载"], + ), + Subcommand( + "up", + Args["name", str], + alias=["上移"], + ), + Subcommand( + "down", + Args["name", str], + alias=["下移"], + ), + Subcommand( + "top", + Args["name", str], + alias=["置顶"], + ), + Subcommand( + "reload", + alias=["重载"], + ), + ), + permission=SUPERUSER +).handle() +async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher): + ulang = get_user_lang(str(event.user_id)) + reply = "" + send_as_md = False + if result.subcommands.get("list"): + send_as_md = True + loaded_rps = get_loaded_resource_packs() + reply += f"{ulang.get('liteyuki.loaded_resources', NUM=len(loaded_rps))}\n" + for rp in loaded_rps: + btn_unload = md.btn_cmd( + ulang.get("npm.uninstall"), + f"rpm unload {rp.folder}" + ) + btn_move_up = md.btn_cmd( + ulang.get("rpm.move_up"), + f"rpm up {rp.folder}" + ) + btn_move_down = md.btn_cmd( + ulang.get("rpm.move_down"), + f"rpm down {rp.folder}" + ) + btn_move_top = md.btn_cmd( + ulang.get("rpm.move_top"), + f"rpm top {rp.folder}" + ) + # 添加新行 + reply += (f"\n**{md.escape(rp.name)}**({md.escape(rp.folder)})\n\n" + f"> {btn_move_up} {btn_move_down} {btn_move_top} {btn_unload}\n\n***") + reply += f"\n\n{ulang.get('liteyuki.unloaded_resources')}\n" + loaded_folders = [rp.folder for rp in get_loaded_resource_packs()] + # 遍历resources文件夹,获取未加载的资源包 + for folder in os.listdir("resources"): + if folder not in loaded_folders: + if os.path.exists(os.path.join("resources", folder, "metadata.yml")): + metadata = ResourceMetadata( + **yaml.load( + open( + os.path.join("resources", folder, "metadata.yml"), + encoding="utf-8" + ), + Loader=yaml.FullLoader + ) + ) + metadata.folder = folder + metadata.path = os.path.join("resources", folder) + btn_load = md.btn_cmd( + ulang.get("npm.install"), + f"rpm load {metadata.folder}" + ) + # 添加新行 + reply += (f"\n**{md.escape(metadata.name)}**({md.escape(metadata.folder)})\n\n" + f"> {btn_load}\n\n***") + elif os.path.isfile(os.path.join("resources", folder)) and folder.endswith(".zip"): + # zip文件 + # 临时解压并读取metadata.yml + with zipfile.ZipFile(os.path.join("resources", folder), "r") as zip_ref: + with zip_ref.open("metadata.yml") as f: + metadata = ResourceMetadata( + **yaml.load(f, Loader=yaml.FullLoader) + ) + btn_load = md.btn_cmd( + ulang.get("npm.install"), + f"rpm load {folder}" + ) + # 添加新行 + reply += (f"\n**{md.escape(metadata.name)}**({md.escape(folder)})\n\n" + f"> {btn_load}\n\n***") + elif result.subcommands.get("load") or result.subcommands.get("unload"): + load = result.subcommands.get("load") is not None + rp_name = result.args.get("name") + r = False # 操作结果 + if check_exist(rp_name): + if load != check_status(rp_name): + # 状态不同 + if load: + r = add_resource_pack(rp_name) + else: + r = remove_resource_pack(rp_name) + rp_meta = get_resource_metadata(rp_name) + reply += ulang.get( + f"liteyuki.{'load' if load else 'unload'}_resource_{'success' if r else 'failed'}", + NAME=rp_meta.name + ) + else: + # 重复操作 + reply += ulang.get(f"liteyuki.resource_already_{'load' if load else 'unload'}ed", NAME=rp_name) + else: + reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name) + if r: + btn_reload = md.btn_cmd( + ulang.get("liteyuki.reload_resources"), + f"rpm reload" + ) + reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload) + elif result.subcommands.get("up") or result.subcommands.get("down") or result.subcommands.get("top"): + rp_name = result.args.get("name") + if result.subcommands.get("up"): + delta = -1 + elif result.subcommands.get("down"): + delta = 1 + else: + delta = 0 + if check_exist(rp_name): + if check_status(rp_name): + r = change_priority(rp_name, delta) + reply += ulang.get(f"liteyuki.change_priority_{'success' if r else 'failed'}", NAME=rp_name) + if r: + btn_reload = md.btn_cmd( + ulang.get("liteyuki.reload_resources"), + f"rpm reload" + ) + reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload) + else: + reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name) + else: + reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name) + elif result.subcommands.get("reload"): + load_resources() + reply = ulang.get( + "liteyuki.reload_resources_success", + NUM=len(get_loaded_resource_packs()) + ) + else: + pass + if send_as_md: + await md.send_md(reply, bot, event=event) + else: + await matcher.finish(reply) diff --git a/src/plugins/liteyuki_satori_user_info/__init__.py b/src/nonebot_plugins/liteyuki_satori_user_info/__init__.py similarity index 96% rename from src/plugins/liteyuki_satori_user_info/__init__.py rename to src/nonebot_plugins/liteyuki_satori_user_info/__init__.py index 5f27c40a..5960e12d 100644 --- a/src/plugins/liteyuki_satori_user_info/__init__.py +++ b/src/nonebot_plugins/liteyuki_satori_user_info/__init__.py @@ -1,16 +1,16 @@ -from nonebot.plugin import PluginMetadata -from .auto_update import * - -__author__ = "expliyh" -__plugin_meta__ = PluginMetadata( - name="Satori 用户数据自动更新(临时措施)", - description="", - usage="", - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - "toggleable" : True, - "default_enable" : True, - } -) +from nonebot.plugin import PluginMetadata +from .auto_update import * + +__author__ = "expliyh" +__plugin_meta__ = PluginMetadata( + name="Satori 用户数据自动更新(临时措施)", + description="", + usage="", + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + "toggleable" : True, + "default_enable" : True, + } +) diff --git a/src/plugins/liteyuki_satori_user_info/auto_update.py b/src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py similarity index 88% rename from src/plugins/liteyuki_satori_user_info/auto_update.py rename to src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py index f654e66b..bb4b40bd 100644 --- a/src/plugins/liteyuki_satori_user_info/auto_update.py +++ b/src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py @@ -1,21 +1,21 @@ -import nonebot - -from nonebot.message import event_preprocessor -# from nonebot_plugin_alconna.typings import Event -from src.utils.base.ly_typing import T_MessageEvent -from src.utils import satori_utils -from nonebot.adapters import satori -from nonebot_plugin_alconna.typings import Event -from src.plugins.liteyuki_status.counter_for_satori import satori_counter - - -@event_preprocessor -async def pre_handle(event: Event): - if isinstance(event, satori.MessageEvent): - if event.user.id == event.self_id: - satori_counter.msg_sent += 1 - else: - satori_counter.msg_received += 1 - if event.user.name is not None: - if await satori_utils.user_infos.put(event.user): - nonebot.logger.info(f"Satori user {event.user.name}<{event.user.id}> updated") +import nonebot + +from nonebot.message import event_preprocessor +# from nonebot_plugin_alconna.typings import Event +from src.utils.base.ly_typing import T_MessageEvent +from src.utils import satori_utils +from nonebot.adapters import satori +from nonebot_plugin_alconna.typings import Event +from src.nonebot_plugins.liteyuki_status.counter_for_satori import satori_counter + + +@event_preprocessor +async def pre_handle(event: Event): + if isinstance(event, satori.MessageEvent): + if event.user.id == event.self_id: + satori_counter.msg_sent += 1 + else: + satori_counter.msg_received += 1 + if event.user.name is not None: + if await satori_utils.user_infos.put(event.user): + nonebot.logger.info(f"Satori user {event.user.name}<{event.user.id}> updated") diff --git a/src/plugins/liteyuki_sign_status.py b/src/nonebot_plugins/liteyuki_sign_status.py similarity index 96% rename from src/plugins/liteyuki_sign_status.py rename to src/nonebot_plugins/liteyuki_sign_status.py index 4cff8629..5f3ad442 100644 --- a/src/plugins/liteyuki_sign_status.py +++ b/src/nonebot_plugins/liteyuki_sign_status.py @@ -1,163 +1,163 @@ -import datetime -import time - -import aiohttp -from nonebot import require -from nonebot.plugin import PluginMetadata - -from src.utils.base.config import get_config -from src.utils.base.data import Database, LiteModel -from src.utils.base.resource import get_path -from src.utils.message.html_tool import template2image - -require("nonebot_plugin_alconna") -require("nonebot_plugin_apscheduler") -from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_alconna import Alconna, AlconnaResult, CommandResult, Subcommand, UniMessage, on_alconna, Args - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="签名服务器状态", - description="适用于ntqq的签名状态查看", - usage=( - "sign count 查看当前签名数\n" - "sign data 查看签名数变化\n" - "sign chart [limit] 查看签名数变化图表\n" - ), - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki" : True, - "toggleable" : True, - "default_enable": True, - } -) - -SIGN_COUNT_URLS: dict[str, str] = get_config("sign_count_urls", None) -SIGN_COUNT_DURATION = get_config("sign_count_duration", 10) - - -class SignCount(LiteModel): - TABLE_NAME: str = "sign_count" - time: float = 0.0 - count: int = 0 - sid: str = "" - - -sign_db = Database("data/liteyuki/ntqq_sign.ldb") -sign_db.auto_migrate(SignCount()) - -sign_status = on_alconna(Alconna( - "sign", - Subcommand( - "chart", - Args["limit", int, 10000] - ), - Subcommand( - "count" - ), - Subcommand( - "data" - ) -)) - -cache_img: bytes = None - - -@sign_status.assign("count") -async def _(): - reply = "Current sign count:" - for name, count in (await get_now_sign()).items(): - reply += f"\n{name}: {count[1]}" - await sign_status.send(reply) - - -@sign_status.assign("data") -async def _(): - query_stamp = [1, 5, 10, 15] - - reply = "QPS from last " + ", ".join([str(i) for i in query_stamp]) + "mins" - for name, url in SIGN_COUNT_URLS.items(): - count_data = [] - for stamp in query_stamp: - count_rows = sign_db.where_all(SignCount(), "sid = ? and time > ?", url, time.time() - 60 * stamp) - if len(count_rows) < 2: - count_data.append(-1) - else: - count_data.append((count_rows[-1].count - count_rows[0].count)/(stamp*60)) - reply += f"\n{name}: " + ", ".join([f"{i:.1f}" for i in count_data]) - await sign_status.send(reply) - - -@sign_status.assign("chart") -async def _(arp: CommandResult = AlconnaResult()): - limit = arp.result.subcommands.get("chart").args.get("limit") - if limit == 10000: - if cache_img: - await sign_status.send(UniMessage.image(raw=cache_img)) - return - img = await generate_chart(limit) - await sign_status.send(UniMessage.image(raw=img)) - - -@scheduler.scheduled_job("interval", seconds=SIGN_COUNT_DURATION, next_run_time=datetime.datetime.now()) -async def update_sign_count(): - global cache_img - if not SIGN_COUNT_URLS: - return - data = await get_now_sign() - for name, count in data.items(): - await save_sign_count(count[0], count[1], SIGN_COUNT_URLS[name]) - - cache_img = await generate_chart(10000) - - -async def get_now_sign() -> dict[str, tuple[float, int]]: - """ - Get the sign count and the time of the latest sign - Returns: - tuple[float, int] | None: (time, count) - """ - data = {} - now = time.time() - async with aiohttp.ClientSession() as client: - for name, url in SIGN_COUNT_URLS.items(): - async with client.get(url) as resp: - count = (await resp.json())["count"] - data[name] = (now, count) - return data - - -async def save_sign_count(timestamp: float, count: int, sid: str): - """ - Save the sign count to the database - Args: - sid: the sign id, use url as the id - count: - timestamp (float): the time of the sign count (int): the count of the sign - """ - sign_db.save(SignCount(time=timestamp, count=count, sid=sid)) - - -async def generate_chart(limit): - data = [] - for name, url in SIGN_COUNT_URLS.items(): - count_rows = sign_db.where_all(SignCount(), "sid = ? ORDER BY id DESC LIMIT ?", url, limit) - count_rows.reverse() - data.append( - { - "name" : name, - # "data": [[row.time, row.count] for row in count_rows] - "times" : [row.time for row in count_rows], - "counts": [row.count for row in count_rows] - } - ) - - img = await template2image( - template=get_path("templates/sign_status.html"), - templates={ - "data": data - }, - ) - - return img +import datetime +import time + +import aiohttp +from nonebot import require +from nonebot.plugin import PluginMetadata + +from src.utils.base.config import get_config +from src.utils.base.data import Database, LiteModel +from src.utils.base.resource import get_path +from src.utils.message.html_tool import template2image + +require("nonebot_plugin_alconna") +require("nonebot_plugin_apscheduler") +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_alconna import Alconna, AlconnaResult, CommandResult, Subcommand, UniMessage, on_alconna, Args + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="签名服务器状态", + description="适用于ntqq的签名状态查看", + usage=( + "sign count 查看当前签名数\n" + "sign data 查看签名数变化\n" + "sign chart [limit] 查看签名数变化图表\n" + ), + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki" : True, + "toggleable" : True, + "default_enable": True, + } +) + +SIGN_COUNT_URLS: dict[str, str] = get_config("sign_count_urls", None) +SIGN_COUNT_DURATION = get_config("sign_count_duration", 10) + + +class SignCount(LiteModel): + TABLE_NAME: str = "sign_count" + time: float = 0.0 + count: int = 0 + sid: str = "" + + +sign_db = Database("data/liteyuki/ntqq_sign.ldb") +sign_db.auto_migrate(SignCount()) + +sign_status = on_alconna(Alconna( + "sign", + Subcommand( + "chart", + Args["limit", int, 10000] + ), + Subcommand( + "count" + ), + Subcommand( + "data" + ) +)) + +cache_img: bytes = None + + +@sign_status.assign("count") +async def _(): + reply = "Current sign count:" + for name, count in (await get_now_sign()).items(): + reply += f"\n{name}: {count[1]}" + await sign_status.send(reply) + + +@sign_status.assign("data") +async def _(): + query_stamp = [1, 5, 10, 15] + + reply = "QPS from last " + ", ".join([str(i) for i in query_stamp]) + "mins" + for name, url in SIGN_COUNT_URLS.items(): + count_data = [] + for stamp in query_stamp: + count_rows = sign_db.where_all(SignCount(), "sid = ? and time > ?", url, time.time() - 60 * stamp) + if len(count_rows) < 2: + count_data.append(-1) + else: + count_data.append((count_rows[-1].count - count_rows[0].count)/(stamp*60)) + reply += f"\n{name}: " + ", ".join([f"{i:.1f}" for i in count_data]) + await sign_status.send(reply) + + +@sign_status.assign("chart") +async def _(arp: CommandResult = AlconnaResult()): + limit = arp.result.subcommands.get("chart").args.get("limit") + if limit == 10000: + if cache_img: + await sign_status.send(UniMessage.image(raw=cache_img)) + return + img = await generate_chart(limit) + await sign_status.send(UniMessage.image(raw=img)) + + +@scheduler.scheduled_job("interval", seconds=SIGN_COUNT_DURATION, next_run_time=datetime.datetime.now()) +async def update_sign_count(): + global cache_img + if not SIGN_COUNT_URLS: + return + data = await get_now_sign() + for name, count in data.items(): + await save_sign_count(count[0], count[1], SIGN_COUNT_URLS[name]) + + cache_img = await generate_chart(10000) + + +async def get_now_sign() -> dict[str, tuple[float, int]]: + """ + Get the sign count and the time of the latest sign + Returns: + tuple[float, int] | None: (time, count) + """ + data = {} + now = time.time() + async with aiohttp.ClientSession() as client: + for name, url in SIGN_COUNT_URLS.items(): + async with client.get(url) as resp: + count = (await resp.json())["count"] + data[name] = (now, count) + return data + + +async def save_sign_count(timestamp: float, count: int, sid: str): + """ + Save the sign count to the database + Args: + sid: the sign id, use url as the id + count: + timestamp (float): the time of the sign count (int): the count of the sign + """ + sign_db.save(SignCount(time=timestamp, count=count, sid=sid)) + + +async def generate_chart(limit): + data = [] + for name, url in SIGN_COUNT_URLS.items(): + count_rows = sign_db.where_all(SignCount(), "sid = ? ORDER BY id DESC LIMIT ?", url, limit) + count_rows.reverse() + data.append( + { + "name" : name, + # "data": [[row.time, row.count] for row in count_rows] + "times" : [row.time for row in count_rows], + "counts": [row.count for row in count_rows] + } + ) + + img = await template2image( + template=get_path("templates/sign_status.html"), + templates={ + "data": data + }, + ) + + return img diff --git a/src/plugins/liteyuki_smart_reply/__init__.py b/src/nonebot_plugins/liteyuki_smart_reply/__init__.py similarity index 96% rename from src/plugins/liteyuki_smart_reply/__init__.py rename to src/nonebot_plugins/liteyuki_smart_reply/__init__.py index a058572b..2e97258c 100644 --- a/src/plugins/liteyuki_smart_reply/__init__.py +++ b/src/nonebot_plugins/liteyuki_smart_reply/__init__.py @@ -1,18 +1,18 @@ -from nonebot.plugin import PluginMetadata -from .monitors import * -from .matchers import * - - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="轻雪智障回复", - description="", - usage="", - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - "toggleable" : True, - "default_enable" : True, - } +from nonebot.plugin import PluginMetadata +from .monitors import * +from .matchers import * + + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="轻雪智障回复", + description="", + usage="", + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + "toggleable" : True, + "default_enable" : True, + } ) \ No newline at end of file diff --git a/src/plugins/liteyuki_smart_reply/matchers.py b/src/nonebot_plugins/liteyuki_smart_reply/matchers.py similarity index 97% rename from src/plugins/liteyuki_smart_reply/matchers.py rename to src/nonebot_plugins/liteyuki_smart_reply/matchers.py index 0bb9ef32..716e2cae 100644 --- a/src/plugins/liteyuki_smart_reply/matchers.py +++ b/src/nonebot_plugins/liteyuki_smart_reply/matchers.py @@ -1,106 +1,106 @@ -import asyncio -import random - -import nonebot -from nonebot import Bot, on_message, get_driver, require -from nonebot.internal.matcher import Matcher -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me -from nonebot.typing import T_State - -from src.utils.base.ly_typing import T_MessageEvent -from .utils import get_keywords -from src.utils.base.word_bank import get_reply -from src.utils.event import get_message_type -from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER -from src.utils.base.data_manager import group_db, Group - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma - -nicknames = set() -driver = get_driver() -group_reply_probability: dict[str, float] = { -} -default_reply_probability = 0.05 -cut_probability = 0.4 # 分几句话的概率 - - -@on_alconna( - Alconna( - "set-reply-probability", - Args["probability", float, default_reply_probability], - ), - aliases={"设置回复概率"}, - permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER, -).handle() -async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher): - # 修改内存和数据库的概率值 - if get_message_type(event) == "group": - group_id = event.group_id - probability = result.main_args.get("probability") - # 保存到数据库 - group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=str(group_id))) - group.config["reply_probability"] = probability - group_db.save(group) - - await matcher.send(f"已将群组{group_id}的回复概率设置为{probability}") - return - - -@group_db.on_save -def _(model: Group): - """ - 在数据库更新时,更新内存中的回复概率 - Args: - model: - - Returns: - - """ - group_reply_probability[model.group_id] = model.config.get("reply_probability", default_reply_probability) - - -@driver.on_bot_connect -async def _(bot: Bot): - global nicknames - nicknames.update(bot.config.nickname) - # 从数据库加载群组的回复概率 - groups = group_db.where_all(Group(), default=[]) - for group in groups: - group_reply_probability[group.group_id] = group.config.get("reply_probability", default_reply_probability) - - -@on_message(priority=100).handle() -async def _(event: T_MessageEvent, bot: Bot, state: T_State, matcher: Matcher): - kws = await get_keywords(event.message.extract_plain_text()) - - tome = False - if await to_me()(event=event, bot=bot, state=state): - tome = True - else: - for kw in kws: - if kw in nicknames: - tome = True - break - - # 回复概率 - message_type = get_message_type(event) - if tome or message_type == "private": - p = 1.0 - else: - p = group_reply_probability.get(event.group_id, default_reply_probability) - - if random.random() < p: - if reply := get_reply(kws): - if random.random() < cut_probability: - reply = reply.replace("。", "||").replace(",", "||").replace("!", "||").replace("?", "||") - replies = reply.split("||") - for r in replies: - if r: # 防止空字符串 - await asyncio.sleep(random.random() * 2) - await matcher.send(r) - else: - await asyncio.sleep(random.random() * 3) - await matcher.send(reply) - return +import asyncio +import random + +import nonebot +from nonebot import Bot, on_message, get_driver, require +from nonebot.internal.matcher import Matcher +from nonebot.permission import SUPERUSER +from nonebot.rule import to_me +from nonebot.typing import T_State + +from src.utils.base.ly_typing import T_MessageEvent +from .utils import get_keywords +from src.utils.base.word_bank import get_reply +from src.utils.event import get_message_type +from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER +from src.utils.base.data_manager import group_db, Group + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma + +nicknames = set() +driver = get_driver() +group_reply_probability: dict[str, float] = { +} +default_reply_probability = 0.05 +cut_probability = 0.4 # 分几句话的概率 + + +@on_alconna( + Alconna( + "set-reply-probability", + Args["probability", float, default_reply_probability], + ), + aliases={"设置回复概率"}, + permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER, +).handle() +async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher): + # 修改内存和数据库的概率值 + if get_message_type(event) == "group": + group_id = event.group_id + probability = result.main_args.get("probability") + # 保存到数据库 + group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=str(group_id))) + group.config["reply_probability"] = probability + group_db.save(group) + + await matcher.send(f"已将群组{group_id}的回复概率设置为{probability}") + return + + +@group_db.on_save +def _(model: Group): + """ + 在数据库更新时,更新内存中的回复概率 + Args: + model: + + Returns: + + """ + group_reply_probability[model.group_id] = model.config.get("reply_probability", default_reply_probability) + + +@driver.on_bot_connect +async def _(bot: Bot): + global nicknames + nicknames.update(bot.config.nickname) + # 从数据库加载群组的回复概率 + groups = group_db.where_all(Group(), default=[]) + for group in groups: + group_reply_probability[group.group_id] = group.config.get("reply_probability", default_reply_probability) + + +@on_message(priority=100).handle() +async def _(event: T_MessageEvent, bot: Bot, state: T_State, matcher: Matcher): + kws = await get_keywords(event.message.extract_plain_text()) + + tome = False + if await to_me()(event=event, bot=bot, state=state): + tome = True + else: + for kw in kws: + if kw in nicknames: + tome = True + break + + # 回复概率 + message_type = get_message_type(event) + if tome or message_type == "private": + p = 1.0 + else: + p = group_reply_probability.get(event.group_id, default_reply_probability) + + if random.random() < p: + if reply := get_reply(kws): + if random.random() < cut_probability: + reply = reply.replace("。", "||").replace(",", "||").replace("!", "||").replace("?", "||") + replies = reply.split("||") + for r in replies: + if r: # 防止空字符串 + await asyncio.sleep(random.random() * 2) + await matcher.send(r) + else: + await asyncio.sleep(random.random() * 3) + await matcher.send(reply) + return diff --git a/src/plugins/liteyuki_smart_reply/monitors.py b/src/nonebot_plugins/liteyuki_smart_reply/monitors.py similarity index 100% rename from src/plugins/liteyuki_smart_reply/monitors.py rename to src/nonebot_plugins/liteyuki_smart_reply/monitors.py diff --git a/src/plugins/liteyuki_smart_reply/utils.py b/src/nonebot_plugins/liteyuki_smart_reply/utils.py similarity index 94% rename from src/plugins/liteyuki_smart_reply/utils.py rename to src/nonebot_plugins/liteyuki_smart_reply/utils.py index 21e17648..a3b5d8e5 100644 --- a/src/plugins/liteyuki_smart_reply/utils.py +++ b/src/nonebot_plugins/liteyuki_smart_reply/utils.py @@ -1,13 +1,13 @@ -from jieba import lcut -from nonebot.utils import run_sync - - -@run_sync -def get_keywords(text: str) -> list[str, ...]: - """ - 获取关键词 - Args: - text: 文本 - Returns: - """ - return lcut(text) +from jieba import lcut +from nonebot.utils import run_sync + + +@run_sync +def get_keywords(text: str) -> list[str, ...]: + """ + 获取关键词 + Args: + text: 文本 + Returns: + """ + return lcut(text) diff --git a/src/plugins/liteyuki_statistics/__init__.py b/src/nonebot_plugins/liteyuki_statistics/__init__.py similarity index 97% rename from src/plugins/liteyuki_statistics/__init__.py rename to src/nonebot_plugins/liteyuki_statistics/__init__.py index 06186b3b..13330941 100644 --- a/src/plugins/liteyuki_statistics/__init__.py +++ b/src/nonebot_plugins/liteyuki_statistics/__init__.py @@ -1,29 +1,29 @@ -from nonebot.plugin import PluginMetadata -from .stat_matchers import * -from .stat_monitors import * -from .stat_restful_api import * - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="统计信息", - description="统计机器人的信息,包括消息、群聊等,支持排名、图表等功能", - usage=( - "```\nstatistic message 查看统计消息\n" - "可选参数:\n" - " -g|--group [group_id] 指定群聊\n" - " -u|--user [user_id] 指定用户\n" - " -d|--duration [duration] 指定时长\n" - " -p|--period [period] 指定次数统计周期\n" - " -b|--bot [bot_id] 指定机器人\n" - "命令别名:\n" - " statistic|stat message|msg|m\n" - "```" - ), - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki" : True, - "toggleable" : False, - "default_enable": True, - } -) +from nonebot.plugin import PluginMetadata +from .stat_matchers import * +from .stat_monitors import * +from .stat_restful_api import * + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="统计信息", + description="统计机器人的信息,包括消息、群聊等,支持排名、图表等功能", + usage=( + "```\nstatistic message 查看统计消息\n" + "可选参数:\n" + " -g|--group [group_id] 指定群聊\n" + " -u|--user [user_id] 指定用户\n" + " -d|--duration [duration] 指定时长\n" + " -p|--period [period] 指定次数统计周期\n" + " -b|--bot [bot_id] 指定机器人\n" + "命令别名:\n" + " statistic|stat message|msg|m\n" + "```" + ), + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki" : True, + "toggleable" : False, + "default_enable": True, + } +) diff --git a/src/plugins/liteyuki_statistics/common.py b/src/nonebot_plugins/liteyuki_statistics/common.py similarity index 95% rename from src/plugins/liteyuki_statistics/common.py rename to src/nonebot_plugins/liteyuki_statistics/common.py index b581c74e..38fd6b2d 100644 --- a/src/plugins/liteyuki_statistics/common.py +++ b/src/nonebot_plugins/liteyuki_statistics/common.py @@ -1,21 +1,21 @@ -from src.utils.base.data import Database, LiteModel - - -class MessageEventModel(LiteModel): - TABLE_NAME: str = "message_event" - time: int = 0 - - bot_id: str = "" - adapter: str = "" - - user_id: str = "" - group_id: str = "" - - message_id: str = "" - message: list = [] - message_text: str = "" - message_type: str = "" - - -msg_db = Database("data/liteyuki/msg.ldb") +from src.utils.base.data import Database, LiteModel + + +class MessageEventModel(LiteModel): + TABLE_NAME: str = "message_event" + time: int = 0 + + bot_id: str = "" + adapter: str = "" + + user_id: str = "" + group_id: str = "" + + message_id: str = "" + message: list = [] + message_text: str = "" + message_type: str = "" + + +msg_db = Database("data/liteyuki/msg.ldb") msg_db.auto_migrate(MessageEventModel()) \ No newline at end of file diff --git a/src/plugins/liteyuki_statistics/data_source.py b/src/nonebot_plugins/liteyuki_statistics/data_source.py similarity index 96% rename from src/plugins/liteyuki_statistics/data_source.py rename to src/nonebot_plugins/liteyuki_statistics/data_source.py index 91241da0..ea1236c0 100644 --- a/src/plugins/liteyuki_statistics/data_source.py +++ b/src/nonebot_plugins/liteyuki_statistics/data_source.py @@ -1,172 +1,172 @@ -import time -from typing import Any - -from collections import Counter - -from nonebot import Bot - -from src.utils.message.html_tool import template2image -from .common import MessageEventModel, msg_db -from src.utils.base.language import Language -from src.utils.base.resource import get_path -from src.utils.message.string_tool import convert_seconds_to_time -from ...utils.external.logo import get_group_icon, get_user_icon - - -async def count_msg_by_bot_id(bot_id: str) -> int: - condition = " AND bot_id = ?" - condition_args = [bot_id] - - msg_rows = msg_db.where_all( - MessageEventModel(), - condition, - *condition_args - ) - - return len(msg_rows) - - -async def get_stat_msg_image( - duration: int, - period: int, - group_id: str = None, - bot_id: str = None, - user_id: str = None, - ulang: Language = Language() -) -> bytes: - """ - 获取统计消息 - Args: - user_id: - ulang: - bot_id: - group_id: - duration: 统计时间,单位秒 - period: 统计周期,单位秒 - - Returns: - tuple: [int,], [int,] 两个列表,分别为周期中心时间戳和消息数量 - """ - now = int(time.time()) - start_time = (now - duration) - - condition = "time > ?" - condition_args = [start_time] - - if group_id: - condition += " AND group_id = ?" - condition_args.append(group_id) - if bot_id: - condition += " AND bot_id = ?" - condition_args.append(bot_id) - - if user_id: - condition += " AND user_id = ?" - condition_args.append(user_id) - - msg_rows = msg_db.where_all( - MessageEventModel(), - condition, - *condition_args - ) - timestamps = [] - msg_count = [] - msg_rows.sort(key=lambda x: x.time) - - start_time = max(msg_rows[0].time, start_time) - - for i in range(start_time, now, period): - timestamps.append(i + period // 2) - msg_count.append(0) - - for msg in msg_rows: - period_start_time = start_time + (msg.time - start_time) // period * period - period_center_time = period_start_time + period // 2 - index = timestamps.index(period_center_time) - msg_count[index] += 1 - - templates = { - "data": [ - { - "name" : ulang.get("stat.message") - + f" Period {convert_seconds_to_time(period)}" + f" Duration {convert_seconds_to_time(duration)}" - + (f" Group {group_id}" if group_id else "") + (f" Bot {bot_id}" if bot_id else "") + ( - f" User {user_id}" if user_id else ""), - "times" : timestamps, - "counts": msg_count - } - ] - } - - return await template2image(get_path("templates/stat_msg.html"), templates) - - -async def get_stat_rank_image( - rank_type: str, - limit: dict[str, Any], - ulang: Language = Language(), - bot: Bot = None, -) -> bytes: - if rank_type == "user": - condition = "user_id != ''" - condition_args = [] - else: - condition = "group_id != ''" - condition_args = [] - - for k, v in limit.items(): - match k: - case "user_id": - condition += " AND user_id = ?" - condition_args.append(v) - case "group_id": - condition += " AND group_id = ?" - condition_args.append(v) - case "bot_id": - condition += " AND bot_id = ?" - condition_args.append(v) - case "duration": - condition += " AND time > ?" - condition_args.append(v) - - msg_rows = msg_db.where_all( - MessageEventModel(), - condition, - *condition_args - ) - - """ - { - name: string, # user name or group name - count: int, # message count - icon: string # icon url - } - """ - - if rank_type == "user": - ranking_counter = Counter([msg.user_id for msg in msg_rows]) - else: - ranking_counter = Counter([msg.group_id for msg in msg_rows]) - sorted_data = sorted(ranking_counter.items(), key=lambda x: x[1], reverse=True) - - ranking: list[dict[str, Any]] = [ - { - "name" : _[0], - "count": _[1], - "icon" : await (get_group_icon(platform="qq", group_id=_[0]) if rank_type == "group" else get_user_icon( - platform="qq", user_id=_[0] - )) - } - for _ in sorted_data[0:min(len(sorted_data), limit["rank"])] - ] - - templates = { - "data": - { - "name" : ulang.get("stat.rank") + f" Type {rank_type}" + f" Limit {limit}", - "ranking": ranking - } - - } - - return await template2image(get_path("templates/stat_rank.html"), templates, debug=True) +import time +from typing import Any + +from collections import Counter + +from nonebot import Bot + +from src.utils.message.html_tool import template2image +from .common import MessageEventModel, msg_db +from src.utils.base.language import Language +from src.utils.base.resource import get_path +from src.utils.message.string_tool import convert_seconds_to_time +from ...utils.external.logo import get_group_icon, get_user_icon + + +async def count_msg_by_bot_id(bot_id: str) -> int: + condition = " AND bot_id = ?" + condition_args = [bot_id] + + msg_rows = msg_db.where_all( + MessageEventModel(), + condition, + *condition_args + ) + + return len(msg_rows) + + +async def get_stat_msg_image( + duration: int, + period: int, + group_id: str = None, + bot_id: str = None, + user_id: str = None, + ulang: Language = Language() +) -> bytes: + """ + 获取统计消息 + Args: + user_id: + ulang: + bot_id: + group_id: + duration: 统计时间,单位秒 + period: 统计周期,单位秒 + + Returns: + tuple: [int,], [int,] 两个列表,分别为周期中心时间戳和消息数量 + """ + now = int(time.time()) + start_time = (now - duration) + + condition = "time > ?" + condition_args = [start_time] + + if group_id: + condition += " AND group_id = ?" + condition_args.append(group_id) + if bot_id: + condition += " AND bot_id = ?" + condition_args.append(bot_id) + + if user_id: + condition += " AND user_id = ?" + condition_args.append(user_id) + + msg_rows = msg_db.where_all( + MessageEventModel(), + condition, + *condition_args + ) + timestamps = [] + msg_count = [] + msg_rows.sort(key=lambda x: x.time) + + start_time = max(msg_rows[0].time, start_time) + + for i in range(start_time, now, period): + timestamps.append(i + period // 2) + msg_count.append(0) + + for msg in msg_rows: + period_start_time = start_time + (msg.time - start_time) // period * period + period_center_time = period_start_time + period // 2 + index = timestamps.index(period_center_time) + msg_count[index] += 1 + + templates = { + "data": [ + { + "name" : ulang.get("stat.message") + + f" Period {convert_seconds_to_time(period)}" + f" Duration {convert_seconds_to_time(duration)}" + + (f" Group {group_id}" if group_id else "") + (f" Bot {bot_id}" if bot_id else "") + ( + f" User {user_id}" if user_id else ""), + "times" : timestamps, + "counts": msg_count + } + ] + } + + return await template2image(get_path("templates/stat_msg.html"), templates) + + +async def get_stat_rank_image( + rank_type: str, + limit: dict[str, Any], + ulang: Language = Language(), + bot: Bot = None, +) -> bytes: + if rank_type == "user": + condition = "user_id != ''" + condition_args = [] + else: + condition = "group_id != ''" + condition_args = [] + + for k, v in limit.items(): + match k: + case "user_id": + condition += " AND user_id = ?" + condition_args.append(v) + case "group_id": + condition += " AND group_id = ?" + condition_args.append(v) + case "bot_id": + condition += " AND bot_id = ?" + condition_args.append(v) + case "duration": + condition += " AND time > ?" + condition_args.append(v) + + msg_rows = msg_db.where_all( + MessageEventModel(), + condition, + *condition_args + ) + + """ + { + name: string, # user name or group name + count: int, # message count + icon: string # icon url + } + """ + + if rank_type == "user": + ranking_counter = Counter([msg.user_id for msg in msg_rows]) + else: + ranking_counter = Counter([msg.group_id for msg in msg_rows]) + sorted_data = sorted(ranking_counter.items(), key=lambda x: x[1], reverse=True) + + ranking: list[dict[str, Any]] = [ + { + "name" : _[0], + "count": _[1], + "icon" : await (get_group_icon(platform="qq", group_id=_[0]) if rank_type == "group" else get_user_icon( + platform="qq", user_id=_[0] + )) + } + for _ in sorted_data[0:min(len(sorted_data), limit["rank"])] + ] + + templates = { + "data": + { + "name" : ulang.get("stat.rank") + f" Type {rank_type}" + f" Limit {limit}", + "ranking": ranking + } + + } + + return await template2image(get_path("templates/stat_rank.html"), templates, debug=True) diff --git a/src/plugins/liteyuki_statistics/stat_matchers.py b/src/nonebot_plugins/liteyuki_statistics/stat_matchers.py similarity index 96% rename from src/plugins/liteyuki_statistics/stat_matchers.py rename to src/nonebot_plugins/liteyuki_statistics/stat_matchers.py index 144e094b..780a83d1 100644 --- a/src/plugins/liteyuki_statistics/stat_matchers.py +++ b/src/nonebot_plugins/liteyuki_statistics/stat_matchers.py @@ -1,134 +1,134 @@ -from nonebot import Bot, require -from src.utils.message.string_tool import convert_duration, convert_time_to_seconds -from .data_source import * -from src.utils import event as event_utils -from src.utils.base.language import Language -from src.utils.base.ly_typing import T_MessageEvent - -require("nonebot_plugin_alconna") - -from nonebot_plugin_alconna import ( - UniMessage, - on_alconna, - Alconna, - Args, - Subcommand, - Arparma, - Option, - MultiVar -) - -stat_msg = on_alconna( - Alconna( - "statistic", - Subcommand( - "message", - # Args["duration", str, "2d"]["period", str, "60s"], # 默认为1天 - Option( - "-d|--duration", - Args["duration", str, "2d"], - help_text="统计时间", - ), - Option( - "-p|--period", - Args["period", str, "60s"], - help_text="统计周期", - ), - Option( - "-b|--bot", # 生成图表 - Args["bot_id", str, "current"], - help_text="是否指定机器人", - ), - Option( - "-g|--group", - Args["group_id", str, "current"], - help_text="指定群组" - ), - Option( - "-u|--user", - Args["user_id", str, "current"], - help_text="指定用户" - ), - alias={"msg", "m"}, - help_text="查看统计次数内的消息" - ), - Subcommand( - "rank", - Option( - "-u|--user", - help_text="以用户为指标", - ), - Option( - "-g|--group", - help_text="以群组为指标", - ), - Option( - "-l|--limit", - Args["limit", MultiVar(str)], - help_text="限制参数,使用key=val格式", - ), - Option( - "-d|--duration", - Args["duration", str, "1d"], - help_text="统计时间", - ), - Option( - "-r|--rank", - Args["rank", int, 20], - help_text="指定排名", - ), - alias={"r"}, - ) - ), - aliases={"stat"} -) - - -@stat_msg.assign("message") -async def _(result: Arparma, event: T_MessageEvent, bot: Bot): - ulang = Language(event_utils.get_user_id(event)) - try: - duration = convert_time_to_seconds(result.other_args.get("duration", "2d")) # 秒数 - period = convert_time_to_seconds(result.other_args.get("period", "1m")) - except BaseException as e: - await stat_msg.send(ulang.get("liteyuki.invalid_command", TEXT=str(e.__str__()))) - return - - group_id = result.other_args.get("group_id") - bot_id = result.other_args.get("bot_id") - user_id = result.other_args.get("user_id") - - if group_id in ["current", "c"]: - group_id = str(event_utils.get_group_id(event)) - - if group_id in ["all", "a"]: - group_id = "all" - - if bot_id in ["current", "c"]: - bot_id = str(bot.self_id) - - if user_id in ["current", "c"]: - user_id = str(event_utils.get_user_id(event)) - - img = await get_stat_msg_image(duration=duration, period=period, group_id=group_id, bot_id=bot_id, user_id=user_id, ulang=ulang) - await stat_msg.send(UniMessage.image(raw=img)) - - -@stat_msg.assign("rank") -async def _(result: Arparma, event: T_MessageEvent, bot: Bot): - ulang = Language(event_utils.get_user_id(event)) - rank_type = "user" - duration = convert_time_to_seconds(result.other_args.get("duration", "1d")) - if result.subcommands.get("rank").options.get("user"): - rank_type = "user" - elif result.subcommands.get("rank").options.get("group"): - rank_type = "group" - - limit = result.other_args.get("limit", {}) - if limit: - limit = dict([i.split("=") for i in limit]) - limit["duration"] = time.time() - duration # 起始时间戳 - limit["rank"] = result.other_args.get("rank", 20) - - img = await get_stat_rank_image(rank_type=rank_type, limit=limit, ulang=ulang) - await stat_msg.send(UniMessage.image(raw=img)) +from nonebot import Bot, require +from src.utils.message.string_tool import convert_duration, convert_time_to_seconds +from .data_source import * +from src.utils import event as event_utils +from src.utils.base.language import Language +from src.utils.base.ly_typing import T_MessageEvent + +require("nonebot_plugin_alconna") + +from nonebot_plugin_alconna import ( + UniMessage, + on_alconna, + Alconna, + Args, + Subcommand, + Arparma, + Option, + MultiVar +) + +stat_msg = on_alconna( + Alconna( + "statistic", + Subcommand( + "message", + # Args["duration", str, "2d"]["period", str, "60s"], # 默认为1天 + Option( + "-d|--duration", + Args["duration", str, "2d"], + help_text="统计时间", + ), + Option( + "-p|--period", + Args["period", str, "60s"], + help_text="统计周期", + ), + Option( + "-b|--bot", # 生成图表 + Args["bot_id", str, "current"], + help_text="是否指定机器人", + ), + Option( + "-g|--group", + Args["group_id", str, "current"], + help_text="指定群组" + ), + Option( + "-u|--user", + Args["user_id", str, "current"], + help_text="指定用户" + ), + alias={"msg", "m"}, + help_text="查看统计次数内的消息" + ), + Subcommand( + "rank", + Option( + "-u|--user", + help_text="以用户为指标", + ), + Option( + "-g|--group", + help_text="以群组为指标", + ), + Option( + "-l|--limit", + Args["limit", MultiVar(str)], + help_text="限制参数,使用key=val格式", + ), + Option( + "-d|--duration", + Args["duration", str, "1d"], + help_text="统计时间", + ), + Option( + "-r|--rank", + Args["rank", int, 20], + help_text="指定排名", + ), + alias={"r"}, + ) + ), + aliases={"stat"} +) + + +@stat_msg.assign("message") +async def _(result: Arparma, event: T_MessageEvent, bot: Bot): + ulang = Language(event_utils.get_user_id(event)) + try: + duration = convert_time_to_seconds(result.other_args.get("duration", "2d")) # 秒数 + period = convert_time_to_seconds(result.other_args.get("period", "1m")) + except BaseException as e: + await stat_msg.send(ulang.get("liteyuki.invalid_command", TEXT=str(e.__str__()))) + return + + group_id = result.other_args.get("group_id") + bot_id = result.other_args.get("bot_id") + user_id = result.other_args.get("user_id") + + if group_id in ["current", "c"]: + group_id = str(event_utils.get_group_id(event)) + + if group_id in ["all", "a"]: + group_id = "all" + + if bot_id in ["current", "c"]: + bot_id = str(bot.self_id) + + if user_id in ["current", "c"]: + user_id = str(event_utils.get_user_id(event)) + + img = await get_stat_msg_image(duration=duration, period=period, group_id=group_id, bot_id=bot_id, user_id=user_id, ulang=ulang) + await stat_msg.send(UniMessage.image(raw=img)) + + +@stat_msg.assign("rank") +async def _(result: Arparma, event: T_MessageEvent, bot: Bot): + ulang = Language(event_utils.get_user_id(event)) + rank_type = "user" + duration = convert_time_to_seconds(result.other_args.get("duration", "1d")) + if result.subcommands.get("rank").options.get("user"): + rank_type = "user" + elif result.subcommands.get("rank").options.get("group"): + rank_type = "group" + + limit = result.other_args.get("limit", {}) + if limit: + limit = dict([i.split("=") for i in limit]) + limit["duration"] = time.time() - duration # 起始时间戳 + limit["rank"] = result.other_args.get("rank", 20) + + img = await get_stat_rank_image(rank_type=rank_type, limit=limit, ulang=ulang) + await stat_msg.send(UniMessage.image(raw=img)) diff --git a/src/plugins/liteyuki_statistics/stat_monitors.py b/src/nonebot_plugins/liteyuki_statistics/stat_monitors.py similarity index 96% rename from src/plugins/liteyuki_statistics/stat_monitors.py rename to src/nonebot_plugins/liteyuki_statistics/stat_monitors.py index bbebc530..043499c3 100644 --- a/src/plugins/liteyuki_statistics/stat_monitors.py +++ b/src/nonebot_plugins/liteyuki_statistics/stat_monitors.py @@ -1,92 +1,92 @@ -import time - -from nonebot import require -from nonebot.message import event_postprocessor - -from src.utils.base.data import Database, LiteModel -from src.utils.base.ly_typing import v11, v12, satori - -from src.utils.base.ly_typing import T_Bot, T_MessageEvent - -from .common import MessageEventModel, msg_db -from src.utils import event as event_utils - -require("nonebot_plugin_alconna") - - -async def general_event_monitor(bot: T_Bot, event: T_MessageEvent): - pass - # if isinstance(bot, satori.Bot): - # print("POST PROCESS SATORI EVENT") - # return await satori_event_monitor(bot, event) - # elif isinstance(bot, v11.Bot): - # print("POST PROCESS V11 EVENT") - # return await onebot_v11_event_monitor(bot, event) - - -@event_postprocessor -async def onebot_v11_event_monitor(bot: v11.Bot, event: v11.MessageEvent): - if event.message_type == "group": - event: v11.GroupMessageEvent - group_id = str(event.group_id) - else: - group_id = "" - mem = MessageEventModel( - time=int(time.time()), - bot_id=bot.self_id, - adapter="onebot.v11", - group_id=group_id, - user_id=str(event.user_id), - - message_id=str(event.message_id), - - message=[ms.__dict__ for ms in event.message], - message_text=event.raw_message, - message_type=event.message_type, - ) - msg_db.save(mem) - - -@event_postprocessor -async def onebot_v12_event_monitor(bot: v12.Bot, event: v12.MessageEvent): - if event.message_type == "group": - event: v12.GroupMessageEvent - group_id = str(event.group_id) - else: - group_id = "" - mem = MessageEventModel( - time=int(time.time()), - bot_id=bot.self_id, - adapter="onebot.v12", - group_id=group_id, - user_id=str(event.user_id), - - message_id=[ms.__dict__ for ms in event.message], - - message=event.message, - message_text=event.raw_message, - message_type=event.message_type, - ) - msg_db.save(mem) - - -@event_postprocessor -async def satori_event_monitor(bot: satori.Bot, event: satori.MessageEvent): - if event.guild is not None: - event: satori.MessageEvent - group_id = str(event.guild.id) - else: - group_id = "" - - mem = MessageEventModel( - time=int(time.time()), - bot_id=bot.self_id, - adapter="satori", - group_id=group_id, - user_id=str(event.user.id), - message_id=[ms.__str__() for ms in event.message], - message=event.message, - message_text=event.message.content, - message_type=event_utils.get_message_type(event), - ) - msg_db.save(mem) +import time + +from nonebot import require +from nonebot.message import event_postprocessor + +from src.utils.base.data import Database, LiteModel +from src.utils.base.ly_typing import v11, v12, satori + +from src.utils.base.ly_typing import T_Bot, T_MessageEvent + +from .common import MessageEventModel, msg_db +from src.utils import event as event_utils + +require("nonebot_plugin_alconna") + + +async def general_event_monitor(bot: T_Bot, event: T_MessageEvent): + pass + # if isinstance(bot, satori.Bot): + # print("POST PROCESS SATORI EVENT") + # return await satori_event_monitor(bot, event) + # elif isinstance(bot, v11.Bot): + # print("POST PROCESS V11 EVENT") + # return await onebot_v11_event_monitor(bot, event) + + +@event_postprocessor +async def onebot_v11_event_monitor(bot: v11.Bot, event: v11.MessageEvent): + if event.message_type == "group": + event: v11.GroupMessageEvent + group_id = str(event.group_id) + else: + group_id = "" + mem = MessageEventModel( + time=int(time.time()), + bot_id=bot.self_id, + adapter="onebot.v11", + group_id=group_id, + user_id=str(event.user_id), + + message_id=str(event.message_id), + + message=[ms.__dict__ for ms in event.message], + message_text=event.raw_message, + message_type=event.message_type, + ) + msg_db.save(mem) + + +@event_postprocessor +async def onebot_v12_event_monitor(bot: v12.Bot, event: v12.MessageEvent): + if event.message_type == "group": + event: v12.GroupMessageEvent + group_id = str(event.group_id) + else: + group_id = "" + mem = MessageEventModel( + time=int(time.time()), + bot_id=bot.self_id, + adapter="onebot.v12", + group_id=group_id, + user_id=str(event.user_id), + + message_id=[ms.__dict__ for ms in event.message], + + message=event.message, + message_text=event.raw_message, + message_type=event.message_type, + ) + msg_db.save(mem) + + +@event_postprocessor +async def satori_event_monitor(bot: satori.Bot, event: satori.MessageEvent): + if event.guild is not None: + event: satori.MessageEvent + group_id = str(event.guild.id) + else: + group_id = "" + + mem = MessageEventModel( + time=int(time.time()), + bot_id=bot.self_id, + adapter="satori", + group_id=group_id, + user_id=str(event.user.id), + message_id=[ms.__str__() for ms in event.message], + message=event.message, + message_text=event.message.content, + message_type=event_utils.get_message_type(event), + ) + msg_db.save(mem) diff --git a/src/plugins/liteyuki_statistics/stat_restful_api.py b/src/nonebot_plugins/liteyuki_statistics/stat_restful_api.py similarity index 100% rename from src/plugins/liteyuki_statistics/stat_restful_api.py rename to src/nonebot_plugins/liteyuki_statistics/stat_restful_api.py diff --git a/src/plugins/liteyuki_statistics/word_cloud/LICENSE b/src/nonebot_plugins/liteyuki_statistics/word_cloud/LICENSE similarity index 98% rename from src/plugins/liteyuki_statistics/word_cloud/LICENSE rename to src/nonebot_plugins/liteyuki_statistics/word_cloud/LICENSE index 49bc087b..efaee552 100644 --- a/src/plugins/liteyuki_statistics/word_cloud/LICENSE +++ b/src/nonebot_plugins/liteyuki_statistics/word_cloud/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2022 hemengyang - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2022 hemengyang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/plugins/liteyuki_statistics/word_cloud/data_source.py b/src/nonebot_plugins/liteyuki_statistics/word_cloud/data_source.py similarity index 97% rename from src/plugins/liteyuki_statistics/word_cloud/data_source.py rename to src/nonebot_plugins/liteyuki_statistics/word_cloud/data_source.py index d23e0904..2c14ef72 100644 --- a/src/plugins/liteyuki_statistics/word_cloud/data_source.py +++ b/src/nonebot_plugins/liteyuki_statistics/word_cloud/data_source.py @@ -1,107 +1,107 @@ -import asyncio -import concurrent.futures -import contextlib -import re -from functools import partial -from io import BytesIO -from random import choice -from typing import Optional - -import jieba -import jieba.analyse -import numpy as np -from emoji import replace_emoji -from PIL import Image -from wordcloud import WordCloud - -from .config import global_config, plugin_config - - -def pre_precess(msg: str) -> str: - """对消息进行预处理""" - # 去除网址 - # https://stackoverflow.com/a/17773849/9212748 - url_regex = re.compile( - r"(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]" - r"+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})" - ) - msg = url_regex.sub("", msg) - - # 去除 \u200b - msg = re.sub(r"\u200b", "", msg) - - # 去除 emoji - # https://github.com/carpedm20/emoji - msg = replace_emoji(msg) - - return msg - - -def analyse_message(msg: str) -> dict[str, float]: - """分析消息 - - 分词,并统计词频 - """ - # 设置停用词表 - if plugin_config.wordcloud_stopwords_path: - jieba.analyse.set_stop_words(plugin_config.wordcloud_stopwords_path) - # 加载用户词典 - if plugin_config.wordcloud_userdict_path: - jieba.load_userdict(str(plugin_config.wordcloud_userdict_path)) - # 基于 TF-IDF 算法的关键词抽取 - # 返回所有关键词,因为设置了数量其实也只是 tags[:topK],不如交给词云库处理 - words = jieba.analyse.extract_tags(msg, topK=0, withWeight=True) - return dict(words) - - -def get_mask(key: str): - """获取 mask""" - mask_path = plugin_config.get_mask_path(key) - if mask_path.exists(): - return np.array(Image.open(mask_path)) - # 如果指定 mask 文件不存在,则尝试默认 mask - default_mask_path = plugin_config.get_mask_path() - if default_mask_path.exists(): - return np.array(Image.open(default_mask_path)) - - -def _get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]: - # 过滤掉命令 - command_start = tuple(i for i in global_config.command_start if i) - message = " ".join(m for m in messages if not m.startswith(command_start)) - # 预处理 - message = pre_precess(message) - # 分析消息。分词,并统计词频 - frequency = analyse_message(message) - # 词云参数 - wordcloud_options = {} - wordcloud_options.update(plugin_config.wordcloud_options) - wordcloud_options.setdefault("font_path", str(plugin_config.wordcloud_font_path)) - wordcloud_options.setdefault("width", plugin_config.wordcloud_width) - wordcloud_options.setdefault("height", plugin_config.wordcloud_height) - wordcloud_options.setdefault( - "background_color", plugin_config.wordcloud_background_color - ) - # 如果 colormap 是列表,则随机选择一个 - colormap = ( - plugin_config.wordcloud_colormap - if isinstance(plugin_config.wordcloud_colormap, str) - else choice(plugin_config.wordcloud_colormap) - ) - wordcloud_options.setdefault("colormap", colormap) - wordcloud_options.setdefault("mask", get_mask(mask_key)) - with contextlib.suppress(ValueError): - wordcloud = WordCloud(**wordcloud_options) - image = wordcloud.generate_from_frequencies(frequency).to_image() - image_bytes = BytesIO() - image.save(image_bytes, format="PNG") - return image_bytes.getvalue() - - -async def get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]: - loop = asyncio.get_running_loop() - pfunc = partial(_get_wordcloud, messages, mask_key) - # 虽然不知道具体是哪里泄漏了,但是通过每次关闭线程池可以避免这个问题 - # https://github.com/he0119/nonebot-plugin-wordcloud/issues/99 - with concurrent.futures.ThreadPoolExecutor() as pool: - return await loop.run_in_executor(pool, pfunc) +import asyncio +import concurrent.futures +import contextlib +import re +from functools import partial +from io import BytesIO +from random import choice +from typing import Optional + +import jieba +import jieba.analyse +import numpy as np +from emoji import replace_emoji +from PIL import Image +from wordcloud import WordCloud + +from .config import global_config, plugin_config + + +def pre_precess(msg: str) -> str: + """对消息进行预处理""" + # 去除网址 + # https://stackoverflow.com/a/17773849/9212748 + url_regex = re.compile( + r"(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]" + r"+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})" + ) + msg = url_regex.sub("", msg) + + # 去除 \u200b + msg = re.sub(r"\u200b", "", msg) + + # 去除 emoji + # https://github.com/carpedm20/emoji + msg = replace_emoji(msg) + + return msg + + +def analyse_message(msg: str) -> dict[str, float]: + """分析消息 + + 分词,并统计词频 + """ + # 设置停用词表 + if plugin_config.wordcloud_stopwords_path: + jieba.analyse.set_stop_words(plugin_config.wordcloud_stopwords_path) + # 加载用户词典 + if plugin_config.wordcloud_userdict_path: + jieba.load_userdict(str(plugin_config.wordcloud_userdict_path)) + # 基于 TF-IDF 算法的关键词抽取 + # 返回所有关键词,因为设置了数量其实也只是 tags[:topK],不如交给词云库处理 + words = jieba.analyse.extract_tags(msg, topK=0, withWeight=True) + return dict(words) + + +def get_mask(key: str): + """获取 mask""" + mask_path = plugin_config.get_mask_path(key) + if mask_path.exists(): + return np.array(Image.open(mask_path)) + # 如果指定 mask 文件不存在,则尝试默认 mask + default_mask_path = plugin_config.get_mask_path() + if default_mask_path.exists(): + return np.array(Image.open(default_mask_path)) + + +def _get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]: + # 过滤掉命令 + command_start = tuple(i for i in global_config.command_start if i) + message = " ".join(m for m in messages if not m.startswith(command_start)) + # 预处理 + message = pre_precess(message) + # 分析消息。分词,并统计词频 + frequency = analyse_message(message) + # 词云参数 + wordcloud_options = {} + wordcloud_options.update(plugin_config.wordcloud_options) + wordcloud_options.setdefault("font_path", str(plugin_config.wordcloud_font_path)) + wordcloud_options.setdefault("width", plugin_config.wordcloud_width) + wordcloud_options.setdefault("height", plugin_config.wordcloud_height) + wordcloud_options.setdefault( + "background_color", plugin_config.wordcloud_background_color + ) + # 如果 colormap 是列表,则随机选择一个 + colormap = ( + plugin_config.wordcloud_colormap + if isinstance(plugin_config.wordcloud_colormap, str) + else choice(plugin_config.wordcloud_colormap) + ) + wordcloud_options.setdefault("colormap", colormap) + wordcloud_options.setdefault("mask", get_mask(mask_key)) + with contextlib.suppress(ValueError): + wordcloud = WordCloud(**wordcloud_options) + image = wordcloud.generate_from_frequencies(frequency).to_image() + image_bytes = BytesIO() + image.save(image_bytes, format="PNG") + return image_bytes.getvalue() + + +async def get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]: + loop = asyncio.get_running_loop() + pfunc = partial(_get_wordcloud, messages, mask_key) + # 虽然不知道具体是哪里泄漏了,但是通过每次关闭线程池可以避免这个问题 + # https://github.com/he0119/nonebot-plugin-wordcloud/issues/99 + with concurrent.futures.ThreadPoolExecutor() as pool: + return await loop.run_in_executor(pool, pfunc) diff --git a/src/plugins/liteyuki_status/__init__.py b/src/nonebot_plugins/liteyuki_status/__init__.py similarity index 96% rename from src/plugins/liteyuki_status/__init__.py rename to src/nonebot_plugins/liteyuki_status/__init__.py index 69d2b5ff..b1a2588b 100644 --- a/src/plugins/liteyuki_status/__init__.py +++ b/src/nonebot_plugins/liteyuki_status/__init__.py @@ -1,24 +1,24 @@ -from nonebot.plugin import PluginMetadata -from .status import * - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="状态查看器", - description="", - usage=( - "MARKDOWN### 状态查看器\n" - "查看机器人的状态\n" - "### 用法\n" - "- `/status` 查看基本情况\n" - "- `/status memory` 查看内存使用情况\n" - "- `/status process` 查看进程情况\n" - ), - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - "toggleable" : False, - "default_enable" : True, - } -) - +from nonebot.plugin import PluginMetadata +from .status import * + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="状态查看器", + description="", + usage=( + "MARKDOWN### 状态查看器\n" + "查看机器人的状态\n" + "### 用法\n" + "- `/status` 查看基本情况\n" + "- `/status memory` 查看内存使用情况\n" + "- `/status process` 查看进程情况\n" + ), + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + "toggleable" : False, + "default_enable" : True, + } +) + diff --git a/src/plugins/liteyuki_status/api.py b/src/nonebot_plugins/liteyuki_status/api.py similarity index 96% rename from src/plugins/liteyuki_status/api.py rename to src/nonebot_plugins/liteyuki_status/api.py index c6783150..5363c605 100644 --- a/src/plugins/liteyuki_status/api.py +++ b/src/nonebot_plugins/liteyuki_status/api.py @@ -1,292 +1,292 @@ -import platform -import time - -import nonebot -import psutil -from cpuinfo import cpuinfo -from nonebot import require -from nonebot.adapters import satori - -from src.utils import __NAME__, __VERSION__ -from src.utils.base.config import get_config -from src.utils.base.data_manager import TempConfig, common_db -from src.utils.base.language import Language -from src.utils.base.resource import get_loaded_resource_packs, get_path -from src.utils.message.html_tool import template2image -from src.utils import satori_utils -from .counter_for_satori import satori_counter -from git import Repo - -# require("nonebot_plugin_apscheduler") -# from nonebot_plugin_apscheduler import scheduler - -commit_hash = Repo(".").head.commit.hexsha - -protocol_names = { - 0: "iPad", - 1: "Android Phone", - 2: "Android Watch", - 3: "Mac", - 5: "iPad", - 6: "Android Pad", -} - -""" -Universal Interface -data -- bot - - name: str - icon: str - id: int - protocol_name: str - groups: int - friends: int - message_sent: int - message_received: int - app_name: str -- hardware - - cpu - - percent: float - - name: str - - mem - - percent: float - - total: int - - used: int - - free: int - - swap - - percent: float - - total: int - - used: int - - free: int - - disk: list - - name: str - - percent: float - - total: int -""" -# status_card_cache = {} # lang -> bytes - - -# 60s刷新一次 -# 之前写的什么鬼玩意,这么重要的功能这样写??? -# @scheduler.scheduled_job("cron", second="*/40") -# async def refresh_status_card(): -# nonebot.logger.debug("Refreshing status card cache.") -# global status_card_cache -# status_card_cache = {} -# bot_data = await get_bots_data() -# hardware_data = await get_hardware_data() -# liteyuki_data = await get_liteyuki_data() -# for lang in status_card_cache.keys(): -# status_card_cache[lang] = await generate_status_card( -# bot_data, -# hardware_data, -# liteyuki_data, -# lang=lang, -# use_cache=False -# ) - - -# 获取状态卡片 -# bot_id 参数已经是bot参数的一部分了,不需要保留,但为了“兼容性”…… -async def generate_status_card( - bot: dict, - hardware: dict, - liteyuki: dict, - lang="zh-CN", - bot_id="0", -) -> bytes: - return await template2image( - get_path("templates/status.html", abs_path=True), - { - "data": { - "bot": bot, - "hardware": hardware, - "liteyuki": liteyuki, - "localization": get_local_data(lang), - } - }, - ) - - -def get_local_data(lang_code) -> dict: - lang = Language(lang_code) - return { - "friends": lang.get("status.friends"), - "groups": lang.get("status.groups"), - "plugins": lang.get("status.plugins"), - "bots": lang.get("status.bots"), - "message_sent": lang.get("status.message_sent"), - "message_received": lang.get("status.message_received"), - "cpu": lang.get("status.cpu"), - "memory": lang.get("status.memory"), - "swap": lang.get("status.swap"), - "disk": lang.get("status.disk"), - "usage": lang.get("status.usage"), - "total": lang.get("status.total"), - "used": lang.get("status.used"), - "free": lang.get("status.free"), - "days": lang.get("status.days"), - "hours": lang.get("status.hours"), - "minutes": lang.get("status.minutes"), - "seconds": lang.get("status.seconds"), - "runtime": lang.get("status.runtime"), - "threads": lang.get("status.threads"), - "cores": lang.get("status.cores"), - "process": lang.get("status.process"), - "resources": lang.get("status.resources"), - "description": lang.get("status.description"), - } - - -async def get_bots_data(self_id: str = "0") -> dict: - """获取当前所有机器人数据 - Returns: - """ - result = { - "self_id": self_id, - "bots": [], - } - for bot_id, bot in nonebot.get_bots().items(): - groups = 0 - friends = 0 - status = {} - bot_name = bot_id - version_info = {} - if isinstance(bot, satori.Bot): - try: - bot_name = (await satori_utils.user_infos.get(bot.self_id)).name - groups = str(await satori_utils.count_groups(bot)) - friends = str(await satori_utils.count_friends(bot)) - status = {} - version_info = await bot.get_version_info() - except Exception: - pass - else: - try: - # API fetch - bot_name = (await bot.get_login_info())["nickname"] - groups = len(await bot.get_group_list()) - friends = len(await bot.get_friend_list()) - status = await bot.get_status() - version_info = await bot.get_version_info() - except Exception: - pass - - statistics = status.get("stat", {}) - app_name = version_info.get("app_name", "UnknownImplementation") - if app_name in ["Lagrange.OneBot", "LLOneBot", "Shamrock", "NapCat.Onebot"]: - icon = f"https://q.qlogo.cn/g?b=qq&nk={bot_id}&s=640" - elif isinstance(bot, satori.Bot): - app_name = "Satori" - icon = (await bot.login_get()).user.avatar - else: - icon = None - bot_data = { - "name": bot_name, - "icon": icon, - "id": bot_id, - "protocol_name": protocol_names.get( - version_info.get("protocol_name"), "Online" - ), - "groups": groups, - "friends": friends, - "message_sent": ( - satori_counter.msg_sent - if isinstance(bot, satori.Bot) - else statistics.get("message_sent", 0) - ), - "message_received": ( - satori_counter.msg_received - if isinstance(bot, satori.Bot) - else statistics.get("message_received", 0) - ), - "app_name": app_name, - } - result["bots"].append(bot_data) - - return result - - -async def get_hardware_data() -> dict: - mem = psutil.virtual_memory() - all_processes = psutil.Process().children(recursive=True) - all_processes.append(psutil.Process()) - - mem_used_process = 0 - process_mem = {} - for process in all_processes: - try: - ps_name = process.name().replace(".exe", "") - if ps_name not in process_mem: - process_mem[ps_name] = 0 - process_mem[ps_name] += process.memory_info().rss - mem_used_process += process.memory_info().rss - except Exception: - pass - swap = psutil.swap_memory() - cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "Unknown") - if "AMD" in cpu_brand_raw: - brand = "AMD" - elif "Intel" in cpu_brand_raw: - brand = "Intel" - else: - brand = "Unknown" - result = { - "cpu": { - "percent": psutil.cpu_percent(), - "name": f"{brand} {cpuinfo.get_cpu_info().get('arch', 'Unknown')}", - "cores": psutil.cpu_count(logical=False), - "threads": psutil.cpu_count(logical=True), - "freq": psutil.cpu_freq().current, # MHz - }, - "memory": { - "percent": mem.percent, - "total": mem.total, - "used": mem.used, - "free": mem.free, - "usedProcess": mem_used_process, - }, - "swap": { - "percent": swap.percent, - "total": swap.total, - "used": swap.used, - "free": swap.free, - }, - "disk": [], - } - - for disk in psutil.disk_partitions(all=True): - try: - disk_usage = psutil.disk_usage(disk.mountpoint) - if disk_usage.total == 0: - continue # 虚拟磁盘 - result["disk"].append( - { - "name": disk.mountpoint, - "percent": disk_usage.percent, - "total": disk_usage.total, - "used": disk_usage.used, - "free": disk_usage.free, - } - ) - except: - pass - - return result - - -async def get_liteyuki_data() -> dict: - temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig()) - result = { - "name": list(get_config("nickname", [__NAME__]))[0], - "version": f"{__VERSION__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}", - "plugins": len(nonebot.get_loaded_plugins()), - "resources": len(get_loaded_resource_packs()), - "nonebot": f"{nonebot.__version__}", - "python": f"{platform.python_implementation()} {platform.python_version()}", - "system": f"{platform.system()} {platform.release()}", - "runtime": time.time() - - temp_data.data.get("start_time", time.time()), # 运行时间秒数 - "bots": len(nonebot.get_bots()), - } - return result +import platform +import time + +import nonebot +import psutil +from cpuinfo import cpuinfo +from nonebot import require +from nonebot.adapters import satori + +from src.utils import __NAME__, __VERSION__ +from src.utils.base.config import get_config +from src.utils.base.data_manager import TempConfig, common_db +from src.utils.base.language import Language +from src.utils.base.resource import get_loaded_resource_packs, get_path +from src.utils.message.html_tool import template2image +from src.utils import satori_utils +from .counter_for_satori import satori_counter +from git import Repo + +# require("nonebot_plugin_apscheduler") +# from nonebot_plugin_apscheduler import scheduler + +commit_hash = Repo(".").head.commit.hexsha + +protocol_names = { + 0: "iPad", + 1: "Android Phone", + 2: "Android Watch", + 3: "Mac", + 5: "iPad", + 6: "Android Pad", +} + +""" +Universal Interface +data +- bot + - name: str + icon: str + id: int + protocol_name: str + groups: int + friends: int + message_sent: int + message_received: int + app_name: str +- hardware + - cpu + - percent: float + - name: str + - mem + - percent: float + - total: int + - used: int + - free: int + - swap + - percent: float + - total: int + - used: int + - free: int + - disk: list + - name: str + - percent: float + - total: int +""" +# status_card_cache = {} # lang -> bytes + + +# 60s刷新一次 +# 之前写的什么鬼玩意,这么重要的功能这样写??? +# @scheduler.scheduled_job("cron", second="*/40") +# async def refresh_status_card(): +# nonebot.logger.debug("Refreshing status card cache.") +# global status_card_cache +# status_card_cache = {} +# bot_data = await get_bots_data() +# hardware_data = await get_hardware_data() +# liteyuki_data = await get_liteyuki_data() +# for lang in status_card_cache.keys(): +# status_card_cache[lang] = await generate_status_card( +# bot_data, +# hardware_data, +# liteyuki_data, +# lang=lang, +# use_cache=False +# ) + + +# 获取状态卡片 +# bot_id 参数已经是bot参数的一部分了,不需要保留,但为了“兼容性”…… +async def generate_status_card( + bot: dict, + hardware: dict, + liteyuki: dict, + lang="zh-CN", + bot_id="0", +) -> bytes: + return await template2image( + get_path("templates/status.html", abs_path=True), + { + "data": { + "bot": bot, + "hardware": hardware, + "liteyuki": liteyuki, + "localization": get_local_data(lang), + } + }, + ) + + +def get_local_data(lang_code) -> dict: + lang = Language(lang_code) + return { + "friends": lang.get("status.friends"), + "groups": lang.get("status.groups"), + "plugins": lang.get("status.plugins"), + "bots": lang.get("status.bots"), + "message_sent": lang.get("status.message_sent"), + "message_received": lang.get("status.message_received"), + "cpu": lang.get("status.cpu"), + "memory": lang.get("status.memory"), + "swap": lang.get("status.swap"), + "disk": lang.get("status.disk"), + "usage": lang.get("status.usage"), + "total": lang.get("status.total"), + "used": lang.get("status.used"), + "free": lang.get("status.free"), + "days": lang.get("status.days"), + "hours": lang.get("status.hours"), + "minutes": lang.get("status.minutes"), + "seconds": lang.get("status.seconds"), + "runtime": lang.get("status.runtime"), + "threads": lang.get("status.threads"), + "cores": lang.get("status.cores"), + "process": lang.get("status.process"), + "resources": lang.get("status.resources"), + "description": lang.get("status.description"), + } + + +async def get_bots_data(self_id: str = "0") -> dict: + """获取当前所有机器人数据 + Returns: + """ + result = { + "self_id": self_id, + "bots": [], + } + for bot_id, bot in nonebot.get_bots().items(): + groups = 0 + friends = 0 + status = {} + bot_name = bot_id + version_info = {} + if isinstance(bot, satori.Bot): + try: + bot_name = (await satori_utils.user_infos.get(bot.self_id)).name + groups = str(await satori_utils.count_groups(bot)) + friends = str(await satori_utils.count_friends(bot)) + status = {} + version_info = await bot.get_version_info() + except Exception: + pass + else: + try: + # API fetch + bot_name = (await bot.get_login_info())["nickname"] + groups = len(await bot.get_group_list()) + friends = len(await bot.get_friend_list()) + status = await bot.get_status() + version_info = await bot.get_version_info() + except Exception: + pass + + statistics = status.get("stat", {}) + app_name = version_info.get("app_name", "UnknownImplementation") + if app_name in ["Lagrange.OneBot", "LLOneBot", "Shamrock", "NapCat.Onebot"]: + icon = f"https://q.qlogo.cn/g?b=qq&nk={bot_id}&s=640" + elif isinstance(bot, satori.Bot): + app_name = "Satori" + icon = (await bot.login_get()).user.avatar + else: + icon = None + bot_data = { + "name": bot_name, + "icon": icon, + "id": bot_id, + "protocol_name": protocol_names.get( + version_info.get("protocol_name"), "Online" + ), + "groups": groups, + "friends": friends, + "message_sent": ( + satori_counter.msg_sent + if isinstance(bot, satori.Bot) + else statistics.get("message_sent", 0) + ), + "message_received": ( + satori_counter.msg_received + if isinstance(bot, satori.Bot) + else statistics.get("message_received", 0) + ), + "app_name": app_name, + } + result["bots"].append(bot_data) + + return result + + +async def get_hardware_data() -> dict: + mem = psutil.virtual_memory() + all_processes = psutil.Process().children(recursive=True) + all_processes.append(psutil.Process()) + + mem_used_process = 0 + process_mem = {} + for process in all_processes: + try: + ps_name = process.name().replace(".exe", "") + if ps_name not in process_mem: + process_mem[ps_name] = 0 + process_mem[ps_name] += process.memory_info().rss + mem_used_process += process.memory_info().rss + except Exception: + pass + swap = psutil.swap_memory() + cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "Unknown") + if "AMD" in cpu_brand_raw: + brand = "AMD" + elif "Intel" in cpu_brand_raw: + brand = "Intel" + else: + brand = "Unknown" + result = { + "cpu": { + "percent": psutil.cpu_percent(), + "name": f"{brand} {cpuinfo.get_cpu_info().get('arch', 'Unknown')}", + "cores": psutil.cpu_count(logical=False), + "threads": psutil.cpu_count(logical=True), + "freq": psutil.cpu_freq().current, # MHz + }, + "memory": { + "percent": mem.percent, + "total": mem.total, + "used": mem.used, + "free": mem.free, + "usedProcess": mem_used_process, + }, + "swap": { + "percent": swap.percent, + "total": swap.total, + "used": swap.used, + "free": swap.free, + }, + "disk": [], + } + + for disk in psutil.disk_partitions(all=True): + try: + disk_usage = psutil.disk_usage(disk.mountpoint) + if disk_usage.total == 0: + continue # 虚拟磁盘 + result["disk"].append( + { + "name": disk.mountpoint, + "percent": disk_usage.percent, + "total": disk_usage.total, + "used": disk_usage.used, + "free": disk_usage.free, + } + ) + except: + pass + + return result + + +async def get_liteyuki_data() -> dict: + temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig()) + result = { + "name": list(get_config("nickname", [__NAME__]))[0], + "version": f"{__VERSION__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}", + "plugins": len(nonebot.get_loaded_plugins()), + "resources": len(get_loaded_resource_packs()), + "nonebot": f"{nonebot.__version__}", + "python": f"{platform.python_implementation()} {platform.python_version()}", + "system": f"{platform.system()} {platform.release()}", + "runtime": time.time() + - temp_data.data.get("start_time", time.time()), # 运行时间秒数 + "bots": len(nonebot.get_bots()), + } + return result diff --git a/src/plugins/liteyuki_status/counter_for_satori.py b/src/nonebot_plugins/liteyuki_status/counter_for_satori.py similarity index 94% rename from src/plugins/liteyuki_status/counter_for_satori.py rename to src/nonebot_plugins/liteyuki_status/counter_for_satori.py index 4a5bd224..74907304 100644 --- a/src/plugins/liteyuki_status/counter_for_satori.py +++ b/src/nonebot_plugins/liteyuki_status/counter_for_satori.py @@ -1,10 +1,10 @@ -class SatoriCounter: - msg_sent: int - msg_received: int - - def __init__(self): - self.msg_sent = 0 - self.msg_received = 0 - - -satori_counter = SatoriCounter() +class SatoriCounter: + msg_sent: int + msg_received: int + + def __init__(self): + self.msg_sent = 0 + self.msg_received = 0 + + +satori_counter = SatoriCounter() diff --git a/src/plugins/liteyuki_status/status.py b/src/nonebot_plugins/liteyuki_status/status.py similarity index 96% rename from src/plugins/liteyuki_status/status.py rename to src/nonebot_plugins/liteyuki_status/status.py index 237dfd57..8ca7138e 100644 --- a/src/plugins/liteyuki_status/status.py +++ b/src/nonebot_plugins/liteyuki_status/status.py @@ -1,60 +1,60 @@ -from src.utils import event as event_utils -from src.utils.base.language import get_user_lang -from src.utils.base.ly_typing import T_Bot, T_MessageEvent -from .api import * - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, UniMessage - -status_alc = on_alconna( - aliases={"状态"}, - command=Alconna( - "status", - Subcommand( - "memory", - alias={"mem", "m", "内存"}, - ), - Subcommand( - "process", - alias={"proc", "p", "进程"}, - ), - Subcommand( - "refresh", - alias={"refr", "r", "刷新"}, - ), - ), -) - -status_card_cache = {} # lang -> bytes - - -@status_alc.handle() -async def _(event: T_MessageEvent, bot: T_Bot): - ulang = get_user_lang(event_utils.get_user_id(event)) - global status_card_cache - if ulang.lang_code not in status_card_cache.keys() or ( - ulang.lang_code in status_card_cache.keys() - and time.time() - status_card_cache[ulang.lang_code][1] > 60 - ): - status_card_cache[ulang.lang_code] = ( - await generate_status_card( - bot=await get_bots_data(), - hardware=await get_hardware_data(), - liteyuki=await get_liteyuki_data(), - lang=ulang.lang_code, - bot_id=bot.self_id, - ), - time.time(), - ) - image = status_card_cache[ulang.lang_code][0] - await status_alc.finish(UniMessage.image(raw=image)) - - -@status_alc.assign("memory") -async def _(): - pass - - -@status_alc.assign("process") -async def _(): - pass +from src.utils import event as event_utils +from src.utils.base.language import get_user_lang +from src.utils.base.ly_typing import T_Bot, T_MessageEvent +from .api import * + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, UniMessage + +status_alc = on_alconna( + aliases={"状态"}, + command=Alconna( + "status", + Subcommand( + "memory", + alias={"mem", "m", "内存"}, + ), + Subcommand( + "process", + alias={"proc", "p", "进程"}, + ), + Subcommand( + "refresh", + alias={"refr", "r", "刷新"}, + ), + ), +) + +status_card_cache = {} # lang -> bytes + + +@status_alc.handle() +async def _(event: T_MessageEvent, bot: T_Bot): + ulang = get_user_lang(event_utils.get_user_id(event)) + global status_card_cache + if ulang.lang_code not in status_card_cache.keys() or ( + ulang.lang_code in status_card_cache.keys() + and time.time() - status_card_cache[ulang.lang_code][1] > 60 + ): + status_card_cache[ulang.lang_code] = ( + await generate_status_card( + bot=await get_bots_data(), + hardware=await get_hardware_data(), + liteyuki=await get_liteyuki_data(), + lang=ulang.lang_code, + bot_id=bot.self_id, + ), + time.time(), + ) + image = status_card_cache[ulang.lang_code][0] + await status_alc.finish(UniMessage.image(raw=image)) + + +@status_alc.assign("memory") +async def _(): + pass + + +@status_alc.assign("process") +async def _(): + pass diff --git a/src/plugins/liteyuki_uniblacklist/__init__.py b/src/nonebot_plugins/liteyuki_uniblacklist/__init__.py similarity index 95% rename from src/plugins/liteyuki_uniblacklist/__init__.py rename to src/nonebot_plugins/liteyuki_uniblacklist/__init__.py index b1fa2479..0f599d1f 100644 --- a/src/plugins/liteyuki_uniblacklist/__init__.py +++ b/src/nonebot_plugins/liteyuki_uniblacklist/__init__.py @@ -1,17 +1,17 @@ -from nonebot.plugin import PluginMetadata -from .api import * - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="联合黑名单(测试中...)", - description="", - usage="", - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki": True, - "toggleable" : True, - "default_enable" : True, - } -) - +from nonebot.plugin import PluginMetadata +from .api import * + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="联合黑名单(测试中...)", + description="", + usage="", + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki": True, + "toggleable" : True, + "default_enable" : True, + } +) + diff --git a/src/plugins/liteyuki_uniblacklist/api.py b/src/nonebot_plugins/liteyuki_uniblacklist/api.py similarity index 95% rename from src/plugins/liteyuki_uniblacklist/api.py rename to src/nonebot_plugins/liteyuki_uniblacklist/api.py index 31695c4c..52ca276c 100644 --- a/src/plugins/liteyuki_uniblacklist/api.py +++ b/src/nonebot_plugins/liteyuki_uniblacklist/api.py @@ -1,59 +1,59 @@ -import datetime - -import aiohttp -import httpx -import nonebot -from nonebot import require -from nonebot.exception import IgnoredException -from nonebot.message import event_preprocessor -from nonebot_plugin_alconna.typings import Event - -require("nonebot_plugin_apscheduler") - -from nonebot_plugin_apscheduler import scheduler - -blacklist_data: dict[str, set[str]] = {} -blacklist: set[str] = set() - - -@scheduler.scheduled_job("interval", minutes=10, next_run_time=datetime.datetime.now()) -async def update_blacklist(): - await request_for_blacklist() - - -async def request_for_blacklist(): - global blacklist - urls = [ - "https://cdn.liteyuki.icu/static/ubl/" - ] - - platforms = [ - "qq" - ] - - for plat in platforms: - for url in urls: - url += f"{plat}.txt" - async with aiohttp.ClientSession() as client: - resp = await client.get(url) - blacklist_data[plat] = set((await resp.text()).splitlines()) - blacklist = get_uni_set() - nonebot.logger.info("blacklists updated") - - -def get_uni_set() -> set: - s = set() - for new_set in blacklist_data.values(): - s.update(new_set) - return s - - -@event_preprocessor -async def pre_handle(event: Event): - try: - user_id = str(event.get_user_id()) - except: - return - - if user_id in get_uni_set(): - raise IgnoredException("UserId in blacklist") +import datetime + +import aiohttp +import httpx +import nonebot +from nonebot import require +from nonebot.exception import IgnoredException +from nonebot.message import event_preprocessor +from nonebot_plugin_alconna.typings import Event + +require("nonebot_plugin_apscheduler") + +from nonebot_plugin_apscheduler import scheduler + +blacklist_data: dict[str, set[str]] = {} +blacklist: set[str] = set() + + +@scheduler.scheduled_job("interval", minutes=10, next_run_time=datetime.datetime.now()) +async def update_blacklist(): + await request_for_blacklist() + + +async def request_for_blacklist(): + global blacklist + urls = [ + "https://cdn.liteyuki.icu/static/ubl/" + ] + + platforms = [ + "qq" + ] + + for plat in platforms: + for url in urls: + url += f"{plat}.txt" + async with aiohttp.ClientSession() as client: + resp = await client.get(url) + blacklist_data[plat] = set((await resp.text()).splitlines()) + blacklist = get_uni_set() + nonebot.logger.info("blacklists updated") + + +def get_uni_set() -> set: + s = set() + for new_set in blacklist_data.values(): + s.update(new_set) + return s + + +@event_preprocessor +async def pre_handle(event: Event): + try: + user_id = str(event.get_user_id()) + except: + return + + if user_id in get_uni_set(): + raise IgnoredException("UserId in blacklist") diff --git a/src/plugins/liteyuki_user/__init__.py b/src/nonebot_plugins/liteyuki_user/__init__.py similarity index 96% rename from src/plugins/liteyuki_user/__init__.py rename to src/nonebot_plugins/liteyuki_user/__init__.py index 8023d50c..13abb265 100644 --- a/src/plugins/liteyuki_user/__init__.py +++ b/src/nonebot_plugins/liteyuki_user/__init__.py @@ -1,16 +1,16 @@ -from nonebot.plugin import PluginMetadata - -from .profile_manager import * - -__author__ = "snowykami" -__plugin_meta__ = PluginMetadata( - name="轻雪用户管理", - description="用户管理插件", - usage="", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki" : True, - "toggleable" : False, - "default_enable": True, - } -) +from nonebot.plugin import PluginMetadata + +from .profile_manager import * + +__author__ = "snowykami" +__plugin_meta__ = PluginMetadata( + name="轻雪用户管理", + description="用户管理插件", + usage="", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki" : True, + "toggleable" : False, + "default_enable": True, + } +) diff --git a/src/plugins/liteyuki_user/const.py b/src/nonebot_plugins/liteyuki_user/const.py similarity index 98% rename from src/plugins/liteyuki_user/const.py rename to src/nonebot_plugins/liteyuki_user/const.py index 42fde237..9228da90 100644 --- a/src/plugins/liteyuki_user/const.py +++ b/src/nonebot_plugins/liteyuki_user/const.py @@ -1,23 +1,23 @@ -representative_timezones_list = [ - "Etc/GMT+12", # 国际日期变更线西 - "Pacific/Honolulu", # 夏威夷标准时间 - "America/Anchorage", # 阿拉斯加标准时间 - "America/Los_Angeles", # 美国太平洋标准时间 - "America/Denver", # 美国山地标准时间 - "America/Chicago", # 美国中部标准时间 - "America/New_York", # 美国东部标准时间 - "Europe/London", # 英国标准时间 - "Europe/Paris", # 中欧标准时间 - "Europe/Moscow", # 莫斯科标准时间 - "Asia/Dubai", # 阿联酋标准时间 - "Asia/Kolkata", # 印度标准时间 - "Asia/Shanghai", # 中国标准时间 - "Asia/Hong_Kong", # 中国香港标准时间 - "Asia/Chongqing", # 中国重庆标准时间 - "Asia/Macau", # 中国澳门标准时间 - "Asia/Taipei", # 中国台湾标准时间 - "Asia/Tokyo", # 日本标准时间 - "Australia/Sydney", # 澳大利亚东部标准时间 - "Pacific/Auckland" # 新西兰标准时间 -] +representative_timezones_list = [ + "Etc/GMT+12", # 国际日期变更线西 + "Pacific/Honolulu", # 夏威夷标准时间 + "America/Anchorage", # 阿拉斯加标准时间 + "America/Los_Angeles", # 美国太平洋标准时间 + "America/Denver", # 美国山地标准时间 + "America/Chicago", # 美国中部标准时间 + "America/New_York", # 美国东部标准时间 + "Europe/London", # 英国标准时间 + "Europe/Paris", # 中欧标准时间 + "Europe/Moscow", # 莫斯科标准时间 + "Asia/Dubai", # 阿联酋标准时间 + "Asia/Kolkata", # 印度标准时间 + "Asia/Shanghai", # 中国标准时间 + "Asia/Hong_Kong", # 中国香港标准时间 + "Asia/Chongqing", # 中国重庆标准时间 + "Asia/Macau", # 中国澳门标准时间 + "Asia/Taipei", # 中国台湾标准时间 + "Asia/Tokyo", # 日本标准时间 + "Australia/Sydney", # 澳大利亚东部标准时间 + "Pacific/Auckland" # 新西兰标准时间 +] representative_timezones_list.sort() \ No newline at end of file diff --git a/src/plugins/liteyuki_user/input_handle.py b/src/nonebot_plugins/liteyuki_user/input_handle.py similarity index 100% rename from src/plugins/liteyuki_user/input_handle.py rename to src/nonebot_plugins/liteyuki_user/input_handle.py diff --git a/src/plugins/liteyuki_user/profile_manager.py b/src/nonebot_plugins/liteyuki_user/profile_manager.py similarity index 97% rename from src/plugins/liteyuki_user/profile_manager.py rename to src/nonebot_plugins/liteyuki_user/profile_manager.py index 1044e71e..bdde211e 100644 --- a/src/plugins/liteyuki_user/profile_manager.py +++ b/src/nonebot_plugins/liteyuki_user/profile_manager.py @@ -1,150 +1,150 @@ -from typing import Optional - -import pytz -from nonebot import require - -from src.utils.base.data import LiteModel, Database -from src.utils.base.data_manager import User, user_db, group_db -from src.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang -from src.utils.base.ly_typing import T_Bot, T_MessageEvent -from src.utils.message.message import MarkdownMessage as md -from .const import representative_timezones_list -from src.utils import event as event_utils - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna - -profile_alc = on_alconna( - Alconna( - "profile", - Subcommand( - "set", - Args["key", str]["value", str, None], - alias=["s", "设置"], - ), - Subcommand( - "get", - Args["key", str], - alias=["g", "查询"], - ), - ), - aliases={"用户信息"} -) - - -# json储存 -class Profile(LiteModel): - lang: str = "zh-CN" - nickname: str = "" - timezone: str = "Asia/Shanghai" - location: str = "" - - -@profile_alc.handle() -async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): - user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), - default=User(user_id=str(event_utils.get_user_id(event)))) - ulang = get_user_lang(str(event_utils.get_user_id(event))) - if result.subcommands.get("set"): - if result.subcommands["set"].args.get("value"): - # 对合法性进行校验后设置 - r = set_profile(result.args["key"], result.args["value"], str(event_utils.get_user_id(event))) - if r: - user.profile[result.args["key"]] = result.args["value"] - user_db.save(user) # 数据库保存 - await profile_alc.finish( - ulang.get( - "user.profile.set_success", - ATTR=ulang.get(f"user.profile.{result.args['key']}"), - VALUE=result.args["value"] - ) - ) - else: - await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}"))) - else: - # 未输入值,尝试呼出菜单 - menu = get_profile_menu(result.args["key"], ulang) - if menu: - await md.send_md(menu, bot, event=event) - else: - await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}"))) - - user.profile[result.args["key"]] = result.args["value"] - - elif result.subcommands.get("get"): - if result.args["key"] in user.profile: - await profile_alc.finish(user.profile[result.args["key"]]) - else: - await profile_alc.finish("无此键值") - else: - profile = Profile(**user.profile) - - for k, v in user.profile.items(): - profile.__setattr__(k, v) - - reply = f"# {ulang.get('user.profile.info')}\n***\n" - - hidden_attr = ["id", "TABLE_NAME"] - enter_attr = ["lang", "timezone"] - - for key in sorted(profile.dict().keys()): - if key in hidden_attr: - continue - val = profile.dict()[key] - key_text = ulang.get(f"user.profile.{key}") - btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}", - enter=True if key in enter_attr else False) - reply += (f"\n**{key_text}** **{val}**\n" - f"\n> {ulang.get(f'user.profile.{key}.desc')}" - f"\n> {btn_set} \n\n***\n") - await md.send_md(reply, bot, event=event) - - -def get_profile_menu(key: str, ulang: Language) -> Optional[str]: - """获取属性的markdown菜单 - Args: - ulang: 用户语言 - key: 属性键 - - Returns: - - """ - setting_name = ulang.get(f"user.profile.{key}") - - no_menu = ["id", "nickname", "location"] - - if key in no_menu: - return None - - reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n" - if key == "lang": - for lang_code, lang_name in get_all_lang().items(): - btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}") - reply += f"\n{btn_set_lang}\n***\n" - elif key == "timezone": - for tz in representative_timezones_list: - btn_set_tz = md.btn_cmd(tz, f"profile set {key} {tz}") - reply += f"{btn_set_tz}\n***\n" - return reply - - -def set_profile(key: str, value: str, user_id: str) -> bool: - """设置属性,使用if分支对每一个合法性进行检查 - Args: - user_id: - key: - value: - - Returns: - 是否成功设置,输入合法性不通过返回False - - """ - if key == "lang": - if value in get_all_lang(): - change_user_lang(user_id, value) - return True - elif key == "timezone": - if value in pytz.all_timezones: - return True - elif key == "nickname": - return True +from typing import Optional + +import pytz +from nonebot import require + +from src.utils.base.data import LiteModel, Database +from src.utils.base.data_manager import User, user_db, group_db +from src.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang +from src.utils.base.ly_typing import T_Bot, T_MessageEvent +from src.utils.message.message import MarkdownMessage as md +from .const import representative_timezones_list +from src.utils import event as event_utils + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna + +profile_alc = on_alconna( + Alconna( + "profile", + Subcommand( + "set", + Args["key", str]["value", str, None], + alias=["s", "设置"], + ), + Subcommand( + "get", + Args["key", str], + alias=["g", "查询"], + ), + ), + aliases={"用户信息"} +) + + +# json储存 +class Profile(LiteModel): + lang: str = "zh-CN" + nickname: str = "" + timezone: str = "Asia/Shanghai" + location: str = "" + + +@profile_alc.handle() +async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot): + user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), + default=User(user_id=str(event_utils.get_user_id(event)))) + ulang = get_user_lang(str(event_utils.get_user_id(event))) + if result.subcommands.get("set"): + if result.subcommands["set"].args.get("value"): + # 对合法性进行校验后设置 + r = set_profile(result.args["key"], result.args["value"], str(event_utils.get_user_id(event))) + if r: + user.profile[result.args["key"]] = result.args["value"] + user_db.save(user) # 数据库保存 + await profile_alc.finish( + ulang.get( + "user.profile.set_success", + ATTR=ulang.get(f"user.profile.{result.args['key']}"), + VALUE=result.args["value"] + ) + ) + else: + await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}"))) + else: + # 未输入值,尝试呼出菜单 + menu = get_profile_menu(result.args["key"], ulang) + if menu: + await md.send_md(menu, bot, event=event) + else: + await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}"))) + + user.profile[result.args["key"]] = result.args["value"] + + elif result.subcommands.get("get"): + if result.args["key"] in user.profile: + await profile_alc.finish(user.profile[result.args["key"]]) + else: + await profile_alc.finish("无此键值") + else: + profile = Profile(**user.profile) + + for k, v in user.profile.items(): + profile.__setattr__(k, v) + + reply = f"# {ulang.get('user.profile.info')}\n***\n" + + hidden_attr = ["id", "TABLE_NAME"] + enter_attr = ["lang", "timezone"] + + for key in sorted(profile.dict().keys()): + if key in hidden_attr: + continue + val = profile.dict()[key] + key_text = ulang.get(f"user.profile.{key}") + btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}", + enter=True if key in enter_attr else False) + reply += (f"\n**{key_text}** **{val}**\n" + f"\n> {ulang.get(f'user.profile.{key}.desc')}" + f"\n> {btn_set} \n\n***\n") + await md.send_md(reply, bot, event=event) + + +def get_profile_menu(key: str, ulang: Language) -> Optional[str]: + """获取属性的markdown菜单 + Args: + ulang: 用户语言 + key: 属性键 + + Returns: + + """ + setting_name = ulang.get(f"user.profile.{key}") + + no_menu = ["id", "nickname", "location"] + + if key in no_menu: + return None + + reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n" + if key == "lang": + for lang_code, lang_name in get_all_lang().items(): + btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}") + reply += f"\n{btn_set_lang}\n***\n" + elif key == "timezone": + for tz in representative_timezones_list: + btn_set_tz = md.btn_cmd(tz, f"profile set {key} {tz}") + reply += f"{btn_set_tz}\n***\n" + return reply + + +def set_profile(key: str, value: str, user_id: str) -> bool: + """设置属性,使用if分支对每一个合法性进行检查 + Args: + user_id: + key: + value: + + Returns: + 是否成功设置,输入合法性不通过返回False + + """ + if key == "lang": + if value in get_all_lang(): + change_user_lang(user_id, value) + return True + elif key == "timezone": + if value in pytz.all_timezones: + return True + elif key == "nickname": + return True diff --git a/src/plugins/liteyuki_weather/__init__.py b/src/nonebot_plugins/liteyuki_weather/__init__.py similarity index 96% rename from src/plugins/liteyuki_weather/__init__.py rename to src/nonebot_plugins/liteyuki_weather/__init__.py index e939b573..2127a6ac 100644 --- a/src/plugins/liteyuki_weather/__init__.py +++ b/src/nonebot_plugins/liteyuki_weather/__init__.py @@ -1,27 +1,27 @@ -from nonebot.plugin import PluginMetadata -from nonebot import get_driver -from .qweather import * - -__plugin_meta__ = PluginMetadata( - name="轻雪天气", - description="基于和风天气api的天气插件", - usage="", - type="application", - homepage="https://github.com/snowykami/LiteyukiBot", - extra={ - "liteyuki" : True, - "toggleable" : True, - "default_enable": True, - } -) - -from ...utils.base.data_manager import set_memory_data - -driver = get_driver() - - -@driver.on_startup -async def _(): - # 检查是否为开发者模式 - is_dev = await check_key_dev(get_config("weather_key", "")) - set_memory_data("weather.is_dev", is_dev) +from nonebot.plugin import PluginMetadata +from nonebot import get_driver +from .qweather import * + +__plugin_meta__ = PluginMetadata( + name="轻雪天气", + description="基于和风天气api的天气插件", + usage="", + type="application", + homepage="https://github.com/snowykami/LiteyukiBot", + extra={ + "liteyuki" : True, + "toggleable" : True, + "default_enable": True, + } +) + +from ...utils.base.data_manager import set_memory_data + +driver = get_driver() + + +@driver.on_startup +async def _(): + # 检查是否为开发者模式 + is_dev = await check_key_dev(get_config("weather_key", "")) + set_memory_data("weather.is_dev", is_dev) diff --git a/src/plugins/liteyuki_weather/qw_api.py b/src/nonebot_plugins/liteyuki_weather/qw_api.py similarity index 96% rename from src/plugins/liteyuki_weather/qw_api.py rename to src/nonebot_plugins/liteyuki_weather/qw_api.py index 510d8215..8f28cd91 100644 --- a/src/plugins/liteyuki_weather/qw_api.py +++ b/src/nonebot_plugins/liteyuki_weather/qw_api.py @@ -1,171 +1,171 @@ -import aiohttp - -from .qw_models import * -import httpx - -from ...utils.base.data_manager import get_memory_data -from ...utils.base.language import Language - -dev_url = "https://devapi.qweather.com/" # 开发HBa -com_url = "https://api.qweather.com/" # 正式环境 - - -def get_qw_lang(lang: str) -> str: - if lang in ["zh-HK", "zh-TW"]: - return "zh-hant" - elif lang.startswith("zh"): - return "zh" - elif lang.startswith("en"): - return "en" - else: - return lang - - -async def check_key_dev(key: str) -> bool: - url = "https://api.qweather.com/v7/weather/now?" - params = { - "location": "101010100", - "key" : key, - } - async with aiohttp.ClientSession() as client: - resp = await client.get(url, params=params) - return (await resp.json()).get("code") != "200" # 查询不到付费数据为开发版 - - -def get_local_data(ulang_code: str) -> dict: - """ - 获取本地化数据 - Args: - ulang_code: - - Returns: - - """ - ulang = Language(ulang_code) - return { - "monday" : ulang.get("weather.monday"), - "tuesday" : ulang.get("weather.tuesday"), - "wednesday": ulang.get("weather.wednesday"), - "thursday" : ulang.get("weather.thursday"), - "friday" : ulang.get("weather.friday"), - "saturday" : ulang.get("weather.saturday"), - "sunday" : ulang.get("weather.sunday"), - "today" : ulang.get("weather.today"), - "tomorrow" : ulang.get("weather.tomorrow"), - "day" : ulang.get("weather.day"), - "night" : ulang.get("weather.night"), - "no_aqi" : ulang.get("weather.no_aqi"), - } - - -async def city_lookup( - location: str, - key: str, - adm: str = "", - number: int = 20, - lang: str = "zh", -) -> CityLookup: - """ - 通过关键字搜索城市信息 - Args: - location: - key: - adm: - number: - lang: 可传入标准i18n语言代码,如zh-CN、en-US等 - - Returns: - - """ - url = "https://geoapi.qweather.com/v2/city/lookup?" - params = { - "location": location, - "adm" : adm, - "number" : number, - "key" : key, - "lang" : lang, - } - async with httpx.AsyncClient() as client: - resp = await client.get(url, params=params) - return CityLookup.parse_obj(resp.json()) - - -async def get_weather_now( - key: str, - location: str, - lang: str = "zh", - unit: str = "m", - dev: bool = get_memory_data("is_dev", True), -) -> dict: - url_path = "v7/weather/now?" - url = dev_url + url_path if dev else com_url + url_path - params = { - "location": location, - "key" : key, - "lang" : lang, - "unit" : unit, - } - async with httpx.AsyncClient() as client: - resp = await client.get(url, params=params) - return resp.json() - - -async def get_weather_daily( - key: str, - location: str, - lang: str = "zh", - unit: str = "m", - dev: bool = get_memory_data("is_dev", True), -) -> dict: - url_path = "v7/weather/%dd?" % (7 if dev else 30) - url = dev_url + url_path if dev else com_url + url_path - params = { - "location": location, - "key" : key, - "lang" : lang, - "unit" : unit, - } - async with httpx.AsyncClient() as client: - resp = await client.get(url, params=params) - return resp.json() - - -async def get_weather_hourly( - key: str, - location: str, - lang: str = "zh", - unit: str = "m", - dev: bool = get_memory_data("is_dev", True), -) -> dict: - url_path = "v7/weather/%dh?" % (24 if dev else 168) - url = dev_url + url_path if dev else com_url + url_path - params = { - "location": location, - "key" : key, - "lang" : lang, - "unit" : unit, - } - async with httpx.AsyncClient() as client: - resp = await client.get(url, params=params) - return resp.json() - - -async def get_airquality( - key: str, - location: str, - lang: str, - pollutant: bool = False, - station: bool = False, - dev: bool = get_memory_data("is_dev", True), -) -> dict: - url_path = f"airquality/v1/now/{location}?" - url = dev_url + url_path if dev else com_url + url_path - params = { - "key" : key, - "lang" : lang, - "pollutant": pollutant, - "station" : station, - } - async with httpx.AsyncClient() as client: - resp = await client.get(url, params=params) - return resp.json() +import aiohttp + +from .qw_models import * +import httpx + +from ...utils.base.data_manager import get_memory_data +from ...utils.base.language import Language + +dev_url = "https://devapi.qweather.com/" # 开发HBa +com_url = "https://api.qweather.com/" # 正式环境 + + +def get_qw_lang(lang: str) -> str: + if lang in ["zh-HK", "zh-TW"]: + return "zh-hant" + elif lang.startswith("zh"): + return "zh" + elif lang.startswith("en"): + return "en" + else: + return lang + + +async def check_key_dev(key: str) -> bool: + url = "https://api.qweather.com/v7/weather/now?" + params = { + "location": "101010100", + "key" : key, + } + async with aiohttp.ClientSession() as client: + resp = await client.get(url, params=params) + return (await resp.json()).get("code") != "200" # 查询不到付费数据为开发版 + + +def get_local_data(ulang_code: str) -> dict: + """ + 获取本地化数据 + Args: + ulang_code: + + Returns: + + """ + ulang = Language(ulang_code) + return { + "monday" : ulang.get("weather.monday"), + "tuesday" : ulang.get("weather.tuesday"), + "wednesday": ulang.get("weather.wednesday"), + "thursday" : ulang.get("weather.thursday"), + "friday" : ulang.get("weather.friday"), + "saturday" : ulang.get("weather.saturday"), + "sunday" : ulang.get("weather.sunday"), + "today" : ulang.get("weather.today"), + "tomorrow" : ulang.get("weather.tomorrow"), + "day" : ulang.get("weather.day"), + "night" : ulang.get("weather.night"), + "no_aqi" : ulang.get("weather.no_aqi"), + } + + +async def city_lookup( + location: str, + key: str, + adm: str = "", + number: int = 20, + lang: str = "zh", +) -> CityLookup: + """ + 通过关键字搜索城市信息 + Args: + location: + key: + adm: + number: + lang: 可传入标准i18n语言代码,如zh-CN、en-US等 + + Returns: + + """ + url = "https://geoapi.qweather.com/v2/city/lookup?" + params = { + "location": location, + "adm" : adm, + "number" : number, + "key" : key, + "lang" : lang, + } + async with httpx.AsyncClient() as client: + resp = await client.get(url, params=params) + return CityLookup.parse_obj(resp.json()) + + +async def get_weather_now( + key: str, + location: str, + lang: str = "zh", + unit: str = "m", + dev: bool = get_memory_data("is_dev", True), +) -> dict: + url_path = "v7/weather/now?" + url = dev_url + url_path if dev else com_url + url_path + params = { + "location": location, + "key" : key, + "lang" : lang, + "unit" : unit, + } + async with httpx.AsyncClient() as client: + resp = await client.get(url, params=params) + return resp.json() + + +async def get_weather_daily( + key: str, + location: str, + lang: str = "zh", + unit: str = "m", + dev: bool = get_memory_data("is_dev", True), +) -> dict: + url_path = "v7/weather/%dd?" % (7 if dev else 30) + url = dev_url + url_path if dev else com_url + url_path + params = { + "location": location, + "key" : key, + "lang" : lang, + "unit" : unit, + } + async with httpx.AsyncClient() as client: + resp = await client.get(url, params=params) + return resp.json() + + +async def get_weather_hourly( + key: str, + location: str, + lang: str = "zh", + unit: str = "m", + dev: bool = get_memory_data("is_dev", True), +) -> dict: + url_path = "v7/weather/%dh?" % (24 if dev else 168) + url = dev_url + url_path if dev else com_url + url_path + params = { + "location": location, + "key" : key, + "lang" : lang, + "unit" : unit, + } + async with httpx.AsyncClient() as client: + resp = await client.get(url, params=params) + return resp.json() + + +async def get_airquality( + key: str, + location: str, + lang: str, + pollutant: bool = False, + station: bool = False, + dev: bool = get_memory_data("is_dev", True), +) -> dict: + url_path = f"airquality/v1/now/{location}?" + url = dev_url + url_path if dev else com_url + url_path + params = { + "key" : key, + "lang" : lang, + "pollutant": pollutant, + "station" : station, + } + async with httpx.AsyncClient() as client: + resp = await client.get(url, params=params) + return resp.json() diff --git a/src/plugins/liteyuki_weather/qw_models.py b/src/nonebot_plugins/liteyuki_weather/qw_models.py similarity index 94% rename from src/plugins/liteyuki_weather/qw_models.py rename to src/nonebot_plugins/liteyuki_weather/qw_models.py index 98795839..e546ea16 100644 --- a/src/plugins/liteyuki_weather/qw_models.py +++ b/src/nonebot_plugins/liteyuki_weather/qw_models.py @@ -1,62 +1,62 @@ -from src.utils.base.data import LiteModel - - -class Location(LiteModel): - name: str = "" - id: str = "" - lat: str = "" - lon: str = "" - adm2: str = "" - adm1: str = "" - country: str = "" - tz: str = "" - utcOffset: str = "" - isDst: str = "" - type: str = "" - rank: str = "" - fxLink: str = "" - sources: str = "" - license: str = "" - - -class CityLookup(LiteModel): - code: str = "" - location: list[Location] = [Location()] - - -class Now(LiteModel): - obsTime: str = "" - temp: str = "" - feelsLike: str = "" - icon: str = "" - text: str = "" - wind360: str = "" - windDir: str = "" - windScale: str = "" - windSpeed: str = "" - humidity: str = "" - precip: str = "" - pressure: str = "" - vis: str = "" - cloud: str = "" - dew: str = "" - sources: str = "" - license: str = "" - - -class WeatherNow(LiteModel): - code: str = "" - updateTime: str = "" - fxLink: str = "" - now: Now = Now() - - -class Daily(LiteModel): - pass - - -class WeatherDaily(LiteModel): - code: str = "" - updateTime: str = "" - fxLink: str = "" - daily: list[str] = [] +from src.utils.base.data import LiteModel + + +class Location(LiteModel): + name: str = "" + id: str = "" + lat: str = "" + lon: str = "" + adm2: str = "" + adm1: str = "" + country: str = "" + tz: str = "" + utcOffset: str = "" + isDst: str = "" + type: str = "" + rank: str = "" + fxLink: str = "" + sources: str = "" + license: str = "" + + +class CityLookup(LiteModel): + code: str = "" + location: list[Location] = [Location()] + + +class Now(LiteModel): + obsTime: str = "" + temp: str = "" + feelsLike: str = "" + icon: str = "" + text: str = "" + wind360: str = "" + windDir: str = "" + windScale: str = "" + windSpeed: str = "" + humidity: str = "" + precip: str = "" + pressure: str = "" + vis: str = "" + cloud: str = "" + dew: str = "" + sources: str = "" + license: str = "" + + +class WeatherNow(LiteModel): + code: str = "" + updateTime: str = "" + fxLink: str = "" + now: Now = Now() + + +class Daily(LiteModel): + pass + + +class WeatherDaily(LiteModel): + code: str = "" + updateTime: str = "" + fxLink: str = "" + daily: list[str] = [] diff --git a/src/plugins/liteyuki_weather/qweather.py b/src/nonebot_plugins/liteyuki_weather/qweather.py similarity index 97% rename from src/plugins/liteyuki_weather/qweather.py rename to src/nonebot_plugins/liteyuki_weather/qweather.py index 0be60f10..72d3aaee 100644 --- a/src/plugins/liteyuki_weather/qweather.py +++ b/src/nonebot_plugins/liteyuki_weather/qweather.py @@ -1,102 +1,102 @@ -from nonebot import require, on_endswith -from nonebot.adapters import satori -from nonebot.adapters.onebot.v11 import MessageSegment -from nonebot.internal.matcher import Matcher - -from src.utils.base.config import get_config -from src.utils.base.ly_typing import T_MessageEvent - -from .qw_api import * -from src.utils.base.data_manager import User, user_db -from src.utils.base.language import Language, get_user_lang -from src.utils.base.resource import get_path -from src.utils.message.html_tool import template2image -from src.utils import event as event_utils - -require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma, UniMessage - -wx_alc = on_alconna( - aliases={"天气"}, - command=Alconna( - "weather", - Args["keywords", MultiVar(str), []], - ), -) - - -@wx_alc.handle() -async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher): - """await alconna.send("weather", city)""" - kws = result.main_args.get("keywords") - image = await get_weather_now_card(matcher, event, kws) - await wx_alc.finish(UniMessage.image(raw=image)) - - -@on_endswith(("天气", "weather")).handle() -async def _(event: T_MessageEvent, matcher: Matcher): - """await alconna.send("weather", city)""" - # kws = event.message.extract_plain_text() - kws = event.get_plaintext() - image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False) - if isinstance(event, satori.event.Event): - await matcher.finish(satori.MessageSegment.image(raw=image, mime="image/png")) - else: - await matcher.finish(MessageSegment.image(image)) - - -async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True): - ulang = get_user_lang(event_utils.get_user_id(event)) - qw_lang = get_qw_lang(ulang.lang_code) - key = get_config("weather_key") - is_dev = get_memory_data("weather.is_dev", True) - user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), default=User()) - # params - unit = user.profile.get("unit", "m") - stored_location = user.profile.get("location", None) - - if not key: - await matcher.finish(ulang.get("weather.no_key") if tip else None) - - if keyword: - if len(keyword) >= 2: - adm = keyword[0] - city = keyword[-1] - else: - adm = "" - city = keyword[0] - city_info = await city_lookup(city, key, adm=adm, lang=qw_lang) - city_name = " ".join(keyword) - else: - if not stored_location: - await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None) - city_info = await city_lookup(stored_location, key, lang=qw_lang) - city_name = stored_location - if city_info.code == "200": - location_data = city_info.location[0] - else: - await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None) - weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev) - weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev) - weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev) - aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev) - - image = await template2image( - template=get_path("templates/weather_now.html", abs_path=True), - templates={ - "data": { - "params" : { - "unit": unit, - "lang": ulang.lang_code, - }, - "weatherNow" : weather_now, - "weatherDaily" : weather_daily, - "weatherHourly": weather_hourly, - "aqi" : aqi, - "location" : location_data.dump(), - "localization" : get_local_data(ulang.lang_code), - "is_dev": 1 if is_dev else 0 - } - }, - ) - return image +from nonebot import require, on_endswith +from nonebot.adapters import satori +from nonebot.adapters.onebot.v11 import MessageSegment +from nonebot.internal.matcher import Matcher + +from src.utils.base.config import get_config +from src.utils.base.ly_typing import T_MessageEvent + +from .qw_api import * +from src.utils.base.data_manager import User, user_db +from src.utils.base.language import Language, get_user_lang +from src.utils.base.resource import get_path +from src.utils.message.html_tool import template2image +from src.utils import event as event_utils + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma, UniMessage + +wx_alc = on_alconna( + aliases={"天气"}, + command=Alconna( + "weather", + Args["keywords", MultiVar(str), []], + ), +) + + +@wx_alc.handle() +async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher): + """await alconna.send("weather", city)""" + kws = result.main_args.get("keywords") + image = await get_weather_now_card(matcher, event, kws) + await wx_alc.finish(UniMessage.image(raw=image)) + + +@on_endswith(("天气", "weather")).handle() +async def _(event: T_MessageEvent, matcher: Matcher): + """await alconna.send("weather", city)""" + # kws = event.message.extract_plain_text() + kws = event.get_plaintext() + image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False) + if isinstance(event, satori.event.Event): + await matcher.finish(satori.MessageSegment.image(raw=image, mime="image/png")) + else: + await matcher.finish(MessageSegment.image(image)) + + +async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True): + ulang = get_user_lang(event_utils.get_user_id(event)) + qw_lang = get_qw_lang(ulang.lang_code) + key = get_config("weather_key") + is_dev = get_memory_data("weather.is_dev", True) + user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), default=User()) + # params + unit = user.profile.get("unit", "m") + stored_location = user.profile.get("location", None) + + if not key: + await matcher.finish(ulang.get("weather.no_key") if tip else None) + + if keyword: + if len(keyword) >= 2: + adm = keyword[0] + city = keyword[-1] + else: + adm = "" + city = keyword[0] + city_info = await city_lookup(city, key, adm=adm, lang=qw_lang) + city_name = " ".join(keyword) + else: + if not stored_location: + await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None) + city_info = await city_lookup(stored_location, key, lang=qw_lang) + city_name = stored_location + if city_info.code == "200": + location_data = city_info.location[0] + else: + await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None) + weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev) + weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev) + weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev) + aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev) + + image = await template2image( + template=get_path("templates/weather_now.html", abs_path=True), + templates={ + "data": { + "params" : { + "unit": unit, + "lang": ulang.lang_code, + }, + "weatherNow" : weather_now, + "weatherDaily" : weather_daily, + "weatherHourly": weather_hourly, + "aqi" : aqi, + "location" : location_data.dump(), + "localization" : get_local_data(ulang.lang_code), + "is_dev": 1 if is_dev else 0 + } + }, + ) + return image diff --git a/src/plugins/packmanv2/__init__.py b/src/nonebot_plugins/packmanv2/__init__.py similarity index 100% rename from src/plugins/packmanv2/__init__.py rename to src/nonebot_plugins/packmanv2/__init__.py diff --git a/src/plugins/packmanv2/handle.py b/src/nonebot_plugins/packmanv2/handle.py similarity index 100% rename from src/plugins/packmanv2/handle.py rename to src/nonebot_plugins/packmanv2/handle.py diff --git a/src/plugins/packmanv2/npm/__init__.py b/src/nonebot_plugins/packmanv2/npm/__init__.py similarity index 100% rename from src/plugins/packmanv2/npm/__init__.py rename to src/nonebot_plugins/packmanv2/npm/__init__.py diff --git a/src/plugins/packmanv2/npm/data_source.py b/src/nonebot_plugins/packmanv2/npm/data_source.py similarity index 100% rename from src/plugins/packmanv2/npm/data_source.py rename to src/nonebot_plugins/packmanv2/npm/data_source.py diff --git a/src/plugins/packmanv2/rpm/__init__.py b/src/nonebot_plugins/packmanv2/rpm/__init__.py similarity index 100% rename from src/plugins/packmanv2/rpm/__init__.py rename to src/nonebot_plugins/packmanv2/rpm/__init__.py diff --git a/src/plugins/webdash/__init__.py b/src/nonebot_plugins/webdash/__init__.py similarity index 100% rename from src/plugins/webdash/__init__.py rename to src/nonebot_plugins/webdash/__init__.py diff --git a/src/plugins/webdash/common.py b/src/nonebot_plugins/webdash/common.py similarity index 100% rename from src/plugins/webdash/common.py rename to src/nonebot_plugins/webdash/common.py diff --git a/src/plugins/webdash/main.py b/src/nonebot_plugins/webdash/main.py similarity index 100% rename from src/plugins/webdash/main.py rename to src/nonebot_plugins/webdash/main.py diff --git a/src/plugins/webdash/restful_api.py b/src/nonebot_plugins/webdash/restful_api.py similarity index 100% rename from src/plugins/webdash/restful_api.py rename to src/nonebot_plugins/webdash/restful_api.py diff --git a/src/utils/base/__init__.py b/src/utils/base/__init__.py index af167e0c..184e58d1 100644 --- a/src/utils/base/__init__.py +++ b/src/utils/base/__init__.py @@ -1,7 +1,7 @@ import threading 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"): @@ -14,6 +14,13 @@ def reload(delay: float = 0.0, receiver: str = "nonebot"): 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) - logger.info(f"Reloading LiteyukiBot({receiver})...") + if delay > 0: + threading.Timer(delay, chan.send, args=(1,)).start() + return + else: + chan.send(1)