Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
90337a7
Rename `status_response` to `responses`
PerchunPak May 14, 2023
482cd59
Rewrite Query response class to 306's style
PerchunPak May 14, 2023
0a8b293
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Aug 14, 2023
8d74db9
Ignore pyright's warning about wildcard import
PerchunPak Aug 14, 2023
11727d6
Merge remote-tracking branch 'upstream/master' into rewrite-query-ans…
PerchunPak Oct 1, 2023
aa9fec2
Merge remote-tracking branch 'origin/master' into rewrite-query-answe…
PerchunPak Oct 11, 2023
7a883e7
Rename `transform_connection_to_objects` to `_parse_response`
PerchunPak Oct 11, 2023
8e7fed4
Do not use star import
PerchunPak Oct 11, 2023
67a516e
Merge remote-tracking branch 'origin/master' into rewrite-query-answe…
PerchunPak Dec 4, 2023
3a531d0
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Jan 7, 2024
e78a9a8
Return `utils.deprecated` import
PerchunPak Jan 7, 2024
1580c6c
Replace usage of deprecated `QueryResponse.map` to `.map_name` in CLI
PerchunPak Jan 7, 2024
c1de661
Rename `responses` back to `status_response`
PerchunPak Feb 19, 2024
252855a
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Feb 19, 2024
4a3f5d6
Freeze all dataclasses
PerchunPak Feb 19, 2024
8f34ffb
Merge branch 'master' into rewrite-query-answer-class
PerchunPak May 26, 2024
8003e7c
Merge branch 'master' into rewrite-query-answer-class
PerchunPak May 28, 2024
d8b2053
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Jun 3, 2024
3f55640
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Jul 18, 2024
923a083
Replace all usages of `status_response` with `responses`
PerchunPak Jul 18, 2024
a392b14
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Feb 5, 2025
4ea1b99
Replace weird `1 + 1 + 1` to `+ 3`
PerchunPak Feb 5, 2025
46fd847
Update deprecation date
PerchunPak Feb 5, 2025
f0dabf0
Update all deprecation dates
PerchunPak Feb 5, 2025
f2559d5
Update more deprecation dates
PerchunPak Feb 5, 2025
6a0a013
more deprecation dates
PerchunPak Feb 5, 2025
c42e6af
Merge branch 'master' into rewrite-query-answer-class
PerchunPak May 9, 2025
dd20481
Merge branch 'master' into rewrite-query-answer-class
PerchunPak May 9, 2025
cedadbb
Fix tests
PerchunPak May 9, 2025
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
92 changes: 15 additions & 77 deletions docs/api/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,85 +60,23 @@ For Java Server
:inherited-members:
:exclude-members: build

.. module:: mcstatus.querier

.. class:: QueryResponse
:canonical: mcstatus.querier.QueryResponse

The response object for :meth:`JavaServer.query() <mcstatus.server.JavaServer.query>`.

.. class:: Players
:canonical: mcstatus.querier.QueryResponse.Players

Class for storing information about players on the server.

.. attribute:: online
:type: int
:canonical: mcstatus.querier.QueryResponse.Players.online

The number of online players.

.. attribute:: max
:type: int
:canonical: mcstatus.querier.QueryResponse.Players.max

The maximum allowed number of players (server slots).

.. attribute:: names
:type: list[str]
:canonical: mcstatus.querier.QueryResponse.Players.names

The list of online players.

.. class:: Software
:canonical: mcstatus.querier.QueryResponse.Software

Class for storing information about software on the server.

.. attribute:: version
:type: str
:canonical: mcstatus.querier.QueryResponse.Software.version

The version of the software.

.. attribute:: brand
:type: str
:value: "vanilla"
:canonical: mcstatus.querier.QueryResponse.Software.brand

The brand of the software. Like `Paper <https://papermc.io>`_ or `Spigot <https://www.spigotmc.org>`_.

.. attribute:: plugins
:type: list[str]
:canonical: mcstatus.querier.QueryResponse.Software.plugins

The list of plugins. Can be empty if hidden.

.. attribute:: motd
:type: ~mcstatus.motd.Motd
:canonical: mcstatus.querier.QueryResponse.motd

The MOTD of the server. Also known as description.

.. seealso:: :doc:`/api/motd_parsing`.

.. attribute:: map
:type: str
:canonical: mcstatus.querier.QueryResponse.map

The name of the map.

.. attribute:: players
:type: ~QueryResponse.Players
:canonical: mcstatus.querier.QueryResponse.players

The players information.
.. autoclass:: mcstatus.responses.QueryResponse()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build

.. attribute:: software
:type: ~QueryResponse.Software
:canonical: mcstatus.querier.QueryResponse.software
.. autoclass:: mcstatus.responses.QueryPlayers()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build

The software information.
.. autoclass:: mcstatus.responses.QuerySoftware()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build


For Bedrock Servers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
server = JavaServer.lookup("play.hypixel.net")
query = server.query()

if query.players.names:
print("Players online:", ", ".join(query.players.names))
if query.players.list:
print("Players online:", ", ".join(query.players.list))
else:
status = server.status()

Expand Down
145 changes: 45 additions & 100 deletions mcstatus/querier.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
from __future__ import annotations

from abc import abstractmethod
from dataclasses import dataclass, field
import random
import re
import struct
from typing import TYPE_CHECKING, ClassVar, final
from abc import abstractmethod
from collections.abc import Awaitable
from dataclasses import dataclass, field
from typing import ClassVar, final

from mcstatus.motd import Motd
from mcstatus.protocol.connection import Connection, UDPAsyncSocketConnection, UDPSocketConnection

if TYPE_CHECKING:
from typing_extensions import Self
from mcstatus.responses import QueryResponse, RawQueryResponse


@dataclass
Expand Down Expand Up @@ -58,6 +55,45 @@ def handshake(self) -> None | Awaitable[None]:
def read_query(self) -> QueryResponse | Awaitable[QueryResponse]:
raise NotImplementedError

def _parse_response(self, response: Connection) -> tuple[RawQueryResponse, list[str]]:
"""Transform the connection object (the result) into dict which is passed to the QueryResponse constructor.

:return: A tuple with two elements. First is `raw` answer and second is list of players.
"""
response.read(len("splitnum") + 3)
data = {}

while True:
key = response.read_ascii()
if key == "hostname": # hostname is actually motd in the query protocol
match = re.search(
b"(.*?)\x00(hostip|hostport|game_id|gametype|map|maxplayers|numplayers|plugins|version)",
response.received,
flags=re.DOTALL,
)
motd = match.group(1) if match else ""
# Since the query protocol does not properly support unicode, the motd is still not resolved
# correctly; however, this will avoid other parameter parsing errors.
data[key] = response.read(len(motd)).decode("ISO-8859-1")
response.read(1) # ignore null byte
elif len(key) == 0:
response.read(1)
break
else:
value = response.read_ascii()
data[key] = value

response.read(len("player_") + 2)

players_list = []
while True:
player = response.read_ascii()
if len(player) == 0:
break
players_list.append(player)

return RawQueryResponse(**data), players_list


@final
@dataclass
Expand All @@ -81,7 +117,7 @@ def read_query(self) -> QueryResponse:
self.connection.write(request)

response = self._read_packet()
return QueryResponse.from_connection(response)
return QueryResponse.build(*self._parse_response(response))


@final
Expand All @@ -106,95 +142,4 @@ async def read_query(self) -> QueryResponse:
await self.connection.write(request)

response = await self._read_packet()
return QueryResponse.from_connection(response)


class QueryResponse:
"""Documentation for this class is written by hand, without docstrings.

This is because the class is not supposed to be auto-documented.

Please see https://mcstatus.readthedocs.io/en/latest/api/basic/#mcstatus.querier.QueryResponse
for the actual documentation.
"""

# THIS IS SO UNPYTHONIC
# it's staying just because the tests depend on this structure
class Players:
online: int
max: int
names: list[str]

# TODO: It's a bit weird that we accept str for number parameters, just to convert them in init
def __init__(self, online: str | int, max: str | int, names: list[str]):
self.online = int(online)
self.max = int(max)
self.names = names

class Software:
version: str
brand: str
plugins: list[str]

def __init__(self, version: str, plugins: str):
self.version = version
self.brand = "vanilla"
self.plugins = []

if plugins:
parts = plugins.split(":", 1)
self.brand = parts[0].strip()

if len(parts) == 2:
self.plugins = [s.strip() for s in parts[1].split(";")]

motd: Motd
map: str
players: Players
software: Software

def __init__(self, raw: dict[str, str], players: list[str]):
try:
self.raw = raw
self.motd = Motd.parse(raw["hostname"], bedrock=False)
self.map = raw["map"]
self.players = QueryResponse.Players(raw["numplayers"], raw["maxplayers"], players)
self.software = QueryResponse.Software(raw["version"], raw["plugins"])
except KeyError:
raise ValueError("The provided data is not valid")

@classmethod
def from_connection(cls, response: Connection) -> Self:
response.read(len("splitnum") + 1 + 1 + 1)
data = {}

while True:
key = response.read_ascii()
if key == "hostname": # hostname is actually motd in the query protocol
match = re.search(
b"(.*?)\x00(hostip|hostport|game_id|gametype|map|maxplayers|numplayers|plugins|version)",
response.received,
flags=re.DOTALL,
)
motd = match.group(1) if match else ""
# Since the query protocol does not properly support unicode, the motd is still not resolved
# correctly; however, this will avoid other parameter parsing errors.
data[key] = response.read(len(motd)).decode("ISO-8859-1")
response.read(1) # ignore null byte
elif len(key) == 0:
response.read(1)
break
else:
value = response.read_ascii()
data[key] = value

response.read(len("player_") + 1 + 1)

players = []
while True:
player = response.read_ascii()
if len(player) == 0:
break
players.append(player)

return cls(data, players)
return QueryResponse.build(*self._parse_response(response))
Loading