mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-06 04:10:02 +08:00
feat✨: 更新内置插件
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import base64
|
||||
import math
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import List, Literal, Tuple, TypeAlias, overload
|
||||
from typing import Literal, Tuple, TypeAlias, overload
|
||||
|
||||
from nonebot.utils import run_sync
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
@@ -40,12 +41,13 @@ class BuildImage:
|
||||
self,
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
color: ColorAlias = None,
|
||||
color: ColorAlias = (255, 255, 255),
|
||||
mode: ModeType = "RGBA",
|
||||
font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf",
|
||||
font_size: int = 20,
|
||||
background: str | BytesIO | Path | None = None,
|
||||
) -> None:
|
||||
self.uid = uuid.uuid1()
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.color = color
|
||||
@@ -72,7 +74,7 @@ class BuildImage:
|
||||
async def build_text_image(
|
||||
cls,
|
||||
text: str,
|
||||
font: str | Path = "HYWenHei-85W.ttf",
|
||||
font: str | FreeTypeFont | Path = "HYWenHei-85W.ttf",
|
||||
size: int = 10,
|
||||
font_color: str | Tuple[int, int, int] = (0, 0, 0),
|
||||
color: ColorAlias = None,
|
||||
@@ -91,12 +93,16 @@ class BuildImage:
|
||||
返回:
|
||||
Self: Self
|
||||
"""
|
||||
_font = cls.load_font(font, size)
|
||||
width, height = cls.get_text_size(text, _font)
|
||||
if type(padding) == int:
|
||||
_font = None
|
||||
if isinstance(font, FreeTypeFont):
|
||||
_font = font
|
||||
elif isinstance(font, (str, Path)):
|
||||
_font = cls.load_font(font, size)
|
||||
width, height = cls.get_text_size(text or "A", _font)
|
||||
if isinstance(padding, int):
|
||||
width += padding * 2
|
||||
height += padding * 2
|
||||
elif type(padding) == tuple:
|
||||
elif isinstance(padding, tuple):
|
||||
width += padding[1] + padding[3]
|
||||
height += padding[0] + padding[2]
|
||||
markImg = cls(width, height, color)
|
||||
@@ -112,9 +118,9 @@ class BuildImage:
|
||||
row: int,
|
||||
space: int = 10,
|
||||
padding: int = 50,
|
||||
color: ColorAlias = (255, 255, 255, 0),
|
||||
color: ColorAlias = (255, 255, 255),
|
||||
background: str | BytesIO | Path | None = None,
|
||||
) -> Self | None:
|
||||
) -> Self:
|
||||
"""自动贴图
|
||||
|
||||
参数:
|
||||
@@ -129,11 +135,15 @@ class BuildImage:
|
||||
Self: Self
|
||||
"""
|
||||
if not img_list:
|
||||
return None
|
||||
raise ValueError("贴图类别为空...")
|
||||
width, height = img_list[0].size
|
||||
background_width = width * row + space * (row - 1) + padding * 2
|
||||
column = math.ceil(len(img_list) / row)
|
||||
background_height = height * column + space * (column - 1) + padding * 2
|
||||
row_count = math.ceil(len(img_list) / row)
|
||||
if row_count == 1:
|
||||
background_width = (
|
||||
sum([img.width for img in img_list]) + space * (row - 1) + padding * 2
|
||||
)
|
||||
background_height = height * row_count + space * (row_count - 1) + padding * 2
|
||||
background_image = cls(
|
||||
background_width, background_height, color=color, background=background
|
||||
)
|
||||
@@ -141,15 +151,16 @@ class BuildImage:
|
||||
for img in img_list:
|
||||
await background_image.paste(img, (_cur_width, _cur_height))
|
||||
_cur_width += space + img.width
|
||||
_cur_height += space + img.height
|
||||
if _cur_width + padding >= background_image.width:
|
||||
_cur_height += space + img.height
|
||||
_cur_width = padding
|
||||
return background_image
|
||||
|
||||
@classmethod
|
||||
def load_font(cls, font: str | Path, font_size: int) -> FreeTypeFont:
|
||||
"""
|
||||
加载字体
|
||||
def load_font(
|
||||
cls, font: str | Path = "HYWenHei-85W.ttf", font_size: int = 10
|
||||
) -> FreeTypeFont:
|
||||
"""加载字体
|
||||
|
||||
参数:
|
||||
font: 字体名称
|
||||
@@ -165,19 +176,20 @@ class BuildImage:
|
||||
@classmethod
|
||||
def get_text_size(
|
||||
cls, text: str, font: FreeTypeFont | None = None
|
||||
) -> Tuple[int, int]:
|
||||
...
|
||||
) -> Tuple[int, int]: ...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def get_text_size(
|
||||
cls, text: str, font: str | None = None, font_size: int = 10
|
||||
) -> Tuple[int, int]:
|
||||
...
|
||||
) -> Tuple[int, int]: ...
|
||||
|
||||
@classmethod
|
||||
def get_text_size(
|
||||
cls, text: str, font: str | FreeTypeFont | None = None, font_size: int = 10
|
||||
cls,
|
||||
text: str,
|
||||
font: str | FreeTypeFont | None = "HYWenHei-85W.ttf",
|
||||
font_size: int = 10,
|
||||
) -> Tuple[int, int]:
|
||||
"""获取该字体下文本需要的长宽
|
||||
|
||||
@@ -192,7 +204,7 @@ class BuildImage:
|
||||
_font = font
|
||||
if font and type(font) == str:
|
||||
_font = cls.load_font(font, font_size)
|
||||
return _font.getsize(text) # type: ignore
|
||||
return _font.getsize(str(text)) # type: ignore
|
||||
|
||||
def getsize(self, msg: str) -> Tuple[int, int]:
|
||||
"""
|
||||
@@ -265,7 +277,10 @@ class BuildImage:
|
||||
_image = image.markImg
|
||||
if _image.width and _image.height and center_type:
|
||||
pos = self.__center_xy(pos, _image.width, _image.height, center_type)
|
||||
self.markImg.paste(_image, pos, _image) # type: ignore
|
||||
try:
|
||||
self.markImg.paste(_image, pos, _image) # type: ignore
|
||||
except ValueError:
|
||||
self.markImg.paste(_image, pos) # type: ignore
|
||||
return self
|
||||
|
||||
@run_sync
|
||||
@@ -335,6 +350,7 @@ class BuildImage:
|
||||
异常:
|
||||
ValueError: 居中类型错误
|
||||
"""
|
||||
text = str(text)
|
||||
if center_type and center_type not in ["center", "height", "width"]:
|
||||
raise ValueError("center_type must be 'center', 'width' or 'height'")
|
||||
width, height = 0, 0
|
||||
@@ -484,7 +500,7 @@ class BuildImage:
|
||||
@run_sync
|
||||
def polygon(
|
||||
self,
|
||||
xy: List[Tuple[int, int]],
|
||||
xy: list[Tuple[int, int]],
|
||||
fill: Tuple[int, int, int] = (0, 0, 0),
|
||||
outline: int = 1,
|
||||
) -> Self:
|
||||
@@ -560,7 +576,7 @@ class BuildImage:
|
||||
def circle_corner(
|
||||
self,
|
||||
radii: int = 30,
|
||||
point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"],
|
||||
point_list: list[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"],
|
||||
) -> Self:
|
||||
"""
|
||||
矩形四角变圆
|
||||
@@ -654,3 +670,11 @@ class BuildImage:
|
||||
self.markImg = self.markImg.filter(_type)
|
||||
self.draw = ImageDraw.Draw(self.markImg)
|
||||
return self
|
||||
|
||||
def tobytes(self) -> bytes:
|
||||
"""转换为bytes
|
||||
|
||||
返回:
|
||||
bytes: bytes
|
||||
"""
|
||||
return self.markImg.tobytes()
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
from email.mime import image
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from PIL.ImageFont import FreeTypeFont
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ._build_image import BuildImage
|
||||
|
||||
|
||||
class RowStyle(BaseModel):
|
||||
|
||||
font: FreeTypeFont | str | Path | None = "HYWenHei-85W.ttf"
|
||||
"""字体"""
|
||||
font_size: int = 20
|
||||
"""字体大小"""
|
||||
font_color: str | tuple[int, int, int] = (0, 0, 0)
|
||||
"""字体颜色"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class ImageTemplate:
|
||||
|
||||
@classmethod
|
||||
async def table_page(
|
||||
cls,
|
||||
head_text: str,
|
||||
tip_text: str | None,
|
||||
column_name: list[str],
|
||||
data_list: list[list[str]],
|
||||
row_space: int = 35,
|
||||
column_space: int = 30,
|
||||
padding: int = 5,
|
||||
text_style: Callable[[str, str], RowStyle] | None = None,
|
||||
) -> BuildImage:
|
||||
"""表格页
|
||||
|
||||
参数:
|
||||
head_text: 标题文本.
|
||||
tip_text: 标题注释.
|
||||
column_name: 表头列表.
|
||||
data_list: 数据列表.
|
||||
row_space: 行间距.
|
||||
column_space: 列间距.
|
||||
padding: 文本内间距.
|
||||
text_style: 文本样式.
|
||||
|
||||
返回:
|
||||
BuildImage: 表格图片
|
||||
"""
|
||||
table = await cls.table(
|
||||
column_name, data_list, row_space, column_space, padding, text_style
|
||||
)
|
||||
await table.circle_corner()
|
||||
table_bk = BuildImage(table.width + 100, table.height + 50, "#EAEDF2")
|
||||
await table_bk.paste(table, center_type="center")
|
||||
height = table_bk.height + 200
|
||||
background = BuildImage(table_bk.width, height, (255, 255, 255), font_size=50)
|
||||
await background.paste(table_bk, (0, 200))
|
||||
await background.text((0, 50), head_text, "#334762", center_type="width")
|
||||
if tip_text:
|
||||
text_image = await BuildImage.build_text_image(tip_text, size=22)
|
||||
await background.paste(text_image, (0, 110), center_type="width")
|
||||
return background
|
||||
|
||||
@classmethod
|
||||
async def table(
|
||||
cls,
|
||||
column_name: list[str],
|
||||
data_list: list[list[str | tuple[Path, int, int]]],
|
||||
row_space: int = 25,
|
||||
column_space: int = 10,
|
||||
padding: int = 5,
|
||||
text_style: Callable[[str, str], RowStyle] | None = None,
|
||||
) -> BuildImage:
|
||||
"""表格
|
||||
|
||||
参数:
|
||||
column_name: 表头列表
|
||||
data_list: 数据列表
|
||||
row_space: 行间距.
|
||||
column_space: 列间距.
|
||||
padding: 文本内间距.
|
||||
text_style: 文本样式.
|
||||
|
||||
返回:
|
||||
BuildImage: 表格图片
|
||||
"""
|
||||
font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
|
||||
column_num = max([len(l) for l in data_list])
|
||||
list_data = []
|
||||
column_data = []
|
||||
for i in range(len(column_name)):
|
||||
c = []
|
||||
for l in data_list:
|
||||
if len(l) > i:
|
||||
c.append(l[i])
|
||||
else:
|
||||
c.append("")
|
||||
column_data.append(c)
|
||||
build_data_list = []
|
||||
_, base_h = BuildImage.get_text_size("A", font)
|
||||
for i, column_list in enumerate(column_data):
|
||||
name_width, name_height = BuildImage.get_text_size(column_name[i], font)
|
||||
_temp = {"width": name_width, "data": column_list}
|
||||
for s in column_list:
|
||||
if isinstance(s, tuple):
|
||||
w = s[1]
|
||||
else:
|
||||
w, _ = BuildImage.get_text_size(s, font)
|
||||
if w > _temp["width"]:
|
||||
_temp["width"] = w
|
||||
build_data_list.append(_temp)
|
||||
column_image_list = []
|
||||
for i, data in enumerate(build_data_list):
|
||||
width = data["width"] + padding * 2
|
||||
height = (base_h + row_space) * (len(data["data"]) + 1) + padding * 2
|
||||
background = BuildImage(width, height, (255, 255, 255))
|
||||
column_name_image = await BuildImage.build_text_image(
|
||||
column_name[i], font, 12, "#C8CCCF"
|
||||
)
|
||||
await background.paste(column_name_image, (0, 20), center_type="width")
|
||||
cur_h = column_name_image.height + row_space + 20
|
||||
for item in data["data"]:
|
||||
style = RowStyle(font=font)
|
||||
if text_style:
|
||||
style = text_style(column_name[i], item)
|
||||
if isinstance(item, tuple):
|
||||
"""图片"""
|
||||
data, width, height = item
|
||||
if isinstance(data, Path):
|
||||
image_ = BuildImage(width, height, background=data)
|
||||
elif isinstance(data, bytes):
|
||||
image_ = BuildImage(width, height, background=BytesIO(data))
|
||||
elif isinstance(data, BuildImage):
|
||||
image_ = data
|
||||
await background.paste(image_, (padding, cur_h))
|
||||
else:
|
||||
await background.text(
|
||||
(padding, cur_h),
|
||||
item if item is not None else "",
|
||||
style.font_color,
|
||||
font=style.font,
|
||||
font_size=style.font_size,
|
||||
)
|
||||
cur_h += base_h + row_space
|
||||
column_image_list.append(background)
|
||||
height = max([bk.height for bk in column_image_list])
|
||||
width = sum([bk.width for bk in column_image_list])
|
||||
return await BuildImage.auto_paste(
|
||||
column_image_list, len(column_image_list), column_space
|
||||
)
|
||||
@@ -1,14 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from nonebot import get_driver
|
||||
from playwright.async_api import Browser, Playwright, async_playwright
|
||||
|
||||
from services.log import logger
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
_playwright: Optional[Playwright] = None
|
||||
_browser: Optional[Browser] = None
|
||||
_playwright: Playwright | None = None
|
||||
_browser: Browser | None = None
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
from typing import Callable, Union, Tuple, Optional
|
||||
from nonebot.adapters.onebot.v11 import MessageSegment, Message
|
||||
from nonebot.plugin import require
|
||||
|
||||
|
||||
class ShopRegister(dict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ShopRegister, self).__init__(*args, **kwargs)
|
||||
self._data = {}
|
||||
self._flag = True
|
||||
|
||||
def before_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True):
|
||||
"""
|
||||
说明:
|
||||
使用前检查方法
|
||||
参数:
|
||||
:param name: 道具名称
|
||||
:param load_status: 加载状态
|
||||
"""
|
||||
def register_before_handle(name_list: Tuple[str, ...], func: Callable):
|
||||
if load_status:
|
||||
for name_ in name_list:
|
||||
if not self._data[name_]:
|
||||
self._data[name_] = {}
|
||||
if not self._data[name_].get('before_handle'):
|
||||
self._data[name_]['before_handle'] = []
|
||||
self._data[name]['before_handle'].append(func)
|
||||
_name = (name,) if isinstance(name, str) else name
|
||||
return lambda func: register_before_handle(_name, func)
|
||||
|
||||
def after_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True):
|
||||
"""
|
||||
说明:
|
||||
使用后执行方法
|
||||
参数:
|
||||
:param name: 道具名称
|
||||
:param load_status: 加载状态
|
||||
"""
|
||||
def register_after_handle(name_list: Tuple[str, ...], func: Callable):
|
||||
if load_status:
|
||||
for name_ in name_list:
|
||||
if not self._data[name_]:
|
||||
self._data[name_] = {}
|
||||
if not self._data[name_].get('after_handle'):
|
||||
self._data[name_]['after_handle'] = []
|
||||
self._data[name_]['after_handle'].append(func)
|
||||
_name = (name,) if isinstance(name, str) else name
|
||||
return lambda func: register_after_handle(_name, func)
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: Tuple[str, ...],
|
||||
price: Tuple[float, ...],
|
||||
des: Tuple[str, ...],
|
||||
discount: Tuple[float, ...],
|
||||
limit_time: Tuple[int, ...],
|
||||
load_status: Tuple[bool, ...],
|
||||
daily_limit: Tuple[int, ...],
|
||||
is_passive: Tuple[bool, ...],
|
||||
icon: Tuple[str, ...],
|
||||
**kwargs,
|
||||
):
|
||||
def add_register_item(func: Callable):
|
||||
if name in self._data.keys():
|
||||
raise ValueError("该商品已注册,请替换其他名称!")
|
||||
for n, p, d, dd, l, s, dl, pa, i in zip(
|
||||
name, price, des, discount, limit_time, load_status, daily_limit, is_passive, icon
|
||||
):
|
||||
if s:
|
||||
_temp_kwargs = {}
|
||||
for key, value in kwargs.items():
|
||||
if key.startswith(f"{n}_"):
|
||||
_temp_kwargs[key.split("_", maxsplit=1)[-1]] = value
|
||||
else:
|
||||
_temp_kwargs[key] = value
|
||||
temp = self._data.get(n, {})
|
||||
temp.update({
|
||||
"price": p,
|
||||
"des": d,
|
||||
"discount": dd,
|
||||
"limit_time": l,
|
||||
"daily_limit": dl,
|
||||
"icon": i,
|
||||
"is_passive": pa,
|
||||
"func": func,
|
||||
"kwargs": _temp_kwargs,
|
||||
})
|
||||
self._data[n] = temp
|
||||
return func
|
||||
|
||||
return lambda func: add_register_item(func)
|
||||
|
||||
async def load_register(self):
|
||||
require("use")
|
||||
require("shop_handle")
|
||||
from basic_plugins.shop.use.data_source import register_use, func_manager
|
||||
from basic_plugins.shop.shop_handle.data_source import register_goods
|
||||
# 统一进行注册
|
||||
if self._flag:
|
||||
# 只进行一次注册
|
||||
self._flag = False
|
||||
for name in self._data.keys():
|
||||
await register_goods(
|
||||
name,
|
||||
self._data[name]["price"],
|
||||
self._data[name]["des"],
|
||||
self._data[name]["discount"],
|
||||
self._data[name]["limit_time"],
|
||||
self._data[name]["daily_limit"],
|
||||
self._data[name]["is_passive"],
|
||||
self._data[name]["icon"],
|
||||
)
|
||||
register_use(
|
||||
name, self._data[name]["func"], **self._data[name]["kwargs"]
|
||||
)
|
||||
func_manager.register_use_before_handle(name, self._data[name].get('before_handle', []))
|
||||
func_manager.register_use_after_handle(name, self._data[name].get('after_handle', []))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
name: Union[str, Tuple[str, ...]], # 名称
|
||||
price: Union[float, Tuple[float, ...]], # 价格
|
||||
des: Union[str, Tuple[str, ...]], # 简介
|
||||
discount: Union[float, Tuple[float, ...]] = 1, # 折扣
|
||||
limit_time: Union[int, Tuple[int, ...]] = 0, # 限时
|
||||
load_status: Union[bool, Tuple[bool, ...]] = True, # 加载状态
|
||||
daily_limit: Union[int, Tuple[int, ...]] = 0, # 每日限购
|
||||
is_passive: Union[bool, Tuple[bool, ...]] = False, # 被动道具(无法被'使用道具'命令消耗)
|
||||
icon: Union[str, Tuple[str, ...]] = False, # 图标
|
||||
**kwargs,
|
||||
):
|
||||
_tuple_list = []
|
||||
_current_len = -1
|
||||
for x in [name, price, des, discount, limit_time, load_status]:
|
||||
if isinstance(x, tuple):
|
||||
if _current_len == -1:
|
||||
_current_len = len(x)
|
||||
if _current_len != len(x):
|
||||
raise ValueError(
|
||||
f"注册商品 {name} 中 name,price,des,discount,limit_time,load_status,daily_limit 数量不符!"
|
||||
)
|
||||
_current_len = _current_len if _current_len > -1 else 1
|
||||
_name = self.__get(name, _current_len)
|
||||
_price = self.__get(price, _current_len)
|
||||
_discount = self.__get(discount, _current_len)
|
||||
_limit_time = self.__get(limit_time, _current_len)
|
||||
_des = self.__get(des, _current_len)
|
||||
_load_status = self.__get(load_status, _current_len)
|
||||
_daily_limit = self.__get(daily_limit, _current_len)
|
||||
_is_passive = self.__get(is_passive, _current_len)
|
||||
_icon = self.__get(icon, _current_len)
|
||||
return self.register(
|
||||
_name,
|
||||
_price,
|
||||
_des,
|
||||
_discount,
|
||||
_limit_time,
|
||||
_load_status,
|
||||
_daily_limit,
|
||||
_is_passive,
|
||||
_icon,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def __get(self, value, _current_len):
|
||||
return value if isinstance(value, tuple) else tuple([value for _ in range(_current_len)])
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._data[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
def __str__(self):
|
||||
return str(self._data)
|
||||
|
||||
def keys(self):
|
||||
return self._data.keys()
|
||||
|
||||
def values(self):
|
||||
return self._data.values()
|
||||
|
||||
def items(self):
|
||||
return self._data.items()
|
||||
|
||||
|
||||
class NotMeetUseConditionsException(Exception):
|
||||
|
||||
"""
|
||||
不满足条件异常类
|
||||
"""
|
||||
|
||||
def __init__(self, info: Optional[Union[str, MessageSegment, Message]]):
|
||||
super().__init__(self)
|
||||
self._info = info
|
||||
|
||||
def get_info(self):
|
||||
return self._info
|
||||
|
||||
|
||||
shop_register = ShopRegister()
|
||||
+27
-4
@@ -1,15 +1,38 @@
|
||||
from strenum import StrEnum
|
||||
|
||||
|
||||
class GoldHandle(StrEnum):
|
||||
"""
|
||||
金币处理
|
||||
"""
|
||||
|
||||
BUY = "BUY"
|
||||
"""购买"""
|
||||
GET = "GET"
|
||||
"""获取"""
|
||||
|
||||
|
||||
class PropHandle(StrEnum):
|
||||
"""
|
||||
道具处理
|
||||
"""
|
||||
|
||||
BUY = "BUY"
|
||||
"""购买"""
|
||||
USE = "USE"
|
||||
"""使用"""
|
||||
|
||||
|
||||
class PluginType(StrEnum):
|
||||
"""
|
||||
插件类型
|
||||
"""
|
||||
|
||||
SUPERUSER = "超级管理员插件"
|
||||
ADMIN = "管理员插件"
|
||||
NORMAL = "普通插件"
|
||||
HIDDEN = "被动插件"
|
||||
SUPERUSER = "SUPERUSER"
|
||||
ADMIN = "ADMIN"
|
||||
SUPER_AND_ADMIN = "ADMIN_SUPER"
|
||||
NORMAL = "NORMAL"
|
||||
HIDDEN = "HIDDEN"
|
||||
|
||||
|
||||
class BlockType(StrEnum):
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GroupInfoNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserAndGroupIsNone(Exception):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
import asyncio
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncGenerator, Dict, Literal
|
||||
|
||||
import aiofiles
|
||||
import httpx
|
||||
import rich
|
||||
from httpx import ConnectTimeout, Response
|
||||
from nonebot import require
|
||||
from playwright.async_api import Page
|
||||
from retrying import retry
|
||||
|
||||
from zhenxun.configs.config import SYSTEM_PROXY
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.user_agent import get_user_agent
|
||||
|
||||
from .browser import get_browser
|
||||
|
||||
require("nonebot_plugin_saa")
|
||||
|
||||
from nonebot_plugin_saa import Image
|
||||
|
||||
|
||||
class AsyncHttpx:
|
||||
|
||||
proxy = {"http://": SYSTEM_PROXY, "https://": SYSTEM_PROXY}
|
||||
|
||||
@classmethod
|
||||
@retry(stop_max_attempt_number=3)
|
||||
async def get(
|
||||
cls,
|
||||
url: str,
|
||||
*,
|
||||
params: Dict[str, Any] | None = None,
|
||||
headers: Dict[str, str] | None = None,
|
||||
cookies: Dict[str, str] | None = None,
|
||||
verify: bool = True,
|
||||
use_proxy: bool = True,
|
||||
proxy: Dict[str, str] | None = None,
|
||||
timeout: int = 30,
|
||||
**kwargs,
|
||||
) -> Response:
|
||||
"""Get
|
||||
|
||||
参数:
|
||||
url: url
|
||||
params: params
|
||||
headers: 请求头
|
||||
cookies: cookies
|
||||
verify: verify
|
||||
use_proxy: 使用默认代理
|
||||
proxy: 指定代理
|
||||
timeout: 超时时间
|
||||
"""
|
||||
if not headers:
|
||||
headers = get_user_agent()
|
||||
_proxy = proxy if proxy else cls.proxy if use_proxy else None
|
||||
async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore
|
||||
return await client.get(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def post(
|
||||
cls,
|
||||
url: str,
|
||||
*,
|
||||
data: Dict[str, str] | None = None,
|
||||
content: Any = None,
|
||||
files: Any = None,
|
||||
verify: bool = True,
|
||||
use_proxy: bool = True,
|
||||
proxy: Dict[str, str] | None = None,
|
||||
json: Dict[str, Any] | None = None,
|
||||
params: Dict[str, str] | None = None,
|
||||
headers: Dict[str, str] | None = None,
|
||||
cookies: Dict[str, str] | None = None,
|
||||
timeout: int = 30,
|
||||
**kwargs,
|
||||
) -> Response:
|
||||
"""
|
||||
说明:
|
||||
Post
|
||||
参数:
|
||||
url: url
|
||||
data: data
|
||||
content: content
|
||||
files: files
|
||||
use_proxy: 是否默认代理
|
||||
proxy: 指定代理
|
||||
json: json
|
||||
params: params
|
||||
headers: 请求头
|
||||
cookies: cookies
|
||||
timeout: 超时时间
|
||||
"""
|
||||
if not headers:
|
||||
headers = get_user_agent()
|
||||
_proxy = proxy if proxy else cls.proxy if use_proxy else None
|
||||
async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore
|
||||
return await client.post(
|
||||
url,
|
||||
content=content,
|
||||
data=data,
|
||||
files=files,
|
||||
json=json,
|
||||
params=params,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def download_file(
|
||||
cls,
|
||||
url: str,
|
||||
path: str | Path,
|
||||
*,
|
||||
params: Dict[str, str] | None = None,
|
||||
verify: bool = True,
|
||||
use_proxy: bool = True,
|
||||
proxy: Dict[str, str] | None = None,
|
||||
headers: Dict[str, str] | None = None,
|
||||
cookies: Dict[str, str] | None = None,
|
||||
timeout: int = 30,
|
||||
stream: bool = False,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""下载文件
|
||||
|
||||
参数:
|
||||
url: url
|
||||
path: 存储路径
|
||||
params: params
|
||||
verify: verify
|
||||
use_proxy: 使用代理
|
||||
proxy: 指定代理
|
||||
headers: 请求头
|
||||
cookies: cookies
|
||||
timeout: 超时时间
|
||||
stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件)
|
||||
"""
|
||||
if isinstance(path, str):
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
for _ in range(3):
|
||||
if not stream:
|
||||
try:
|
||||
content = (
|
||||
await cls.get(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
use_proxy=use_proxy,
|
||||
proxy=proxy,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
).content
|
||||
async with aiofiles.open(path, "wb") as wf:
|
||||
await wf.write(content)
|
||||
logger.info(f"下载 {url} 成功.. Path:{path.absolute()}")
|
||||
return True
|
||||
except (TimeoutError, ConnectTimeout):
|
||||
pass
|
||||
else:
|
||||
if not headers:
|
||||
headers = get_user_agent()
|
||||
_proxy = proxy if proxy else cls.proxy if use_proxy else None
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
proxies=_proxy, verify=verify # type: ignore
|
||||
) as client:
|
||||
async with client.stream(
|
||||
"GET",
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
) as response:
|
||||
logger.info(
|
||||
f"开始下载 {path.name}.. Path: {path.absolute()}"
|
||||
)
|
||||
async with aiofiles.open(path, "wb") as wf:
|
||||
total = int(response.headers["Content-Length"])
|
||||
with rich.progress.Progress( # type: ignore
|
||||
rich.progress.TextColumn(path.name), # type: ignore
|
||||
"[progress.percentage]{task.percentage:>3.0f}%", # type: ignore
|
||||
rich.progress.BarColumn(bar_width=None), # type: ignore
|
||||
rich.progress.DownloadColumn(), # type: ignore
|
||||
rich.progress.TransferSpeedColumn(), # type: ignore
|
||||
) as progress:
|
||||
download_task = progress.add_task(
|
||||
"Download", total=total
|
||||
)
|
||||
async for chunk in response.aiter_bytes():
|
||||
await wf.write(chunk)
|
||||
await wf.flush()
|
||||
progress.update(
|
||||
download_task,
|
||||
completed=response.num_bytes_downloaded,
|
||||
)
|
||||
logger.info(
|
||||
f"下载 {url} 成功.. Path:{path.absolute()}"
|
||||
)
|
||||
return True
|
||||
except (TimeoutError, ConnectTimeout):
|
||||
pass
|
||||
else:
|
||||
logger.error(f"下载 {url} 下载超时.. Path:{path.absolute()}")
|
||||
except Exception as e:
|
||||
logger.error(f"下载 {url} 错误 Path:{path.absolute()}", e=e)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def gather_download_file(
|
||||
cls,
|
||||
url_list: list[str],
|
||||
path_list: list[str | Path],
|
||||
*,
|
||||
limit_async_number: int | None = None,
|
||||
params: Dict[str, str] | None = None,
|
||||
use_proxy: bool = True,
|
||||
proxy: Dict[str, str] | None = None,
|
||||
headers: Dict[str, str] | None = None,
|
||||
cookies: Dict[str, str] | None = None,
|
||||
timeout: int = 30,
|
||||
**kwargs,
|
||||
) -> list[bool]:
|
||||
"""分组同时下载文件
|
||||
|
||||
参数:
|
||||
url_list: url列表
|
||||
path_list: 存储路径列表
|
||||
limit_async_number: 限制同时请求数量
|
||||
params: params
|
||||
use_proxy: 使用代理
|
||||
proxy: 指定代理
|
||||
headers: 请求头
|
||||
cookies: cookies
|
||||
timeout: 超时时间
|
||||
"""
|
||||
if n := len(url_list) != len(path_list):
|
||||
raise UrlPathNumberNotEqual(
|
||||
f"Url数量与Path数量不对等,Url:{len(url_list)},Path:{len(path_list)}"
|
||||
)
|
||||
if limit_async_number and n > limit_async_number:
|
||||
m = float(n) / limit_async_number
|
||||
x = 0
|
||||
j = limit_async_number
|
||||
_split_url_list = []
|
||||
_split_path_list = []
|
||||
for _ in range(int(m)):
|
||||
_split_url_list.append(url_list[x:j])
|
||||
_split_path_list.append(path_list[x:j])
|
||||
x += limit_async_number
|
||||
j += limit_async_number
|
||||
if int(m) < m:
|
||||
_split_url_list.append(url_list[j:])
|
||||
_split_path_list.append(path_list[j:])
|
||||
else:
|
||||
_split_url_list = [url_list]
|
||||
_split_path_list = [path_list]
|
||||
tasks = []
|
||||
result_ = []
|
||||
for x, y in zip(_split_url_list, _split_path_list):
|
||||
for url, path in zip(x, y):
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
cls.download_file(
|
||||
url,
|
||||
path,
|
||||
params=params,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
use_proxy=use_proxy,
|
||||
timeout=timeout,
|
||||
proxy=proxy,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
)
|
||||
_x = await asyncio.gather(*tasks)
|
||||
result_ = result_ + list(_x)
|
||||
tasks.clear()
|
||||
return result_
|
||||
|
||||
|
||||
class AsyncPlaywright:
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
async def new_page(cls, **kwargs) -> AsyncGenerator[Page, None]:
|
||||
"""获取一个新页面
|
||||
|
||||
参数:
|
||||
user_agent: 请求头
|
||||
"""
|
||||
browser = get_browser()
|
||||
ctx = await browser.new_context(**kwargs)
|
||||
page = await ctx.new_page()
|
||||
try:
|
||||
yield page
|
||||
finally:
|
||||
await page.close()
|
||||
await ctx.close()
|
||||
|
||||
@classmethod
|
||||
async def screenshot(
|
||||
cls,
|
||||
url: str,
|
||||
path: Path | str,
|
||||
element: str | list[str],
|
||||
*,
|
||||
wait_time: int | None = None,
|
||||
viewport_size: Dict[str, int] | None = None,
|
||||
wait_until: (
|
||||
Literal["domcontentloaded", "load", "networkidle"] | None
|
||||
) = "networkidle",
|
||||
timeout: float | None = None,
|
||||
type_: Literal["jpeg", "png"] | None = None,
|
||||
user_agent: str | None = None,
|
||||
**kwargs,
|
||||
) -> Image | None:
|
||||
"""截图,该方法仅用于简单快捷截图,复杂截图请操作 page
|
||||
|
||||
参数:
|
||||
url: 网址
|
||||
path: 存储路径
|
||||
element: 元素选择
|
||||
wait_time: 等待截取超时时间
|
||||
viewport_size: 窗口大小
|
||||
wait_until: 等待类型
|
||||
timeout: 超时限制
|
||||
type_: 保存类型
|
||||
"""
|
||||
if viewport_size is None:
|
||||
viewport_size = dict(width=2560, height=1080)
|
||||
if isinstance(path, str):
|
||||
path = Path(path)
|
||||
wait_time = wait_time * 1000 if wait_time else None
|
||||
if isinstance(element, str):
|
||||
element_list = [element]
|
||||
else:
|
||||
element_list = element
|
||||
async with cls.new_page(
|
||||
viewport=viewport_size,
|
||||
user_agent=user_agent,
|
||||
**kwargs,
|
||||
) as page:
|
||||
await page.goto(url, timeout=timeout, wait_until=wait_until)
|
||||
card = page
|
||||
for e in element_list:
|
||||
if not card:
|
||||
return None
|
||||
card = await card.wait_for_selector(e, timeout=wait_time)
|
||||
if card:
|
||||
await card.screenshot(path=path, timeout=timeout, type=type_)
|
||||
return Image(path)
|
||||
return None
|
||||
|
||||
|
||||
class UrlPathNumberNotEqual(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BrowserIsNone(Exception):
|
||||
pass
|
||||
@@ -1,2 +1,339 @@
|
||||
from ._build_image import BuildImage
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
from nonebot.utils import is_coroutine_callable
|
||||
|
||||
from ._build_image import BuildImage, ColorAlias
|
||||
from ._build_mat import BuildMat
|
||||
from ._image_template import ImageTemplate, RowStyle
|
||||
|
||||
# TODO: text2image 长度错误
|
||||
|
||||
|
||||
async def text2image(
|
||||
text: str,
|
||||
auto_parse: bool = True,
|
||||
font_size: int = 20,
|
||||
color: str | tuple[int, int, int] = (255, 255, 255),
|
||||
font: str = "HYWenHei-85W.ttf",
|
||||
font_color: str | tuple[int, int, int] = (0, 0, 0),
|
||||
padding: int | tuple[int, int, int, int] = 0,
|
||||
_add_height: float = 0,
|
||||
) -> BuildImage:
|
||||
"""解析文本并转为图片
|
||||
使用标签
|
||||
<f> </f>
|
||||
可选配置项
|
||||
font: str -> 特殊文本字体
|
||||
fs / font_size: int -> 特殊文本大小
|
||||
fc / font_color: Union[str, Tuple[int, int, int]] -> 特殊文本颜色
|
||||
示例
|
||||
在不在,<f font=YSHaoShenTi-2.ttf font_size=30 font_color=red>HibiKi小姐</f>,
|
||||
你最近还好吗,<f font_size=15 font_color=black>我非常想你</f>,这段时间我非常不好过,
|
||||
<f font_size=25>抽卡抽不到金色</f>,这让我很痛苦
|
||||
参数:
|
||||
text: 文本
|
||||
auto_parse: 是否自动解析,否则原样发送
|
||||
font_size: 普通字体大小
|
||||
color: 背景颜色
|
||||
font: 普通字体
|
||||
font_color: 普通字体颜色
|
||||
padding: 文本外边距,元组类型时为 (上,左,下,右)
|
||||
_add_height: 由于get_size无法返回正确的高度,采用手动方式额外添加高度
|
||||
"""
|
||||
if not text:
|
||||
raise ValueError("文本转图片 text 不能为空...")
|
||||
pw = ph = top_padding = left_padding = 0
|
||||
if padding:
|
||||
if isinstance(padding, int):
|
||||
pw = padding * 2
|
||||
ph = padding * 2
|
||||
top_padding = left_padding = padding
|
||||
elif isinstance(padding, tuple):
|
||||
pw = padding[0] + padding[2]
|
||||
ph = padding[1] + padding[3]
|
||||
top_padding = padding[0]
|
||||
left_padding = padding[1]
|
||||
_font = BuildImage.load_font(font, font_size)
|
||||
if auto_parse and re.search(r"<f(.*)>(.*)</f>", text):
|
||||
_data = []
|
||||
new_text = ""
|
||||
placeholder_index = 0
|
||||
for s in text.split("</f>"):
|
||||
r = re.search(r"<f(.*)>(.*)", s)
|
||||
if r:
|
||||
start, end = r.span()
|
||||
if start != 0 and (t := s[:start]):
|
||||
new_text += t
|
||||
_data.append(
|
||||
[
|
||||
(start, end),
|
||||
f"[placeholder_{placeholder_index}]",
|
||||
r.group(1).strip(),
|
||||
r.group(2),
|
||||
]
|
||||
)
|
||||
new_text += f"[placeholder_{placeholder_index}]"
|
||||
placeholder_index += 1
|
||||
new_text += text.split("</f>")[-1]
|
||||
image_list = []
|
||||
current_placeholder_index = 0
|
||||
# 切分换行,每行为单张图片
|
||||
for s in new_text.split("\n"):
|
||||
_tmp_text = s
|
||||
img_width = 0
|
||||
img_height = BuildImage.get_text_size("正", _font)[1]
|
||||
_tmp_index = current_placeholder_index
|
||||
for _ in range(s.count("[placeholder_")):
|
||||
placeholder = _data[_tmp_index]
|
||||
if "font_size" in placeholder[2]:
|
||||
r = re.search(r"font_size=['\"]?(\d+)", placeholder[2])
|
||||
if r:
|
||||
w, h = BuildImage.get_text_size(
|
||||
placeholder[3], font, int(r.group(1))
|
||||
)
|
||||
img_height = img_height if img_height > h else h
|
||||
img_width += w
|
||||
else:
|
||||
img_width += BuildImage.get_text_size(placeholder[3], _font)[0]
|
||||
_tmp_text = _tmp_text.replace(f"[placeholder_{_tmp_index}]", "")
|
||||
_tmp_index += 1
|
||||
img_width += BuildImage.get_text_size(_tmp_text, _font)[0]
|
||||
# 开始画图
|
||||
A = BuildImage(
|
||||
img_width, img_height, color=color, font=font, font_size=font_size
|
||||
)
|
||||
basic_font_h = A.getsize("正")[1]
|
||||
current_width = 0
|
||||
# 遍历占位符
|
||||
for _ in range(s.count("[placeholder_")):
|
||||
if not s.startswith(f"[placeholder_{current_placeholder_index}]"):
|
||||
slice_ = s.split(f"[placeholder_{current_placeholder_index}]")
|
||||
await A.text(
|
||||
(current_width, A.height - basic_font_h - 1),
|
||||
slice_[0],
|
||||
font_color,
|
||||
)
|
||||
current_width += A.getsize(slice_[0])[0]
|
||||
placeholder = _data[current_placeholder_index]
|
||||
# 解析配置
|
||||
_font = font
|
||||
_font_size = font_size
|
||||
_font_color = font_color
|
||||
for e in placeholder[2].split():
|
||||
if e.startswith("font="):
|
||||
_font = e.split("=")[-1]
|
||||
if e.startswith("font_size=") or e.startswith("fs="):
|
||||
_font_size = int(e.split("=")[-1])
|
||||
if _font_size > 1000:
|
||||
_font_size = 1000
|
||||
if _font_size < 1:
|
||||
_font_size = 1
|
||||
if e.startswith("font_color") or e.startswith("fc="):
|
||||
_font_color = e.split("=")[-1]
|
||||
text_img = await BuildImage.build_text_image(
|
||||
placeholder[3], font=_font, size=_font_size, font_color=_font_color
|
||||
)
|
||||
_img_h = (
|
||||
int(A.height / 2 - text_img.height / 2)
|
||||
if new_text == "[placeholder_0]"
|
||||
else A.height - text_img.height
|
||||
)
|
||||
await A.paste(text_img, (current_width, _img_h - 1))
|
||||
current_width += text_img.width
|
||||
s = s[
|
||||
s.index(f"[placeholder_{current_placeholder_index}]")
|
||||
+ len(f"[placeholder_{current_placeholder_index}]") :
|
||||
]
|
||||
current_placeholder_index += 1
|
||||
if s:
|
||||
slice_ = s.split(f"[placeholder_{current_placeholder_index}]")
|
||||
await A.text((current_width, A.height - basic_font_h), slice_[0])
|
||||
current_width += A.getsize(slice_[0])[0]
|
||||
await A.crop((0, 0, current_width, A.height))
|
||||
# A.show()
|
||||
image_list.append(A)
|
||||
height = 0
|
||||
width = 0
|
||||
for img in image_list:
|
||||
height += img.h
|
||||
width = width if width > img.w else img.w
|
||||
width += pw
|
||||
height += ph
|
||||
A = BuildImage(width + left_padding, height + top_padding, color=color)
|
||||
current_height = top_padding
|
||||
for img in image_list:
|
||||
await A.paste(img, (left_padding, current_height))
|
||||
current_height += img.h
|
||||
else:
|
||||
width = 0
|
||||
height = 0
|
||||
_, h = BuildImage.get_text_size("正", _font)
|
||||
line_height = int(font_size / 3)
|
||||
image_list = []
|
||||
for s in text.split("\n"):
|
||||
w, _ = BuildImage.get_text_size(s.strip() or "正", _font)
|
||||
height += h + line_height
|
||||
width = width if width > w else w
|
||||
image_list.append(
|
||||
await BuildImage.build_text_image(
|
||||
s.strip(), font, font_size, font_color
|
||||
)
|
||||
)
|
||||
width += pw
|
||||
height += ph
|
||||
A = BuildImage(
|
||||
width + left_padding,
|
||||
height + top_padding + 2,
|
||||
color=color,
|
||||
)
|
||||
cur_h = ph
|
||||
for img in image_list:
|
||||
await A.paste(img, (pw, cur_h))
|
||||
cur_h += img.height + line_height
|
||||
return A
|
||||
|
||||
|
||||
def group_image(image_list: list[BuildImage]) -> tuple[list[list[BuildImage]], int]:
|
||||
"""
|
||||
说明:
|
||||
根据图片大小进行分组
|
||||
参数:
|
||||
image_list: 排序图片列表
|
||||
"""
|
||||
image_list.sort(key=lambda x: x.height, reverse=True)
|
||||
max_image = max(image_list, key=lambda x: x.height)
|
||||
|
||||
image_list.remove(max_image)
|
||||
max_h = max_image.height
|
||||
total_w = 0
|
||||
|
||||
# 图片分组
|
||||
image_group = [[max_image]]
|
||||
is_use = []
|
||||
surplus_list = image_list[:]
|
||||
|
||||
for image in image_list:
|
||||
if image.uid not in is_use:
|
||||
group = [image]
|
||||
is_use.append(image.uid)
|
||||
curr_h = image.height
|
||||
while True:
|
||||
surplus_list = [x for x in surplus_list if x.uid not in is_use]
|
||||
for tmp in surplus_list:
|
||||
temp_h = curr_h + tmp.height + 10
|
||||
if temp_h < max_h or abs(max_h - temp_h) < 100:
|
||||
curr_h += tmp.height + 15
|
||||
is_use.append(tmp.uid)
|
||||
group.append(tmp)
|
||||
break
|
||||
else:
|
||||
break
|
||||
total_w += max([x.width for x in group]) + 15
|
||||
image_group.append(group)
|
||||
while surplus_list:
|
||||
surplus_list = [x for x in surplus_list if x.uid not in is_use]
|
||||
if not surplus_list:
|
||||
break
|
||||
surplus_list.sort(key=lambda x: x.height, reverse=True)
|
||||
for img in surplus_list:
|
||||
if img.uid not in is_use:
|
||||
_w = 0
|
||||
index = -1
|
||||
for i, ig in enumerate(image_group):
|
||||
if s := sum([x.height for x in ig]) > _w:
|
||||
_w = s
|
||||
index = i
|
||||
if index != -1:
|
||||
image_group[index].append(img)
|
||||
is_use.append(img.uid)
|
||||
|
||||
max_h = 0
|
||||
max_w = 0
|
||||
for ig in image_group:
|
||||
if (_h := sum([x.height + 15 for x in ig])) > max_h:
|
||||
max_h = _h
|
||||
max_w += max([x.width for x in ig]) + 30
|
||||
is_use.clear()
|
||||
while abs(max_h - max_w) > 200 and len(image_group) - 1 >= len(image_group[-1]):
|
||||
for img in image_group[-1]:
|
||||
_min_h = 999999
|
||||
_min_index = -1
|
||||
for i, ig in enumerate(image_group):
|
||||
# if i not in is_use and (_h := sum([x.h for x in ig]) + img.h) > _min_h:
|
||||
if (_h := sum([x.height for x in ig]) + img.height) < _min_h:
|
||||
_min_h = _h
|
||||
_min_index = i
|
||||
is_use.append(_min_index)
|
||||
image_group[_min_index].append(img)
|
||||
max_w -= max([x.width for x in image_group[-1]]) - 30
|
||||
image_group.pop(-1)
|
||||
max_h = max([sum([x.height + 15 for x in ig]) for ig in image_group])
|
||||
return image_group, max(max_h + 250, max_w + 70)
|
||||
|
||||
|
||||
async def build_sort_image(
|
||||
image_group: list[list[BuildImage]],
|
||||
h: int | None = None,
|
||||
padding_top: int = 200,
|
||||
color: ColorAlias = (
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
),
|
||||
background_path: Path | None = None,
|
||||
background_handle: Callable[[BuildImage], Awaitable] | None = None,
|
||||
) -> BuildImage:
|
||||
"""
|
||||
说明:
|
||||
对group_image的图片进行组装
|
||||
参数:
|
||||
image_group: 分组图片列表
|
||||
h: max(宽,高),一般为group_image的返回值,有值时,图片必定为正方形
|
||||
padding_top: 图像列表与最顶层间距
|
||||
color: 背景颜色
|
||||
background_path: 背景图片文件夹路径(随机)
|
||||
background_handle: 背景图额外操作
|
||||
"""
|
||||
bk_file = None
|
||||
if background_path:
|
||||
random_bk = os.listdir(background_path)
|
||||
if random_bk:
|
||||
bk_file = random.choice(random_bk)
|
||||
image_w = 0
|
||||
image_h = 0
|
||||
if not h:
|
||||
for ig in image_group:
|
||||
_w = max([x.width + 30 for x in ig])
|
||||
image_w += _w + 30
|
||||
_h = sum([x.height + 10 for x in ig])
|
||||
if _h > image_h:
|
||||
image_h = _h
|
||||
image_h += padding_top
|
||||
else:
|
||||
image_w = h
|
||||
image_h = h
|
||||
A = BuildImage(
|
||||
image_w,
|
||||
image_h,
|
||||
font_size=24,
|
||||
font="CJGaoDeGuo.otf",
|
||||
color=color,
|
||||
background=(background_path / bk_file) if background_path and bk_file else None,
|
||||
)
|
||||
if background_handle:
|
||||
if is_coroutine_callable(background_handle):
|
||||
await background_handle(A)
|
||||
else:
|
||||
background_handle(A)
|
||||
curr_w = 50
|
||||
for ig in image_group:
|
||||
curr_h = padding_top - 20
|
||||
for img in ig:
|
||||
await A.paste(img, (curr_w, curr_h))
|
||||
curr_h += img.height + 10
|
||||
curr_w += max([x.width for x in ig]) + 30
|
||||
return A
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import random
|
||||
|
||||
user_agent = [
|
||||
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
|
||||
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
|
||||
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko",
|
||||
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
|
||||
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
|
||||
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
|
||||
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
|
||||
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
|
||||
"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)",
|
||||
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
|
||||
"Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
|
||||
"Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
|
||||
"Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
|
||||
"Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
"MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
"Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10",
|
||||
"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13",
|
||||
"Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+",
|
||||
"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0",
|
||||
"Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124",
|
||||
"Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)",
|
||||
"UCWEB7.0.2.37/28/999",
|
||||
"NOKIA5700/ UCWEB7.0.2.37/28/999",
|
||||
"Openwave/ UCWEB7.0.2.37/28/999",
|
||||
"Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999",
|
||||
# iPhone 6:
|
||||
"Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25",
|
||||
]
|
||||
|
||||
|
||||
def get_user_agent():
|
||||
return {"User-Agent": random.choice(user_agent)}
|
||||
|
||||
|
||||
def get_user_agent_str():
|
||||
return random.choice(user_agent)
|
||||
@@ -1,11 +1,42 @@
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
|
||||
class WithdrawManager:
|
||||
"""
|
||||
消息撤回
|
||||
"""
|
||||
|
||||
_data = {}
|
||||
|
||||
@classmethod
|
||||
def append(cls, message_id: str, second: int):
|
||||
"""添加一个撤回消息id和时间
|
||||
|
||||
参数:
|
||||
message_id: 撤回消息id
|
||||
time: 延迟时间
|
||||
"""
|
||||
cls._data[message_id] = second
|
||||
|
||||
@classmethod
|
||||
def remove(cls, message_id: str):
|
||||
"""删除一个数据
|
||||
|
||||
参数:
|
||||
message_id: 撤回消息id
|
||||
"""
|
||||
if message_id in cls._data:
|
||||
del cls._data[message_id]
|
||||
|
||||
|
||||
class ResourceDirManager:
|
||||
"""
|
||||
临时文件管理器
|
||||
@@ -45,6 +76,69 @@ class ResourceDirManager:
|
||||
cls.__tree_append(path)
|
||||
|
||||
|
||||
class CountLimiter:
|
||||
"""
|
||||
次数检测工具,检测调用次数是否超过设定值
|
||||
"""
|
||||
|
||||
def __init__(self, max_count: int):
|
||||
self.count = defaultdict(int)
|
||||
self.max_count = max_count
|
||||
|
||||
def add(self, key: Any):
|
||||
self.count[key] += 1
|
||||
|
||||
def check(self, key: Any) -> bool:
|
||||
if self.count[key] >= self.max_count:
|
||||
self.count[key] = 0
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class UserBlockLimiter:
|
||||
"""
|
||||
检测用户是否正在调用命令
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.flag_data = defaultdict(bool)
|
||||
self.time = time.time()
|
||||
|
||||
def set_true(self, key: Any):
|
||||
self.time = time.time()
|
||||
self.flag_data[key] = True
|
||||
|
||||
def set_false(self, key: Any):
|
||||
self.flag_data[key] = False
|
||||
|
||||
def check(self, key: Any) -> bool:
|
||||
if time.time() - self.time > 30:
|
||||
self.set_false(key)
|
||||
return False
|
||||
return self.flag_data[key]
|
||||
|
||||
|
||||
class FreqLimiter:
|
||||
"""
|
||||
命令冷却,检测用户是否处于冷却状态
|
||||
"""
|
||||
|
||||
def __init__(self, default_cd_seconds: int):
|
||||
self.next_time = defaultdict(float)
|
||||
self.default_cd = default_cd_seconds
|
||||
|
||||
def check(self, key: Any) -> bool:
|
||||
return time.time() >= self.next_time[key]
|
||||
|
||||
def start_cd(self, key: Any, cd_time: int = 0):
|
||||
self.next_time[key] = time.time() + (
|
||||
cd_time if cd_time > 0 else self.default_cd
|
||||
)
|
||||
|
||||
def left_time(self, key: Any) -> float:
|
||||
return self.next_time[key] - time.time()
|
||||
|
||||
|
||||
async def get_user_avatar(uid: int | str) -> bytes | None:
|
||||
"""快捷获取用户头像
|
||||
|
||||
|
||||
Reference in New Issue
Block a user