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 查询 图片版 ✨_
-
-
-
+
+
+
@@ -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"