Ⓜ️手动从旧梦 81a191f merge

This commit is contained in:
2024-08-12 11:44:30 +08:00
parent 068feaa591
commit 2d67a703bd
214 changed files with 6457 additions and 10418 deletions

View 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子进程已接收到数据")

View 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")

View 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))

View 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: 分辨率1reso
:return: 两个方向的站牌
"""
if ratio is None:
ratio = [4, 1]
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW))
# 加黑色图框
baseCanvas.linePanel = Panel(
uv_size=(1, 1),
box_size=(0.8, 0.15),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
# 直线块
baseCanvas.linePanel.recLine = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=line.color,
),
keep_ratio=False
)
# 灰色直线块
baseCanvas.linePanel.recLineGrey = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=Color.GREY,
),
keep_ratio=False
)
# 生成各站圆点
outline_width = 40
circleForward = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
)
circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0)))
circleThisPanel.circleOuter = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
),
)
circleThisPanel.circleOuter.circleInner = Img(
uv_size=(1, 1),
box_size=(0.7, 0.7),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=line.color,
width=0,
outline=line.color,
),
)
circleThisPanel.export("a.png", alpha=True)
circleThis = circleThisPanel.base_img
circlePassed = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=Color.GREY,
)
arrival = False
distance = 1 / (len(line.stations) - 1)
for i, sta in enumerate(line.stations):
box_size = (1.618, 1.618)
if sta.identifier == station.identifier:
arrival = True
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=(1.8, 1.8),
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleThis,
keep_ratio=True
)
continue
if arrival:
# 后方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleForward,
keep_ratio=True
)
else:
# 前方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circlePassed,
keep_ratio=True
)
return baseCanvas
def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution
):
pass
# def main():
# generate_entrance_sign(
# "璧山",
# aliases=["Bishan"],
# lineInfo=[
#
# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]),
# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]),
# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]),
# ],
# entranceIdentifier="1",
# ratio=(8, 1)
# )
#
#
# main()

View File

@ -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,
}
)

View 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,
}
)

View 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,
}
)

View 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

View 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("参数错误")

View 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,
}
)

View 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)

View 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

View 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("根本没做帮助页,赶快去催神羽")

View 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)

View 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))

File diff suppressed because it is too large Load Diff

View 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")

View 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,
}
)

View 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"
)

View 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

View 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,
},
)

View 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())

View 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
)

View 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))

View 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)

View File

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

View File

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

View File

@ -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,
},
)

View 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

View 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()

View 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())
)

View 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,
}
)

View 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")

View 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,
}
)

View 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()

View 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

View 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)。\
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
详细的准许和限制条款请见原协议文本。

View 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))
)

View 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)

View 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)

View File

@ -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.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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")

View 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)。\
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
详细的准许和限制条款请见原协议文本。

View File

@ -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.
```

View File

@ -0,0 +1,9 @@
"""
@Author: Envision
@Github: ElapsingDreams
@Gitee: ElapsingDreams
@Email: None
@FileName: __init__.py
@DateTime: 2024/3/8 20:48
@SoftWare: PyCharm
"""

View File

@ -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”打击乐器对照表"""

View File

@ -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

View File

@ -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.

View 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,
}
)

View 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))

View File

@ -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
}
}

View File

@ -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

View File

@ -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__()
# 没写完,我也不知道咋写,但是总得写不是吗

File diff suppressed because it is too large Load Diff

View 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,
)

View 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()

View 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,
}
)

View File

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

View 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"
}

View 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)