nonebot-plugin-l4d2-server 1.1.1__py3-none-any.whl → 1.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. nonebot_plugin_l4d2_server/config.py +82 -45
  2. nonebot_plugin_l4d2_server/l4_help/__init__.py +1 -1
  3. nonebot_plugin_l4d2_server/l4_image/convert.py +0 -69
  4. nonebot_plugin_l4d2_server/l4_image/html_img.py +41 -17
  5. nonebot_plugin_l4d2_server/l4_image/image_tools.py +1 -104
  6. nonebot_plugin_l4d2_server/l4_image/img/back_img/background.jpg +0 -0
  7. nonebot_plugin_l4d2_server/l4_image/img/template/normal.html +73 -167
  8. nonebot_plugin_l4d2_server/l4_image/img/template/style.css +264 -0
  9. nonebot_plugin_l4d2_server/l4_request/draw_msg.py +1 -18
  10. {nonebot_plugin_l4d2_server-1.1.1.dist-info → nonebot_plugin_l4d2_server-1.1.3.dist-info}/METADATA +4 -8
  11. nonebot_plugin_l4d2_server-1.1.3.dist-info/RECORD +57 -0
  12. nonebot_plugin_l4d2_server/l4_help/icon//344/273/213/347/273/215.png +0 -0
  13. nonebot_plugin_l4d2_server/l4_help/icon//344/273/273/345/212/241.png +0 -0
  14. nonebot_plugin_l4d2_server/l4_help/icon//344/277/241/346/201/257.png +0 -0
  15. nonebot_plugin_l4d2_server/l4_help/icon//345/205/254/345/221/212.png +0 -0
  16. nonebot_plugin_l4d2_server/l4_help/icon//345/210/200/345/211/221.png +0 -0
  17. nonebot_plugin_l4d2_server/l4_help/icon//345/210/207/346/215/242.png +0 -0
  18. nonebot_plugin_l4d2_server/l4_help/icon//345/210/240/351/231/244.png +0 -0
  19. nonebot_plugin_l4d2_server/l4_help/icon//345/210/267/346/226/260.png +0 -0
  20. nonebot_plugin_l4d2_server/l4_help/icon//345/215/241/347/273/204.png +0 -0
  21. nonebot_plugin_l4d2_server/l4_help/icon//345/223/252/351/207/214.png +0 -0
  22. nonebot_plugin_l4d2_server/l4_help/icon//345/234/260/345/233/276.png +0 -0
  23. nonebot_plugin_l4d2_server/l4_help/icon//345/257/274/345/205/245.png +0 -0
  24. nonebot_plugin_l4d2_server/l4_help/icon//345/257/274/345/207/272.png +0 -0
  25. nonebot_plugin_l4d2_server/l4_help/icon//345/275/261.png +0 -0
  26. nonebot_plugin_l4d2_server/l4_help/icon//346/213/274/345/233/276.png +0 -0
  27. nonebot_plugin_l4d2_server/l4_help/icon//346/216/242/347/264/242.png +0 -0
  28. nonebot_plugin_l4d2_server/l4_help/icon//346/216/250/351/200/201.png +0 -0
  29. nonebot_plugin_l4d2_server/l4_help/icon//346/224/266/351/233/206.png +0 -0
  30. nonebot_plugin_l4d2_server/l4_help/icon//346/224/273/347/225/245.png +0 -0
  31. nonebot_plugin_l4d2_server/l4_help/icon//346/233/264/346/226/260.png +0 -0
  32. nonebot_plugin_l4d2_server/l4_help/icon//346/235/220/346/226/231.png +0 -0
  33. nonebot_plugin_l4d2_server/l4_help/icon//346/237/245/350/257/242.png +0 -0
  34. nonebot_plugin_l4d2_server/l4_help/icon//346/240/241/351/252/214.png +0 -0
  35. nonebot_plugin_l4d2_server/l4_help/icon//346/257/217/346/234/210.png +0 -0
  36. nonebot_plugin_l4d2_server/l4_help/icon//346/267/261/346/270/212.png +0 -0
  37. nonebot_plugin_l4d2_server/l4_help/icon//346/267/273/345/212/240.png +0 -0
  38. nonebot_plugin_l4d2_server/l4_help/icon//346/270/205/351/231/244.png +0 -0
  39. nonebot_plugin_l4d2_server/l4_help/icon//347/212/266/346/200/201.png +0 -0
  40. nonebot_plugin_l4d2_server/l4_help/icon//347/255/276/345/210/260.png +0 -0
  41. nonebot_plugin_l4d2_server/l4_help/icon//347/273/221/345/256/232.png +0 -0
  42. nonebot_plugin_l4d2_server/l4_help/icon//350/241/250.png +0 -0
  43. nonebot_plugin_l4d2_server/l4_help/icon//350/241/250/346/203/205.png +0 -0
  44. nonebot_plugin_l4d2_server/l4_help/icon//350/247/222/350/211/262.png +0 -0
  45. nonebot_plugin_l4d2_server/l4_help/icon//350/256/260/345/275/225.png +0 -0
  46. nonebot_plugin_l4d2_server/l4_help/icon//351/205/215/347/275/256.png +0 -0
  47. nonebot_plugin_l4d2_server/l4_help/icon//351/207/215/345/220/257.png +0 -0
  48. nonebot_plugin_l4d2_server/l4_image/img/template/Bocchi_The_Rock.html +0 -299
  49. nonebot_plugin_l4d2_server/l4_image/img/template/Bocchi_The_Rock.png +0 -0
  50. nonebot_plugin_l4d2_server/l4_image/img/template/Pixel.html +0 -341
  51. nonebot_plugin_l4d2_server/l4_image/img/template/Pixel.png +0 -0
  52. nonebot_plugin_l4d2_server/l4_image/img/template/Rainbow.html +0 -355
  53. nonebot_plugin_l4d2_server/l4_image/img/template/Rainbow.png +0 -0
  54. nonebot_plugin_l4d2_server/l4_image/img/template/fingerprint.svg +0 -15
  55. nonebot_plugin_l4d2_server-1.1.1.dist-info/RECORD +0 -98
  56. /nonebot_plugin_l4d2_server/l4_image/img/template/{normal_.html → normal_old.html} +0 -0
  57. {nonebot_plugin_l4d2_server-1.1.1.dist-info → nonebot_plugin_l4d2_server-1.1.3.dist-info}/WHEEL +0 -0
  58. {nonebot_plugin_l4d2_server-1.1.1.dist-info → nonebot_plugin_l4d2_server-1.1.3.dist-info}/entry_points.txt +0 -0
  59. {nonebot_plugin_l4d2_server-1.1.1.dist-info → nonebot_plugin_l4d2_server-1.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,43 +1,73 @@
1
1
  from pathlib import Path
2
+ from typing import List
2
3
 
3
4
  from nonebot import get_plugin_config
4
5
  from nonebot.log import logger
5
- from pydantic import BaseModel
6
+ from pydantic import BaseModel, Field
6
7
 
8
+ # 常量定义
7
9
  DATAPATH = Path(__file__).parent.joinpath("data")
8
- DATAOUT = Path("data/L4D2")
9
- DATAOUT.mkdir(parents=True, exist_ok=True)
10
- if not Path(DATAOUT / "l4d2.json").exists():
11
- logger.info("文件 l4d2.json 不存在,已创建并初始化为 {}")
12
- Path(DATAOUT / "l4d2.json").write_text("{}", encoding="utf-8")
10
+ DEFAULT_DATA_DIR = "data/L4D2"
11
+ DEFAULT_FONT = str(Path(__file__).parent.joinpath("data/font/loli.ttf"))
12
+
13
+
14
+ # 初始化数据目录
15
+ def init_data_directory(data_dir: Path) -> None:
16
+ """初始化数据目录和必要文件"""
17
+ data_dir.mkdir(parents=True, exist_ok=True)
18
+ json_file = data_dir / "l4d2.json"
19
+
20
+ if not json_file.exists():
21
+ logger.info(f"文件 {json_file.name} 不存在,已创建并初始化为 {{}}")
22
+ json_file.write_text("{}", encoding="utf-8")
23
+
24
+
25
+ # 初始化目录结构
26
+ DATAOUT = Path(DEFAULT_DATA_DIR)
27
+ init_data_directory(DATAOUT)
28
+ init_data_directory(DATAOUT / "l4d2")
13
29
  server_all_path = DATAOUT / "l4d2"
14
30
  server_all_path.mkdir(parents=True, exist_ok=True)
15
31
 
16
32
  ICONPATH = DATAPATH / "icon"
17
-
18
33
  global map_index
19
34
  map_index = 0
20
35
 
21
36
 
22
37
  class ConfigModel(BaseModel):
23
- l4_enable: bool = True
24
- """是否全局启用求生功能"""
25
- l4_image: bool = True
26
- """是否启用图片"""
27
- l4_connect: bool = True
28
- """是否在查服命令后加入connect ip"""
29
- l4_path: str = "data/L4D2"
30
- """插件数据路径"""
31
- l4_players: int = 4
32
- """查询总图的时候展示的用户数量"""
33
- l4_style: str = "default"
34
- """图片风格"""
35
- l4_font: str = str(Path(__file__).parent.joinpath("data/font/loli.ttf"))
36
- """字体"""
37
- l4_show_ip: bool = True
38
- """单服务器查询时候是否展示ip直连地址"""
39
- l4_local: list[str] = []
40
- """本地服务器路径,填写路径下有`steam_appid.txt`文件"""
38
+ """插件配置模型"""
39
+
40
+ l4_enable: bool = Field(default=True, description="是否全局启用求生功能")
41
+ l4_image: bool = Field(default=True, description="是否启用图片")
42
+ l4_connect: bool = Field(default=True, description="是否在查服命令后加入connect ip")
43
+ l4_path: str = Field(default=DEFAULT_DATA_DIR, description="插件数据路径")
44
+ l4_players: int = Field(default=4, ge=1, description="查询总图时展示的用户数量")
45
+ l4_style: str = Field(default="default", description="图片风格")
46
+ l4_font: str = Field(default=DEFAULT_FONT, description="字体文件路径")
47
+ l4_show_ip: bool = Field(
48
+ default=True,
49
+ description="单服务器查询时是否展示ip直连地址",
50
+ )
51
+ l4_local: List[str] = Field(default_factory=list, description="本地服务器路径列表")
52
+
53
+ @classmethod
54
+ def validate_players(cls, v):
55
+ if v < 1:
56
+ raise ValueError("玩家数量必须大于0")
57
+ return v
58
+
59
+ @classmethod
60
+ def validate_font(cls, v):
61
+ if not Path(v).exists():
62
+ logger.warning(f"字体文件 {v} 不存在")
63
+ return v
64
+
65
+ @classmethod
66
+ def validate_local_paths(cls, v):
67
+ path = Path(v)
68
+ if not (path / "steam_appid.txt").exists():
69
+ raise ValueError(f"路径 {v} 下缺少 steam_appid.txt 文件")
70
+ return str(path.resolve())
41
71
 
42
72
 
43
73
  config = get_plugin_config(ConfigModel)
@@ -49,46 +79,53 @@ class ConfigManager:
49
79
  def __init__(self):
50
80
  self._config = config
51
81
 
52
- def update_image_config(self, enabled: bool) -> None:
53
- """更新图片配置
82
+ @property
83
+ def current_config(self) -> ConfigModel:
84
+ """获取当前配置"""
85
+ return self._config
54
86
 
55
- Args:
56
- enabled: 是否启用图片功能
57
- """
87
+ def update_image_config(self, enabled: bool) -> None:
88
+ """更新图片配置"""
58
89
  self._config.l4_image = enabled
59
90
 
60
91
  def update_style_config(self, style: str) -> None:
61
- """更新图片风格配置
62
-
63
- Args:
64
- style: 图片风格名称
65
- """
92
+ """更新图片风格配置"""
66
93
  if not isinstance(style, str):
67
94
  raise TypeError("style必须是字符串")
68
95
  self._config.l4_style = style
69
96
 
70
97
  def update_connect_config(self, enabled: bool) -> None:
71
- """更新connect ip配置
72
-
73
- Args:
74
- enabled: 是否在查服命令后加入connect ip
75
- """
98
+ """更新connect ip配置"""
76
99
  self._config.l4_connect = enabled
77
100
 
78
101
  def update(self, **kwargs) -> None:
79
- """通用配置更新方法
102
+ """
103
+ 通用配置更新方法
80
104
 
81
105
  Args:
82
106
  **kwargs: 要更新的配置项键值对
83
107
 
84
108
  Raises:
85
- ValueError: 当传入无效的配置项时
109
+ ValueError: 当传入无效的配置项或值不合法时
110
+ TypeError: 当传入值的类型不正确时
86
111
  """
112
+ valid_keys = ConfigModel.model_fields.keys()
113
+
87
114
  for key, value in kwargs.items():
88
- if hasattr(self._config, key):
89
- setattr(self._config, key, value)
90
- else:
115
+ if key not in valid_keys:
91
116
  raise ValueError(f"无效的配置项: {key}")
92
117
 
118
+ field_type = ConfigModel[key].type_
119
+ if not isinstance(value, field_type):
120
+ raise TypeError(f"{key} 必须是 {field_type.__name__} 类型")
121
+
122
+ setattr(self._config, key, value)
123
+
124
+ # 验证更新后的配置
125
+ try:
126
+ self._config = ConfigModel(**self._config.dict())
127
+ except ValueError as e:
128
+ logger.error(f"配置更新失败: {e!s}")
129
+
93
130
 
94
131
  config_manager = ConfigManager()
@@ -10,7 +10,7 @@ from ..l4_image.convert import core_font
10
10
  from ..l4_image.model import PluginHelp
11
11
  from .draw import get_help
12
12
 
13
- __version__ = "1.1.1"
13
+ __version__ = "1.1.3"
14
14
  TEXT_PATH = Path(__file__).parent / "texture2d"
15
15
  HELP_DATA = Path(__file__).parent / "Help.json"
16
16
 
@@ -79,75 +79,6 @@ async def convert_img(
79
79
  return f"base64://{b64encode(img).decode()}"
80
80
 
81
81
 
82
- def convert_img_sync(img_path: Path):
83
- with img_path.open("rb") as fp:
84
- img = fp.read()
85
-
86
- return f"base64://{b64encode(img).decode()}"
87
-
88
-
89
- async def str_lenth(r: str, size: int, limit: int = 540) -> str: # noqa: RUF029
90
- result = ""
91
- temp = 0
92
- for i in r:
93
- if i == "\n":
94
- temp = 0
95
- result += i
96
- continue
97
-
98
- if temp >= limit:
99
- result += "\n" + i
100
- temp = 0
101
- else:
102
- result += i
103
-
104
- if i.isdigit():
105
- temp += round(size / 10 * 6)
106
- elif i == "/":
107
- temp += round(size / 10 * 2.2)
108
- elif i == ".":
109
- temp += round(size / 10 * 3)
110
- elif i == "%":
111
- temp += round(size / 10 * 9.4)
112
- else:
113
- temp += size
114
- return result
115
-
116
-
117
- def get_str_size(
118
- r: str,
119
- font: ImageFont.FreeTypeFont,
120
- limit: int = 540,
121
- ) -> str:
122
- result = ""
123
- line = ""
124
- for i in r:
125
- if i == "\n":
126
- result += f"{line}\n"
127
- line = ""
128
- continue
129
-
130
- line += i
131
-
132
- if hasattr(font, "getsize"):
133
- size, _ = font.getsize(line) # type: ignore
134
- else:
135
- bbox = font.getbbox(line)
136
- size, _ = bbox[2] - bbox[0], bbox[3] - bbox[1]
137
-
138
- if size >= limit:
139
- result += f"{line}\n"
140
- line = ""
141
- else:
142
- result += line
143
- return result
144
-
145
-
146
- def get_height(content: str, size: int) -> int:
147
- line_count = content.count("\n")
148
- return (line_count + 1) * size
149
-
150
-
151
82
  async def text2pic(text: str, max_size: int = 800, font_size: int = 24):
152
83
  if text.endswith("\n"):
153
84
  text = text[:-1]
@@ -1,10 +1,10 @@
1
- import random
2
1
  from pathlib import Path
2
+ import re
3
3
  from typing import List, Optional
4
4
 
5
5
  import jinja2
6
6
  from nonebot.log import logger
7
- from nonebot_plugin_htmlrender import html_to_pic
7
+ from nonebot_plugin_htmlrender import html_to_pic, template_to_html, template_to_pic
8
8
 
9
9
  from ..config import config
10
10
  from ..utils.api.models import OutServer
@@ -46,12 +46,20 @@ async def server_ip_pic(server_dict: List[OutServer]):
46
46
  reverse=True,
47
47
  )[:max_number]
48
48
  logger.debug(sorted_players)
49
+
50
+ # 时间转换
51
+ max_duration_len = max(
52
+ [len(str(await convert_duration(i.duration))) for i in sorted_players],
53
+ )
54
+ for player in sorted_players:
55
+ chines_dur = await convert_duration(player.duration)
56
+ dur = "{:^{}}".format(chines_dur, max_duration_len)
57
+ player.name = str(player.name) + " | " + dur
58
+
49
59
  server_info["player"] = sorted_players
50
60
  else:
51
61
  server_info["player"] = []
52
62
 
53
- # server_info["server"].server_type= f"{server_info['server'].server_type}.svg"
54
- print(server_dict)
55
63
  pic = await get_server_img(server_dict)
56
64
  if pic:
57
65
  logger.success("正在输出图片")
@@ -62,23 +70,18 @@ async def server_ip_pic(server_dict: List[OutServer]):
62
70
 
63
71
  async def get_server_img(plugins: List[OutServer]) -> Optional[bytes]:
64
72
  try:
65
- if config.l4_style == "孤独摇滚":
66
- template = env.get_template("Bocchi_The_Rock.html")
67
- elif config.l4_style == "电玩像素":
68
- template = env.get_template("Pixel.html")
69
- elif config.l4_style == "缤纷彩虹":
70
- template = env.get_template("Rainbow.html")
71
- elif config.l4_style == "随机":
72
- html_files = [
73
- str(f.name) for f in template_path.rglob("*.html") if f.is_file()
74
- ]
75
- template = env.get_template(random.choice(html_files))
76
- else:
73
+ if config.l4_style == "default":
74
+
75
+
77
76
  template = env.get_template("normal.html")
77
+ else:
78
+ template = env.get_template("normal_old.html")
78
79
  content = await template.render_async(
79
80
  servers=plugins,
80
81
  max_count=config.l4_players,
81
82
  )
83
+ # with open("test.html", "w", encoding="utf-8") as f:
84
+ # f.write(content)
82
85
  return await html_to_pic(
83
86
  content,
84
87
  wait=0,
@@ -87,4 +90,25 @@ async def get_server_img(plugins: List[OutServer]) -> Optional[bytes]:
87
90
  )
88
91
  except Exception as e:
89
92
  logger.warning(f"Error in get_server_img: {e}")
90
- return None
93
+ return None
94
+
95
+
96
+ async def convert_duration(duration: float) -> str:
97
+ """将秒数转换为易读的时间字符串格式(例如 '1h 30m 15s')
98
+
99
+ 参数:
100
+ duration: 秒数
101
+
102
+ 返回:
103
+ 格式化的时间字符串
104
+ """
105
+ total_seconds = int(duration)
106
+ minutes, seconds = divmod(total_seconds, 60)
107
+ hours, minutes = divmod(minutes, 60)
108
+ time_str = ""
109
+ if hours > 0:
110
+ time_str += f"{hours}h "
111
+ if minutes > 0:
112
+ time_str += f"{minutes}m "
113
+ time_str += f"{seconds}s"
114
+ return time_str
@@ -6,83 +6,17 @@ from typing import Optional, Tuple, Union
6
6
 
7
7
  import httpx
8
8
  from httpx import get
9
- from PIL import Image, ImageDraw, ImageFilter, ImageFont
9
+ from PIL import Image, ImageDraw, ImageFont
10
10
 
11
11
  TEXT_PATH = Path(__file__).parent / "texture2d"
12
12
  BG_PATH = Path(__file__).parents[1] / "default_bg"
13
13
 
14
14
 
15
- def get_div():
16
- return Image.open(TEXT_PATH / "div.png")
17
-
18
-
19
15
  async def sget(url: str):
20
16
  async with httpx.AsyncClient(timeout=None) as client: # noqa: S113
21
17
  return await client.get(url=url)
22
18
 
23
19
 
24
- def get_status_icon(status: Union[int, bool]) -> Image.Image:
25
- if status:
26
- img = Image.open(TEXT_PATH / "yes.png")
27
- else:
28
- img = Image.open(TEXT_PATH / "no.png")
29
- return img
30
-
31
-
32
- def get_v4_footer():
33
- return Image.open(TEXT_PATH / "footer.png")
34
-
35
-
36
- def get_v4_bg(w: int, h: int, is_dark: bool = False, is_blur: bool = False):
37
- ci_img = CustomizeImage(BG_PATH)
38
- img = ci_img.get_image(None, w, h)
39
- if is_blur:
40
- img = img.filter(ImageFilter.GaussianBlur(radius=20))
41
- if is_dark:
42
- black_img = Image.new("RGBA", (w, h), (0, 0, 0, 180))
43
- img.paste(black_img, (0, 0), black_img)
44
- return img.convert("RGBA")
45
-
46
-
47
- async def shift_image_hue(
48
- img: Image.Image,
49
- angle: float = 30,
50
- ) -> Image.Image: # noqa: RUF029
51
- alpha = img.getchannel("A")
52
- img = img.convert("HSV")
53
-
54
- pixels = img.load()
55
- assert pixels is not None
56
- hue_shift = angle
57
-
58
- for y in range(img.height):
59
- for x in range(img.width):
60
- h, s, v = pixels[x, y] # type: ignore
61
- h = (h + hue_shift) % 360
62
- pixels[x, y] = (h, s, v) # type: ignore
63
-
64
- img = img.convert("RGBA")
65
- img.putalpha(alpha)
66
- return img
67
-
68
-
69
- async def get_pic(url: str, size: Optional[Tuple[int, int]] = None) -> Image.Image:
70
- """
71
- 从网络获取图片, 格式化为RGBA格式的指定尺寸
72
- """
73
- async with httpx.AsyncClient(timeout=None) as client: # noqa: S113
74
- resp = await client.get(url=url)
75
- if resp.status_code != 200:
76
- if size is None:
77
- size = (960, 600)
78
- return Image.new("RGBA", size)
79
- pic = Image.open(BytesIO(resp.read()))
80
- pic = pic.convert("RGBA")
81
- if size is not None:
82
- pic = pic.resize(size)
83
- return pic
84
-
85
-
86
20
  def draw_center_text_by_line(
87
21
  img: ImageDraw.ImageDraw,
88
22
  pos: Tuple[int, int],
@@ -191,43 +125,6 @@ def draw_text_by_line(
191
125
  return y
192
126
 
193
127
 
194
- def easy_paste(
195
- im: Image.Image,
196
- im_paste: Image.Image,
197
- pos=(0, 0), # noqa: ANN001
198
- direction="lt", # noqa: ANN001
199
- ):
200
- """
201
- inplace method
202
- 快速粘贴, 自动获取被粘贴图像的坐标。
203
- pos应当是粘贴点坐标,direction指定粘贴点方位,例如lt为左上
204
- """
205
- x, y = pos
206
- size_x, size_y = im_paste.size
207
- if "d" in direction:
208
- y = y - size_y
209
- if "r" in direction:
210
- x = x - size_x
211
- if "c" in direction:
212
- x = x - int(0.5 * size_x)
213
- y = y - int(0.5 * size_y)
214
- im.paste(im_paste, (x, y, x + size_x, y + size_y), im_paste)
215
-
216
-
217
- def easy_alpha_composite(
218
- im: Image.Image,
219
- im_paste: Image.Image,
220
- pos=(0, 0), # noqa: ANN001
221
- direction="lt", # noqa: ANN001
222
- ) -> Image.Image:
223
- """
224
- 透明图像快速粘贴
225
- """
226
- base = Image.new("RGBA", im.size)
227
- easy_paste(base, im_paste, pos, direction)
228
- return Image.alpha_composite(im, base)
229
-
230
-
231
128
  async def get_qq_avatar(
232
129
  qid: Optional[Union[int, str]] = None,
233
130
  avatar_url: Optional[str] = None,