517 lines
17 KiB
Python
517 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Any, Literal, TYPE_CHECKING
|
|
|
|
from mcstatus.forge_data import ForgeData, RawForgeData
|
|
from mcstatus.motd import Motd
|
|
|
|
if TYPE_CHECKING:
|
|
from typing_extensions import NotRequired, Self, TypeAlias, TypedDict
|
|
|
|
class RawJavaResponsePlayer(TypedDict):
|
|
name: str
|
|
id: str
|
|
|
|
class RawJavaResponsePlayers(TypedDict):
|
|
online: int
|
|
max: int
|
|
sample: NotRequired[list[RawJavaResponsePlayer] | None]
|
|
|
|
class RawJavaResponseVersion(TypedDict):
|
|
name: str
|
|
protocol: int
|
|
|
|
class RawJavaResponseMotdWhenDict(TypedDict, total=False):
|
|
text: str # only present if `translate` is set
|
|
translate: str # same to the above field
|
|
extra: list[RawJavaResponseMotdWhenDict | str]
|
|
|
|
color: str
|
|
bold: bool
|
|
strikethrough: bool
|
|
italic: bool
|
|
underlined: bool
|
|
obfuscated: bool
|
|
|
|
RawJavaResponseMotd: TypeAlias = "RawJavaResponseMotdWhenDict | list[RawJavaResponseMotdWhenDict | str] | str"
|
|
|
|
class RawJavaResponse(TypedDict):
|
|
description: RawJavaResponseMotd
|
|
players: RawJavaResponsePlayers
|
|
version: RawJavaResponseVersion
|
|
favicon: NotRequired[str]
|
|
forgeData: NotRequired[RawForgeData | None]
|
|
modinfo: NotRequired[RawForgeData | None]
|
|
enforcesSecureChat: NotRequired[bool]
|
|
|
|
class RawQueryResponse(TypedDict):
|
|
hostname: str
|
|
gametype: Literal["SMP"]
|
|
game_id: Literal["MINECRAFT"]
|
|
version: str
|
|
plugins: str
|
|
map: str
|
|
numplayers: str # can be transformed into `int`
|
|
maxplayers: str # can be transformed into `int`
|
|
hostport: str # can be transformed into `int`
|
|
hostip: str
|
|
|
|
else:
|
|
RawJavaResponsePlayer = dict
|
|
RawJavaResponsePlayers = dict
|
|
RawJavaResponseVersion = dict
|
|
RawJavaResponseMotdWhenDict = dict
|
|
RawJavaResponse = dict
|
|
RawQueryResponse = dict
|
|
|
|
from mcstatus.utils import deprecated
|
|
|
|
__all__ = [
|
|
"BaseStatusPlayers",
|
|
"BaseStatusResponse",
|
|
"BaseStatusVersion",
|
|
"BedrockStatusPlayers",
|
|
"BedrockStatusResponse",
|
|
"BedrockStatusVersion",
|
|
"JavaStatusPlayer",
|
|
"JavaStatusPlayers",
|
|
"JavaStatusResponse",
|
|
"JavaStatusVersion",
|
|
"QueryResponse",
|
|
]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BaseStatusResponse(ABC):
|
|
"""Class for storing shared data from a status response."""
|
|
|
|
players: BaseStatusPlayers
|
|
"""The players information."""
|
|
version: BaseStatusVersion
|
|
"""The version information."""
|
|
motd: Motd
|
|
"""Message Of The Day. Also known as description.
|
|
|
|
.. seealso:: :doc:`/api/motd_parsing`.
|
|
"""
|
|
latency: float
|
|
"""Latency between a server and the client (you). In milliseconds."""
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
"""Alias to the :meth:`mcstatus.motd.Motd.to_minecraft` method."""
|
|
return self.motd.to_minecraft()
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def build(cls, *args, **kwargs) -> Self:
|
|
"""Build BaseStatusResponse and check is it valid.
|
|
|
|
:param args: Arguments in specific realisation.
|
|
:param kwargs: Keyword arguments in specific realisation.
|
|
:return: :class:`BaseStatusResponse` object.
|
|
"""
|
|
raise NotImplementedError("You can't use abstract methods.")
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
"""Return the dataclass as JSON-serializable :class:`dict`.
|
|
|
|
Do note that this method doesn't return :class:`string <str>` but
|
|
:class:`dict`, so you can do some processing on returned value.
|
|
|
|
Difference from
|
|
:attr:`~mcstatus.responses.JavaStatusResponse.raw` is in that,
|
|
:attr:`~mcstatus.responses.JavaStatusResponse.raw` returns raw response
|
|
in the same format as we got it. This method returns the response
|
|
in a more user-friendly JSON serializable format (for example,
|
|
:attr:`~mcstatus.responses.BaseStatusResponse.motd` is returned as a
|
|
:func:`Minecraft string <mcstatus.motd.Motd.to_minecraft>` and not
|
|
:class:`dict`).
|
|
"""
|
|
as_dict = asdict(self)
|
|
as_dict["motd"] = self.motd.simplify().to_minecraft()
|
|
return as_dict
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class JavaStatusResponse(BaseStatusResponse):
|
|
"""The response object for :meth:`JavaServer.status() <mcstatus.server.JavaServer.status>`."""
|
|
|
|
raw: RawJavaResponse
|
|
"""Raw response from the server.
|
|
|
|
This is :class:`~typing.TypedDict` actually, please see sources to find what is here.
|
|
"""
|
|
players: JavaStatusPlayers
|
|
version: JavaStatusVersion
|
|
enforces_secure_chat: bool | None
|
|
"""Whether the server enforces secure chat (every message is signed up with a key).
|
|
|
|
.. seealso::
|
|
`Signed Chat explanation <https://gist.github.com/kennytv/ed783dd244ca0321bbd882c347892874>`_,
|
|
`22w17a changelog, where this was added <https://www.minecraft.net/nl-nl/article/minecraft-snapshot-22w17a>`_.
|
|
|
|
.. versionadded:: 11.1.0
|
|
"""
|
|
icon: str | None
|
|
"""The icon of the server. In `Base64 <https://en.wikipedia.org/wiki/Base64>`_ encoded PNG image format.
|
|
|
|
.. seealso:: :ref:`pages/faq:how to get server image?`
|
|
"""
|
|
forge_data: ForgeData | None
|
|
"""Forge mod data (mod list, channels, etc). Only present if this is a forge (modded) server."""
|
|
|
|
@classmethod
|
|
def build(cls, raw: RawJavaResponse, latency: float = 0) -> Self:
|
|
"""Build JavaStatusResponse and check is it valid.
|
|
|
|
:param raw: Raw response :class:`dict`.
|
|
:param latency: Time that server took to response (in milliseconds).
|
|
:raise ValueError: If the required keys (``players``, ``version``, ``description``) are not present.
|
|
:raise TypeError:
|
|
If the required keys (``players`` - :class:`dict`, ``version`` - :class:`dict`,
|
|
``description`` - :class:`str`) are not of the expected type.
|
|
:return: :class:`JavaStatusResponse` object.
|
|
"""
|
|
forge_data: ForgeData | None = None
|
|
if (raw_forge := raw.get("forgeData") or raw.get("modinfo")) and raw_forge is not None:
|
|
forge_data = ForgeData.build(raw_forge)
|
|
|
|
return cls(
|
|
raw=raw,
|
|
players=JavaStatusPlayers.build(raw["players"]),
|
|
version=JavaStatusVersion.build(raw["version"]),
|
|
motd=Motd.parse(raw.get("description", ""), bedrock=False),
|
|
enforces_secure_chat=raw.get("enforcesSecureChat"),
|
|
icon=raw.get("favicon"),
|
|
latency=latency,
|
|
forge_data=forge_data,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BedrockStatusResponse(BaseStatusResponse):
|
|
"""The response object for :meth:`BedrockServer.status() <mcstatus.server.BedrockServer.status>`."""
|
|
|
|
players: BedrockStatusPlayers
|
|
version: BedrockStatusVersion
|
|
map_name: str | None
|
|
"""The name of the map."""
|
|
gamemode: str | None
|
|
"""The name of the gamemode on the server."""
|
|
|
|
@classmethod
|
|
def build(cls, decoded_data: list[Any], latency: float) -> Self:
|
|
"""Build BaseStatusResponse and check is it valid.
|
|
|
|
:param decoded_data: Raw decoded response object.
|
|
:param latency: Latency of the request.
|
|
:return: :class:`BedrockStatusResponse` object.
|
|
"""
|
|
|
|
try:
|
|
map_name = decoded_data[7]
|
|
except IndexError:
|
|
map_name = None
|
|
try:
|
|
gamemode = decoded_data[8]
|
|
except IndexError:
|
|
gamemode = None
|
|
|
|
return cls(
|
|
players=BedrockStatusPlayers(
|
|
online=int(decoded_data[4]),
|
|
max=int(decoded_data[5]),
|
|
),
|
|
version=BedrockStatusVersion(
|
|
name=decoded_data[3],
|
|
protocol=int(decoded_data[2]),
|
|
brand=decoded_data[0],
|
|
),
|
|
motd=Motd.parse(decoded_data[1], bedrock=True),
|
|
latency=latency,
|
|
map_name=map_name,
|
|
gamemode=gamemode,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BaseStatusPlayers(ABC):
|
|
"""Class for storing information about players on the server."""
|
|
|
|
online: int
|
|
"""Current number of online players."""
|
|
max: int
|
|
"""The maximum allowed number of players (aka server slots)."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class JavaStatusPlayers(BaseStatusPlayers):
|
|
"""Class for storing information about players on the server."""
|
|
|
|
sample: list[JavaStatusPlayer] | None
|
|
"""List of players, who are online. If server didn't provide this, it will be :obj:`None`.
|
|
|
|
Actually, this is what appears when you hover over the slot count on the multiplayer screen.
|
|
|
|
.. note::
|
|
It's often empty or even contains some advertisement, because the specific server implementations or plugins can
|
|
disable providing this information or even change it to something custom.
|
|
|
|
There is nothing that ``mcstatus`` can to do here if the player sample was modified/disabled like this.
|
|
"""
|
|
|
|
@classmethod
|
|
def build(cls, raw: RawJavaResponsePlayers) -> Self:
|
|
"""Build :class:`JavaStatusPlayers` from raw response :class:`dict`.
|
|
|
|
:param raw: Raw response :class:`dict`.
|
|
:raise ValueError: If the required keys (``online``, ``max``) are not present.
|
|
:raise TypeError:
|
|
If the required keys (``online`` - :class:`int`, ``max`` - :class:`int`,
|
|
``sample`` - :class:`list`) are not of the expected type.
|
|
:return: :class:`JavaStatusPlayers` object.
|
|
"""
|
|
sample = None
|
|
if (sample := raw.get("sample")) is not None:
|
|
sample = [JavaStatusPlayer.build(player) for player in sample]
|
|
return cls(
|
|
online=raw["online"],
|
|
max=raw["max"],
|
|
sample=sample,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BedrockStatusPlayers(BaseStatusPlayers):
|
|
"""Class for storing information about players on the server."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class JavaStatusPlayer:
|
|
"""Class with information about a single player."""
|
|
|
|
name: str
|
|
"""Name of the player."""
|
|
id: str
|
|
"""ID of the player (in `UUID <https://en.wikipedia.org/wiki/Universally_unique_identifier>`_ format)."""
|
|
|
|
@property
|
|
def uuid(self) -> str:
|
|
"""Alias to :attr:`.id` field."""
|
|
return self.id
|
|
|
|
@classmethod
|
|
def build(cls, raw: RawJavaResponsePlayer) -> Self:
|
|
"""Build :class:`JavaStatusPlayer` from raw response :class:`dict`.
|
|
|
|
:param raw: Raw response :class:`dict`.
|
|
:raise ValueError: If the required keys (``name``, ``id``) are not present.
|
|
:raise TypeError: If the required keys (``name`` - :class:`str`, ``id`` - :class:`str`)
|
|
are not of the expected type.
|
|
:return: :class:`JavaStatusPlayer` object.
|
|
"""
|
|
return cls(name=raw["name"], id=raw["id"])
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BaseStatusVersion(ABC):
|
|
"""A class for storing version information."""
|
|
|
|
name: str
|
|
"""The version name, like ``1.19.3``.
|
|
|
|
See `Minecraft wiki <https://minecraft.wiki/w/Java_Edition_version_history#Full_release>`__
|
|
for complete list.
|
|
"""
|
|
protocol: int
|
|
"""The protocol version, like ``761``.
|
|
|
|
See `Minecraft wiki <https://minecraft.wiki/w/Protocol_version#Java_Edition_2>`__.
|
|
"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class JavaStatusVersion(BaseStatusVersion):
|
|
"""A class for storing version information."""
|
|
|
|
@classmethod
|
|
def build(cls, raw: RawJavaResponseVersion) -> Self:
|
|
"""Build :class:`JavaStatusVersion` from raw response dict.
|
|
|
|
:param raw: Raw response :class:`dict`.
|
|
:raise ValueError: If the required keys (``name``, ``protocol``) are not present.
|
|
:raise TypeError: If the required keys (``name`` - :class:`str`, ``protocol`` - :class:`int`)
|
|
are not of the expected type.
|
|
:return: :class:`JavaStatusVersion` object.
|
|
"""
|
|
return cls(name=raw["name"], protocol=raw["protocol"])
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BedrockStatusVersion(BaseStatusVersion):
|
|
"""A class for storing version information."""
|
|
|
|
name: str
|
|
"""The version name, like ``1.19.60``.
|
|
|
|
See `Minecraft wiki <https://minecraft.wiki/w/Bedrock_Edition_version_history#Bedrock_Edition>`__
|
|
for complete list.
|
|
"""
|
|
brand: str
|
|
"""``MCPE`` or ``MCEE`` for Education Edition."""
|
|
|
|
@property
|
|
@deprecated(replacement="name", date="2025-12")
|
|
def version(self) -> str:
|
|
"""
|
|
.. deprecated:: 12.0.0
|
|
Will be removed 2025-12, use :attr:`.name` instead.
|
|
"""
|
|
return self.name
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class QueryResponse:
|
|
"""The response object for :meth:`JavaServer.query() <mcstatus.server.JavaServer.query>`."""
|
|
|
|
raw: RawQueryResponse
|
|
"""Raw response from the server.
|
|
|
|
This is :class:`~typing.TypedDict` actually, please see sources to find what is here.
|
|
"""
|
|
motd: Motd
|
|
"""The MOTD of the server. Also known as description.
|
|
|
|
.. seealso:: :doc:`/api/motd_parsing`.
|
|
"""
|
|
map_name: str
|
|
"""The name of the map. Default is ``world``."""
|
|
players: QueryPlayers
|
|
"""The players information."""
|
|
software: QuerySoftware
|
|
"""The software information."""
|
|
ip: str
|
|
"""The IP address the server is listening/was contacted on."""
|
|
port: int
|
|
"""The port the server is listening/was contacted on."""
|
|
game_type: str = "SMP"
|
|
"""The game type of the server. Hardcoded to ``SMP`` (survival multiplayer)."""
|
|
game_id: str = "MINECRAFT"
|
|
"""The game ID of the server. Hardcoded to ``MINECRAFT``."""
|
|
|
|
@classmethod
|
|
def build(cls, raw: RawQueryResponse, players_list: list[str]) -> Self:
|
|
return cls(
|
|
raw=raw,
|
|
motd=Motd.parse(raw["hostname"], bedrock=False),
|
|
map_name=raw["map"],
|
|
players=QueryPlayers.build(raw, players_list),
|
|
software=QuerySoftware.build(raw["version"], raw["plugins"]),
|
|
ip=raw["hostip"],
|
|
port=int(raw["hostport"]),
|
|
game_type=raw["gametype"],
|
|
game_id=raw["game_id"],
|
|
)
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
"""Return the dataclass as JSON-serializable :class:`dict`.
|
|
|
|
Do note that this method doesn't return :class:`string <str>` but
|
|
:class:`dict`, so you can do some processing on returned value.
|
|
|
|
Difference from
|
|
:attr:`~mcstatus.responses.JavaStatusResponse.raw` is in that,
|
|
:attr:`~mcstatus.responses.JavaStatusResponse.raw` returns raw response
|
|
in the same format as we got it. This method returns the response
|
|
in a more user-friendly JSON serializable format (for example,
|
|
:attr:`~mcstatus.responses.BaseStatusResponse.motd` is returned as a
|
|
:func:`Minecraft string <mcstatus.motd.Motd.to_minecraft>` and not
|
|
:class:`dict`).
|
|
"""
|
|
as_dict = asdict(self)
|
|
as_dict["motd"] = self.motd.simplify().to_minecraft()
|
|
as_dict["players"] = asdict(self.players)
|
|
as_dict["software"] = asdict(self.software)
|
|
return as_dict
|
|
|
|
@property
|
|
@deprecated(replacement="map_name", date="2025-12")
|
|
def map(self) -> str | None:
|
|
"""
|
|
.. deprecated:: 12.0.0
|
|
Will be removed 2025-12, use :attr:`.map_name` instead.
|
|
"""
|
|
return self.map_name
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class QueryPlayers:
|
|
"""Class for storing information about players on the server."""
|
|
|
|
online: int
|
|
"""The number of online players."""
|
|
max: int
|
|
"""The maximum allowed number of players (server slots)."""
|
|
list: list[str]
|
|
"""The list of online players."""
|
|
|
|
@classmethod
|
|
def build(cls, raw: RawQueryResponse, players_list: list[str]) -> Self:
|
|
return cls(
|
|
online=int(raw["numplayers"]),
|
|
max=int(raw["maxplayers"]),
|
|
list=players_list,
|
|
)
|
|
|
|
@property
|
|
@deprecated(replacement="'list' attribute", date="2025-12")
|
|
def names(self) -> list[str]:
|
|
"""
|
|
.. deprecated:: 12.0.0
|
|
Will be removed 2025-12, use :attr:`.list` instead.
|
|
"""
|
|
return self.list
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class QuerySoftware:
|
|
"""Class for storing information about software on the server."""
|
|
|
|
version: str
|
|
"""The version of the software."""
|
|
brand: str
|
|
"""The brand of the software. Like `Paper <https://papermc.io>`_ or `Spigot <https://www.spigotmc.org>`_."""
|
|
plugins: list[str]
|
|
"""The list of plugins. Can be an empty list if hidden."""
|
|
|
|
@classmethod
|
|
def build(cls, version: str, plugins: str) -> Self:
|
|
brand, parsed_plugins = cls._parse_plugins(plugins)
|
|
return cls(
|
|
version=version,
|
|
brand=brand,
|
|
plugins=parsed_plugins,
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse_plugins(plugins: str) -> tuple[str, list[str]]:
|
|
"""Parse plugins string to list.
|
|
|
|
Returns:
|
|
:class:`tuple` with two elements. First is brand of server (:attr:`.brand`)
|
|
and second is a list of :attr:`plugins`.
|
|
"""
|
|
brand = "vanilla"
|
|
parsed_plugins = []
|
|
|
|
if plugins:
|
|
parts = plugins.split(":", 1)
|
|
brand = parts[0].strip()
|
|
|
|
if len(parts) == 2:
|
|
parsed_plugins = [s.strip() for s in parts[1].split(";")]
|
|
|
|
return brand, parsed_plugins
|