Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ python3 -m pip install mcstatus

### Python API

#### Java Edition
#### Java Edition (1.7+)

```python
from mcstatus import JavaServer
Expand All @@ -48,6 +48,20 @@ query = server.query()
print(f"The server has the following players online: {', '.join(query.players.names)}")
```

#### Java Edition (pre-1.7)

```python
from mcstatus import LegacyServer

# You can pass the same address you'd enter into the address field in minecraft into the 'lookup' function
# If you know the host and port, you may skip this and use LegacyServer("example.org", 1234)
server = LegacyServer.lookup("example.org:1234")

# 'status' is supported by all Minecraft servers.
status = server.status()
print(f"The server has {status.players.online} player(s) online and replied in {status.latency} ms")
```

#### Bedrock Edition

```python
Expand Down
41 changes: 39 additions & 2 deletions docs/api/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@ These are classes, that you use to send a request to server.
:undoc-members:
:show-inheritance:

.. autoclass:: mcstatus.server.BaseJavaServer
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: mcstatus.server.JavaServer
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: mcstatus.server.LegacyServer
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: mcstatus.server.BedrockServer
:members:
:undoc-members:
Expand All @@ -31,8 +41,8 @@ Response Objects

These are the classes that you get back after making a request.

For Java Server
***************
For Java Server (1.7+)
**********************

.. module:: mcstatus.responses

Expand Down Expand Up @@ -79,6 +89,33 @@ For Java Server
:exclude-members: build


For Java Server (pre-1.7)
*************************

.. versionadded:: 12.1.0

.. module:: mcstatus.responses
:no-index:

.. autoclass:: mcstatus.responses.LegacyStatusResponse()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build

.. autoclass:: mcstatus.responses.LegacyStatusPlayers()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build

.. autoclass:: mcstatus.responses.LegacyStatusVersion()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build


For Bedrock Servers
*******************

Expand Down
10 changes: 10 additions & 0 deletions docs/api/internal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ versions. They are only documented here for linkable reference to them.
:undoc-members:
:show-inheritance:

.. autoclass:: mcstatus.legacy_status.LegacyServerStatus
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: mcstatus.legacy_status.AsyncLegacyServerStatus
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: mcstatus.bedrock_status.BedrockServerStatus
:members:
:undoc-members:
Expand Down
3 changes: 2 additions & 1 deletion mcstatus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from mcstatus.server import BedrockServer, JavaServer, MCServer
from mcstatus.server import BedrockServer, JavaServer, LegacyServer, MCServer

__all__ = [
"BedrockServer",
"JavaServer",
"LegacyServer",
"MCServer",
]
23 changes: 14 additions & 9 deletions mcstatus/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import dataclasses
from typing import TypeAlias

from mcstatus import JavaServer, BedrockServer
from mcstatus import JavaServer, LegacyServer, BedrockServer
from mcstatus.responses import JavaStatusResponse
from mcstatus.motd import Motd

SupportedServers: TypeAlias = "JavaServer | BedrockServer"
SupportedServers: TypeAlias = "JavaServer | LegacyServer | BedrockServer"

PING_PACKET_FAIL_WARNING = (
"warning: contacting {address} failed with a 'ping' packet but succeeded with a 'status' packet,\n"
Expand All @@ -39,15 +39,17 @@ def _motd(motd: Motd) -> str:
def _kind(serv: SupportedServers) -> str:
if isinstance(serv, JavaServer):
return "Java"
elif isinstance(serv, LegacyServer):
return "Java (pre-1.7)"
elif isinstance(serv, BedrockServer):
return "Bedrock"
else:
raise ValueError(f"unsupported server for kind: {serv}")


def _ping_with_fallback(server: SupportedServers) -> float:
# bedrock doesn't have ping method
if isinstance(server, BedrockServer):
# only Java has ping method
if not isinstance(server, JavaServer):
return server.status().latency

# try faster ping packet first, falling back to status with a warning.
Expand Down Expand Up @@ -161,14 +163,17 @@ def main(argv: list[str] = sys.argv[1:]) -> int:
parser = argparse.ArgumentParser(
"mcstatus",
description="""
mcstatus provides an easy way to query 1.7 or newer Minecraft servers for any
information they can expose. It provides three modes of access: query, status,
ping and json.
mcstatus provides an easy way to query Minecraft servers for any information
they can expose. It provides three modes of access: query, status, ping and json.
""",
)

parser.add_argument("address", help="The address of the server.")
parser.add_argument("--bedrock", help="Specifies that 'address' is a Bedrock server (default: Java).", action="store_true")
group = parser.add_mutually_exclusive_group()
group.add_argument("--bedrock", help="Specifies that 'address' is a Bedrock server (default: Java).", action="store_true")
group.add_argument(
"--legacy", help="Specifies that 'address' is a pre-1.7 Java server (default: 1.7+).", action="store_true"
)

subparsers = parser.add_subparsers(title="commands", description="Command to run, defaults to 'status'.")
parser.set_defaults(func=status_cmd)
Expand All @@ -184,7 +189,7 @@ def main(argv: list[str] = sys.argv[1:]) -> int:
).set_defaults(func=json_cmd)

args = parser.parse_args(argv)
lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup
lookup = (JavaServer.lookup if not args.legacy else LegacyServer.lookup) if not args.bedrock else BedrockServer.lookup

try:
server = lookup(args.address)
Expand Down
53 changes: 53 additions & 0 deletions mcstatus/legacy_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from time import perf_counter

from mcstatus.protocol.connection import BaseAsyncReadSyncWriteConnection, BaseSyncConnection
from mcstatus.responses import LegacyStatusResponse


class _BaseLegacyServerStatus:
request_status_data = bytes.fromhex(
# see https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Client_to_server
"fe01fa"
)

@staticmethod
def parse_response(data: bytes, latency: float) -> LegacyStatusResponse:
decoded_data = data.decode("UTF-16BE").split("\0")
if decoded_data[0] != "§1":
raise IOError("Recieved invalid kick packet reason")

return LegacyStatusResponse.build(decoded_data[1:], latency)


class LegacyServerStatus(_BaseLegacyServerStatus):
def __init__(self, connection: BaseSyncConnection):
self.connection = connection

def read_status(self) -> LegacyStatusResponse:
"""Send the status request and read the response."""
start = perf_counter()
self.connection.write(self.request_status_data)
id = self.connection.read(1)
end = perf_counter()
if id != b"\xff":
raise IOError("Received invalid packet ID")
length = self.connection.read_ushort()
data = self.connection.read(length * 2)
return self.parse_response(data, (end - start) * 1000)


class AsyncLegacyServerStatus(_BaseLegacyServerStatus):
def __init__(self, connection: BaseAsyncReadSyncWriteConnection):
self.connection = connection

async def read_status(self) -> LegacyStatusResponse:
"""Send the status request and read the response."""
start = perf_counter()
self.connection.write(self.request_status_data)
id = await self.connection.read(1)
if id != b"\xff":
raise IOError("Received invalid packet ID")
length = await self.connection.read_ushort()
data = await self.connection.read(length * 2)
end = perf_counter()
return self.parse_response(data, (end - start) * 1000)
43 changes: 43 additions & 0 deletions mcstatus/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class RawQueryResponse(TypedDict):
"JavaStatusPlayers",
"JavaStatusResponse",
"JavaStatusVersion",
"LegacyStatusPlayers",
"LegacyStatusResponse",
"LegacyStatusVersion",
"QueryResponse",
]

Expand Down Expand Up @@ -191,6 +194,36 @@ def build(cls, raw: RawJavaResponse, latency: float = 0) -> Self:
)


@dataclass(frozen=True)
class LegacyStatusResponse(BaseStatusResponse):
"""The response object for :meth:`LegacyServerStatus.status() <mcstatus.server.LegacyServer.status>`."""

players: LegacyStatusPlayers
version: LegacyStatusVersion

@classmethod
def build(cls, decoded_data: list[str], 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:`LegacyStatusResponse` object.
"""

return cls(
players=LegacyStatusPlayers(
online=int(decoded_data[3]),
max=int(decoded_data[4]),
),
version=LegacyStatusVersion(
name=decoded_data[1],
protocol=int(decoded_data[0]),
),
motd=Motd.parse(decoded_data[2]),
latency=latency,
)


@dataclass(frozen=True)
class BedrockStatusResponse(BaseStatusResponse):
"""The response object for :meth:`BedrockServer.status() <mcstatus.server.BedrockServer.status>`."""
Expand Down Expand Up @@ -284,6 +317,11 @@ def build(cls, raw: RawJavaResponsePlayers) -> Self:
)


@dataclass(frozen=True)
class LegacyStatusPlayers(BaseStatusPlayers):
"""Class for storing information about players on the server."""


@dataclass(frozen=True)
class BedrockStatusPlayers(BaseStatusPlayers):
"""Class for storing information about players on the server."""
Expand Down Expand Up @@ -350,6 +388,11 @@ def build(cls, raw: RawJavaResponseVersion) -> Self:
return cls(name=raw["name"], protocol=raw["protocol"])


@dataclass(frozen=True)
class LegacyStatusVersion(BaseStatusVersion):
"""A class for storing version information."""


@dataclass(frozen=True)
class BedrockStatusVersion(BaseStatusVersion):
"""A class for storing version information."""
Expand Down
Loading