mirror of
https://github.com/TriM-Organization/LiteyukiBot-TriM.git
synced 2025-09-08 13:16:23 +00:00
Ⓜ️手动从旧梦 81a191f merge
This commit is contained in:
27
src/nonebot_plugins/liteyuki_crt_utils/__init__.py
Normal file
27
src/nonebot_plugins/liteyuki_crt_utils/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
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子进程已接收到数据")
|
575
src/nonebot_plugins/liteyuki_crt_utils/canvas.py
Normal file
575
src/nonebot_plugins/liteyuki_crt_utils/canvas.py
Normal file
@ -0,0 +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")
|
0
src/nonebot_plugins/liteyuki_crt_utils/crt.py
Normal file
0
src/nonebot_plugins/liteyuki_crt_utils/crt.py
Normal file
78
src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py
Normal file
78
src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py
Normal file
@ -0,0 +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))
|
419
src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py
Normal file
419
src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py
Normal file
@ -0,0 +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()
|
125
src/nonebot_plugins/liteyuki_eventpush.py
Normal file
125
src/nonebot_plugins/liteyuki_eventpush.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
)
|
52
src/nonebot_plugins/liteyuki_markdowntest.py
Normal file
52
src/nonebot_plugins/liteyuki_markdowntest.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
)
|
15
src/nonebot_plugins/liteyuki_minigame/__init__.py
Normal file
15
src/nonebot_plugins/liteyuki_minigame/__init__.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
)
|
168
src/nonebot_plugins/liteyuki_minigame/game.py
Normal file
168
src/nonebot_plugins/liteyuki_minigame/game.py
Normal file
@ -0,0 +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
|
103
src/nonebot_plugins/liteyuki_minigame/minesweeper.py
Normal file
103
src/nonebot_plugins/liteyuki_minigame/minesweeper.py
Normal file
@ -0,0 +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("参数错误")
|
22
src/nonebot_plugins/liteyuki_pacman/__init__.py
Normal file
22
src/nonebot_plugins/liteyuki_pacman/__init__.py
Normal file
@ -0,0 +1,22 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .npm import *
|
||||
from .rpm import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪包管理器",
|
||||
description="本地插件管理和插件商店支持,资源包管理,支持启用/停用,安装/卸载插件",
|
||||
usage=(
|
||||
"npm list\n"
|
||||
"npm enable/disable <plugin_name>\n"
|
||||
"npm search <keywords...>\n"
|
||||
"npm install/uninstall <plugin_name>\n"
|
||||
),
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
"toggleable" : False,
|
||||
"default_enable" : True,
|
||||
}
|
||||
)
|
294
src/nonebot_plugins/liteyuki_pacman/common.py
Normal file
294
src/nonebot_plugins/liteyuki_pacman/common.py
Normal file
@ -0,0 +1,294 @@
|
||||
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)
|
846
src/nonebot_plugins/liteyuki_pacman/npm.py
Normal file
846
src/nonebot_plugins/liteyuki_pacman/npm.py
Normal file
@ -0,0 +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 <plugin_name')}*>\n"
|
||||
f"\n{btn_uninstall_}"
|
||||
f"\n\n>*{md.escape('npm uninstall <plugin_name')}*>\n"
|
||||
f"\n{btn_search}"
|
||||
f"\n\n>*{md.escape('npm search <keywords...')}*>\n"
|
||||
f"\n{btn_disable_global}"
|
||||
f"\n\n>*{md.escape('npm disable-global <plugin_name')}*>\n"
|
||||
f"\n{btn_enable_global}"
|
||||
f"\n\n>*{md.escape('npm enable-global <plugin_name')}*>\n"
|
||||
f"\n{btn_disable}"
|
||||
f"\n\n>*{md.escape('npm disable <plugin_name')}*>\n"
|
||||
f"\n{btn_enable}"
|
||||
f"\n\n>*{md.escape('npm enable <plugin_name')}*>\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 <plugin_name')}*>\n"
|
||||
f"\n{btn_enable}"
|
||||
f"\n\n>*{md.escape('npm enable <plugin_name')}*>\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
|
206
src/nonebot_plugins/liteyuki_pacman/rpm.py
Normal file
206
src/nonebot_plugins/liteyuki_pacman/rpm.py
Normal file
@ -0,0 +1,206 @@
|
||||
# 轻雪资源包管理器
|
||||
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:
|
||||
if reply:
|
||||
await matcher.finish(reply)
|
||||
else:
|
||||
await matcher.finish("根本没做帮助页,赶快去催神羽")
|
305
src/nonebot_plugins/liteyuki_plugin_dockdragon/__init__.py
Normal file
305
src/nonebot_plugins/liteyuki_plugin_dockdragon/__init__.py
Normal file
@ -0,0 +1,305 @@
|
||||
'''
|
||||
接龙
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
from asyncio import TimerHandle
|
||||
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import re
|
||||
import random
|
||||
import pypinyin
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot import on_regex, require
|
||||
from nonebot.params import RegexDict
|
||||
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
|
||||
|
||||
from typing_extensions import Annotated
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
require("nonebot_plugin_session")
|
||||
|
||||
|
||||
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
AlconnaQuery,
|
||||
Option,
|
||||
Query,
|
||||
Text,
|
||||
UniMessage,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_session import SessionId, SessionIdType
|
||||
|
||||
from .utils import random_idiom, legal_idiom, legal_patted_idiom, get_idiom
|
||||
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="接龙",
|
||||
description="汉字词语或成语接龙",
|
||||
usage=(
|
||||
"@我 + “接龙”开始游戏;\n"
|
||||
# "你有十次的机会猜一个四字词语;\n"
|
||||
# "每次猜测后,汉字与拼音的颜色将会标识其与正确答案的区别;\n"
|
||||
# "青色 表示其出现在答案中且在正确的位置;\n"
|
||||
# "橙色 表示其出现在答案中但不在正确的位置;\n"
|
||||
# "每个格子的 汉字、声母、韵母、声调 都会独立进行颜色的指示。\n"
|
||||
# "当四个格子都为青色时,你便赢得了游戏!\n"
|
||||
# "可发送“结束”结束游戏;可发送“提示”查看提示。\n"
|
||||
# "使用 --strict 选项开启非默认的成语检查,即猜测的短语必须是成语,\n"
|
||||
# "如:@我 猜成语 --strict"
|
||||
),
|
||||
type="application",
|
||||
# homepage="https://github.com/noneplugin/nonebot-plugin-handle",
|
||||
# config=Config,
|
||||
supported_adapters=inherit_supported_adapters(
|
||||
"nonebot_plugin_alconna", "nonebot_plugin_session"
|
||||
),
|
||||
extra={
|
||||
"example": "@小羿 接龙",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
# games: Dict[str, Dragle] = {}
|
||||
games = {}
|
||||
auto_echo = {}
|
||||
timers: Dict[str, TimerHandle] = {}
|
||||
|
||||
UserId = Annotated[str, SessionId(SessionIdType.GROUP)]
|
||||
|
||||
|
||||
def game_is_running(user_id: UserId) -> bool:
|
||||
return user_id in games
|
||||
|
||||
|
||||
def game_not_running(user_id: UserId) -> bool:
|
||||
return user_id not in games
|
||||
|
||||
|
||||
handle = on_alconna(
|
||||
Alconna("dockdragon", Option("-s|--strict", default=False, action=store_true)),
|
||||
aliases=("接龙",),
|
||||
rule=to_me() & game_not_running,
|
||||
use_cmd_start=True,
|
||||
block=True,
|
||||
priority=13,
|
||||
)
|
||||
handle_hint = on_alconna(
|
||||
"提示",
|
||||
rule=game_is_running,
|
||||
use_cmd_start=True,
|
||||
block=True,
|
||||
priority=13,
|
||||
)
|
||||
handle_stop = on_alconna(
|
||||
"结束",
|
||||
aliases=("结束游戏", "结束接龙"),
|
||||
rule=game_is_running,
|
||||
use_cmd_start=True,
|
||||
block=True,
|
||||
priority=13,
|
||||
)
|
||||
|
||||
|
||||
# handle_update = on_alconna(
|
||||
# "更新词库",
|
||||
# aliases=("刷新词库", "猜成语刷新词库"),
|
||||
# rule=to_me(),
|
||||
# use_cmd_start=True,
|
||||
# block=True,
|
||||
# priority=13,
|
||||
# )
|
||||
|
||||
def is_auto_echo(user_id: UserId) -> bool:
|
||||
return auto_echo.get(user_id, True)
|
||||
|
||||
|
||||
handle_idiom = on_regex(
|
||||
r"^(?P<idiom>[\u4e00-\u9fa5]{4})$",
|
||||
rule=is_auto_echo,
|
||||
block=True,
|
||||
priority=14,
|
||||
)
|
||||
|
||||
|
||||
停止自动回复 = on_alconna(
|
||||
"自动接龙",
|
||||
aliases=("自动成语接龙",),
|
||||
rule=None,
|
||||
use_cmd_start=True,
|
||||
block=True,
|
||||
priority=14,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def stop_game(user_id: str):
|
||||
if timer := timers.pop(user_id, None):
|
||||
timer.cancel()
|
||||
games.pop(user_id, None)
|
||||
|
||||
|
||||
async def stop_game_timeout(matcher: Matcher, user_id: str):
|
||||
game = games.get(user_id, None)
|
||||
stop_game(user_id)
|
||||
if game:
|
||||
msg = "接龙超时,游戏结束。"
|
||||
if len(game.guessed_idiom) >= 1:
|
||||
msg += f"\n{game.result}"
|
||||
await matcher.finish(msg)
|
||||
|
||||
|
||||
def set_timeout(matcher: Matcher, user_id: str, timeout: float = 300):
|
||||
if timer := timers.get(user_id, None):
|
||||
timer.cancel()
|
||||
loop = asyncio.get_running_loop()
|
||||
timer = loop.call_later(
|
||||
timeout, lambda: asyncio.ensure_future(stop_game_timeout(matcher, user_id))
|
||||
)
|
||||
timers[user_id] = timer
|
||||
|
||||
|
||||
|
||||
# @handle.handle()
|
||||
# async def _(
|
||||
# matcher: Matcher,
|
||||
# user_id: UserId,
|
||||
# strict: Query[bool] = AlconnaQuery("strict.value", False),
|
||||
# ):
|
||||
# # is_strict = handle_config.handle_strict_mode or strict.result
|
||||
# idiom, explanation = random_idiom()
|
||||
# game = Handle(idiom, explanation, strict=is_strict)
|
||||
|
||||
# games[user_id] = game
|
||||
# set_timeout(matcher, user_id)
|
||||
|
||||
# msg = Text(
|
||||
# f"你有{game.times}次机会猜一个四字成语,"
|
||||
# + ("发送有效成语以参与游戏。" if is_strict else "发送任意四字词语以参与游戏。")
|
||||
# ) + Image(raw=await run_sync(game.draw)())
|
||||
# await msg.send()
|
||||
|
||||
|
||||
@停止自动回复.handle()
|
||||
async def _(matcher: Matcher, user_id: UserId):
|
||||
if auto_echo.get(user_id, True):
|
||||
auto_echo[user_id] = False
|
||||
await matcher.finish("已关闭自动接龙回复")
|
||||
else:
|
||||
auto_echo[user_id] = True
|
||||
await matcher.finish("已开启自动接龙回复")
|
||||
|
||||
|
||||
|
||||
@handle_idiom.handle()
|
||||
async def _(matcher: Matcher, user_id: UserId, matched: Dict[str, Any] = RegexDict()):
|
||||
# game = games[user_id]
|
||||
# set_timeout(matcher, user_id)
|
||||
|
||||
idiom = str(matched["idiom"])
|
||||
# result = game.guess(idiom)
|
||||
|
||||
if legal_idiom(idiom):
|
||||
# stop_game(user_id)
|
||||
print(matcher.get_target())
|
||||
await matcher.finish(get_idiom(idiom,True,True))
|
||||
|
||||
# elif result == GuessResult.DUPLICATE:
|
||||
# await matcher.finish("你已经猜过这个成语了呢")
|
||||
|
||||
# elif result == GuessResult.ILLEGAL:
|
||||
# await matcher.finish(f"你确定“{idiom}”是个成语吗?")
|
||||
|
||||
# else:
|
||||
# await UniMessage.image(raw=await run_sync(game.draw)()).send()
|
||||
|
||||
|
||||
|
||||
|
||||
# zh = re.compile(r"[\u4e00-\u9fff]+")
|
||||
|
||||
|
||||
# @cat.on_text(states=["", "idle"])
|
||||
# async def handled():
|
||||
# '''自动接龙'''
|
||||
# text = cat.arg
|
||||
# r = zh.search(text)
|
||||
# if not r:
|
||||
# return
|
||||
|
||||
# word = r.group()
|
||||
|
||||
# for dragon in dragon_list:
|
||||
# # 跳过不启用的接龙
|
||||
# if not dragon.use:
|
||||
# continue
|
||||
|
||||
# # 当前词语符合接龙词库
|
||||
# if dragon.check(word):
|
||||
|
||||
# # 上次接龙
|
||||
# last = cat.cache.get("dragon", {}).get(dragon.name, "")
|
||||
|
||||
# # 成功接龙
|
||||
# if last and word:
|
||||
# p1 = lazy_pinyin(last)[-1]
|
||||
# p2 = lazy_pinyin(word)[0]
|
||||
# if p1 == p2:
|
||||
# await cat.send(f"[{cat.user.name}] 接龙成功!")
|
||||
|
||||
|
||||
# # 无论是否成功接龙都发送下一个词
|
||||
# word = dragon.next(word)
|
||||
# cat.cache.setdefault("dragon", {})
|
||||
# cat.cache["dragon"][dragon.name] = word
|
||||
# if not word:
|
||||
# word = choice(["%$#*-_", "你赢了", "接不上来..."])
|
||||
# await cat.send(word)
|
||||
# break
|
||||
|
||||
|
||||
# cat.set_wakeup_cmds(cmds="接龙管理")
|
||||
# cat.set_rest_cmds(cmds=["exit", "退出"])
|
||||
|
||||
|
||||
# @cat.on_cmd(cmds="list", states="idle")
|
||||
# async def list_all():
|
||||
# '''列出所有词库'''
|
||||
# items = ["所有词库:"]
|
||||
# for dragon in dragon_list:
|
||||
# if dragon.use:
|
||||
# items.append(f"[{dragon.name}] 正在使用")
|
||||
# else:
|
||||
# items.append(f"[{dragon.name}]")
|
||||
# await cat.send("\n".join(items))
|
||||
|
||||
|
||||
# @cat.on_cmd(cmds="data", states="idle")
|
||||
# async def show_data():
|
||||
# '''展示你的答题数据'''
|
||||
# gid = cat.group.id
|
||||
# uid = cat.user.id
|
||||
|
||||
# stmt = select(DragonUserData).filter_by(group_id=gid, user_id=uid)
|
||||
# cursor = cat.db_session.exec(stmt)
|
||||
# user_datas = cursor.all()
|
||||
|
||||
# if user_datas:
|
||||
# info = "\n".join(
|
||||
# f"[{u.dragon_name}] 接龙次数 {u.cnt}"
|
||||
# for u in user_datas
|
||||
# )
|
||||
# else:
|
||||
# info = "你还没有用过我...T_T"
|
||||
|
||||
# await cat.send(info)
|
||||
|
97
src/nonebot_plugins/liteyuki_plugin_dockdragon/debug.py
Normal file
97
src/nonebot_plugins/liteyuki_plugin_dockdragon/debug.py
Normal file
@ -0,0 +1,97 @@
|
||||
|
||||
import random
|
||||
|
||||
from pypinyin import Style, pinyin
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# fmt: off
|
||||
# 声母
|
||||
INITIALS = [
|
||||
"zh", "z", "y", "x", "w", "t", "sh", "s", "r", "q", "p",
|
||||
"n", "m", "l", "k", "j", "h", "g", "f", "d", "ch", "c", "b"
|
||||
]
|
||||
# 韵母
|
||||
FINALS = [
|
||||
"ün", "üe", "üan", "ü", "uo", "un", "ui", "ue", "uang",
|
||||
"uan", "uai","ua", "ou", "iu", "iong", "ong", "io", "ing",
|
||||
"in", "ie", "iao", "iang", "ian", "ia", "er", "eng", "en",
|
||||
"ei", "ao", "ang", "an", "ai", "u", "o", "i", "e", "a"
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def get_pinyin_of_n(word: str, which: int) -> List[Tuple[str, str, str]]:
|
||||
pys = pinyin(word, style=Style.TONE3, v_to_u=True,heteronym=True)[which]
|
||||
# py = p[0]
|
||||
results = []
|
||||
for py in pys:
|
||||
if py[-1].isdigit():
|
||||
tone = py[-1]
|
||||
py = py[:-1]
|
||||
else:
|
||||
tone = ""
|
||||
initial = ""
|
||||
for i in INITIALS:
|
||||
if py.startswith(i):
|
||||
initial = i
|
||||
break
|
||||
final = ""
|
||||
for f in FINALS:
|
||||
if py.endswith(f):
|
||||
final = f
|
||||
break
|
||||
results.append((initial, final, tone)) # 声母,韵母,声调
|
||||
return results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
LEGAL_PHRASES = [
|
||||
idiom.strip() for idiom in open("./resources/idioms_p.txt","r", encoding="utf-8").readlines()
|
||||
]
|
||||
sorted_phrases = dict([i for j in [[(py[0]+py[1],{"":[],"1":[],"2":[],"3":[],"4":[]}) for py in get_pinyin_of_n(idiom[0],0)] for idiom in LEGAL_PHRASES] for i in j])
|
||||
|
||||
for idiom in LEGAL_PHRASES:
|
||||
for py in get_pinyin_of_n(idiom[0],0):
|
||||
sorted_phrases[py[0]+py[1]][py[2]].append(idiom)
|
||||
|
||||
|
||||
|
||||
def legal_idiom(word: str) -> bool:
|
||||
return word in LEGAL_PHRASES
|
||||
|
||||
|
||||
|
||||
def legal_patted_idiom(former:str, laster: str, diff_word: bool,homophonic: bool) -> bool:
|
||||
"""
|
||||
判断成语是否符合接龙条件
|
||||
|
||||
Parameters
|
||||
==========
|
||||
former: str
|
||||
前一个成语
|
||||
laster: str
|
||||
后一个成语
|
||||
diff_word: bool
|
||||
异字模式:接龙之字无须一致
|
||||
homophonic: bool
|
||||
谐音模式:接龙之字可不同音调
|
||||
|
||||
"""
|
||||
return legal_idiom(laster) and legal_idiom(former) and ((((len({i[:2] for i in get_pinyin_of_n(laster[0],0)}.intersection({i[:2] for i in get_pinyin_of_n(former[-1],0)})))>0) if homophonic else (get_pinyin_of_n(laster,0)[0] == get_pinyin_of_n(former,-1)[0])) if diff_word else (former[-1] == laster[0] if homophonic else ((former[-1] == laster[0])and(get_pinyin_of_n(laster,0)[0] == get_pinyin_of_n(former,-1)[0]))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_idiom(idiom: str,diff_word: bool,homophonic: bool) -> str:
|
||||
return random.choice(([k for o in [[i for j in sorted_phrases[py[0]+py[1]].values() for i in j] for py in get_pinyin_of_n(idiom[-1],0)] for k in o] if homophonic else sorted_phrases[(py:=get_pinyin_of_n(idiom,-1)[0])[0]+py[1]][py[2]])if diff_word else ([k for o in [[i for j in sorted_phrases[py[0]+py[1]].values() for i in j if i[0] == idiom[-1]] for py in get_pinyin_of_n(idiom[-1],0)] for k in o] if homophonic else (lambda py:[i for i in sorted_phrases[py[0]+py[1]][py[2]] if i[0] == idiom[-1]])(get_pinyin_of_n(idiom,-1)[0])))
|
||||
|
||||
|
||||
while True:
|
||||
dw, homo = (bool(int(i)) for i in input("异字 异音:").split(" "))
|
||||
print(legal_patted_idiom((phra:=input("成语A:")),(phrb:=input("成语B:")),dw,homo),legal_idiom(phra),legal_idiom(phrb),"\n",get_idiom(phra,dw,homo),get_pinyin_of_n(phra,-1),get_pinyin_of_n(phrb,0))
|
||||
|
||||
|
30091
src/nonebot_plugins/liteyuki_plugin_dockdragon/resources/idioms_p.txt
Normal file
30091
src/nonebot_plugins/liteyuki_plugin_dockdragon/resources/idioms_p.txt
Normal file
File diff suppressed because it is too large
Load Diff
141
src/nonebot_plugins/liteyuki_plugin_dockdragon/utils.py
Normal file
141
src/nonebot_plugins/liteyuki_plugin_dockdragon/utils.py
Normal file
@ -0,0 +1,141 @@
|
||||
import random
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
from PIL import ImageFont
|
||||
|
||||
from PIL.ImageFont import FreeTypeFont
|
||||
|
||||
# from watchdog.observers import Observer
|
||||
# from watchdog.events import FileSystemEventHandler, FileModifiedEvent
|
||||
|
||||
from pypinyin import Style, pinyin
|
||||
|
||||
resource_dir = Path(__file__).parent / "resources"
|
||||
fonts_dir = resource_dir / "fonts"
|
||||
data_dir = resource_dir / "data"
|
||||
idiom_path = resource_dir / "idioms_p.txt"
|
||||
|
||||
|
||||
|
||||
|
||||
# fmt: off
|
||||
# 声母
|
||||
INITIALS = [
|
||||
"zh", "z", "y", "x", "w", "t", "sh", "s", "r", "q", "p",
|
||||
"n", "m", "l", "k", "j", "h", "g", "f", "d", "ch", "c", "b"
|
||||
]
|
||||
# 韵母
|
||||
FINALS = [
|
||||
"ün", "üe", "üan", "ü", "uo", "un", "ui", "ue", "uang",
|
||||
"uan", "uai","ua", "ou", "iu", "iong", "ong", "io", "ing",
|
||||
"in", "ie", "iao", "iang", "ian", "ia", "er", "eng", "en",
|
||||
"ei", "ao", "ang", "an", "ai", "u", "o", "i", "e", "a"
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def get_pinyin_of_n(word: str, which: int) -> List[Tuple[str, str, str]]:
|
||||
pys = pinyin(word, style=Style.TONE3, v_to_u=True,heteronym=True)[which]
|
||||
# py = p[0]
|
||||
results = []
|
||||
for py in pys:
|
||||
if py[-1].isdigit():
|
||||
tone = py[-1]
|
||||
py = py[:-1]
|
||||
else:
|
||||
tone = ""
|
||||
initial = ""
|
||||
for i in INITIALS:
|
||||
if py.startswith(i):
|
||||
initial = i
|
||||
break
|
||||
final = ""
|
||||
for f in FINALS:
|
||||
if py.endswith(f):
|
||||
final = f
|
||||
break
|
||||
results.append((initial, final, tone)) # 声母,韵母,声调
|
||||
return results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
LEGAL_PHRASES = [
|
||||
idiom.strip() for idiom in idiom_path.open("r", encoding="utf-8").readlines()
|
||||
]
|
||||
sorted_phrases = dict([i for j in [[(py[0]+py[1],{"":[],"1":[],"2":[],"3":[],"4":[]}) for py in get_pinyin_of_n(idiom[0],0)] for idiom in LEGAL_PHRASES] for i in j])
|
||||
|
||||
for idiom in LEGAL_PHRASES:
|
||||
for py in get_pinyin_of_n(idiom[0],0):
|
||||
sorted_phrases[py[0]+py[1]][py[2]].append(idiom)
|
||||
|
||||
|
||||
# class LegalPhrasesModifiedHandler(FileSystemEventHandler):
|
||||
# """
|
||||
# Handler for resource file changes
|
||||
# """
|
||||
|
||||
# def on_modified(self, event):
|
||||
# print(f"{event.src_path} modified, reloading resource...")
|
||||
# if "idioms.txt" in event.src_path:
|
||||
# global LEGAL_PHRASES
|
||||
# LEGAL_PHRASES = [
|
||||
# idiom.strip()
|
||||
# for idiom in idiom_path.open("r", encoding="utf-8").readlines()
|
||||
# ]
|
||||
# sorted_phrases = dict([i for j in [[(py[0]+py[1],{"":[],"1":[],"2":[],"3":[],"4":[]}) for py in get_pinyin_of_n(idiom[0],0)] for idiom in LEGAL_PHRASES] for i in j])
|
||||
|
||||
# for idiom in LEGAL_PHRASES:
|
||||
# for py in get_pinyin_of_n(idiom[0],0):
|
||||
# sorted_phrases[py[0]+py[1]][py[2]].append(idiom)
|
||||
|
||||
|
||||
# Observer().schedule(
|
||||
# LegalPhrasesModifiedHandler(),
|
||||
# data_dir,
|
||||
# recursive=False,
|
||||
# event_filter=FileModifiedEvent,
|
||||
# )
|
||||
|
||||
|
||||
def legal_idiom(word: str) -> bool:
|
||||
return word in LEGAL_PHRASES
|
||||
|
||||
|
||||
def random_idiom() -> str:
|
||||
return random.choice(LEGAL_PHRASES)
|
||||
|
||||
def legal_patted_idiom(former:str, laster: str, diff_word: bool,homophonic: bool) -> bool:
|
||||
"""
|
||||
判断成语是否符合接龙条件
|
||||
|
||||
Parameters
|
||||
==========
|
||||
former: str
|
||||
前一个成语
|
||||
laster: str
|
||||
后一个成语
|
||||
diff_word: bool
|
||||
异字模式:接龙之字无须一致
|
||||
homophonic: bool
|
||||
谐音模式:接龙之字可不同音调
|
||||
|
||||
"""
|
||||
return legal_idiom(laster) and legal_idiom(former) and ((((len({i[:2] for i in get_pinyin_of_n(laster[0],0)}.intersection({i[:2] for i in get_pinyin_of_n(former[-1],0)})))>0) if homophonic else (get_pinyin_of_n(laster,0)[0] == get_pinyin_of_n(former,-1)[0])) if diff_word else (former[-1] == laster[0] if homophonic else ((former[-1] == laster[0])and(get_pinyin_of_n(laster,0)[0] == get_pinyin_of_n(former,-1)[0]))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_idiom(idiom: str,diff_word: bool,homophonic: bool) -> str:
|
||||
return random.choice(([k for o in [[i for j in sorted_phrases[py[0]+py[1]].values() for i in j] for py in get_pinyin_of_n(idiom[-1],0)] for k in o] if homophonic else sorted_phrases[(py:=get_pinyin_of_n(idiom,-1)[0])[0]+py[1]][py[2]])if diff_word else ([k for o in [[i for j in sorted_phrases[py[0]+py[1]].values() for i in j if i[0] == idiom[-1]] for py in get_pinyin_of_n(idiom[-1],0)] for k in o] if homophonic else (lambda py:[i for i in sorted_phrases[py[0]+py[1]][py[2]] if i[0] == idiom[-1]])(get_pinyin_of_n(idiom,-1)[0])))
|
||||
|
||||
|
||||
|
||||
def load_font(name: str, fontsize: int) -> FreeTypeFont:
|
||||
return ImageFont.truetype(str(fonts_dir / name), fontsize, encoding="utf-8")
|
16
src/nonebot_plugins/liteyuki_satori_user_info/__init__.py
Normal file
16
src/nonebot_plugins/liteyuki_satori_user_info/__init__.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
)
|
24
src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py
Normal file
24
src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py
Normal file
@ -0,0 +1,24 @@
|
||||
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"
|
||||
)
|
163
src/nonebot_plugins/liteyuki_sign_status.py
Normal file
163
src/nonebot_plugins/liteyuki_sign_status.py
Normal file
@ -0,0 +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
|
29
src/nonebot_plugins/liteyuki_statistics/__init__.py
Normal file
29
src/nonebot_plugins/liteyuki_statistics/__init__.py
Normal file
@ -0,0 +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,
|
||||
},
|
||||
)
|
21
src/nonebot_plugins/liteyuki_statistics/common.py
Normal file
21
src/nonebot_plugins/liteyuki_statistics/common.py
Normal file
@ -0,0 +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")
|
||||
msg_db.auto_migrate(MessageEventModel())
|
168
src/nonebot_plugins/liteyuki_statistics/data_source.py
Normal file
168
src/nonebot_plugins/liteyuki_statistics/data_source.py
Normal file
@ -0,0 +1,168 @@
|
||||
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": "以{}为分割,在{}范围内的 {}".format(
|
||||
convert_seconds_to_time(period),
|
||||
convert_seconds_to_time(duration),
|
||||
ulang.get("stat.message"),
|
||||
)
|
||||
+ (f" 群聊:{group_id}" if group_id else "")
|
||||
+ (f" 机器:{bot_id}" if bot_id else "")
|
||||
+ (f" 用户:{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" 类别:{rank_type}"
|
||||
+ f" 制约:{limit}",
|
||||
"ranking": ranking,
|
||||
}
|
||||
}
|
||||
|
||||
return await template2image(
|
||||
get_path("templates/stat_rank.html"), templates, debug=True
|
||||
)
|
139
src/nonebot_plugins/liteyuki_statistics/stat_matchers.py
Normal file
139
src/nonebot_plugins/liteyuki_statistics/stat_matchers.py
Normal file
@ -0,0 +1,139 @@
|
||||
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))
|
88
src/nonebot_plugins/liteyuki_statistics/stat_monitors.py
Normal file
88
src/nonebot_plugins/liteyuki_statistics/stat_monitors.py
Normal file
@ -0,0 +1,88 @@
|
||||
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)
|
21
src/nonebot_plugins/liteyuki_statistics/word_cloud/LICENSE
Normal file
21
src/nonebot_plugins/liteyuki_statistics/word_cloud/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 hemengyang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,107 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import contextlib
|
||||
import re
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
from random import choice
|
||||
from typing import Optional
|
||||
|
||||
import jieba
|
||||
import jieba.analyse
|
||||
import numpy as np
|
||||
from emoji import replace_emoji
|
||||
from PIL import Image
|
||||
from wordcloud import WordCloud
|
||||
|
||||
from .config import global_config, plugin_config
|
||||
|
||||
|
||||
def pre_precess(msg: str) -> str:
|
||||
"""对消息进行预处理"""
|
||||
# 去除网址
|
||||
# https://stackoverflow.com/a/17773849/9212748
|
||||
url_regex = re.compile(
|
||||
r"(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]"
|
||||
r"+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})"
|
||||
)
|
||||
msg = url_regex.sub("", msg)
|
||||
|
||||
# 去除 \u200b
|
||||
msg = re.sub(r"\u200b", "", msg)
|
||||
|
||||
# 去除 emoji
|
||||
# https://github.com/carpedm20/emoji
|
||||
msg = replace_emoji(msg)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def analyse_message(msg: str) -> dict[str, float]:
|
||||
"""分析消息
|
||||
|
||||
分词,并统计词频
|
||||
"""
|
||||
# 设置停用词表
|
||||
if plugin_config.wordcloud_stopwords_path:
|
||||
jieba.analyse.set_stop_words(plugin_config.wordcloud_stopwords_path)
|
||||
# 加载用户词典
|
||||
if plugin_config.wordcloud_userdict_path:
|
||||
jieba.load_userdict(str(plugin_config.wordcloud_userdict_path))
|
||||
# 基于 TF-IDF 算法的关键词抽取
|
||||
# 返回所有关键词,因为设置了数量其实也只是 tags[:topK],不如交给词云库处理
|
||||
words = jieba.analyse.extract_tags(msg, topK=0, withWeight=True)
|
||||
return dict(words)
|
||||
|
||||
|
||||
def get_mask(key: str):
|
||||
"""获取 mask"""
|
||||
mask_path = plugin_config.get_mask_path(key)
|
||||
if mask_path.exists():
|
||||
return np.array(Image.open(mask_path))
|
||||
# 如果指定 mask 文件不存在,则尝试默认 mask
|
||||
default_mask_path = plugin_config.get_mask_path()
|
||||
if default_mask_path.exists():
|
||||
return np.array(Image.open(default_mask_path))
|
||||
|
||||
|
||||
def _get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]:
|
||||
# 过滤掉命令
|
||||
command_start = tuple(i for i in global_config.command_start if i)
|
||||
message = " ".join(m for m in messages if not m.startswith(command_start))
|
||||
# 预处理
|
||||
message = pre_precess(message)
|
||||
# 分析消息。分词,并统计词频
|
||||
frequency = analyse_message(message)
|
||||
# 词云参数
|
||||
wordcloud_options = {}
|
||||
wordcloud_options.update(plugin_config.wordcloud_options)
|
||||
wordcloud_options.setdefault("font_path", str(plugin_config.wordcloud_font_path))
|
||||
wordcloud_options.setdefault("width", plugin_config.wordcloud_width)
|
||||
wordcloud_options.setdefault("height", plugin_config.wordcloud_height)
|
||||
wordcloud_options.setdefault(
|
||||
"background_color", plugin_config.wordcloud_background_color
|
||||
)
|
||||
# 如果 colormap 是列表,则随机选择一个
|
||||
colormap = (
|
||||
plugin_config.wordcloud_colormap
|
||||
if isinstance(plugin_config.wordcloud_colormap, str)
|
||||
else choice(plugin_config.wordcloud_colormap)
|
||||
)
|
||||
wordcloud_options.setdefault("colormap", colormap)
|
||||
wordcloud_options.setdefault("mask", get_mask(mask_key))
|
||||
with contextlib.suppress(ValueError):
|
||||
wordcloud = WordCloud(**wordcloud_options)
|
||||
image = wordcloud.generate_from_frequencies(frequency).to_image()
|
||||
image_bytes = BytesIO()
|
||||
image.save(image_bytes, format="PNG")
|
||||
return image_bytes.getvalue()
|
||||
|
||||
|
||||
async def get_wordcloud(messages: list[str], mask_key: str) -> Optional[bytes]:
|
||||
loop = asyncio.get_running_loop()
|
||||
pfunc = partial(_get_wordcloud, messages, mask_key)
|
||||
# 虽然不知道具体是哪里泄漏了,但是通过每次关闭线程池可以避免这个问题
|
||||
# https://github.com/he0119/nonebot-plugin-wordcloud/issues/99
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
return await loop.run_in_executor(pool, pfunc)
|
23
src/nonebot_plugins/liteyuki_status/__init__.py
Normal file
23
src/nonebot_plugins/liteyuki_status/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .status import *
|
||||
|
||||
__author__ = "神羽SnowyKami & 金羿Eilles"
|
||||
__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,
|
||||
},
|
||||
)
|
296
src/nonebot_plugins/liteyuki_status/api.py
Normal file
296
src/nonebot_plugins/liteyuki_status/api.py
Normal file
@ -0,0 +1,296 @@
|
||||
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: "安卓掌机",
|
||||
2: "安卓穿戴",
|
||||
3: "Mac主机",
|
||||
5: "苹果iPad",
|
||||
6: "安卓平板",
|
||||
}
|
||||
|
||||
"""
|
||||
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",
|
||||
motto={"text": "风朗气清", "source": "成语一则"},
|
||||
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),
|
||||
"motto": motto,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
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", "未知应用接口")
|
||||
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"), "在线"
|
||||
),
|
||||
"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", "未知处理器")
|
||||
if "AMD" in cpu_brand_raw:
|
||||
brand = "AMD"
|
||||
elif "Intel" in cpu_brand_raw:
|
||||
brand = "英特尔"
|
||||
elif "Nvidia" in cpu_brand_raw:
|
||||
brand = "英伟达"
|
||||
else:
|
||||
brand = "未知处理器"
|
||||
result = {
|
||||
"cpu": {
|
||||
"percent": psutil.cpu_percent(),
|
||||
"name": f"{brand} {cpuinfo.get_cpu_info().get('arch', '未知架构')}",
|
||||
"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
|
10
src/nonebot_plugins/liteyuki_status/counter_for_satori.py
Normal file
10
src/nonebot_plugins/liteyuki_status/counter_for_satori.py
Normal file
@ -0,0 +1,10 @@
|
||||
class SatoriCounter:
|
||||
msg_sent: int
|
||||
msg_received: int
|
||||
|
||||
def __init__(self):
|
||||
self.msg_sent = 0
|
||||
self.msg_received = 0
|
||||
|
||||
|
||||
satori_counter = SatoriCounter()
|
303
src/nonebot_plugins/liteyuki_status/status.py
Normal file
303
src/nonebot_plugins/liteyuki_status/status.py
Normal file
@ -0,0 +1,303 @@
|
||||
import zhDateTime
|
||||
import requests
|
||||
import random
|
||||
|
||||
from src.utils import event as event_utils
|
||||
from src.utils.base.language import get_user_lang, get_default_lang_code, Language
|
||||
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,
|
||||
Option,
|
||||
store_true,
|
||||
# AlconnaQuery,
|
||||
# Query,
|
||||
Arparma,
|
||||
Args,
|
||||
)
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
status_alc = on_alconna(
|
||||
aliases={"状态"},
|
||||
command=Alconna(
|
||||
"status",
|
||||
Option(
|
||||
"-r|--refresh",
|
||||
default=False,
|
||||
alias={"refr", "r", "刷新"},
|
||||
action=store_true,
|
||||
),
|
||||
Subcommand(
|
||||
"memory",
|
||||
alias={"mem", "m", "内存"},
|
||||
),
|
||||
Subcommand(
|
||||
"process",
|
||||
alias={"proc", "p", "进程"},
|
||||
),
|
||||
# Subcommand(
|
||||
# "refresh",
|
||||
# alias={"refr", "r", "刷新"},
|
||||
# ),
|
||||
),
|
||||
)
|
||||
|
||||
yanlun = on_alconna(
|
||||
aliases={"yanlun", "言·论", "yan_lun"},
|
||||
command=Alconna(
|
||||
"言论",
|
||||
Option(
|
||||
"-r|--refresh",
|
||||
default=False,
|
||||
alias={"刷新", "更新", "update"},
|
||||
action=store_true,
|
||||
),
|
||||
Option("-c|--count", default=False, alias={"统计"}, action=store_true),
|
||||
Option("-l|--length", default=1.0, args=Args["length", float | int, 1.0]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
yanlun_path = (
|
||||
"https://gitee.com/TriM-Organization/LinglunStudio/raw/master/resources/myWords.txt"
|
||||
)
|
||||
|
||||
|
||||
# 每天4点更新
|
||||
@scheduler.scheduled_job("cron", hour=4)
|
||||
async def every_day_update():
|
||||
ulang = Language(get_default_lang_code(), "zh-WY")
|
||||
nonebot.logger.success(ulang.get("yanlun.refresh.success", COUNT=update_yanlun()))
|
||||
|
||||
|
||||
def update_yanlun():
|
||||
global yanlun_texts
|
||||
|
||||
solar_datetime = zhDateTime.DateTime.now()
|
||||
lunar_datetime = solar_datetime.to_lunar()
|
||||
solar_date = (solar_datetime.month, solar_datetime.day)
|
||||
lunar_date = (lunar_datetime.lunar_month, lunar_datetime.lunar_day)
|
||||
|
||||
if solar_date == (4, 3):
|
||||
yanlun_texts = ["金羿ELS 生日快乐~!", "Happy Birthday, Eilles!"]
|
||||
elif solar_date == (8, 6):
|
||||
yanlun_texts = ["诸葛亮与八卦阵 生日快乐~!", "Happy Birthday, bgArray~!"]
|
||||
elif solar_date == (8, 16):
|
||||
yanlun_texts = ["鱼旧梦 生日快乐~!", "Happy Birthday, ElapsingDreams~!"]
|
||||
|
||||
else:
|
||||
try:
|
||||
yanlun_texts = (
|
||||
requests.get(
|
||||
yanlun_path,
|
||||
)
|
||||
.text.strip("\n")
|
||||
.split("\n")
|
||||
)
|
||||
except (ConnectionError, requests.HTTPError, requests.RequestException) as E:
|
||||
nonebot.logger.warning(f"读取言·论信息发生 互联网连接 错误:\n{E}")
|
||||
yanlun_texts = ["以梦想为驱使 创造属于自己的未来"]
|
||||
# noinspection PyBroadException
|
||||
except BaseException as E:
|
||||
nonebot.logger.warning(f"读取言·论信息发生 未知 错误:\n{E}")
|
||||
yanlun_texts = ["灵光焕发 深艺献心"]
|
||||
|
||||
return len(yanlun_texts)
|
||||
|
||||
|
||||
update_yanlun()
|
||||
|
||||
|
||||
def random_yanlun() -> tuple:
|
||||
seq = random.choice(yanlun_texts).replace(" ", "\t").split("\t——", 1)
|
||||
return seq[0], "" if len(seq) == 1 else seq[1]
|
||||
|
||||
|
||||
status_card_cache = {} # lang -> bytes
|
||||
|
||||
|
||||
@status_alc.handle()
|
||||
async def _(
|
||||
result: Arparma,
|
||||
event: T_MessageEvent,
|
||||
bot: T_Bot,
|
||||
# refresh: Query[bool] = AlconnaQuery("refresh.value", False),
|
||||
):
|
||||
ulang = get_user_lang(event_utils.get_user_id(event)) # type: ignore
|
||||
global status_card_cache
|
||||
if (
|
||||
result.options["refresh"].value
|
||||
or 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] > 300 # 缓存
|
||||
)
|
||||
):
|
||||
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,
|
||||
motto=dict(zip(["text", "source"], random_yanlun())),
|
||||
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
|
||||
|
||||
|
||||
@yanlun.handle()
|
||||
async def _(
|
||||
result: Arparma,
|
||||
event: T_MessageEvent,
|
||||
bot: T_Bot,
|
||||
# refresh: Query[bool] = AlconnaQuery("refresh.value", False),
|
||||
# count: Query[bool] = AlconnaQuery("count.value", False),
|
||||
):
|
||||
# print(result.options)
|
||||
ulang = get_user_lang(event_utils.get_user_id(event)) # type: ignore
|
||||
if result.options["refresh"].value:
|
||||
global yanlun_texts
|
||||
try:
|
||||
yanlun_texts = (
|
||||
requests.get(
|
||||
yanlun_path,
|
||||
)
|
||||
.text.strip("\n")
|
||||
.split("\n")
|
||||
)
|
||||
await yanlun.send(
|
||||
UniMessage.text(
|
||||
ulang.get("yanlun.refresh.success", COUNT=len(yanlun_texts))
|
||||
)
|
||||
)
|
||||
except (ConnectionError, requests.HTTPError, requests.RequestException) as E:
|
||||
await yanlun.send(
|
||||
UniMessage.text(
|
||||
ulang.get(
|
||||
"yanlun.refresh.failed",
|
||||
ERR=ulang.get("yanlun.errtype.net"),
|
||||
ERRCODE=f"\n{E}",
|
||||
)
|
||||
)
|
||||
)
|
||||
yanlun_texts = ["以梦想为驱使 创造属于自己的未来"]
|
||||
# noinspection PyBroadException
|
||||
except BaseException as E:
|
||||
await yanlun.send(
|
||||
UniMessage.text(
|
||||
ulang.get(
|
||||
"yanlun.refresh.failed",
|
||||
ERR=ulang.get("yanlun.errtype.unknown"),
|
||||
ERRCODE=f"\n{E}",
|
||||
)
|
||||
)
|
||||
)
|
||||
yanlun_texts = ["灵光焕发 深艺献心"]
|
||||
if result.options["count"].value:
|
||||
authors = [
|
||||
(
|
||||
("B站")
|
||||
if ("\t——B站" in i.upper() or " ——B站" in i.upper())
|
||||
else (
|
||||
i.split("\t——")[1].replace(" ", "")
|
||||
if "\t——" in i
|
||||
else (
|
||||
i.split(" ——")[1].replace(" ", "")
|
||||
if " ——" in i
|
||||
else ("MYH")
|
||||
)
|
||||
)
|
||||
)
|
||||
for i in yanlun_texts
|
||||
]
|
||||
|
||||
total = len(yanlun_texts)
|
||||
|
||||
chart = sorted(
|
||||
[(i, authors.count(i)) for i in set(authors)],
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
await yanlun.send(
|
||||
UniMessage.text(
|
||||
ulang.get("yanlun.count.head").replace("ttt", "\t")
|
||||
+ "\n"
|
||||
+ "".join(
|
||||
[
|
||||
(
|
||||
"{}\t{}({}%)\n".format(
|
||||
aut, cnt, int(cnt * 10000 / total + 0.5) / 100
|
||||
)
|
||||
if cnt * 100 / total >= chart[10][1] * 100 / total
|
||||
else ""
|
||||
)
|
||||
for aut, cnt in chart
|
||||
]
|
||||
)
|
||||
+ ulang.get("yanlun.count.tail", NUM=total)
|
||||
)
|
||||
)
|
||||
|
||||
final_length = 0
|
||||
try:
|
||||
final_length += result.options["length"].args["length"]
|
||||
except:
|
||||
final_length = 1
|
||||
|
||||
(
|
||||
(
|
||||
await yanlun.finish(
|
||||
UniMessage.text(
|
||||
"\n".join([random.choice(yanlun_texts) for i in range(iill)])
|
||||
if iill <= 100
|
||||
else ulang.get("yanlun.length.toolong")
|
||||
)
|
||||
)
|
||||
if iill > 0
|
||||
else await yanlun.finish(
|
||||
UniMessage.text(ulang.get("yanlun.length.tooshort"))
|
||||
)
|
||||
)
|
||||
if (iill := int(final_length)) == final_length
|
||||
else await yanlun.finish(UniMessage.text(ulang.get("yanlun.length.float")))
|
||||
)
|
||||
|
||||
|
||||
time_query = on_alconna(
|
||||
command=Alconna(
|
||||
"时间",
|
||||
),
|
||||
aliases={"时间查询", "timeq", "timequery"},
|
||||
)
|
||||
|
||||
|
||||
@time_query.handle()
|
||||
async def _(
|
||||
event: T_MessageEvent,
|
||||
bot: T_Bot,
|
||||
):
|
||||
# ulang = get_user_lang(event_utils.get_user_id(event)) # type: ignore
|
||||
await time_query.finish(
|
||||
UniMessage.text(zhDateTime.DateTime.now().to_lunar().hanzify())
|
||||
)
|
17
src/nonebot_plugins/liteyuki_uniblacklist/__init__.py
Normal file
17
src/nonebot_plugins/liteyuki_uniblacklist/__init__.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
)
|
||||
|
59
src/nonebot_plugins/liteyuki_uniblacklist/api.py
Normal file
59
src/nonebot_plugins/liteyuki_uniblacklist/api.py
Normal file
@ -0,0 +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")
|
16
src/nonebot_plugins/liteyuki_user/__init__.py
Normal file
16
src/nonebot_plugins/liteyuki_user/__init__.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
)
|
23
src/nonebot_plugins/liteyuki_user/const.py
Normal file
23
src/nonebot_plugins/liteyuki_user/const.py
Normal file
@ -0,0 +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.sort()
|
0
src/nonebot_plugins/liteyuki_user/input_handle.py
Normal file
0
src/nonebot_plugins/liteyuki_user/input_handle.py
Normal file
150
src/nonebot_plugins/liteyuki_user/profile_manager.py
Normal file
150
src/nonebot_plugins/liteyuki_user/profile_manager.py
Normal file
@ -0,0 +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
|
13
src/nonebot_plugins/trimo_plugin_handle/LICENSE.md
Normal file
13
src/nonebot_plugins/trimo_plugin_handle/LICENSE.md
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
## 版权声明
|
||||
|
||||
本插件由 汉钰律许可协议 授权开源,兼容并继承自 MIT 许可协议。
|
||||
|
||||
Copyright (c) 2022 MeetWq
|
||||
|
||||
版权所有 © 2024 EillesWan & MeetWq
|
||||
|
||||
猜成语-睿乐特别版(trimo_plugin_handle)根据 第一版 汉钰律许可协议(“本协议”)授权。\
|
||||
任何人皆可从以下地址获得本协议副本:[汉钰律许可协议 第一版](https://gitee.com/EillesWan/YulvLicenses/raw/master/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE.MD)。\
|
||||
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
|
||||
详细的准许和限制条款请见原协议文本。
|
274
src/nonebot_plugins/trimo_plugin_handle/__init__.py
Normal file
274
src/nonebot_plugins/trimo_plugin_handle/__init__.py
Normal file
@ -0,0 +1,274 @@
|
||||
import asyncio
|
||||
from asyncio import TimerHandle
|
||||
from typing import Any, Dict
|
||||
|
||||
import nonebot
|
||||
from nonebot import on_regex, require, on_command
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import RegexDict
|
||||
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.utils import run_sync
|
||||
from nonebot.permission import SUPERUSER
|
||||
from typing_extensions import Annotated
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
require("nonebot_plugin_session")
|
||||
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
AlconnaQuery,
|
||||
Image,
|
||||
Option,
|
||||
Query,
|
||||
Text,
|
||||
UniMessage,
|
||||
on_alconna,
|
||||
store_true,
|
||||
Args,
|
||||
Arparma,
|
||||
)
|
||||
from nonebot_plugin_session import SessionId, SessionIdType
|
||||
|
||||
from .config import Config, handle_config
|
||||
from .data_source import GuessResult, Handle
|
||||
from .utils import random_idiom
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="猜成语",
|
||||
description="猜成语-睿乐特别版",
|
||||
usage=(
|
||||
"@我 + “猜成语”开始游戏;\n"
|
||||
"你有十次的机会猜一个四字词语;\n"
|
||||
"每次猜测后,汉字与拼音的颜色将会标识其与正确答案的区别;\n"
|
||||
"青色 表示其出现在答案中且在正确的位置;\n"
|
||||
"橙色 表示其出现在答案中但不在正确的位置;\n"
|
||||
"每个格子的 汉字、声母、韵母、声调 都会独立进行颜色的指示。\n"
|
||||
"当四个格子都为青色时,你便赢得了游戏!\n"
|
||||
"可发送“结束”结束游戏;可发送“提示”查看提示。\n"
|
||||
"使用 --strict 选项开启非默认的成语检查,即猜测的短语必须是成语,\n"
|
||||
"如:@我 猜成语 --strict"
|
||||
),
|
||||
type="application",
|
||||
homepage="https://github.com/noneplugin/nonebot-plugin-handle",
|
||||
config=Config,
|
||||
supported_adapters=inherit_supported_adapters(
|
||||
"nonebot_plugin_alconna", "nonebot_plugin_session"
|
||||
),
|
||||
extra={
|
||||
"example": "@小Q 猜成语",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
games: Dict[str, Handle] = {}
|
||||
timers: Dict[str, TimerHandle] = {}
|
||||
|
||||
UserId = Annotated[str, SessionId(SessionIdType.GROUP)]
|
||||
|
||||
|
||||
def game_is_running(user_id: UserId) -> bool:
|
||||
return user_id in games
|
||||
|
||||
|
||||
def game_not_running(user_id: UserId) -> bool:
|
||||
return user_id not in games
|
||||
|
||||
|
||||
handle = on_alconna(
|
||||
Alconna(
|
||||
"handle",
|
||||
Option("-s|--strict", default=False, action=store_true),
|
||||
Option("-d|--hard", default=False, action=store_true),
|
||||
),
|
||||
aliases=("猜成语",),
|
||||
rule=to_me() & game_not_running,
|
||||
use_cmd_start=True,
|
||||
block=True,
|
||||
priority=13,
|
||||
)
|
||||
handle_hint = on_alconna(
|
||||
"提示",
|
||||
rule=game_is_running,
|
||||
use_cmd_start=True,
|
||||
block=True,
|
||||
priority=13,
|
||||
)
|
||||
handle_stop = on_alconna(
|
||||
"结束",
|
||||
aliases=("结束游戏", "结束猜成语"),
|
||||
rule=game_is_running,
|
||||
use_cmd_start=True,
|
||||
block=True,
|
||||
priority=13,
|
||||
)
|
||||
|
||||
handle_answer = on_alconna(
|
||||
Alconna(
|
||||
"答案",
|
||||
Option(
|
||||
"-g|--group",
|
||||
default="Now",
|
||||
args=Args["group", str, "Now"],
|
||||
),
|
||||
Option(
|
||||
"-l|--list",
|
||||
default=False,
|
||||
action=store_true,
|
||||
),
|
||||
),
|
||||
# rule=game_is_running,
|
||||
use_cmd_start=True,
|
||||
permission=SUPERUSER,
|
||||
block=True,
|
||||
priority=13,
|
||||
)
|
||||
|
||||
# handle_update = on_alconna(
|
||||
# "更新词库",
|
||||
# aliases=("刷新词库", "猜成语刷新词库"),
|
||||
# rule=to_me(),
|
||||
# use_cmd_start=True,
|
||||
# block=True,
|
||||
# priority=13,
|
||||
# )
|
||||
|
||||
|
||||
handle_idiom = on_regex(
|
||||
r"^(?P<idiom>[\u4e00-\u9fa5]{4})$",
|
||||
rule=game_is_running,
|
||||
block=True,
|
||||
priority=14,
|
||||
)
|
||||
|
||||
|
||||
def stop_game(user_id: str):
|
||||
if timer := timers.pop(user_id, None):
|
||||
timer.cancel()
|
||||
games.pop(user_id, None)
|
||||
|
||||
|
||||
async def stop_game_timeout(matcher: Matcher, user_id: str):
|
||||
game = games.get(user_id, None)
|
||||
stop_game(user_id)
|
||||
if game:
|
||||
msg = "猜成语超时,游戏结束。"
|
||||
if len(game.guessed_idiom) >= 1:
|
||||
msg += f"\n{game.result}"
|
||||
await matcher.finish(msg)
|
||||
|
||||
|
||||
def set_timeout(matcher: Matcher, user_id: str, timeout: float = 300):
|
||||
if timer := timers.get(user_id, None):
|
||||
timer.cancel()
|
||||
loop = asyncio.get_running_loop()
|
||||
timer = loop.call_later(
|
||||
timeout, lambda: asyncio.ensure_future(stop_game_timeout(matcher, user_id))
|
||||
)
|
||||
timers[user_id] = timer
|
||||
|
||||
|
||||
@handle.handle()
|
||||
async def _(
|
||||
result: Arparma,
|
||||
matcher: Matcher,
|
||||
user_id: UserId,
|
||||
):
|
||||
nonebot.logger.info(result.options)
|
||||
is_strict = handle_config.handle_strict_mode or result.options["strict"].value
|
||||
idiom, explanation = random_idiom(result.options["hard"].value)
|
||||
game = Handle(idiom, explanation, strict=is_strict)
|
||||
|
||||
games[user_id] = game
|
||||
set_timeout(matcher, user_id)
|
||||
|
||||
msg = Text(
|
||||
f"你有{game.times}次机会猜一个四字成语,"
|
||||
+ ("发送有效成语以参与游戏。" if is_strict else "发送任意四字词语以参与游戏。")
|
||||
) + Image(raw=await run_sync(game.draw)())
|
||||
await msg.send()
|
||||
|
||||
|
||||
@handle_hint.handle()
|
||||
async def _(matcher: Matcher, user_id: UserId):
|
||||
game = games[user_id]
|
||||
set_timeout(matcher, user_id)
|
||||
|
||||
await UniMessage.image(raw=await run_sync(game.draw_hint)()).send()
|
||||
|
||||
|
||||
@handle_stop.handle()
|
||||
async def _(matcher: Matcher, user_id: UserId):
|
||||
game = games[user_id]
|
||||
stop_game(user_id)
|
||||
|
||||
msg = "游戏已结束"
|
||||
if len(game.guessed_idiom) >= 1:
|
||||
msg += f"\n{game.result}"
|
||||
await matcher.finish(msg)
|
||||
|
||||
|
||||
# @handle_update.handle()
|
||||
|
||||
|
||||
@handle_idiom.handle()
|
||||
async def _(matcher: Matcher, user_id: UserId, matched: Dict[str, Any] = RegexDict()):
|
||||
game = games[user_id]
|
||||
set_timeout(matcher, user_id)
|
||||
|
||||
idiom = str(matched["idiom"])
|
||||
result = game.guess(idiom)
|
||||
|
||||
if result in [GuessResult.WIN, GuessResult.LOSS]:
|
||||
stop_game(user_id)
|
||||
msg = Text(
|
||||
(
|
||||
"恭喜你猜出了成语!"
|
||||
if result == GuessResult.WIN
|
||||
else "很遗憾,没有人猜出来呢"
|
||||
)
|
||||
+ f"\n{game.result}"
|
||||
) + Image(raw=await run_sync(game.draw)())
|
||||
await msg.send()
|
||||
|
||||
elif result == GuessResult.DUPLICATE:
|
||||
await matcher.finish("你已经猜过这个成语了呢")
|
||||
|
||||
elif result == GuessResult.ILLEGAL:
|
||||
await matcher.finish(f"你确定“{idiom}”是个成语吗?")
|
||||
|
||||
else:
|
||||
await UniMessage.image(raw=await run_sync(game.draw)()).send()
|
||||
|
||||
|
||||
@handle_answer.handle()
|
||||
async def _(
|
||||
result: Arparma,
|
||||
matcher: Matcher,
|
||||
user_id: UserId,
|
||||
):
|
||||
|
||||
if result.options["list"].value:
|
||||
await handle_answer.finish(
|
||||
UniMessage.text(
|
||||
"\n".join("{}-{}".format(i, j.idiom) for i, j in games.items())
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
if result.options["group"].args["group"] == "Now":
|
||||
session_numstr = user_id
|
||||
else:
|
||||
session_numstr = "qq_OneBot V11_2378756507_{}".format(
|
||||
result.options["group"].args["group"]
|
||||
)
|
||||
except:
|
||||
session_numstr = user_id
|
||||
|
||||
if session_numstr in games.keys():
|
||||
await handle_answer.finish(UniMessage.text(games[session_numstr].idiom))
|
||||
else:
|
||||
await handle_answer.finish(
|
||||
UniMessage.text("{} 不存在开局的游戏".format(session_numstr))
|
||||
)
|
10
src/nonebot_plugins/trimo_plugin_handle/config.py
Normal file
10
src/nonebot_plugins/trimo_plugin_handle/config.py
Normal file
@ -0,0 +1,10 @@
|
||||
from nonebot import get_plugin_config
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
handle_strict_mode: bool = False
|
||||
handle_color_enhance: bool = False
|
||||
|
||||
|
||||
handle_config = get_plugin_config(Config)
|
284
src/nonebot_plugins/trimo_plugin_handle/data_source.py
Normal file
284
src/nonebot_plugins/trimo_plugin_handle/data_source.py
Normal file
@ -0,0 +1,284 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL.Image import Image as IMG
|
||||
|
||||
from .config import handle_config
|
||||
from .utils import get_pinyin, legal_idiom, load_font, save_jpg
|
||||
|
||||
|
||||
class GuessResult(Enum):
|
||||
WIN = 0 # 猜出正确成语
|
||||
LOSS = 1 # 达到最大可猜次数,未猜出正确成语
|
||||
DUPLICATE = 2 # 成语重复
|
||||
ILLEGAL = 3 # 成语不合法
|
||||
|
||||
|
||||
class GuessState(Enum):
|
||||
CORRECT = 0 # 存在且位置正确
|
||||
EXIST = 1 # 存在但位置不正确
|
||||
WRONG = 2 # 不存在
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColorGroup:
|
||||
bg_color: str # 背景颜色
|
||||
block_color: str # 方块颜色
|
||||
correct_color: str # 存在且位置正确时的颜色
|
||||
exist_color: str # 存在但位置不正确时的颜色
|
||||
wrong_color_pinyin: str # 不存在时的颜色
|
||||
wrong_color_char: str # 不存在时的颜色
|
||||
|
||||
|
||||
NORMAL_COLOR = ColorGroup(
|
||||
"#ffffff", "#f7f8f9", "#1d9c9c", "#de7525", "#b4b8be", "#5d6673"
|
||||
)
|
||||
|
||||
ENHANCED_COLOR = ColorGroup(
|
||||
"#ffffff", "#f7f8f9", "#5ba554", "#ff46ff", "#b4b8be", "#5d6673"
|
||||
)
|
||||
|
||||
|
||||
class Handle:
|
||||
def __init__(self, idiom: str, explanation: str, strict: bool = False):
|
||||
self.idiom: str = idiom # 成语
|
||||
self.explanation: str = explanation # 释义
|
||||
self.strict: bool = strict # 是否判断输入词语为成语
|
||||
self.result = f"【成语】:{idiom}\n【释义】:{explanation}"
|
||||
self.pinyin: List[Tuple[str, str, str]] = get_pinyin(idiom) # 拼音
|
||||
self.length = 4
|
||||
self.times: int = 10 # 可猜次数
|
||||
self.guessed_idiom: List[str] = [] # 记录已猜成语
|
||||
self.guessed_pinyin: List[List[Tuple[str, str, str]]] = [] # 记录已猜成语的拼音
|
||||
|
||||
self.block_size = (160, 160) # 文字块尺寸
|
||||
self.block_padding = (20, 20) # 文字块之间间距
|
||||
self.padding = (40, 40) # 边界间距
|
||||
font_size_char = 60 # 汉字字体大小
|
||||
font_size_pinyin = 30 # 拼音字体大小
|
||||
font_size_tone = 22 # 声调字体大小
|
||||
self.font_char = load_font("NotoSerifSC-Regular.otf", font_size_char)
|
||||
self.font_pinyin = load_font("NotoSansMono-Regular.ttf", font_size_pinyin)
|
||||
self.font_tone = load_font("NotoSansMono-Regular.ttf", font_size_tone)
|
||||
|
||||
self.colors = (
|
||||
ENHANCED_COLOR if handle_config.handle_color_enhance else NORMAL_COLOR
|
||||
)
|
||||
|
||||
def guess(self, idiom: str) -> Optional[GuessResult]:
|
||||
if self.strict and not legal_idiom(idiom):
|
||||
return GuessResult.ILLEGAL
|
||||
if idiom in self.guessed_idiom:
|
||||
return GuessResult.DUPLICATE
|
||||
self.guessed_idiom.append(idiom)
|
||||
self.guessed_pinyin.append(get_pinyin(idiom))
|
||||
if idiom == self.idiom:
|
||||
return GuessResult.WIN
|
||||
if len(self.guessed_idiom) == self.times:
|
||||
return GuessResult.LOSS
|
||||
|
||||
def draw_block(
|
||||
self,
|
||||
block_color: str,
|
||||
char: str = "",
|
||||
char_color: str = "",
|
||||
initial: str = "",
|
||||
initial_color: str = "",
|
||||
final: str = "",
|
||||
final_color: str = "",
|
||||
tone: str = "",
|
||||
tone_color: str = "",
|
||||
underline: bool = False,
|
||||
underline_color: str = "",
|
||||
) -> IMG:
|
||||
block = Image.new("RGB", self.block_size, block_color)
|
||||
if not char:
|
||||
return block
|
||||
draw = ImageDraw.Draw(block)
|
||||
|
||||
char_size = self.font_char.getbbox(char)[2:]
|
||||
x = (self.block_size[0] - char_size[0]) / 2
|
||||
y = (self.block_size[1] - char_size[1]) / 5 * 3
|
||||
draw.text((x, y), char, font=self.font_char, fill=char_color)
|
||||
|
||||
space = 5
|
||||
need_space = bool(initial and final)
|
||||
py_length = self.font_pinyin.getlength(initial + final)
|
||||
if need_space:
|
||||
py_length += space
|
||||
py_start = (self.block_size[0] - py_length) / 2
|
||||
x = py_start
|
||||
y = self.block_size[0] / 8
|
||||
draw.text((x, y), initial, font=self.font_pinyin, fill=initial_color)
|
||||
x += self.font_pinyin.getlength(initial)
|
||||
if need_space:
|
||||
x += space
|
||||
draw.text((x, y), final, font=self.font_pinyin, fill=final_color)
|
||||
|
||||
tone_size = self.font_tone.getbbox(tone)[2:]
|
||||
x = (self.block_size[0] + py_length) / 2 + tone_size[0] / 3
|
||||
y -= tone_size[1] / 3
|
||||
draw.text((x, y), tone, font=self.font_tone, fill=tone_color)
|
||||
|
||||
if underline:
|
||||
x = py_start
|
||||
py_size = self.font_pinyin.getbbox(initial + final)[2:]
|
||||
y = self.block_size[0] / 8 + py_size[1] + 2
|
||||
draw.line((x, y, x + py_length, y), fill=underline_color, width=1)
|
||||
y += 3
|
||||
draw.line((x, y, x + py_length, y), fill=underline_color, width=1)
|
||||
|
||||
return block
|
||||
|
||||
def draw(self) -> BytesIO:
|
||||
rows = min(len(self.guessed_idiom) + 1, self.times)
|
||||
board_w = self.length * self.block_size[0]
|
||||
board_w += (self.length - 1) * self.block_padding[0] + 2 * self.padding[0]
|
||||
board_h = rows * self.block_size[1]
|
||||
board_h += (rows - 1) * self.block_padding[1] + 2 * self.padding[1]
|
||||
board_size = (board_w, board_h)
|
||||
board = Image.new("RGB", board_size, self.colors.bg_color)
|
||||
|
||||
def get_states(guessed: List[str], answer: List[str]) -> List[GuessState]:
|
||||
states = []
|
||||
incorrect = []
|
||||
for i in range(self.length):
|
||||
if guessed[i] != answer[i]:
|
||||
incorrect.append(answer[i])
|
||||
else:
|
||||
incorrect.append("_")
|
||||
for i in range(self.length):
|
||||
if guessed[i] == answer[i]:
|
||||
states.append(GuessState.CORRECT)
|
||||
elif guessed[i] in incorrect:
|
||||
states.append(GuessState.EXIST)
|
||||
incorrect[incorrect.index(guessed[i])] = "_"
|
||||
else:
|
||||
states.append(GuessState.WRONG)
|
||||
return states
|
||||
|
||||
def get_pinyin_color(state: GuessState) -> str:
|
||||
if state == GuessState.CORRECT:
|
||||
return self.colors.correct_color
|
||||
elif state == GuessState.EXIST:
|
||||
return self.colors.exist_color
|
||||
else:
|
||||
return self.colors.wrong_color_pinyin
|
||||
|
||||
def get_char_color(state: GuessState) -> str:
|
||||
if state == GuessState.CORRECT:
|
||||
return self.colors.correct_color
|
||||
elif state == GuessState.EXIST:
|
||||
return self.colors.exist_color
|
||||
else:
|
||||
return self.colors.wrong_color_char
|
||||
|
||||
def block_pos(row: int, col: int) -> Tuple[int, int]:
|
||||
x = self.padding[0] + (self.block_size[0] + self.block_padding[0]) * col
|
||||
y = self.padding[1] + (self.block_size[1] + self.block_padding[1]) * row
|
||||
return x, y
|
||||
|
||||
for i in range(len(self.guessed_idiom)):
|
||||
idiom = self.guessed_idiom[i]
|
||||
pinyin = self.guessed_pinyin[i]
|
||||
char_states = get_states(list(idiom), list(self.idiom))
|
||||
initial_states = get_states(
|
||||
[p[0] for p in pinyin], [p[0] for p in self.pinyin]
|
||||
)
|
||||
final_states = get_states(
|
||||
[p[1] for p in pinyin], [p[1] for p in self.pinyin]
|
||||
)
|
||||
tone_states = get_states(
|
||||
[p[2] for p in pinyin], [p[2] for p in self.pinyin]
|
||||
)
|
||||
underline_states = get_states(
|
||||
[p[0] + p[1] for p in pinyin], [p[0] + p[1] for p in self.pinyin]
|
||||
)
|
||||
for j in range(self.length):
|
||||
char = idiom[j]
|
||||
i2, f2, t2 = pinyin[j]
|
||||
if char == self.idiom[j]:
|
||||
block_color = self.colors.correct_color
|
||||
char_color = initial_color = final_color = tone_color = (
|
||||
self.colors.bg_color
|
||||
)
|
||||
underline = False
|
||||
underline_color = ""
|
||||
else:
|
||||
block_color = self.colors.block_color
|
||||
char_color = get_char_color(char_states[j])
|
||||
initial_color = get_pinyin_color(initial_states[j])
|
||||
final_color = get_pinyin_color(final_states[j])
|
||||
tone_color = get_pinyin_color(tone_states[j])
|
||||
underline_color = get_pinyin_color(underline_states[j])
|
||||
underline = underline_color in (
|
||||
self.colors.correct_color,
|
||||
self.colors.exist_color,
|
||||
)
|
||||
block = self.draw_block(
|
||||
block_color,
|
||||
char,
|
||||
char_color,
|
||||
i2,
|
||||
initial_color,
|
||||
f2,
|
||||
final_color,
|
||||
t2,
|
||||
tone_color,
|
||||
underline,
|
||||
underline_color,
|
||||
)
|
||||
board.paste(block, block_pos(i, j))
|
||||
|
||||
for i in range(len(self.guessed_idiom), rows):
|
||||
for j in range(self.length):
|
||||
block = self.draw_block(self.colors.block_color)
|
||||
board.paste(block, block_pos(i, j))
|
||||
|
||||
return save_jpg(board)
|
||||
|
||||
def draw_hint(self) -> BytesIO:
|
||||
guessed_char = set("".join(self.guessed_idiom))
|
||||
guessed_initial = set()
|
||||
guessed_final = set()
|
||||
guessed_tone = set()
|
||||
for pinyin in self.guessed_pinyin:
|
||||
for p in pinyin:
|
||||
guessed_initial.add(p[0])
|
||||
guessed_final.add(p[1])
|
||||
guessed_tone.add(p[2])
|
||||
|
||||
board_w = self.length * self.block_size[0]
|
||||
board_w += (self.length - 1) * self.block_padding[0] + 2 * self.padding[0]
|
||||
board_h = self.block_size[1] + 2 * self.padding[1]
|
||||
board = Image.new("RGB", (board_w, board_h), self.colors.bg_color)
|
||||
|
||||
for i in range(self.length):
|
||||
char = self.idiom[i]
|
||||
hi, hf, ht = self.pinyin[i]
|
||||
color = char_c = initial_c = final_c = tone_c = self.colors.correct_color
|
||||
if char not in guessed_char:
|
||||
char = "?"
|
||||
color = self.colors.block_color
|
||||
char_c = self.colors.wrong_color_char
|
||||
else:
|
||||
char_c = initial_c = final_c = tone_c = self.colors.bg_color
|
||||
if hi not in guessed_initial:
|
||||
hi = "?"
|
||||
initial_c = self.colors.wrong_color_pinyin
|
||||
if hf not in guessed_final:
|
||||
hf = "?"
|
||||
final_c = self.colors.wrong_color_pinyin
|
||||
if ht not in guessed_tone:
|
||||
ht = "?"
|
||||
tone_c = self.colors.wrong_color_pinyin
|
||||
block = self.draw_block(
|
||||
color, char, char_c, hi, initial_c, hf, final_c, ht, tone_c
|
||||
)
|
||||
x = self.padding[0] + (self.block_size[0] + self.block_padding[0]) * i
|
||||
y = self.padding[1]
|
||||
board.paste(block, (x, y))
|
||||
return save_jpg(board)
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 MeetWq
|
||||
|
||||
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.
|
5686
src/nonebot_plugins/trimo_plugin_handle/resources/data/answers.json
Normal file
5686
src/nonebot_plugins/trimo_plugin_handle/resources/data/answers.json
Normal file
File diff suppressed because it is too large
Load Diff
73010
src/nonebot_plugins/trimo_plugin_handle/resources/data/answers_hard.json
Normal file
73010
src/nonebot_plugins/trimo_plugin_handle/resources/data/answers_hard.json
Normal file
File diff suppressed because it is too large
Load Diff
30091
src/nonebot_plugins/trimo_plugin_handle/resources/data/idioms.txt
Normal file
30091
src/nonebot_plugins/trimo_plugin_handle/resources/data/idioms.txt
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
118
src/nonebot_plugins/trimo_plugin_handle/utils.py
Normal file
118
src/nonebot_plugins/trimo_plugin_handle/utils.py
Normal file
@ -0,0 +1,118 @@
|
||||
import json
|
||||
import random
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# from watchdog.observers import Observer
|
||||
# from watchdog.events import FileSystemEventHandler, FileModifiedEvent
|
||||
|
||||
from PIL import ImageFont
|
||||
from PIL.Image import Image as IMG
|
||||
from PIL.ImageFont import FreeTypeFont
|
||||
from pypinyin import Style, pinyin
|
||||
|
||||
resource_dir = Path(__file__).parent / "resources"
|
||||
fonts_dir = resource_dir / "fonts"
|
||||
data_dir = resource_dir / "data"
|
||||
idiom_path = data_dir / "idioms.txt"
|
||||
answer_path = data_dir / "answers.json"
|
||||
answer_hard_path = data_dir / "answers_hard.json"
|
||||
|
||||
|
||||
LEGAL_PHRASES = [
|
||||
idiom.strip() for idiom in idiom_path.open("r", encoding="utf-8").readlines()
|
||||
]
|
||||
ANSWER_PHRASES: List[Dict[str, str]] = json.load(
|
||||
answer_path.open("r", encoding="utf-8")
|
||||
)
|
||||
HARD_ANSWER_PHRASES: List[Dict[str, str]] = json.load(
|
||||
answer_hard_path.open("r", encoding="utf-8")
|
||||
)
|
||||
|
||||
# class LegalPhrasesModifiedHandler(FileSystemEventHandler):
|
||||
# """
|
||||
# Handler for resource file changes
|
||||
# """
|
||||
|
||||
# def on_modified(self, event):
|
||||
# print(f"{event.src_path} modified, reloading resource...")
|
||||
# if "idioms.txt" in event.src_path:
|
||||
# global LEGAL_PHRASES
|
||||
# LEGAL_PHRASES = [
|
||||
# idiom.strip()
|
||||
# for idiom in idiom_path.open("r", encoding="utf-8").readlines()
|
||||
# ]
|
||||
# elif "answers.json" in event.src_path:
|
||||
# global ANSWER_PHRASES
|
||||
# ANSWER_PHRASES = json.load(
|
||||
# answer_path.open("r", encoding="utf-8")
|
||||
# )
|
||||
|
||||
|
||||
# Observer().schedule(
|
||||
# LegalPhrasesModifiedHandler(),
|
||||
# data_dir,
|
||||
# recursive=False,
|
||||
# event_filter=FileModifiedEvent,
|
||||
# )
|
||||
|
||||
|
||||
def legal_idiom(word: str) -> bool:
|
||||
return word in LEGAL_PHRASES
|
||||
|
||||
|
||||
def random_idiom(is_hard: bool = False) -> Tuple[str, str]:
|
||||
answer = random.choice(HARD_ANSWER_PHRASES if is_hard else ANSWER_PHRASES)
|
||||
return answer["word"], answer["explanation"]
|
||||
|
||||
|
||||
# fmt: off
|
||||
# 声母
|
||||
INITIALS = [
|
||||
"zh", "z", "y", "x", "w", "t", "sh", "s", "r", "q", "p",
|
||||
"n", "m", "l", "k", "j", "h", "g", "f", "d", "ch", "c", "b"
|
||||
]
|
||||
# 韵母
|
||||
FINALS = [
|
||||
"ün", "üe", "üan", "ü", "uo", "un", "ui", "ue", "uang",
|
||||
"uan", "uai","ua", "ou", "iu", "iong", "ong", "io", "ing",
|
||||
"in", "ie", "iao", "iang", "ian", "ia", "er", "eng", "en",
|
||||
"ei", "ao", "ang", "an", "ai", "u", "o", "i", "e", "a"
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def get_pinyin(idiom: str) -> List[Tuple[str, str, str]]:
|
||||
pys = pinyin(idiom, style=Style.TONE3, v_to_u=True)
|
||||
results = []
|
||||
for p in pys:
|
||||
py = p[0]
|
||||
if py[-1].isdigit():
|
||||
tone = py[-1]
|
||||
py = py[:-1]
|
||||
else:
|
||||
tone = ""
|
||||
initial = ""
|
||||
for i in INITIALS:
|
||||
if py.startswith(i):
|
||||
initial = i
|
||||
break
|
||||
final = ""
|
||||
for f in FINALS:
|
||||
if py.endswith(f):
|
||||
final = f
|
||||
break
|
||||
results.append((initial, final, tone)) # 声母,韵母,声调
|
||||
return results
|
||||
|
||||
|
||||
def save_jpg(frame: IMG) -> BytesIO:
|
||||
output = BytesIO()
|
||||
frame = frame.convert("RGB")
|
||||
frame.save(output, format="jpeg")
|
||||
return output
|
||||
|
||||
|
||||
def load_font(name: str, fontsize: int) -> FreeTypeFont:
|
||||
return ImageFont.truetype(str(fonts_dir / name), fontsize, encoding="utf-8")
|
27
src/nonebot_plugins/trimo_plugin_msctconverter/LICENSE.md
Normal file
27
src/nonebot_plugins/trimo_plugin_msctconverter/LICENSE.md
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
## 版权声明
|
||||
|
||||
本插件引用 [MusicPreview](https://gitee.com/ElapsingDreams/MusicPreview) 项目代码作为库(Library)使用,并加以修改;\
|
||||
此行为经过 Apache 2.0 许可协议 授权。
|
||||
|
||||
MusicPreview 著作声明:
|
||||
|
||||
@Author: Envision\
|
||||
@Github: ElapsingDreams\
|
||||
@Gitee: ElapsingDreams
|
||||
|
||||
|
||||
本插件由 汉钰律许可协议 授权开源,兼容并继承自 [伶伦转换器](https://gitee.com/TriM-Organization/Linglun-Converter) 项目的 Apache 2.0 许可协议。
|
||||
|
||||
继承版权声明:
|
||||
|
||||
Copyright © 2024 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") with TriM Org.
|
||||
|
||||
项目版权声明:
|
||||
|
||||
版权所有 © 2024 金羿(Eilles) & 诸葛亮与八卦阵(bgArray) with TriM Org.
|
||||
|
||||
伶伦转换器(trimo_plugin_msctconverter)根据 第一版 汉钰律许可协议(“本协议”)授权。\
|
||||
任何人皆可从以下地址获得本协议副本:[汉钰律许可协议 第一版](https://gitee.com/EillesWan/YulvLicenses/raw/master/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE.MD)。\
|
||||
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
|
||||
详细的准许和限制条款请见原协议文本。
|
@ -0,0 +1,218 @@
|
||||
**注意,以下条款或版权声明应当且必须是高于此仓库中任何其他声明的**
|
||||
|
||||
1. 伶伦的全部开发者享有其完整版权,其开发者可以在任一时刻终止以后伶伦源代码开放,若经由其开发者授予特殊权利,则授权对象可以将源代码进行特定的被特殊授权的操作
|
||||
2. 伶伦或(及)其代码允许在 Apache2.0 协议的条款与说明下进行非商业使用
|
||||
3. 除部分代码特殊声明外,伶伦允许对其或(及)其代码进行商业化使用,但是需要经过伶伦主要开发者(诸葛亮与八卦阵、金羿)的一致授权,同时,授权对象在商业化授权的使用过程中必须依照 Apache2.0 协议的条款与说明
|
||||
4. 若存在对于伶伦包含的部分代码的特殊开源声明,则此部分代码依照其特定的开源方式授权,但若此部分代码经由此部分代码的主要开发者一致特殊授权后商用,则授权对象在商用时依照此部分的开发者所准许的方式(或条款)进行商用,在经此部分的开发者准许后无其他特殊授权时,默认依照 Apache2.0 协议进行商业化使用
|
||||
5. Apache2.0 协议的英文原文副本可见下文
|
||||
|
||||
> The English Translation of the TERMS AND CONDITIONS above is listed below
|
||||
>
|
||||
> This translated version is for reference only and has no legal effect.
|
||||
>
|
||||
> The version with legal effect is the Chinese version above.
|
||||
|
||||
**Note, The TERMS AND CONDITIONS below should and must be above all others in this repository**
|
||||
|
||||
1. _Linglun Studio_ is fully copyrighted by all its developers, the developers have the right to make _Linglun Studio_ close sourced at any time. Operations are permitted under specific terms instructed by its developer(s).
|
||||
2. Non-commercial use of _Linglun Studio_ and(or) its source code is permitted under Apache License 2.0.
|
||||
3. Commercial use of _Linglun Studio_ is permitted under Apache License 2.0 with the unanimous permission of the steering developers of _Linglun Studio_ (*bgArray*诸葛亮与八卦阵 and *Eilles*金羿).
|
||||
4. _Linglun Studio_ is open sourced under priority given:
|
||||
1. License granted by the core developer(s) of a section after negotiation.
|
||||
2. Explicitly stated license.
|
||||
3. Apache 2.0 License.
|
||||
5. A copy of the original Apache Lisence 2.0 can be found below.
|
||||
|
||||
```text
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
Copyright © 2024 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") with TriM Org.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
@ -0,0 +1,9 @@
|
||||
"""
|
||||
@Author: Envision
|
||||
@Github: ElapsingDreams
|
||||
@Gitee: ElapsingDreams
|
||||
@Email: None
|
||||
@FileName: __init__.py
|
||||
@DateTime: 2024/3/8 20:48
|
||||
@SoftWare: PyCharm
|
||||
"""
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,383 @@
|
||||
from typing import Dict, Tuple
|
||||
|
||||
MM_DISLINK_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
0: "note.harp",
|
||||
1: "note.harp",
|
||||
2: "note.pling",
|
||||
3: "note.harp",
|
||||
4: "note.harp",
|
||||
5: "note.harp",
|
||||
6: "note.harp",
|
||||
7: "note.harp",
|
||||
8: "note.iron_xylophone", # 打击乐器无音域
|
||||
9: "note.bell",
|
||||
10: "note.iron_xylophone",
|
||||
11: "note.iron_xylophone",
|
||||
12: "note.iron_xylophone",
|
||||
13: "note.iron_xylophone",
|
||||
14: "note.chime",
|
||||
15: "note.iron_xylophone",
|
||||
16: "note.harp",
|
||||
17: "note.harp",
|
||||
18: "note.harp",
|
||||
19: "note.harp",
|
||||
20: "note.harp",
|
||||
21: "note.harp",
|
||||
22: "note.harp",
|
||||
23: "note.harp",
|
||||
24: "note.guitar",
|
||||
25: "note.guitar",
|
||||
26: "note.guitar",
|
||||
27: "note.guitar",
|
||||
28: "note.guitar",
|
||||
29: "note.guitar",
|
||||
30: "note.guitar",
|
||||
31: "note.guitar",
|
||||
32: "note.bass",
|
||||
33: "note.bass",
|
||||
34: "note.bass",
|
||||
35: "note.bass",
|
||||
36: "note.bass",
|
||||
37: "note.bass",
|
||||
38: "note.bass",
|
||||
39: "note.bass",
|
||||
40: "note.harp",
|
||||
41: "note.flute",
|
||||
42: "note.flute",
|
||||
43: "note.flute",
|
||||
44: "note.flute",
|
||||
45: "note.harp",
|
||||
46: "note.harp",
|
||||
47: "note.harp",
|
||||
48: "note.harp",
|
||||
49: "note.harp",
|
||||
50: "note.harp",
|
||||
51: "note.harp",
|
||||
52: "note.harp",
|
||||
53: "note.harp",
|
||||
54: "note.harp",
|
||||
55: "note.harp",
|
||||
56: "note.harp",
|
||||
57: "note.harp",
|
||||
58: "note.harp",
|
||||
59: "note.harp",
|
||||
60: "note.harp",
|
||||
61: "note.harp",
|
||||
62: "note.harp",
|
||||
63: "note.harp",
|
||||
64: "note.harp",
|
||||
65: "note.harp",
|
||||
66: "note.harp",
|
||||
67: "note.harp",
|
||||
68: "note.harp",
|
||||
69: "note.harp",
|
||||
70: "note.harp",
|
||||
71: "note.harp",
|
||||
72: "note.flute",
|
||||
73: "note.flute",
|
||||
74: "note.flute",
|
||||
75: "note.flute",
|
||||
76: "note.flute",
|
||||
77: "note.flute",
|
||||
78: "note.flute",
|
||||
79: "note.flute",
|
||||
80: "note.bit",
|
||||
81: "note.bit",
|
||||
82: "note.harp",
|
||||
83: "note.harp",
|
||||
84: "note.harp",
|
||||
85: "note.harp",
|
||||
86: "note.harp",
|
||||
87: "note.harp",
|
||||
88: "note.harp",
|
||||
89: "note.harp",
|
||||
90: "note.harp",
|
||||
91: "note.harp",
|
||||
92: "note.harp",
|
||||
93: "note.harp",
|
||||
94: "note.harp",
|
||||
95: "note.harp",
|
||||
96: "note.harp",
|
||||
97: "note.harp",
|
||||
98: "note.harp",
|
||||
99: "note.harp",
|
||||
100: "note.harp",
|
||||
101: "note.harp",
|
||||
102: "note.harp",
|
||||
103: "note.harp",
|
||||
104: "note.harp",
|
||||
105: "note.banjo",
|
||||
106: "note.harp",
|
||||
107: "note.harp",
|
||||
108: "note.harp",
|
||||
109: "note.harp",
|
||||
110: "note.harp",
|
||||
111: "note.harp",
|
||||
112: "note.cow_bell",
|
||||
113: "note.harp",
|
||||
114: "note.harp",
|
||||
115: "note.bd",
|
||||
116: "note.bd", # 打击乐器无音域
|
||||
117: "note.bd",
|
||||
118: "note.bd",
|
||||
119: "note.harp", # 打击乐器无音域
|
||||
120: "note.harp",
|
||||
121: "note.harp",
|
||||
122: "note.harp",
|
||||
123: "note.harp",
|
||||
124: "note.harp",
|
||||
125: "note.harp", # 打击乐器无音域
|
||||
126: "note.harp", # 打击乐器无音域
|
||||
127: "note.harp", # 打击乐器无音域
|
||||
}
|
||||
"""“断联”乐音乐器对照表"""
|
||||
# 105 'note.banjo'
|
||||
# 32,33,34,35,36,37,38,39 'note.bass'
|
||||
# 115,116,117,118'note.basedrum'
|
||||
# 9'note.bell'
|
||||
# 80,81'note.bit'
|
||||
# 112'note.cow_bell'
|
||||
# 72,73,74,75,76,77,78,79,41,42,43,44'note.flute'
|
||||
# 24,25,26,27,28,29,30,31'note.guitar'
|
||||
# 14'note.chime'
|
||||
# 8,9,10,11,12,13,/*14,*/15'note.iron_xylophone'
|
||||
# 2'note.pling'
|
||||
# 'note.harp'
|
||||
|
||||
MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
34: "note.bd",
|
||||
35: "note.bd",
|
||||
36: "note.snare",
|
||||
37: "note.snare",
|
||||
38: "note.bd",
|
||||
39: "note.snare",
|
||||
40: "note.bd",
|
||||
41: "note.hat",
|
||||
42: "note.bd",
|
||||
43: "note.hat",
|
||||
44: "note.bd",
|
||||
45: "note.hat",
|
||||
46: "note.bd",
|
||||
47: "note.bd",
|
||||
48: "note.bd",
|
||||
49: "note.bd",
|
||||
50: "note.bd",
|
||||
51: "note.bd",
|
||||
52: "note.bd",
|
||||
53: "note.bd",
|
||||
54: "note.bd",
|
||||
55: "note.cow_bell",
|
||||
56: "note.bd",
|
||||
57: "note.bd",
|
||||
58: "note.bd",
|
||||
59: "note.bd",
|
||||
60: "note.bd",
|
||||
61: "note.bd",
|
||||
62: "note.bd",
|
||||
63: "note.bd",
|
||||
64: "note.bd",
|
||||
65: "note.bd",
|
||||
66: "note.bd",
|
||||
67: "note.bd",
|
||||
68: "note.bd",
|
||||
69: "note.bd",
|
||||
70: "note.bd",
|
||||
71: "note.bd",
|
||||
72: "note.bd",
|
||||
73: "note.bd",
|
||||
74: "note.bd",
|
||||
75: "note.bd",
|
||||
76: "note.bd",
|
||||
77: "note.bd",
|
||||
78: "note.bd",
|
||||
79: "note.bd",
|
||||
80: "note.bd",
|
||||
}
|
||||
"""“断联”打击乐器对照表"""
|
||||
# 55'note.cow_bell'
|
||||
# 41,43,45'note.hat'
|
||||
# 36,37,39'note.snare'
|
||||
# 'note.bd'
|
||||
|
||||
MM_HARP_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
0: "note.harp",
|
||||
1: "note.harp",
|
||||
2: "note.harp",
|
||||
3: "note.harp",
|
||||
4: "note.harp",
|
||||
5: "note.harp",
|
||||
6: "note.harp",
|
||||
7: "note.harp",
|
||||
8: "note.harp",
|
||||
9: "note.harp",
|
||||
10: "note.harp",
|
||||
11: "note.harp",
|
||||
12: "note.harp",
|
||||
13: "note.harp",
|
||||
14: "note.harp",
|
||||
15: "note.harp",
|
||||
16: "note.harp",
|
||||
17: "note.harp",
|
||||
18: "note.harp",
|
||||
19: "note.harp",
|
||||
20: "note.harp",
|
||||
21: "note.harp",
|
||||
22: "note.harp",
|
||||
23: "note.harp",
|
||||
24: "note.harp",
|
||||
25: "note.harp",
|
||||
26: "note.harp",
|
||||
27: "note.harp",
|
||||
28: "note.harp",
|
||||
29: "note.harp",
|
||||
30: "note.harp",
|
||||
31: "note.harp",
|
||||
32: "note.harp",
|
||||
33: "note.harp",
|
||||
34: "note.harp",
|
||||
35: "note.harp",
|
||||
36: "note.harp",
|
||||
37: "note.harp",
|
||||
38: "note.harp",
|
||||
39: "note.harp",
|
||||
40: "note.harp",
|
||||
41: "note.harp",
|
||||
42: "note.harp",
|
||||
43: "note.harp",
|
||||
44: "note.harp",
|
||||
45: "note.harp",
|
||||
46: "note.harp",
|
||||
47: "note.harp",
|
||||
48: "note.harp",
|
||||
49: "note.harp",
|
||||
50: "note.harp",
|
||||
51: "note.harp",
|
||||
52: "note.harp",
|
||||
53: "note.harp",
|
||||
54: "note.harp",
|
||||
55: "note.harp",
|
||||
56: "note.harp",
|
||||
57: "note.harp",
|
||||
58: "note.harp",
|
||||
59: "note.harp",
|
||||
60: "note.harp",
|
||||
61: "note.harp",
|
||||
62: "note.harp",
|
||||
63: "note.harp",
|
||||
64: "note.harp",
|
||||
65: "note.harp",
|
||||
66: "note.harp",
|
||||
67: "note.harp",
|
||||
68: "note.harp",
|
||||
69: "note.harp",
|
||||
70: "note.harp",
|
||||
71: "note.harp",
|
||||
72: "note.harp",
|
||||
73: "note.harp",
|
||||
74: "note.harp",
|
||||
75: "note.harp",
|
||||
76: "note.harp",
|
||||
77: "note.harp",
|
||||
78: "note.harp",
|
||||
79: "note.harp",
|
||||
80: "note.harp",
|
||||
81: "note.harp",
|
||||
82: "note.harp",
|
||||
83: "note.harp",
|
||||
84: "note.harp",
|
||||
85: "note.harp",
|
||||
86: "note.harp",
|
||||
87: "note.harp",
|
||||
88: "note.harp",
|
||||
89: "note.harp",
|
||||
90: "note.harp",
|
||||
91: "note.harp",
|
||||
92: "note.harp",
|
||||
93: "note.harp",
|
||||
94: "note.harp",
|
||||
95: "note.harp",
|
||||
96: "note.harp",
|
||||
97: "note.harp",
|
||||
98: "note.harp",
|
||||
99: "note.harp",
|
||||
100: "note.harp",
|
||||
101: "note.harp",
|
||||
102: "note.harp",
|
||||
103: "note.harp",
|
||||
104: "note.harp",
|
||||
105: "note.harp",
|
||||
106: "note.harp",
|
||||
107: "note.harp",
|
||||
108: "note.harp",
|
||||
109: "note.harp",
|
||||
110: "note.harp",
|
||||
111: "note.harp",
|
||||
112: "note.harp",
|
||||
113: "note.harp",
|
||||
114: "note.harp",
|
||||
115: "note.harp",
|
||||
116: "note.harp",
|
||||
117: "note.harp",
|
||||
118: "note.harp",
|
||||
119: "note.harp",
|
||||
120: "note.harp",
|
||||
121: "note.harp",
|
||||
122: "note.harp",
|
||||
123: "note.harp",
|
||||
124: "note.harp",
|
||||
125: "note.harp",
|
||||
126: "note.harp",
|
||||
127: "note.harp",
|
||||
}
|
||||
"""“听个响(纯harp)”音乐乐器对照表"""
|
||||
|
||||
MM_HARP_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
34: "note.harp",
|
||||
35: "note.harp",
|
||||
36: "note.harp",
|
||||
37: "note.harp",
|
||||
38: "note.harp",
|
||||
39: "note.harp",
|
||||
40: "note.harp",
|
||||
41: "note.harp",
|
||||
42: "note.harp",
|
||||
43: "note.harp",
|
||||
44: "note.harp",
|
||||
45: "note.harp",
|
||||
46: "note.harp",
|
||||
47: "note.harp",
|
||||
48: "note.harp",
|
||||
49: "note.harp",
|
||||
50: "note.harp",
|
||||
51: "note.harp",
|
||||
52: "note.harp",
|
||||
53: "note.harp",
|
||||
54: "note.harp",
|
||||
55: "note.harp",
|
||||
56: "note.harp",
|
||||
57: "note.harp",
|
||||
58: "note.harp",
|
||||
59: "note.harp",
|
||||
60: "note.harp",
|
||||
61: "note.harp",
|
||||
62: "note.harp",
|
||||
63: "note.harp",
|
||||
64: "note.harp",
|
||||
65: "note.harp",
|
||||
66: "note.harp",
|
||||
67: "note.harp",
|
||||
68: "note.harp",
|
||||
69: "note.harp",
|
||||
70: "note.harp",
|
||||
71: "note.harp",
|
||||
72: "note.harp",
|
||||
73: "note.harp",
|
||||
74: "note.harp",
|
||||
75: "note.harp",
|
||||
76: "note.harp",
|
||||
77: "note.harp",
|
||||
78: "note.harp",
|
||||
79: "note.harp",
|
||||
80: "note.harp",
|
||||
}
|
||||
"""“听个响(纯harp)”打击乐器对照表"""
|
@ -0,0 +1,491 @@
|
||||
"""
|
||||
@Author: Envision
|
||||
@Github: ElapsingDreams
|
||||
@Gitee: ElapsingDreams
|
||||
@Email: None
|
||||
@FileName: main.py
|
||||
@DateTime: 2024/3/8 18:41
|
||||
@SoftWare: PyCharm
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
# import threading
|
||||
import warnings
|
||||
|
||||
import Musicreater
|
||||
# import mido
|
||||
import numpy as np
|
||||
|
||||
# import sounddevice as sd
|
||||
# import soundfile as sf
|
||||
from Musicreater import MM_INSTRUMENT_DEVIATION_TABLE
|
||||
from librosa import load as librosa_load
|
||||
from librosa import resample as librosa_resample
|
||||
from librosa.effects import pitch_shift as librosa_effects_pitch_shift
|
||||
from librosa.effects import time_stretch as librosa_effects_time_stretch
|
||||
|
||||
# from MusicPreview.classes import MusicSequenceRepair
|
||||
|
||||
# from .constants import MM_DISLINK_PITCHED_INSTRUMENT_TABLE, MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE, MM_HARP_PITCHED_INSTRUMENT_TABLE, MM_HARP_PERCUSSION_INSTRUMENT_TABLE
|
||||
|
||||
PATH = pathlib.Path(__file__)
|
||||
# 我寻思着ASSETS直接内置咯
|
||||
ASSETS_PATH = PATH.parent / "assets" / "wav"
|
||||
|
||||
"""已弃用"""
|
||||
'''
|
||||
INSTRUMENT_OFFSET_POS_TABLE: Dict[str, int] = {
|
||||
"note.harp": 66, #
|
||||
"note.pling": 66,
|
||||
"note.guitar": 54, #
|
||||
"note.iron_xylophone": 66, #
|
||||
"note.bell": 90, #
|
||||
"note.xylophone": 90, #
|
||||
"note.chime": 90, #
|
||||
"note.banjo": 66,
|
||||
"note.flute": 78, #
|
||||
"note.bass": 42, #
|
||||
"note.snare": 0, # #
|
||||
"note.didgeridoo": 42, #
|
||||
"mob.zombie.wood": 0, # #
|
||||
"note.bit": 66,
|
||||
"note.hat": 0, # #
|
||||
"note.bd": 0, # #
|
||||
"note.basedrum": 0, # #
|
||||
"firework.blast": 0, # #
|
||||
"firework.twinkle": 0, # #
|
||||
"fire.ignite": 0, # #
|
||||
"note.cow_bell": 66,
|
||||
}
|
||||
"""不同乐器的音调偏离对照表"""
|
||||
'''
|
||||
|
||||
|
||||
class PreviewMusic:
|
||||
"""
|
||||
将Midi转为音频之参数
|
||||
|
||||
:param usr_input_path: str 用户输入midi文件路径
|
||||
:param usr_output_path: str 用户输入音频文件输出路径
|
||||
:param mode: bool 是否依照中文wiki定义:pitch即 播放速度 比 新播放速度
|
||||
:param out_sr: int 输出音频采样率,即质量
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
musicsq: Musicreater.MusicSequence,
|
||||
mode: int = 0,
|
||||
gvm: int = 0,
|
||||
out_sr: int = 44100,
|
||||
overlay_channels: int = 1,
|
||||
default_channel_num: int = 1,
|
||||
):
|
||||
# mode:
|
||||
# 0-OriginLength
|
||||
# 1-use_mc_player_define
|
||||
# 2-matchMIDI-cut
|
||||
# 3-matchMixing
|
||||
# 4-matchMIDI-TSM
|
||||
|
||||
if (
|
||||
overlay_channels not in [1, 2]
|
||||
or default_channel_num not in [1, 2]
|
||||
or mode not in [0, 1, 2, 3, 4]
|
||||
):
|
||||
raise ValueError("Illegal Value.")
|
||||
|
||||
self.music_seq = musicsq
|
||||
|
||||
self.in_path = None
|
||||
self.out_path = None
|
||||
self.mode = mode
|
||||
self.out_sr = out_sr
|
||||
self.gvm = gvm
|
||||
self.assets_dict = {}
|
||||
self.cache_dict = {}
|
||||
self.oc = overlay_channels
|
||||
self.dc = default_channel_num
|
||||
self.dev_list = self.__init_midi__()
|
||||
|
||||
# self.dev_list = self.__init_midi__()
|
||||
|
||||
# 预读取
|
||||
self.__int_read_assets()
|
||||
|
||||
# 预生成
|
||||
self.__init_cache()
|
||||
|
||||
def __init_midi__(self):
|
||||
# MusicSequence return: Tuple[Mapping[int, List[MineNote]], int, Dict[str, int], Dict[str, int]]
|
||||
# List[List[ str[sound_ID] int[midi_note_pitch] int[mc_tick_pos注意是多少tick《位置》执行] ]]
|
||||
"""ii = 1
|
||||
for i in [i for j in Musicreater.MusicSequence.to_music_note_channels(
|
||||
mido.MidiFile(
|
||||
self.in_path,
|
||||
clip=True,
|
||||
),
|
||||
)[0].values() for i in j]:
|
||||
print(f"{i.sound_name}\t{i.note_pitch - 60 - MM_INSTRUMENT_DEVIATION_TABLE.get(i.sound_name, 6) if not i.percussive else None}\t{i.note_pitch - INSTRUMENT_OFFSET_POS_TABLE[i.sound_name] if not i.percussive else None}")
|
||||
"""
|
||||
return sorted(
|
||||
(
|
||||
(
|
||||
i.sound_name,
|
||||
(
|
||||
i.note_pitch
|
||||
- 60
|
||||
- MM_INSTRUMENT_DEVIATION_TABLE.get(i.sound_name, 6)
|
||||
if not i.percussive
|
||||
else None
|
||||
),
|
||||
i.start_tick,
|
||||
i.velocity / 127,
|
||||
i.duration,
|
||||
)
|
||||
for i in sorted(
|
||||
[i for j in self.music_seq.channels.values() for i in j],
|
||||
key=lambda note: note.start_tick,
|
||||
)
|
||||
),
|
||||
key=lambda x: x[2],
|
||||
)
|
||||
|
||||
|
||||
def __int_read_assets(self):
|
||||
files = [os.path.join(ASSETS_PATH, file) for file in os.listdir(ASSETS_PATH)]
|
||||
for file in files:
|
||||
self.assets_dict[os.path.split(file)[1].rsplit(".wav", 1)[0]] = (
|
||||
librosa_load(file, sr=None)
|
||||
)
|
||||
|
||||
def __init_cache(self):
|
||||
# print(self.dev_list)
|
||||
for item in set(
|
||||
[(ii[0], ii[1], ii[4]) for ii in self.dev_list]
|
||||
): # 初始化音频数据 set( List[List[ str[sound_ID] int[midi_note_pitch] int[mc_tick_delay注意是多少tick《位置》执行] ]])
|
||||
y_orig, sr_orig = self.assets_dict[item[0]]
|
||||
if self.oc == 2 and len(y_orig.shape) == 1:
|
||||
warnings.warn("Meaningless")
|
||||
y_orig = np.array([y_orig, y_orig])
|
||||
# print(y_orig)
|
||||
elif self.oc == 1 and len(y_orig.shape) == 2:
|
||||
y_orig = np.array(y_orig[self.dc])
|
||||
|
||||
if item[1]: # 适配打击乐
|
||||
# n_step = item[1] - INSTRUMENT_OFFSET_POS_TABLE[item[0]]
|
||||
# n_step = item[1]
|
||||
# times = 2 ** (item[1] / 12)
|
||||
raw_name = item[0] + "." + str(item[1])
|
||||
if self.mode == 1:
|
||||
# 变调, 时域压扩, 重采样 mc方法
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
librosa_effects_time_stretch(
|
||||
librosa_effects_pitch_shift(
|
||||
y_orig, sr=sr_orig, n_steps=item[1]
|
||||
),
|
||||
rate=2 ** (item[1] / 12),
|
||||
),
|
||||
orig_sr=sr_orig,
|
||||
target_sr=self.out_sr,
|
||||
fix=False,
|
||||
)
|
||||
elif self.mode == 0:
|
||||
# 重采样, 变调
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
librosa_effects_pitch_shift(
|
||||
y_orig, sr=sr_orig, n_steps=item[1]
|
||||
),
|
||||
orig_sr=sr_orig,
|
||||
target_sr=self.out_sr,
|
||||
fix=False,
|
||||
)
|
||||
elif self.mode == 4:
|
||||
|
||||
# 变调, 时域压扩, 重采样 MIDI-FFT
|
||||
if self.oc == 2:
|
||||
rate = item[2] / 20 / (len(y_orig[0]) / sr_orig)
|
||||
rate = rate if rate != 0 else 1
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
librosa_effects_time_stretch(
|
||||
librosa_effects_pitch_shift(
|
||||
y_orig, sr=sr_orig, n_steps=item[1]
|
||||
),
|
||||
rate=rate,
|
||||
),
|
||||
orig_sr=sr_orig,
|
||||
target_sr=self.out_sr,
|
||||
fix=False,
|
||||
)
|
||||
else:
|
||||
rate = item[2] / 20 / (len(y_orig) / sr_orig)
|
||||
rate = rate if rate != 0 else 1
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
librosa_effects_time_stretch(
|
||||
librosa_effects_pitch_shift(
|
||||
y_orig, sr=sr_orig, n_steps=item[1]
|
||||
),
|
||||
rate=rate,
|
||||
),
|
||||
orig_sr=sr_orig,
|
||||
target_sr=self.out_sr,
|
||||
fix=False,
|
||||
)
|
||||
elif self.mode == 2:
|
||||
# 变调, 时域压扩, 重采样 MIDI-cut
|
||||
if self.oc == 2:
|
||||
deal = librosa_effects_pitch_shift(
|
||||
y_orig, sr=sr_orig, n_steps=item[1]
|
||||
)[
|
||||
...,
|
||||
: (
|
||||
int(item[2] / 20 * sr_orig)
|
||||
if item[2] / 20 * sr_orig > len(y_orig[0])
|
||||
else len(y_orig[0])
|
||||
),
|
||||
]
|
||||
else:
|
||||
deal = librosa_effects_pitch_shift(
|
||||
y_orig, sr=sr_orig, n_steps=item[1]
|
||||
)[
|
||||
: (
|
||||
int(item[2] / 20 * sr_orig)
|
||||
if item[2] / 20 * sr_orig > len(y_orig)
|
||||
else len(y_orig)
|
||||
)
|
||||
]
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
deal, orig_sr=sr_orig, target_sr=self.out_sr, fix=False
|
||||
)
|
||||
else:
|
||||
raw_name = item[0]
|
||||
# if self.mode == 1:
|
||||
# 重采样, 不变调
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
y_orig, orig_sr=sr_orig, target_sr=self.out_sr, fix=False
|
||||
)
|
||||
"""
|
||||
elif self.mode == 0:
|
||||
# 重采样, 不变调, 衰弱
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
y_orig,
|
||||
orig_sr=sr_orig,
|
||||
target_sr=self.out_sr,
|
||||
fix=False
|
||||
)
|
||||
"""
|
||||
del self.assets_dict
|
||||
|
||||
def to_wav(self) -> np.ndarray:
|
||||
# 这玩意,真的太离谱。。虽然早考虑到这个问题,但在眼皮子底下我都没想着去改()
|
||||
# 真的 我盯着这玩意想了大半个小时
|
||||
# 我 是 __ __
|
||||
# 遍历一次devlist,当前位置采样长度+对应音频采样长度 组成数组,找最大
|
||||
# len(self.cache_dict[(self.dev_list[i-1][0] + "." + str(
|
||||
# self.dev_list[i-1][1] - INSTRUMENT_OFFSET_POS_TABLE[self.dev_list[i-1][0]])) if self.dev_list[i-1][1] else
|
||||
# self.dev_list[i-1][0]])
|
||||
# max_duration = int(max([(i[2] * 0.05 * self.out_sr + len((self.cache_dict[i[0] + "." + str(i[1] - INSTRUMENT_OFFSET_POS_TABLE[i[0]])]) if i[1] else self.cache_dict[i[0]])) for i in self.dev_list]))
|
||||
# wav_model = np.zeros(max_duration, dtype=np.float32)
|
||||
# - INSTRUMENT_OFFSET_POS_TABLE[i[0]]
|
||||
if self.oc == 1:
|
||||
|
||||
def overlay(seg_overlay: np.ndarray, pos_tick: int):
|
||||
pos_ = int(out_sr * pos_tick * 0.05)
|
||||
# print(pos_, seg_overlay.size, wav_model.size, wav_model[pos_:seg_overlay.size + pos_].size, seg_overlay.dtype)
|
||||
wav_model[pos_ : seg_overlay.size + pos_] += seg_overlay
|
||||
|
||||
wav_model = np.zeros(
|
||||
int(
|
||||
max(
|
||||
[
|
||||
(
|
||||
i[2] * 0.05 * self.out_sr
|
||||
+ len(
|
||||
(self.cache_dict[i[0] + "." + str(i[1])])
|
||||
if i[1]
|
||||
else self.cache_dict[i[0]]
|
||||
)
|
||||
)
|
||||
for i in self.dev_list
|
||||
]
|
||||
)
|
||||
),
|
||||
dtype=np.float32,
|
||||
)
|
||||
elif self.oc == 2:
|
||||
|
||||
def overlay(seg_overlay: np.ndarray, pos_tick: int):
|
||||
pos_ = int(out_sr * pos_tick * 0.05)
|
||||
# print(pos_, seg_overlay.size, wav_model.size, wav_model[pos_:seg_overlay.size + pos_].size, seg_overlay.dtype)
|
||||
wav_model[..., pos_ : len(seg_overlay[0]) + pos_] += seg_overlay
|
||||
|
||||
wav_model = np.zeros(
|
||||
(
|
||||
2,
|
||||
int(
|
||||
max(
|
||||
[
|
||||
(
|
||||
i[2] * 0.05 * self.out_sr
|
||||
+ len(
|
||||
(self.cache_dict[i[0] + "." + str(i[1])][0])
|
||||
if i[1]
|
||||
else self.cache_dict[i[0]]
|
||||
)
|
||||
)
|
||||
for i in self.dev_list
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
dtype=np.float32,
|
||||
)
|
||||
else:
|
||||
raise ValueError("illegal overlay_mode")
|
||||
|
||||
out_sr = self.out_sr
|
||||
|
||||
i = 0
|
||||
|
||||
for item in self.dev_list:
|
||||
if item[1]: # 适配打击乐
|
||||
# n_step = item[1] - INSTRUMENT_OFFSET_POS_TABLE[item[0]]
|
||||
raw_name = item[0] + "." + str(item[1])
|
||||
# print(self.cache_dict[raw_name].shape, "\n")
|
||||
overlay(self.cache_dict[raw_name] * item[3], item[2])
|
||||
|
||||
else:
|
||||
raw_name = item[0]
|
||||
# print(self.cache_dict[raw_name].shape, "\n")
|
||||
overlay(self.cache_dict[raw_name] * item[3], item[2])
|
||||
# print(self.dev_list[-1][1] ,self.dev_list[-1][0])
|
||||
i += 1
|
||||
# print(i, len(self.dev_list))
|
||||
if self.gvm == 0:
|
||||
# 归一化,抚摸耳朵 (bushi
|
||||
max_val = np.max(np.abs(wav_model))
|
||||
if not max_val == 0:
|
||||
wav_model = wav_model / max_val
|
||||
elif self.gvm == 1:
|
||||
wav_model[wav_model > 1] = 1
|
||||
wav_model[wav_model < -1] = -1
|
||||
if self.oc == 2:
|
||||
return wav_model.T
|
||||
else:
|
||||
return wav_model[:, np.newaxis]
|
||||
|
||||
# # 请使用本函数进行导出
|
||||
# def to_wav_file(self, out_file_path):
|
||||
# sf.write(
|
||||
# out_file_path,
|
||||
# self.to_wav(),
|
||||
# samplerate=self.out_sr,
|
||||
# format="wav",
|
||||
# )
|
||||
|
||||
# def play(self):
|
||||
# event = threading.Event()
|
||||
# data, fs = self.to_wav(), self.out_sr
|
||||
# if self.oc == 1:
|
||||
# data = data[:, np.newaxis]
|
||||
|
||||
# self.current_frame = 0
|
||||
|
||||
# def callback(outdata, frames, time, status): # CALLBACK need
|
||||
# if status:
|
||||
# print(status)
|
||||
# chunksize = min(len(data) - self.current_frame, frames)
|
||||
# outdata[:chunksize] = data[self.current_frame:self.current_frame + chunksize]
|
||||
# if chunksize < frames:
|
||||
# outdata[chunksize:] = 0
|
||||
# raise sd.CallbackStop()
|
||||
# self.current_frame += chunksize
|
||||
|
||||
# stream = sd.OutputStream(
|
||||
# samplerate=fs, device=None, channels=self.oc,
|
||||
# callback=callback, finished_callback=event.set)
|
||||
# with stream:
|
||||
# event.wait() # Wait until playback is finished
|
||||
|
||||
# @staticmethod
|
||||
# def _to_rel_mctick(messages):
|
||||
# rel_messages = []
|
||||
# now = 0
|
||||
# for msg in messages:
|
||||
# delta = msg[2] - now
|
||||
# rel_messages.append((msg[0], msg[1], delta, msg[3], msg[4]))
|
||||
# now = msg[2]
|
||||
# return rel_messages
|
||||
|
||||
# def stream(self):
|
||||
# event = threading.Event()
|
||||
# self.end = int(self.out_sr * self.dev_list[-1][2] * 0.05)
|
||||
# self.current_frame = 0
|
||||
# self.pos = 0
|
||||
# if self.oc == 1:
|
||||
# def overlay(seg_overlay: np.ndarray, pos_tick: int):
|
||||
# pos_ = int(self.out_sr * pos_tick * 0.05)
|
||||
# # print(pos_, seg_overlay.size, wav_model.size, wav_model[pos_:seg_overlay.size + pos_].size, seg_overlay.dtype)
|
||||
# wav_model[pos_:seg_overlay.size + pos_] += seg_overlay
|
||||
|
||||
# wav_model = np.zeros(int(max([(i[2] * 0.05 * self.out_sr +
|
||||
# len((self.cache_dict[i[0] + "." + str(i[1])])
|
||||
# if i[1] else self.cache_dict[i[0]])) for i in self.dev_list])),
|
||||
# dtype=np.float32)
|
||||
# elif self.oc == 2:
|
||||
# def overlay(seg_overlay: np.ndarray, pos_tick: int):
|
||||
# pos_ = int(self.out_sr * pos_tick * 0.05)
|
||||
# # print(pos_, seg_overlay.size, wav_model.size, wav_model[pos_:seg_overlay.size + pos_].size, seg_overlay.dtype)
|
||||
# wav_model[..., pos_:len(seg_overlay[0]) + pos_] += seg_overlay
|
||||
# wav_model[wav_model > 1] = 1
|
||||
# wav_model[wav_model < -1] = -1
|
||||
|
||||
# wav_model = np.zeros((2, int(max([(i[2] * 0.05 * self.out_sr +
|
||||
# len((self.cache_dict[i[0] + "." + str(i[1])][0])
|
||||
# if i[1] else self.cache_dict[i[0]])) for i in self.dev_list]))),
|
||||
# dtype=np.float32)
|
||||
# else:
|
||||
# raise ValueError("illegal overlay_mode")
|
||||
|
||||
# i = 0
|
||||
|
||||
# def callback(outdata, frames, _, status): # CALLBACK need
|
||||
|
||||
# if status:
|
||||
# print(status)
|
||||
|
||||
# chunksize = min(len(wav_model) - self.current_frame, frames)
|
||||
|
||||
# if self.pos < self.current_frame + chunksize and self.pos < self.end:
|
||||
# outdata[:] = 0
|
||||
# else:
|
||||
# if self.oc == 1:
|
||||
# outdata[:chunksize] = wav_model[:, np.newaxis][self.current_frame:self.current_frame + chunksize]
|
||||
# else:
|
||||
# outdata[:chunksize] = wav_model[self.current_frame:self.current_frame + chunksize]
|
||||
# if chunksize < frames:
|
||||
# outdata[chunksize:] = 0
|
||||
# raise sd.CallbackStop()
|
||||
# self.current_frame += chunksize
|
||||
|
||||
# stream = sd.OutputStream(
|
||||
# samplerate=self.out_sr, device=None, channels=self.oc,
|
||||
# callback=callback, finished_callback=event.set)
|
||||
|
||||
# with stream:
|
||||
# for item in self.dev_list:
|
||||
# self.pos = int(self.out_sr * item[2] * 0.05)
|
||||
# if item[1]: # 适配打击乐
|
||||
# # n_step = item[1] - INSTRUMENT_OFFSET_POS_TABLE[item[0]]
|
||||
# raw_name = item[0] + "." + str(item[1])
|
||||
# # print(self.cache_dict[raw_name].shape, "\n")
|
||||
# overlay(self.cache_dict[raw_name] * item[3], item[2])
|
||||
|
||||
# else:
|
||||
# raw_name = item[0]
|
||||
# # print(self.cache_dict[raw_name].shape, "\n")
|
||||
# overlay(self.cache_dict[raw_name] * item[3], item[2])
|
||||
# # print(self.dev_list[-1][1] ,self.dev_list[-1][0])
|
||||
# i += 1
|
||||
# # print(i, len(self.dev_list))
|
||||
# event.wait() # Wait until playback is finished
|
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
28
src/nonebot_plugins/trimo_plugin_msctconverter/__init__.py
Normal file
28
src/nonebot_plugins/trimo_plugin_msctconverter/__init__.py
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .msctexec import *
|
||||
from .mspvexec import *
|
||||
|
||||
__author__ = "金羿Eilles"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="伶伦转换器",
|
||||
description="",
|
||||
usage=(
|
||||
"MARKDOWN## 伶伦转换器\n"
|
||||
"《我的世界》音乐转换,结构生成与转换\n"
|
||||
"### 用法\n"
|
||||
"- `/msctcvt` 转换MIDI音乐\n"
|
||||
# "- `/stctcvt` 各类结构互转\n"
|
||||
# "- `/stctbld` 文本指令转结构\n"
|
||||
# "- `/stctbld` 文本指令转结构\n"
|
||||
),
|
||||
type="application",
|
||||
homepage="https://gitee.com/TriM-Organization/Musicreater",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
"toggleable" : False,
|
||||
"default_enable" : True,
|
||||
}
|
||||
)
|
||||
|
62
src/nonebot_plugins/trimo_plugin_msctconverter/checker.py
Normal file
62
src/nonebot_plugins/trimo_plugin_msctconverter/checker.py
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
|
||||
|
||||
def isin(sth: str, range_list: dict):
|
||||
sth = sth.lower()
|
||||
for bool_value, res_list in range_list.items():
|
||||
if sth in res_list:
|
||||
return bool_value
|
||||
raise ValueError(
|
||||
"不在可选范围内:{}".format([j for i in range_list.values() for j in i])
|
||||
)
|
||||
|
||||
|
||||
# 真假字符串判断
|
||||
def bool_str(sth: str):
|
||||
try:
|
||||
return bool(float(sth))
|
||||
except ValueError:
|
||||
if str(sth).lower() in ("true", "真", "是", "y", "t"):
|
||||
return True
|
||||
elif str(sth).lower() in ("false", "假", "否", "f", "n"):
|
||||
return False
|
||||
else:
|
||||
raise ValueError("非法逻辑字串")
|
||||
|
||||
|
||||
def float_str(sth: str):
|
||||
try:
|
||||
return float(sth)
|
||||
except ValueError:
|
||||
return float(
|
||||
sth.replace("壹", "1")
|
||||
.replace("贰", "2")
|
||||
.replace("叁", "3")
|
||||
.replace("肆", "4")
|
||||
.replace("伍", "5")
|
||||
.replace("陆", "6")
|
||||
.replace("柒", "7")
|
||||
.replace("捌", "8")
|
||||
.replace("玖", "9")
|
||||
.replace("零", "0")
|
||||
.replace("一", "1")
|
||||
.replace("二", "2")
|
||||
.replace("三", "3")
|
||||
.replace("四", "4")
|
||||
.replace("五", "5")
|
||||
.replace("六", "6")
|
||||
.replace("七", "7")
|
||||
.replace("八", "8")
|
||||
.replace("九", "9")
|
||||
.replace("〇", "0")
|
||||
.replace("洞", "0")
|
||||
.replace("幺", "1")
|
||||
.replace("俩", "2")
|
||||
.replace("两", "2")
|
||||
.replace("拐", "7")
|
||||
.replace("点", ".")
|
||||
)
|
||||
|
||||
|
||||
def int_str(sth: str):
|
||||
return int(float_str(sth))
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"donateCodePath": "https://foruda.gitee.com/images/1690165328188128032/d7f24fb5_9911226.jpeg",
|
||||
"donateSite": "",
|
||||
"helpPicPath": "https://foruda.gitee.com/images/1685873169584963569/95fe9b0b_9911226.png",
|
||||
"maxCacheSize": 1048576,
|
||||
"maxCacheTime": {
|
||||
".json": 1800,
|
||||
".mcfunction": 900,
|
||||
".mid .midi": 1800
|
||||
},
|
||||
"maxPersonConvert": {
|
||||
"music": 20,
|
||||
"structure": 20
|
||||
},
|
||||
"maxSingleFileSize": {
|
||||
".json": 8192,
|
||||
".mcfunction": 524288,
|
||||
".mid .midi": 131072
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
## 伶伦转换器 - 机器人版使用文档
|
||||
|
||||
命令为标题,后面是功能和子命令。
|
||||
|
||||
支持传入 `.midi`、`.mid`、`.json` 文件载入服务器缓存。
|
||||
|
||||
<!-- 若子命令为参数项,后面需要参数。
|
||||
|
||||
若子命令为开关项,后面无需参数。 -->
|
||||
|
||||
**以下所有命令中,若字符串类型的参数需要且可以填入多个内容,则可用 `all` 代替参数中的全部数据;此情况下,也可以用和符`&`分割多个你想要填写的信息;同样的,若参数中需要包含空格,则须以英文双引号`"`扩起。**
|
||||
|
||||
### llmscvt | linglun_convert | 音乐转换 | midi 转换 | 转换音乐 | linglun_music_convert
|
||||
|
||||
转换 midi 音乐到指定的格式,支持批量格式批量文件。每次转换基础随机消耗一次点数,每多一种转换格式多消耗一次附加点数,`MSQ`格式不计入后续附加点数消耗。每日点数在凌晨四时整重置。每人每天默认点数 25,每次消耗点数随机于 [0.3,0.8]。**若短时间内已使用同样的参数运行过一次 音乐合成 命令,则不消耗基础点数**
|
||||
|
||||
- `-f | --file <字符串>` : 缓存中的 midi 文件名称,需提前上传 mid 文件;默认为`all`
|
||||
|
||||
- `-emr | --enable-mismatch-error` : 对音符的不匹配报错;默认为关
|
||||
|
||||
- `-ps | --play-speed <小数>` : 播放速度;默认为`1.0`
|
||||
|
||||
- `-dftp | --default-tempo <整数>` : 默认的 tempo;默认为`500000`
|
||||
|
||||
- `-ptc | --pitched-note-table <字符串>` : **不可多填** : 乐音乐器对照表,需要提前上传 json 文件,此处输入缓存中的 json 文件名称,或者默认存有的三组对照表名称:`touch`、`classic`、`dislink`;默认为`touch`
|
||||
|
||||
- `-pcs | --percussion-note-table <字符串>` : **不可多填** : 打击乐器对照表,需要提前上传 json 文件,此处输入缓存中的 json 文件名称,或者默认存有的三组对照表名称:`touch`、`classic`、`dislink`;默认为`touch`
|
||||
|
||||
- `-e | --old-execute-format` : 是否使用旧版 execute 指令格式;默认为关
|
||||
|
||||
- `-mv | --minimal-volume <小数>` : 最小播放音量;默认为`0.1`
|
||||
|
||||
- `-vpf | --volume-processing-function <字符串>` : 音量处理函数,支持两种音量函数:`natural`、`straight`;默认为`natural`
|
||||
|
||||
- `-t | --type <字符串>` : 转换结果类型,支持的类型有:`addon-delay`、`addon-score`、 `mcstructure-dalay`、`mcstructure-score`、`bdx-delay`、`bdx-score`、`msq`;默认为`all`
|
||||
|
||||
- `-htp | --high-time-precision` : **仅当结果类型包含 `msq` 时生效** : 是否使用高精度时间存储 MSQ 文件;默认为关
|
||||
|
||||
- `-pgb | --progress-bar <字符串> <字符串> <字符串>` : **仅当结果包含 `addon-*`、`bdx-*` 之一时生效、不可多填** : 进度条样式,参照[进度条自定义](https://gitee.com/TriM-Organization/Musicreater/blob/master/docs/%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md#%E8%BF%9B%E5%BA%A6%E6%9D%A1%E8%87%AA%E5%AE%9A%E4%B9%89),以空格拆分三个字符串;默认请查阅上述文档
|
||||
|
||||
- `-s | --scoreboard-name <字符串>` : **仅当结果类型包含 `*-score` 之一时生效、不可多填** : 播放使用的计分板名称;默认为`mscplay`
|
||||
|
||||
- `-dsa | --disable-scoreboard-autoreset` : **仅当结果类型包含 `*-score` 之一时生效** : 是否禁用计分板自动重置;默认为关
|
||||
|
||||
- `-p | --player-selector <字符串>` : **仅当结果类型包含 `*-delay` 之一时生效、不可多填** : 播放使用的玩家选择器;默认为`@a`
|
||||
|
||||
- `-l | --height-limit <整数>` : **仅当结果类型包含 `*-delay`、`bdx-*` 之一时生效** : 生成结构的最大高度限制;默认为`32`
|
||||
|
||||
- `-a | --author <字符串>` : **仅当结果类型包含 `bdx-*` 之一时生效、不可多填** : 音乐文件的作者署名;默认为`Eilles`
|
||||
|
||||
- `-fa | --forward-axis <字符串>` : **仅当结果类型包含 `*-repeater` 之一时生效、不可多填** : 生成结构的朝向;默认为`z+`(**未来功能**)
|
||||
|
||||
### mscprv | music_preview | 预览音乐效果 | 预览音乐 | 音乐预览 | 音乐合成 | midi 合成
|
||||
|
||||
生成 midi 音乐的《我的世界》播放预览效果。每次转换基础随机消耗一次点数,**若短时间内已使用同样的参数运行过一次 音乐转换 命令,则不消耗基础点数**,并随机消耗附加点数 [1.3, 2.9] 。该命令与上文中的 `音乐转换` 命令共享点数信息。
|
||||
|
||||
- `-n | -f | --file-name` : 缓存中的 midi 文件名称,需提前上传 mid 文件;默认为`all`
|
||||
- `-m | --mode` : 合成模式,支持以下内容。默认为 `0`
|
||||
- 0 原始长度,不变化时长
|
||||
- 1 拉伸至 mc 播放器定义(我的世界效果)
|
||||
- 2 根据 midi 音符长度裁剪
|
||||
- 3 混音预留
|
||||
- 4 匹配 midi 音符长度(最佳效果)
|
||||
- `-g | --get-value-method` : GVM取值法,支持以下内容。默认为 `1`
|
||||
- 0 均值化
|
||||
- 1 钳制位
|
||||
- `-o | --output-file` : 是否输出文件,默认为`False`
|
||||
- 以下命令同上 音乐转换 参数
|
||||
- `-emr | --enable-mismatch-error`
|
||||
- `-ps | --play-speed`
|
||||
- `-dftp | --default-tempo`
|
||||
- `-ptc | --pitched-note-table`
|
||||
- `-pcs | --percussion-note-table`
|
||||
- `-vpf | --volume-processing-function`
|
||||
|
||||
### 查看缓存 | listCache | 查看文件缓存 | 查看缓存文件
|
||||
|
||||
查看自己上传到服务器的文件
|
||||
|
||||
### 清除缓存 | clearCache | 清除文件缓存 | 清除缓存文件 | 清空缓存
|
||||
|
||||
删除自己所有上传到服务器的文件
|
||||
|
||||
### 转换帮助 | 查看转换帮助 | 查看帮助 | cvt_help | convert_help | cvthlp
|
||||
|
||||
查看此帮助文档
|
Binary file not shown.
After Width: | Height: | Size: 662 KiB |
@ -0,0 +1,139 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
'''
|
||||
|
||||
Copyright © 2022 Team-Ryoun
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
'''
|
||||
|
||||
# 自动转换exe指令格式
|
||||
import uuid
|
||||
|
||||
|
||||
|
||||
def isfloatable(sth: str) -> bool:
|
||||
try:float(sth);return True
|
||||
except:return False
|
||||
|
||||
|
||||
# 极限挑战
|
||||
# execute @a[name="abc 123"] ~~ ~ execute @s ~9 346 ~-8 detect ^6 ^7 ^2 concrete 18 execute @p[r=3,scores={a=3}] 324 ~324 5 scoreboard players add @s[tag="999 888aasd asd "] QWE_AS 2
|
||||
|
||||
# /execute @a~~~/w @s aaa
|
||||
|
||||
# execute@s[tag="[]] 666"]~ 1~576detect^6^^66concrete 1 execute @s [scores={n=0}] ~ ~ ~0.09 execute@s~~~detect 0 0 0 bedrock -1 execute@a [name="999\"]]] jjj\""]~~ ~/execute@s[tag="℃♞\""]~ 32 ~5423 execute@s~~~detect ~~-1~ redstone_block 0 give @s [scores={name=12..}] command_block 1 1 {"name_tag":["\"a ":"b你 KKK\""]}
|
||||
|
||||
# 感谢 尘风、籽怼、邯潘阳(Happy2018New) 为本程序的试错提供了非常有效的支持
|
||||
# 也感谢 尘风、Happy2018New、Dislink Sforza 为作者提供相关参考意见
|
||||
|
||||
|
||||
def auto_translate(sentence:str):
|
||||
'''传入一行旧的execute指令,则将其转换为新格式
|
||||
:param sentence: 旧的execute指令
|
||||
:return: 新的execute指令
|
||||
'''
|
||||
|
||||
|
||||
if not 'execute' in sentence:
|
||||
return sentence
|
||||
elif 'run' in sentence:
|
||||
return sentence[:sentence.find('run')+4]+auto_translate(sentence[sentence.find('run')+4:])
|
||||
|
||||
# 避免不规范的语法
|
||||
sentence = ((__ := str(uuid.uuid4()),strings:=[(r"\"",__),], sentence.replace(r"\"",__)) if r"\"" in sentence else (None,strings:=[],sentence))[2]
|
||||
|
||||
|
||||
# 如果有字符串包含其中
|
||||
# 我们可以看作一个神奇的pattern
|
||||
def foreSentence(sent,ptnA,ptnB):
|
||||
startcatch = False
|
||||
tempstring = ""
|
||||
for i in sent:
|
||||
if startcatch:
|
||||
if i == ptnB:
|
||||
startcatch = False
|
||||
tpp = '{}'.format(tempstring)
|
||||
tag = str(uuid.uuid4())
|
||||
sent = sent.replace(tpp,tag)
|
||||
strings.append((tpp,tag))
|
||||
tempstring = ""
|
||||
else:
|
||||
tempstring += i
|
||||
else:
|
||||
if i == ptnA:
|
||||
startcatch = True
|
||||
# tempstring += i
|
||||
# print(ptnA,ptnB,sent)
|
||||
return sent
|
||||
|
||||
# print(sentence,"\n")
|
||||
# 如果选择器的中括号包括空格
|
||||
sentence = ((sentence[:sentence.find("@")+2]+sentence[sentence.find('['):]) if (sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence).replace("/"," ").lower()
|
||||
|
||||
|
||||
sentence = foreSentence(foreSentence(foreSentence(sentence,'"','"'),"[","]"),"{","}")
|
||||
list_sentence = list(sentence)
|
||||
# 如果有神奇的东西在坐标后面,那就神奇了
|
||||
for i in range(len(list_sentence)):
|
||||
if list_sentence[i] in ('^','~'):
|
||||
j = i + 1
|
||||
while ((isfloatable("".join(list_sentence[i+1:j+1])))or(list_sentence[i+1] == "-" and isfloatable("".join(list_sentence[i+1:j+2]))))and j <= len(list_sentence):
|
||||
j += 1
|
||||
# print(j,"\t","".join(sentence[j:]))
|
||||
if list_sentence[j] == " " or isfloatable(list_sentence[j]):
|
||||
continue
|
||||
else:
|
||||
list_sentence.insert(j,' ')
|
||||
|
||||
sentence = "".join(list_sentence)
|
||||
|
||||
def backfor_sentence(a):
|
||||
return [(a := a.replace(tag,tpp)) for tpp,tag in strings[::-1]][-1] if strings else a
|
||||
|
||||
# 下面是重点,只有我和老天爷看得懂
|
||||
if 'detect' in sentence[:sentence.find("execute",8) if "execute" in sentence[8:] else -1]:
|
||||
___ = [ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ]
|
||||
|
||||
____ = " ".join(___[3:]).split(" ")
|
||||
|
||||
return backfor_sentence('execute as {0} positioned as @s positioned {1} if block {2} {3} {4} at @s positioned {1} run {5}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(),sentence[(sentence.find("]") if "[" in sentence[:sentence.find("@")+3] else sentence.find("@")+1)+1:sentence.find("detect")-1].strip()," ".join(___[0:3]),____[0],____[1],auto_translate(" ".join(____[2:]))))
|
||||
|
||||
else:
|
||||
|
||||
___ = [ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[(sentence.find("]") if ("]" in sentence)and(sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence.find("@")+1)+1:].strip().split(" ",4)] for j in i ]
|
||||
|
||||
return backfor_sentence('execute as {0} positioned as @s positioned {1} at @s positioned {1} run {2}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(), " ".join(___[0:3]),auto_translate(" ".join(___[3:]))))
|
||||
|
||||
# 我是一个善良的人,没有用下面这个恶心你们
|
||||
# backSentence('execute as {0} positioned as @s positioned {1} if block {2} {3} {4} at @s positioned {1} run {5}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(),sentence[(sentence.find("]") if "[" in sentence[:sentence.find("@")+3] else sentence.find("@")+1)+1:sentence.find("detect")-1].strip()," ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][0:3])," ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][3:]).split(" ")[0]," ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][3:]).split(" ")[1],autoTranslate(" ".join(" ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][3:]).split(" ")[2:])))) if 'detect' in sentence[:sentence.find("execute",8) if "execute" in sentence[8:] else -1] else backSentence('execute as {0} positioned as @s positioned {1} at @s positioned {1} run {2}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(), " ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[(sentence.find("]") if ("]" in sentence)and(sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence.find("@")+1)+1:].strip().split(" ",4)] for j in i ][0:3]),autoTranslate(" ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[(sentence.find("]") if ("]" in sentence)and(sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence.find("@")+1)+1:].strip().split(" ",4)] for j in i ][3:]))))
|
||||
|
||||
|
||||
|
||||
|
||||
def __main__():
|
||||
'''主函数
|
||||
'''
|
||||
while True:
|
||||
try:
|
||||
sentence = input()
|
||||
print()
|
||||
print(auto_translate(sentence))
|
||||
print("="*10)
|
||||
except EOFError:
|
||||
break
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
# continue
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
__main__()
|
||||
|
||||
# 没写完,我也不知道咋写,但是总得写不是吗
|
1195
src/nonebot_plugins/trimo_plugin_msctconverter/msctexec.py
Normal file
1195
src/nonebot_plugins/trimo_plugin_msctconverter/msctexec.py
Normal file
File diff suppressed because it is too large
Load Diff
451
src/nonebot_plugins/trimo_plugin_msctconverter/mspvexec.py
Normal file
451
src/nonebot_plugins/trimo_plugin_msctconverter/mspvexec.py
Normal file
@ -0,0 +1,451 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import random
|
||||
|
||||
from io import StringIO
|
||||
|
||||
# from pathlib import Path
|
||||
|
||||
# import nonebot.rule
|
||||
|
||||
import nonebot
|
||||
import soundfile
|
||||
import zhDateTime
|
||||
import Musicreater
|
||||
import Musicreater.plugin
|
||||
import nonebot.adapters.onebot.v11.exception
|
||||
|
||||
from .MusicPreview.main import PreviewMusic
|
||||
|
||||
from nonebot.adapters.onebot.v11.event import (
|
||||
GroupMessageEvent,
|
||||
PrivateMessageEvent,
|
||||
GroupUploadNoticeEvent,
|
||||
)
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
# AlconnaQuery,
|
||||
Args,
|
||||
# Image,
|
||||
Option,
|
||||
# Query,
|
||||
# Text,
|
||||
UniMessage,
|
||||
on_alconna,
|
||||
# Voice,
|
||||
Arparma,
|
||||
Args,
|
||||
store_true,
|
||||
)
|
||||
|
||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from src.utils.message.message import MarkdownMessage
|
||||
|
||||
from .msctexec import (
|
||||
# people_convert_point,
|
||||
query_convert_points,
|
||||
filesaves,
|
||||
configdict,
|
||||
database_dir,
|
||||
temporary_dir,
|
||||
file_to_delete,
|
||||
)
|
||||
from .utils import utime_hanzify
|
||||
|
||||
mspv_sync = on_alconna(
|
||||
Alconna(
|
||||
"音乐合成",
|
||||
Option("-n|-f|--file-name", default="all", args=Args["file-name", str, "all"]),
|
||||
Option("-m|--mode", default=0, args=Args["mode", int, 0]),
|
||||
Option(
|
||||
"-g|--get-value-method", default=1, args=Args["get-value-method", int, 1]
|
||||
),
|
||||
Option("-o|--output-file", default=False, action=store_true),
|
||||
Option("-emr|--enable-mismatch-error", default=False, action=store_true),
|
||||
Option("-ps|--play-speed", default=1.0, args=Args["play-speed", float, 1.0]),
|
||||
Option(
|
||||
"-dftp|--default-tempo",
|
||||
default=Musicreater.mido.midifiles.midifiles.DEFAULT_TEMPO,
|
||||
args=Args[
|
||||
"default-tempo", int, Musicreater.mido.midifiles.midifiles.DEFAULT_TEMPO
|
||||
],
|
||||
),
|
||||
Option(
|
||||
"-ptc|--pitched-note-table",
|
||||
default="touch",
|
||||
args=Args["pitched-note-table", str, "touch"],
|
||||
),
|
||||
Option(
|
||||
"-pcs|--percussion-note-table",
|
||||
default="touch",
|
||||
args=Args["percussion-note-table", str, "touch"],
|
||||
),
|
||||
Option(
|
||||
"-vpf|--volume-processing-function",
|
||||
default="natural",
|
||||
args=Args["volume-processing-function", str, "natural"],
|
||||
),
|
||||
),
|
||||
aliases={
|
||||
"midi合成",
|
||||
"音乐预览",
|
||||
"mscprv",
|
||||
"music_preview",
|
||||
"预览音乐效果",
|
||||
"预览音乐",
|
||||
},
|
||||
# rule=nonebot.rule.to_me(),
|
||||
# use_cmd_start=True,
|
||||
# block=True,
|
||||
# priority=13,
|
||||
)
|
||||
|
||||
|
||||
@mspv_sync.handle()
|
||||
async def _(
|
||||
result: Arparma,
|
||||
event: GroupMessageEvent | PrivateMessageEvent,
|
||||
bot: T_Bot,
|
||||
):
|
||||
# print("E:\\Work2024\\test-midi\\" + name.result)
|
||||
|
||||
nonebot.logger.info(result.options)
|
||||
|
||||
usr_id = str(event.user_id)
|
||||
|
||||
if (qres := query_convert_points(usr_id, "music"))[0] is False:
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text(
|
||||
"转换点数不足,当前剩余:⌊p⌋≈{:.2f}|{}".format(
|
||||
qres[1],
|
||||
configdict["maxPersonConvert"]["music"],
|
||||
)
|
||||
),
|
||||
at_sender=True,
|
||||
)
|
||||
|
||||
if usr_id not in filesaves.keys():
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text("服务器内未存入你的任何文件,请先使用上传midi文件吧")
|
||||
)
|
||||
|
||||
_args: dict = {
|
||||
"file-name": "all",
|
||||
"output-file": False,
|
||||
"mode": 0,
|
||||
"get-value-method": 1,
|
||||
"enable-mismatch-error": False,
|
||||
"play-speed": 1.0,
|
||||
"default-tempo": 500000,
|
||||
"pitched-note-table": "touch",
|
||||
"percussion-note-table": "touch",
|
||||
"volume-processing-function": "natural",
|
||||
}
|
||||
for arg in _args.keys():
|
||||
_args[arg] = (
|
||||
(
|
||||
result.options[arg].args[arg]
|
||||
if arg in result.options[arg].args.keys()
|
||||
else result.options[arg].args
|
||||
)
|
||||
if (_vlu := result.options[arg].value) is None
|
||||
else _vlu
|
||||
)
|
||||
# await musicreater_convert.finish(
|
||||
# UniMessage.text(json.dumps(_args, indent=4, sort_keys=True, ensure_ascii=False))
|
||||
# )
|
||||
nonebot.logger.info(_args)
|
||||
|
||||
if _args["mode"] not in [0, 1, 2, 3, 4]:
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text("模式 {} 不存在,请详阅文档。".format(_args["mode"]))
|
||||
)
|
||||
|
||||
if _args["get-value-method"] not in [
|
||||
0,
|
||||
1,
|
||||
]:
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text(
|
||||
"取值法 {} 不存在,请详阅文档。".format(_args["get-value-method"])
|
||||
)
|
||||
)
|
||||
|
||||
usr_data_path = database_dir / usr_id
|
||||
(usr_temp_path := temporary_dir / usr_id).mkdir(exist_ok=True)
|
||||
|
||||
if (_ppnt := _args["pitched-note-table"].lower()) in [
|
||||
"touch",
|
||||
"classic",
|
||||
"dislink",
|
||||
]:
|
||||
pitched_notechart = (
|
||||
Musicreater.MM_DISLINK_PITCHED_INSTRUMENT_TABLE
|
||||
if _ppnt == "dislink"
|
||||
else (
|
||||
Musicreater.MM_CLASSIC_PITCHED_INSTRUMENT_TABLE
|
||||
if _ppnt == "classic"
|
||||
else Musicreater.MM_TOUCH_PITCHED_INSTRUMENT_TABLE
|
||||
)
|
||||
)
|
||||
elif (_ppnt := (usr_data_path / _args["pitched-note-table"])).exists():
|
||||
pitched_notechart = Musicreater.MM_TOUCH_PITCHED_INSTRUMENT_TABLE.copy()
|
||||
pitched_notechart.update(json.load(_ppnt.open("r")))
|
||||
else:
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text("乐器对照表 {} 不存在".format(_args["pitched-note-table"]))
|
||||
)
|
||||
return
|
||||
|
||||
if (_ppnt := _args["percussion-note-table"].lower()) in [
|
||||
"touch",
|
||||
"classic",
|
||||
"dislink",
|
||||
]:
|
||||
percussion_notechart = (
|
||||
Musicreater.MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE
|
||||
if _ppnt == "dislink"
|
||||
else (
|
||||
Musicreater.MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE
|
||||
if _ppnt == "classic"
|
||||
else Musicreater.MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE
|
||||
)
|
||||
)
|
||||
elif (_ppnt := (usr_data_path / _args["percussion-note-table"])).exists():
|
||||
percussion_notechart = Musicreater.MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE.copy()
|
||||
percussion_notechart.update(json.load(_ppnt.open("r")))
|
||||
else:
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text(
|
||||
"乐器对照表 {} 不存在".format(_args["percussion-note-table"])
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (_ppnt := _args["volume-processing-function"].lower()) in [
|
||||
"natural",
|
||||
"straight",
|
||||
]:
|
||||
volume_curve = (
|
||||
Musicreater.straight_line
|
||||
if _ppnt == "straight"
|
||||
else Musicreater.natural_curve
|
||||
)
|
||||
else:
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text(
|
||||
"音量处理曲线 {} 不存在".format(_args["volume-processing-function"])
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# 重定向标准输出
|
||||
buffer = StringIO()
|
||||
sys.stdout = buffer
|
||||
sys.stderr = buffer
|
||||
|
||||
def go_chk_point():
|
||||
res, pnt = query_convert_points(
|
||||
usr_id,
|
||||
"music",
|
||||
random.random() % 1.6 + 1.3,
|
||||
)
|
||||
if res is False:
|
||||
buffer.write("中途退出,转换点不足:{}\n".format(pnt))
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
await mspv_sync.send(UniMessage.text("转换开始……"))
|
||||
|
||||
try:
|
||||
|
||||
all_files = []
|
||||
|
||||
for file_to_convert in (
|
||||
filesaves[usr_id].keys()
|
||||
if _args["file-name"].lower() == "all"
|
||||
else _args["file-name"].split("&")
|
||||
):
|
||||
if file_to_convert.endswith(".mid") or file_to_convert.endswith(".midi"):
|
||||
nonebot.logger.info("载入待合成文件:{}".format(file_to_convert))
|
||||
# print("1")
|
||||
# await mspv_sync.finish("处理中")
|
||||
|
||||
if isinstance(
|
||||
msct_obj := query_convert_points(usr_id, "music", 0)[0], tuple
|
||||
) and (
|
||||
isinstance(msct_obj[0], Musicreater.MidiConvert)
|
||||
and (
|
||||
msct_obj[1]
|
||||
== (
|
||||
not _args["enable-mismatch-error"],
|
||||
_args["play-speed"],
|
||||
_args["default-tempo"],
|
||||
pitched_notechart,
|
||||
percussion_notechart,
|
||||
volume_curve,
|
||||
)
|
||||
)
|
||||
and (
|
||||
msct_obj[0].music_name
|
||||
== os.path.splitext(
|
||||
os.path.basename(usr_data_path / file_to_convert)
|
||||
)[0].replace(" ", "_")
|
||||
)
|
||||
):
|
||||
nonebot.logger.info("载入已有缓存。")
|
||||
msct_obj = msct_obj[0]
|
||||
else:
|
||||
|
||||
if go_chk_point():
|
||||
msct_obj = Musicreater.MidiConvert.from_midi_file(
|
||||
midi_file_path=usr_data_path / file_to_convert,
|
||||
mismatch_error_ignorance=not _args["enable-mismatch-error"],
|
||||
play_speed=_args["play-speed"],
|
||||
default_tempo=_args["default-tempo"],
|
||||
pitched_note_table=pitched_notechart,
|
||||
percussion_note_table=percussion_notechart,
|
||||
vol_processing_func=volume_curve,
|
||||
)
|
||||
query_convert_points(
|
||||
usr_id,
|
||||
"music",
|
||||
0,
|
||||
(
|
||||
msct_obj,
|
||||
(
|
||||
not _args["enable-mismatch-error"],
|
||||
_args["play-speed"],
|
||||
_args["default-tempo"],
|
||||
pitched_notechart,
|
||||
percussion_notechart,
|
||||
volume_curve,
|
||||
),
|
||||
),
|
||||
)
|
||||
else:
|
||||
buffer.write(
|
||||
"点数不足或出现错误:\n{}".format(
|
||||
_args,
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
all_files.append(file_to_convert)
|
||||
|
||||
music_temp = PreviewMusic(
|
||||
msct_obj,
|
||||
mode=_args["mode"],
|
||||
gvm=_args["get-value-method"],
|
||||
default_channel_num=1,
|
||||
overlay_channels=1,
|
||||
out_sr=44100,
|
||||
)
|
||||
soundfile.write(
|
||||
fp := (
|
||||
usr_temp_path
|
||||
/ "[MP0.2.0]{}-M{}.wav".format(
|
||||
msct_obj.music_name, _args["mode"]
|
||||
)
|
||||
),
|
||||
music_temp.to_wav(),
|
||||
samplerate=music_temp.out_sr,
|
||||
format="wav",
|
||||
)
|
||||
await mspv_sync.send(UniMessage.text("曲目 {}".format(file_to_convert)))
|
||||
|
||||
fp.open("ab").write(b"DM-MPvR0.2.0")
|
||||
|
||||
await mspv_sync.send(
|
||||
UniMessage.voice(
|
||||
path=fp,
|
||||
name="[MP0.2.0]{}-M{}.wav".format(
|
||||
msct_obj.music_name, _args["mode"]
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
elif file_to_convert != "totalSize":
|
||||
nonebot.logger.warning(
|
||||
"文件类型错误:{}".format(file_to_convert),
|
||||
)
|
||||
buffer.write("文件 {} 已跳过\n".format(file_to_convert))
|
||||
|
||||
if not all_files:
|
||||
nonebot.logger.warning(
|
||||
"无可供转换的文件",
|
||||
)
|
||||
await mspv_sync.finish(
|
||||
UniMessage("我服了老弟,这机器人也不能给路易十六理发啊。")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
nonebot.logger.error("合成存在错误:{}".format(e))
|
||||
buffer.write(
|
||||
"[ERROR] {}\n".format(e).replace(
|
||||
"C:\\Users\\Administrator\\Desktop\\RyBot\\", "[]"
|
||||
)
|
||||
)
|
||||
|
||||
sys.stdout = sys.__stdout__
|
||||
sys.stderr = sys.__stderr__
|
||||
|
||||
if _args["output-file"]:
|
||||
Musicreater.plugin.archive.compress_zipfile(
|
||||
usr_temp_path,
|
||||
fp := str(
|
||||
temporary_dir
|
||||
/ (
|
||||
fn := "mprwav[{}]{}.zip".format(
|
||||
utime_hanzify(zhDateTime.DateTime.now().to_lunar()), usr_id
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
if isinstance(event, GroupMessageEvent) or isinstance(
|
||||
event, GroupUploadNoticeEvent
|
||||
):
|
||||
await bot.call_api(
|
||||
"upload_group_file", group_id=event.group_id, name=fn, file=fp
|
||||
)
|
||||
await mspv_sync.send(
|
||||
UniMessage.text("文件已上传群文件,请在群文件查看。")
|
||||
)
|
||||
else:
|
||||
await bot.call_api(
|
||||
"upload_private_file", user_id=event.user_id, name=fn, file=fp
|
||||
)
|
||||
except nonebot.adapters.onebot.v11.exception.NetworkError as E:
|
||||
buffer.write("文件上传发生网络错误:\n{}".format(E))
|
||||
|
||||
global file_to_delete
|
||||
file_to_delete.append(fp)
|
||||
|
||||
await MarkdownMessage.send_md(
|
||||
"##{}\n\n```\n{}\n```".format(
|
||||
MarkdownMessage.escape("日志信息:"),
|
||||
buffer.getvalue().replace("\\", "/"),
|
||||
),
|
||||
bot,
|
||||
event=event,
|
||||
)
|
||||
|
||||
# nonebot.logger.info(buffer.getvalue())
|
||||
|
||||
shutil.rmtree(usr_temp_path)
|
||||
|
||||
await mspv_sync.finish(
|
||||
UniMessage.text(
|
||||
"成功转换:{}\n当前剩余转换点数:⌊p⌋≈{:.2f}|{}".format(
|
||||
"、".join(all_files),
|
||||
query_convert_points(usr_id, "music", 0, None)[1],
|
||||
configdict["maxPersonConvert"]["music"],
|
||||
)
|
||||
),
|
||||
at_sender=True,
|
||||
)
|
27
src/nonebot_plugins/trimo_plugin_msctconverter/utils.py
Normal file
27
src/nonebot_plugins/trimo_plugin_msctconverter/utils.py
Normal file
@ -0,0 +1,27 @@
|
||||
import zhDateTime
|
||||
|
||||
|
||||
def utime_hanzify(
|
||||
zhd: zhDateTime.zhDateTime = zhDateTime.DateTime.now().to_lunar(),
|
||||
) -> str:
|
||||
return "{地支时}{刻}{分}{秒}".format(
|
||||
地支时=zhDateTime.DÌZHĪ[zhd.shichen]
|
||||
+ (
|
||||
""
|
||||
if ((zhd.quarters) or (zhd.minutes) or (zhd.seconds) or (zhd.microseconds))
|
||||
else "整"
|
||||
),
|
||||
刻=(
|
||||
(zhDateTime.HANNUM[zhd.quarters])
|
||||
+ ("" if ((zhd.minutes) or (zhd.seconds) or (zhd.microseconds)) else "整")
|
||||
),
|
||||
分=(
|
||||
zhDateTime.lkint_hànzìfy(zhd.minutes)
|
||||
+ ("" if ((zhd.seconds) or (zhd.microseconds)) else "整")
|
||||
),
|
||||
秒=(
|
||||
zhDateTime.HANNUM[zhd.seconds // 10]
|
||||
+ zhDateTime.HANNUM[zhd.seconds % 10]
|
||||
+ ("" if (zhd.microseconds) else "整")
|
||||
),
|
||||
).strip()
|
21
src/nonebot_plugins/webdash/__init__.py
Normal file
21
src/nonebot_plugins/webdash/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .main import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="网页监控面板",
|
||||
description="网页监控面板,用于查看机器人的状态和信息",
|
||||
usage=(
|
||||
"访问 127.0.0.1:port 查看机器人的状态信息\n"
|
||||
"stat msg -g|--group [group_id] 查看群的统计信息,不带参数为全群\n"
|
||||
"配置项:custom_domain,自定义域名,通常对外用,内网无需"
|
||||
),
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : False,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
4
src/nonebot_plugins/webdash/common.py
Normal file
4
src/nonebot_plugins/webdash/common.py
Normal file
@ -0,0 +1,4 @@
|
||||
from fastapi import FastAPI
|
||||
from nonebot import get_app
|
||||
|
||||
app: FastAPI = get_app()
|
10
src/nonebot_plugins/webdash/main.py
Normal file
10
src/nonebot_plugins/webdash/main.py
Normal file
@ -0,0 +1,10 @@
|
||||
from fastapi import FastAPI
|
||||
from nonebot import get_app
|
||||
from .restful_api import *
|
||||
|
||||
|
||||
@app.get("/ping")
|
||||
async def root():
|
||||
return {
|
||||
"message": "pong"
|
||||
}
|
23
src/nonebot_plugins/webdash/restful_api.py
Normal file
23
src/nonebot_plugins/webdash/restful_api.py
Normal file
@ -0,0 +1,23 @@
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from .common import *
|
||||
|
||||
device_info_router = APIRouter(prefix="/api/device-info")
|
||||
bot_info_router = APIRouter(prefix="/api/bot-info")
|
||||
|
||||
|
||||
@device_info_router.get("/")
|
||||
async def device_info():
|
||||
return {
|
||||
"message": "Hello Device Info"
|
||||
}
|
||||
|
||||
|
||||
@bot_info_router.get("/")
|
||||
async def bot_info():
|
||||
return {
|
||||
"message": "Hello Bot Info"
|
||||
}
|
||||
|
||||
|
||||
app.include_router(device_info_router)
|
||||
app.include_router(bot_info_router)
|
Reference in New Issue
Block a user