diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index c5076cf..b460c2b 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -7,7 +7,7 @@ on: jobs: build-n-publish: - name: Use PDM to Build and publish Python 🐍 distributions 📦 to PyPI + name: Use uv to Build and publish Python 🐍 distributions 📦 to PyPI runs-on: ubuntu-latest permissions: @@ -20,8 +20,15 @@ jobs: with: submodules: true - - name: Setup PDM - uses: pdm-project/setup-pdm@v3 + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: 'Set up Python' + uses: actions/setup-python@v5 + with: + python-version-file: 'pyproject.toml' - name: Build and Publish distribution 📦 to PyPI - run: pdm publish + run: | + uv build + uv publish diff --git a/README.md b/README.md index 98b94a4..698f47b 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ _✨ Minecraft 服务器 MOTD 查询 图片版 ✨_ -python - - pdm-managed +python + + uv wakatime @@ -172,7 +172,7 @@ plugins = [ (注意,nb2 以 JSON 格式解析配置项,所以当你要在正则表达式里表示`\`时,你需要将其转义为`\\`) - `host` - 要查询的服务器地址,格式为 `[:端口]`, 例如 `hypixel.net` 或 `example.com:1919` -- `type` - 要查询服务器的类型,`je` 表示 Java 版服,`be` 表示基岩版服 +- `type` - 要查询服务器的类型,`je` 表示 Java 版服,`be` 表示基岩版服,`auto` 代表自动检测 - `whitelist` - (仅支持 OneBot V11 适配器)群聊白名单,只有里面列出的群号可以查询,可以不填来对所有群开放查询 最终的配置项看起来是这样子的,当你发送 `查服` 时,机器人会把 EaseCation 服务器的状态发送出来 @@ -193,6 +193,15 @@ MCSTAT_SHORTCUTS=' 如果你的服务器在运行 Clash 等拦截了 DNS 解析的软件,且查询部分地址时遇到了问题,请尝试关闭此配置项 此配置项不影响 Java 服务器的 SRV 记录解析 +### `MCSTAT_RESOLVE_DNS_IPV6` - 是否启用 IPv6 解析 + +默认:`True` + +是否优先使用 IPv6 地址进行查询 +当启用此配置项时,会优先尝试使用 IPv6 地址进行连接,如连接失败则自动回落到 IPv4 +如果你的网络环境不支持 IPv6,可以关闭此配置项以避免不必要的等待 +此配置项仅在 `MCSTAT_RESOLVE_DNS` 启用时生效 + ### `MCSTAT_QUERY_TWICE` - 是否查询两遍服务器状态 默认:`True` @@ -204,6 +213,12 @@ MCSTAT_SHORTCUTS=' 默认:`767` +### `MCSTAT_ENABLE_AUTO_DETECT` - 是否在使用未指定服务器类型的 `motd` 指令时自动检测 + +默认:`True` + +如设为 `False` 将默认指定为 Java 版 + ## 🎉 使用 发送 `motd` 指令 查看使用指南 @@ -231,6 +246,15 @@ Telegram:[@lgc2333](https://t.me/lgc2333) ## 📝 更新日志 +### 0.8.1 + +- 添加配置项 `MCSTAT_RESOLVE_DNS_IPV6`,用于禁用 IPv6 解析([#29](https://github.com/lgc-NB2Dev/nonebot-plugin-picmcstat/issues/29)) +- 当 IPv6 连接失败时自动回落到 IPv4 + +### 0.8.0 + +- 加入自动检测服务器类型的功能,默认启用(Thanks to [#28](https://github.com/lgc-NB2Dev/nonebot-plugin-picmcstat/pull/28)) + ### 0.7.1 - 修复文字下对齐的 Bug diff --git a/nonebot_plugin_picmcstat/__init__.py b/nonebot_plugin_picmcstat/__init__.py index fdf9084..a61a82d 100644 --- a/nonebot_plugin_picmcstat/__init__.py +++ b/nonebot_plugin_picmcstat/__init__.py @@ -5,7 +5,7 @@ from . import __main__ as __main__ # noqa: E402 from .config import ConfigClass # noqa: E402 -__version__ = "0.7.2" +__version__ = "0.8.1" __plugin_meta__ = PluginMetadata( name="PicMCStat", description="将一个 Minecraft 服务器的 MOTD 信息绘制为一张图片", diff --git a/nonebot_plugin_picmcstat/__main__.py b/nonebot_plugin_picmcstat/__main__.py index 89258e1..1018d0c 100644 --- a/nonebot_plugin_picmcstat/__main__.py +++ b/nonebot_plugin_picmcstat/__main__.py @@ -3,7 +3,7 @@ from nonebot import logger, on_command, on_regex from nonebot.adapters import Event as BaseEvent, Message from nonebot.exception import FinishedException -from nonebot.params import CommandArg +from nonebot.params import CommandArg, CommandWhitespace from nonebot.typing import T_State from nonebot_plugin_alconna.uniseg import UniMessage @@ -16,34 +16,45 @@ OB11GroupMessageEvent = None -async def finish_with_query(ip: str, svr_type: ServerType) -> NoReturn: - try: - ret = await draw(ip, svr_type) - except Exception: - msg = UniMessage("出现未知错误,请检查后台输出") - else: - msg = UniMessage.image(raw=ret) - await msg.send(reply_to=config.mcstat_reply_target) - raise FinishedException - - +motdje_matcher = on_command( + "motdje", + priority=98, + state={"svr_type": "je"}, +) motdpe_matcher = on_command( "motdpe", - aliases={"motdbe", "!motdpe", "!motdpe", "!motdbe", "!motdbe"}, + aliases={"motdbe"}, priority=98, state={"svr_type": "be"}, ) motd_matcher = on_command( "motd", - aliases={"!motd", "!motd", "motdje", "!motdje", "!motdje"}, priority=99, - state={"svr_type": "je"}, + state={"svr_type": "auto" if config.enable_auto_detect else "je"}, ) +async def finish_with_query(ip: str, svr_type: ServerType) -> NoReturn: + try: + ret = await draw(ip, svr_type) + except Exception: + msg = UniMessage("出现未知错误,请检查后台输出") + else: + msg = UniMessage.image(raw=ret) + await msg.send(reply_to=config.reply_target) + raise FinishedException + + @motd_matcher.handle() +@motdje_matcher.handle() @motdpe_matcher.handle() -async def _(state: T_State, arg_msg: Message = CommandArg()): +async def _( + state: T_State, + arg_msg: Message = CommandArg(), + space: str | None = CommandWhitespace(), +): + if arg_msg and (space is None): + return arg = arg_msg.extract_plain_text().strip() svr_type: ServerType = state["svr_type"] await finish_with_query(arg, svr_type) @@ -64,7 +75,7 @@ async def handler(): def startup(): - if s := config.mcstat_shortcuts: + if s := config.shortcuts: for v in s: append_shortcut_handler(v) diff --git a/nonebot_plugin_picmcstat/config.py b/nonebot_plugin_picmcstat/config.py index 991a826..3a70067 100644 --- a/nonebot_plugin_picmcstat/config.py +++ b/nonebot_plugin_picmcstat/config.py @@ -1,6 +1,6 @@ -from typing import Any, Optional +from typing import Any -from cookit.pyd import field_validator +from cookit.pyd import field_validator, model_with_alias_generator from nonebot import get_plugin_config from pydantic import BaseModel, Field @@ -11,21 +11,24 @@ class ShortcutType(BaseModel): regex: str host: str type: ServerType # noqa: A003 - whitelist: Optional[list[int]] = [] + whitelist: list[int] | None = [] +@model_with_alias_generator(lambda x: f"mcstat_{x}") class ConfigClass(BaseModel): - mcstat_font: list[str] = ["Minecraft Seven", "unifont"] - mcstat_show_addr: bool = False - mcstat_show_delay: bool = True - mcstat_show_mods: bool = False - mcstat_reply_target: bool = True - mcstat_shortcuts: list[ShortcutType] = Field(default_factory=list) - mcstat_resolve_dns: bool = True - mcstat_query_twice: bool = True - mcstat_java_protocol_version: int = 767 - - @field_validator("mcstat_font", mode="before") + font: list[str] = ["Minecraft Seven", "unifont"] + show_addr: bool = False + show_delay: bool = True + show_mods: bool = False + reply_target: bool = True + shortcuts: list[ShortcutType] = Field(default_factory=list) + resolve_dns: bool = True + resolve_dns_ipv6: bool = True + query_twice: bool = True + java_protocol_version: int = 772 + enable_auto_detect: bool = True + + @field_validator("font", mode="before") def transform_to_list(cls, v: Any): # noqa: N805 return v if isinstance(v, list) else [v] diff --git a/nonebot_plugin_picmcstat/const.py b/nonebot_plugin_picmcstat/const.py index b1009ae..21256a5 100644 --- a/nonebot_plugin_picmcstat/const.py +++ b/nonebot_plugin_picmcstat/const.py @@ -1,9 +1,10 @@ import re -from typing import Literal +from typing import Literal, TypeAlias from mcstatus.motd.components import Formatting, MinecraftColor -ServerType = Literal["je", "be"] +ServerTypeRaw: TypeAlias = Literal["je", "be"] +ServerType: TypeAlias = Literal[ServerTypeRaw, "auto"] CODE_COLOR = { "0": "#000000", diff --git a/nonebot_plugin_picmcstat/draw.py b/nonebot_plugin_picmcstat/draw.py index 872efed..d993c53 100644 --- a/nonebot_plugin_picmcstat/draw.py +++ b/nonebot_plugin_picmcstat/draw.py @@ -1,10 +1,10 @@ import base64 +import errno import socket from collections.abc import Sequence from functools import partial from io import BytesIO -from typing import TYPE_CHECKING, Any, Optional, Union, cast -from typing_extensions import TypeAlias +from typing import TYPE_CHECKING, Any, Optional, TypeAlias, Union, cast from mcstatus import BedrockServer, JavaServer from mcstatus.motd import Motd @@ -15,7 +15,7 @@ from pil_utils import BuildImage, Text2Image from .config import config -from .const import CODE_COLOR, GAME_MODE_MAP, STROKE_COLOR, ServerType +from .const import CODE_COLOR, GAME_MODE_MAP, STROKE_COLOR, ServerType, ServerTypeRaw from .res import DEFAULT_ICON_RES, DIRT_RES, GRASS_RES from .util import ( BBCodeTransformer, @@ -28,7 +28,7 @@ ) if TYPE_CHECKING: - from mcstatus.bedrock_status import BedrockStatusResponse + from mcstatus.responses import BedrockStatusResponse from pil_utils.typing import ColorType MARGIN = 32 @@ -42,7 +42,9 @@ JE_HEADER = "[MCJE服务器信息]" BE_HEADER = "[MCBE服务器信息]" +AUTO_HEADER = "[MC服务器信息]" SUCCESS_TITLE = "请求成功" +DEFAULT_ERR_TITLE = "出错了!" ImageType: TypeAlias = Union[BuildImage, Text2Image, "ImageGrid"] @@ -51,7 +53,7 @@ def ex_default_style(text: str, color_code: str = "", **kwargs) -> Text2Image: default_kwargs = { "font_size": EXTRA_FONT_SIZE, "fill": CODE_COLOR[color_code or "f"], - "font_families": config.mcstat_font, + "font_families": config.font, "stroke_ratio": STROKE_RATIO, "stroke_fill": STROKE_COLOR[color_code or "f"], # "spacing": EXTRA_SPACING, @@ -86,8 +88,8 @@ def width(obj: ImageType) -> float: class ImageLine: def __init__( self, - left: Union[ImageType, str], - right: Union[ImageType, str, None] = None, + left: ImageType | str, + right: ImageType | str | None = None, gap: int = LIST_GAP, ): self.left = ex_default_style(left) if isinstance(left, str) else left @@ -117,7 +119,7 @@ def __init__( self, *lines: ImageLine, spacing: int = SPACING, - gap: Optional[int] = None, + gap: int | None = None, align_items: bool = True, ): if gap is not None: @@ -127,17 +129,15 @@ def __init__( self.align_items = align_items @classmethod - def from_list(cls, li: Sequence[Union[ImageType, str]], **kwargs) -> "ImageGrid": + def from_list(cls, li: Sequence[ImageType | str], **kwargs) -> "ImageGrid": return cls( - *(ImageLine(*cast(tuple[Any, Any], x)) for x in chunks(li, 2)), + *(ImageLine(*cast("tuple[Any, Any]", x)) for x in chunks(li, 2)), **kwargs, ) @property def width(self) -> float: - return max(width(x.left) for x in self) + max( - (width(x.right) + x.gap if x.right else 0) for x in self - ) + return max(x.width for x in self) @property def height(self) -> float: @@ -192,7 +192,11 @@ def to_image( def get_header_by_svr_type(svr_type: ServerType) -> str: - return JE_HEADER if svr_type == "je" else BE_HEADER + if svr_type == "je": + return JE_HEADER + if svr_type == "be": + return BE_HEADER + return AUTO_HEADER def draw_bg(width: int, height: int) -> BuildImage: @@ -209,8 +213,8 @@ def draw_bg(width: int, height: int) -> BuildImage: def build_img( header1: str, header2: str, - icon: Optional[BuildImage] = None, - extra: Optional[Union[ImageType, str]] = None, + icon: BuildImage | None = None, + extra: ImageType | str | None = None, ) -> BytesIO: if not icon: icon = DEFAULT_ICON_RES @@ -249,7 +253,7 @@ def build_img( halign="left", fill=header_text_color, max_fontsize=TITLE_FONT_SIZE, - font_families=config.mcstat_font, + font_families=config.font, stroke_ratio=STROKE_RATIO, stroke_fill=header_stroke_color, ) @@ -264,7 +268,7 @@ def build_img( halign="left", fill=header_text_color, max_fontsize=TITLE_FONT_SIZE, - font_families=config.mcstat_font, + font_families=config.font, stroke_ratio=STROKE_RATIO, stroke_fill=header_stroke_color, ) @@ -283,36 +287,44 @@ def draw_help(svr_type: ServerType) -> BytesIO: cmd_prefix_li = list(get_driver().config.command_start) prefix = cmd_prefix_li[0] if cmd_prefix_li else "" - extra_txt = f"查询Java版服务器: {prefix}motd <服务器IP>\n查询基岩版服务器: {prefix}motdpe <服务器IP>" - return build_img(get_header_by_svr_type(svr_type), "使用帮助", extra=extra_txt) + extra_txt = [ + f"查询Java版服务器: {prefix}motdje <服务器IP>", + f"查询基岩版服务器: {prefix}motdpe <服务器IP>", + ] + if config.enable_auto_detect: + extra_txt.append(f"自动检测服务器类型: {prefix}motd <服务器IP>") + extra = ImageGrid() + for x in extra_txt: + extra.append_line(x) + return build_img(get_header_by_svr_type(svr_type), "使用帮助", extra=extra) def draw_java(res: JavaStatusResponse, addr: str) -> BytesIO: transformer = BBCodeTransformer(bedrock=res.motd.bedrock) # there're no line spacing in Text2Image since pil-utils 0.2.0 # so we split lines there then manually add the space - motd = ( + motd = [ transformer.transform(x) for x in split_motd_lines(trim_motd(res.motd.parsed)) - ) + ] online_percent = ( f"{res.players.online / res.players.max * 100:.2f}" if res.players.max else "?.??" ) - mod_svr_type: Optional[str] = None - mod_list: Optional[list[str]] = None + mod_svr_type: str | None = None + mod_list: list[str] | None = None if mod_info := res.raw.get("modinfo"): if tmp := mod_info.get("type"): mod_svr_type = tmp - if tmp := mod_info.get("modList"): + if tmp := (mod_info.get("mods") or mod_info.get("modList")): mod_list = format_mod_list(tmp) l_style = partial(ex_default_style, color_code="7") grid = ImageGrid(align_items=False) for line in motd: grid.append_line(line) - if config.mcstat_show_addr: + if config.show_addr: grid.append_line(l_style("测试地址: "), addr) grid.append_line(l_style("服务端名: "), res.version.name) if mod_svr_type: @@ -328,12 +340,12 @@ def draw_java(res: JavaStatusResponse, addr: str) -> BytesIO: l_style("聊天签名: "), "必需" if res.enforces_secure_chat else "无需", ) - if config.mcstat_show_delay: + if config.show_delay: grid.append_line( l_style("测试延迟: "), ex_default_style(f"{res.latency:.2f}ms", get_latency_color(res.latency)), ) - if mod_list and config.mcstat_show_mods: + if mod_list and config.show_mods: grid.append_line( l_style("Mod 列表: "), ImageGrid.from_list(mod_list), @@ -363,8 +375,8 @@ def draw_bedrock(res: "BedrockStatusResponse", addr: str) -> BytesIO: transformer.transform(x) for x in split_motd_lines(trim_motd(res.motd.parsed)) ) online_percent = ( - f"{int(res.players_online) / int(res.players_max) * 100:.2f}" - if res.players_max + f"{int(res.players.online) / int(res.players.max) * 100:.2f}" + if res.players.max else "?.??" ) @@ -372,7 +384,7 @@ def draw_bedrock(res: "BedrockStatusResponse", addr: str) -> BytesIO: grid = ImageGrid(align_items=False) for line in motd: grid.append_line(line) - if config.mcstat_show_addr: + if config.show_addr: grid.append_line(l_style("测试地址: "), addr) grid.append_line(l_style("协议版本: "), str(res.version.protocol)) grid.append_line(l_style("游戏版本: "), res.version.version) @@ -380,14 +392,14 @@ def draw_bedrock(res: "BedrockStatusResponse", addr: str) -> BytesIO: l_style("当前人数: "), f"{res.players.online}/{res.players.max} ({online_percent}%)", ) - if res.map: - grid.append_line(l_style("存档名称: "), res.map) + if res.map_name: + grid.append_line(l_style("存档名称: "), res.map_name) if res.gamemode: grid.append_line( l_style("游戏模式: "), GAME_MODE_MAP.get(res.gamemode, res.gamemode), ) - if config.mcstat_show_delay: + if config.show_delay: grid.append_line( l_style("测试延迟: "), ex_default_style(f"{res.latency:.2f}ms", get_latency_color(res.latency)), @@ -396,18 +408,50 @@ def draw_bedrock(res: "BedrockStatusResponse", addr: str) -> BytesIO: return build_img(BE_HEADER, SUCCESS_TITLE, extra=grid) -def draw_error(e: Exception, svr_type: ServerType) -> BytesIO: - extra = "" +def parse_error(e: Exception) -> tuple[str, str]: if isinstance(e, TimeoutError): - reason = "请求超时" - elif isinstance(e, socket.gaierror): - reason = "域名解析失败" - extra = str(e) + return "请求超时", "" + if isinstance(e, socket.gaierror): + return "域名解析失败", str(e) + return DEFAULT_ERR_TITLE, f"{e.__class__.__name__}: {e}" + + +def join_strings(*s: str, sp: str = ":") -> str: + return sp.join(x for x in s if x) + + +def draw_error( + svr_type: ServerType, + *e: Exception | tuple[str, Exception], + title: str | None = None, +) -> BytesIO: + if len(e) == 1 and (not title): + title, extra = ( + (x[0], join_strings(*parse_error(x[1]))) + if isinstance((x := e[0]), tuple) + else parse_error(x) + ) + extras = [extra] if extra else None + else: + extras = [ + ( + join_strings(x[0], *parse_error(x[1])) + if isinstance(x, tuple) + else join_strings(*parse_error(x)) + ) + for x in e + ] + if not title: + title = DEFAULT_ERR_TITLE + if extras: + lines = [ + ImageLine(ex_default_style(x).wrap((MIN_WIDTH * 1.5) - MARGIN * 2)) + for x in extras + ] + extra_img = ImageGrid(*lines) else: - reason = "出错了!" - extra = f"{e.__class__.__name__}: {e}" - extra_img = ex_default_style(extra).wrap(MIN_WIDTH - MARGIN * 2) if extra else None - return build_img(get_header_by_svr_type(svr_type), reason, extra=extra_img) + extra_img = None + return build_img(get_header_by_svr_type(svr_type), title, extra=extra_img) def draw_resp( @@ -419,25 +463,77 @@ def draw_resp( return draw_bedrock(resp, addr) -async def draw(ip: str, svr_type: ServerType) -> BytesIO: - try: - if not ip: - return draw_help(svr_type) +def is_ipv6_unreachable_error(e: BaseException) -> bool: + """Check if exception is due to IPv6 network being unreachable.""" + if isinstance(e, OSError) and e.errno in ( + errno.ENETUNREACH, + errno.EHOSTUNREACH, + errno.EADDRNOTAVAIL, + ): + return True + if e.__cause__: + return is_ipv6_unreachable_error(e.__cause__) + return False - is_java = svr_type == "je" - host, port = await resolve_ip(ip, is_java) + +async def draw(ip: str, svr_type: ServerType) -> BytesIO: + async def _inner(t: ServerTypeRaw, *, resolve_dns_ipv6: bool | None = None) -> BytesIO: + is_java = t == "je" + host, port = await resolve_ip(ip, is_java, resolve_dns_ipv6=resolve_dns_ipv6) svr = JavaServer(host, port) if is_java else BedrockServer(host, port) - kw = {"version": config.mcstat_java_protocol_version} if is_java else {} - if config.mcstat_query_twice: + kw = {"version": config.java_protocol_version} if is_java else {} + if config.query_twice: await svr.async_status(**kw) # 第一次延迟通常不准 resp = await svr.async_status(**kw) return draw_resp(resp, ip) + async def _inner_with_fallback(t: ServerTypeRaw) -> BytesIO: + # If IPv6 is disabled, just use IPv4 + if not config.resolve_dns_ipv6: + return await _inner(t, resolve_dns_ipv6=False) + + # Try IPv6 first, fall back to IPv4 if unreachable + try: + return await _inner(t, resolve_dns_ipv6=True) + except Exception as e: + if is_ipv6_unreachable_error(e): + logger.debug( + f"IPv6 connection failed with {e.__class__.__name__}, " + "falling back to IPv4", + ) + return await _inner(t, resolve_dns_ipv6=False) + raise + + try: + if not ip: + return draw_help(svr_type) + + if svr_type != "auto": + return await _inner_with_fallback(svr_type) + + # auto + try: + return await _inner_with_fallback("je") + except Exception as e: + logger.exception("获取JE服务器状态/画服务器状态图出错") + je_exc = e + try: + return await _inner_with_fallback("be") + except Exception as e: + logger.exception("获取BE服务器状态/画服务器状态图出错") + be_exc = e + return draw_error( + svr_type, + ("JE", je_exc), + ("BE", be_exc), + title="所有尝试皆出错", + ) + except Exception as e: logger.exception("获取服务器状态/画服务器状态图出错") try: - return draw_error(e, svr_type) + return draw_error(svr_type, e) except Exception: logger.exception("画异常状态图失败") raise diff --git a/nonebot_plugin_picmcstat/util.py b/nonebot_plugin_picmcstat/util.py index 77c6d2a..bf76779 100644 --- a/nonebot_plugin_picmcstat/util.py +++ b/nonebot_plugin_picmcstat/util.py @@ -2,12 +2,10 @@ import re import string from collections.abc import Iterator, Sequence -from typing import Optional, TypeVar, Union, cast +from typing import TYPE_CHECKING, TypeVar, cast import dns.asyncresolver -import dns.name import dns.rdatatype as rd -from dns.rdtypes.IN.SRV import SRV as SRVRecordAnswer # noqa: N811 from mcstatus.motd.components import ( Formatting, MinecraftColor, @@ -29,6 +27,10 @@ STROKE_COLOR, ) +if TYPE_CHECKING: + from dns.rdtypes.IN.SRV import SRV as SRVRecordAnswer # noqa: N811 + from mcstatus.forge_data import RawForgeDataMod + RANDOM_CHAR_TEMPLATE = f"{string.ascii_letters}{string.digits}!§$%&?#" WHITESPACE_EXCLUDE_NEWLINE = string.whitespace.replace("\n", "") DNS_RESOLVER = dns.asyncresolver.Resolver() @@ -55,12 +57,12 @@ def replace_format_code(txt: str, new_str: str = "") -> str: return re.sub(FORMAT_CODE_REGEX, new_str, txt) -def format_mod_list(li: list[Union[dict, str]]) -> list[str]: - def mapping_func(it: Union[dict, str]) -> Optional[str]: +def format_mod_list(li: list["RawForgeDataMod"] | list[str]) -> list[str]: + def mapping_func(it: "RawForgeDataMod | str") -> str | None: if isinstance(it, str): return it - if isinstance(it, dict) and (name := it.get("modid")): - version = it.get("version") + if isinstance(it, dict) and (name := (it.get("modid") or it.get("modId"))): + version = it.get("version") or it.get("modmarker") return f"{name}-{version}" if version else name return None @@ -69,9 +71,14 @@ def mapping_func(it: Union[dict, str]) -> Optional[str]: async def resolve_host( host: str, - data_types: Optional[list[rd.RdataType]] = None, -) -> Optional[str]: - data_types = data_types or [rd.CNAME, rd.AAAA, rd.A] + data_types: list[rd.RdataType] | None = None, + *, + resolve_dns_ipv6: bool | None = None, +) -> str | None: + if resolve_dns_ipv6 is None: + resolve_dns_ipv6 = config.resolve_dns_ipv6 + if data_types is None: + data_types = [rd.CNAME, rd.AAAA, rd.A] if resolve_dns_ipv6 else [rd.CNAME, rd.A] for rd_type in data_types: try: resp = (await DNS_RESOLVER.resolve(host, rd_type)).response @@ -84,7 +91,7 @@ async def resolve_host( else: logger.debug(f"Resolved {rd_type.name} record for {host}: {name}") if rd_type is rd.CNAME: - return await resolve_host(name) + return await resolve_host(name, resolve_dns_ipv6=resolve_dns_ipv6) return name return None @@ -92,11 +99,16 @@ async def resolve_host( async def resolve_srv(host: str) -> tuple[str, int]: host = "_minecraft._tcp." + host resp = await DNS_RESOLVER.resolve(host, rd.SRV) - answer = cast(SRVRecordAnswer, resp[0]) + answer = cast("SRVRecordAnswer", resp[0]) return str(answer.target), int(answer.port) -async def resolve_ip(ip: str, srv: bool = False) -> tuple[str, Optional[int]]: +async def resolve_ip( + ip: str, + srv: bool = False, + *, + resolve_dns_ipv6: bool | None = None, +) -> tuple[str, int | None]: if ":" in ip: host, port = ip.split(":", maxsplit=1) else: @@ -108,13 +120,12 @@ async def resolve_ip(ip: str, srv: bool = False) -> tuple[str, Optional[int]]: host, port = await resolve_srv(host) except Exception as e: logger.debug( - f"Failed to resolve SRV record for {host}: " - f"{e.__class__.__name__}: {e}", + f"Failed to resolve SRV record for {host}: {e.__class__.__name__}: {e}", ) logger.debug(f"Resolved SRV record for {ip}: {host}:{port}") return ( - (await resolve_host(host) if config.mcstat_resolve_dns else None) or host, + (await resolve_host(host, resolve_dns_ipv6=resolve_dns_ipv6) if config.resolve_dns else None) or host, int(port) if port else None, ) @@ -178,7 +189,7 @@ def split_motd_lines(motd: Sequence[ParsedMotdComponent]): lines: list[list[ParsedMotdComponent]] = [] current_line: list[ParsedMotdComponent] = [] - using_color: Union[MinecraftColor, WebColor, None] = None + using_color: MinecraftColor | WebColor | None = None using_formats: list[Formatting] = [] for comp in motd: @@ -208,7 +219,7 @@ def split_motd_lines(motd: Sequence[ParsedMotdComponent]): continue - if isinstance(comp, (MinecraftColor, WebColor)): + if isinstance(comp, MinecraftColor | WebColor): using_color = comp elif isinstance(comp, Formatting): diff --git a/pyproject.toml b/pyproject.toml index b3f6f1f..6599c7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,28 +4,24 @@ dynamic = ["version"] description = "A NoneBot2 plugin generates a pic from a Minecraft server's MOTD" authors = [{ name = "LgCookie", email = "lgc2333@126.com" }] dependencies = [ - "nonebot2>=2.4.1", - "nonebot-plugin-alconna>=0.54.2", - "mcstatus>=11.1.1", + "nonebot2>=2.4.3", + "nonebot-plugin-alconna>=0.59.4", + "mcstatus>=12.0.5", "pil-utils>=0.2.2", "punycode>=0.2.1", "dnspython>=2.7.0", - "cookit[pydantic]>=0.9.3", + "cookit[pydantic]>=0.13.0", ] -requires-python = ">=3.9,<4.0" +requires-python = ">=3.10,<4.0" readme = "README.md" license = { text = "MIT" } [project.urls] homepage = "https://github.com/lgc-NB2Dev/nonebot-plugin-picmcstat" -[tool.pdm.build] -includes = [] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.pdm.version] -source = "file" +[tool.hatch.version] path = "nonebot_plugin_picmcstat/__init__.py" - -[build-system] -requires = ["pdm-backend"] -build-backend = "pdm.backend"