feat✨: 更新内置插件

This commit is contained in:
HibiKier
2024-02-25 03:18:34 +08:00
parent 131200a28e
commit eb0572ea77
83 changed files with 7588 additions and 450 deletions
+49 -25
View File
@@ -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()
+156
View File
@@ -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
)
+3 -5
View File
@@ -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
+204
View File
@@ -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
View File
@@ -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):
+12
View File
@@ -1,2 +1,14 @@
class NotFoundError(Exception):
pass
class GroupInfoNotFound(Exception):
pass
class EmptyError(Exception):
pass
class UserAndGroupIsNone(Exception):
pass
+379
View File
@@ -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
+338 -1
View File
@@ -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
+50
View File
@@ -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)
+94
View File
@@ -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:
"""快捷获取用户头像