mcsmp 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcsmp/__init__.py +5 -0
- mcsmp/client.py +247 -0
- mcsmp/endpoints/__init__.py +19 -0
- mcsmp/endpoints/allow_list.py +99 -0
- mcsmp/endpoints/bans.py +99 -0
- mcsmp/endpoints/gamerules.py +53 -0
- mcsmp/endpoints/ip_bans.py +99 -0
- mcsmp/endpoints/operators.py +100 -0
- mcsmp/endpoints/players.py +63 -0
- mcsmp/endpoints/server.py +99 -0
- mcsmp/endpoints/server_settings.py +489 -0
- mcsmp/jsonrpc/__init__.py +11 -0
- mcsmp/jsonrpc/error.py +19 -0
- mcsmp/jsonrpc/error_code.py +34 -0
- mcsmp/jsonrpc/request.py +33 -0
- mcsmp/jsonrpc/response.py +36 -0
- mcsmp/schemas/__init__.py +31 -0
- mcsmp/schemas/difficulty.py +16 -0
- mcsmp/schemas/game_type.py +16 -0
- mcsmp/schemas/incoming_ip_ban.py +21 -0
- mcsmp/schemas/ip_ban.py +17 -0
- mcsmp/schemas/kick_player.py +13 -0
- mcsmp/schemas/message.py +14 -0
- mcsmp/schemas/operator.py +15 -0
- mcsmp/schemas/player.py +18 -0
- mcsmp/schemas/server_state.py +16 -0
- mcsmp/schemas/system_message.py +16 -0
- mcsmp/schemas/typed_game_rule.py +15 -0
- mcsmp/schemas/untyped_game_rule.py +11 -0
- mcsmp/schemas/user_ban.py +18 -0
- mcsmp/schemas/version.py +11 -0
- mcsmp-0.0.1.dist-info/METADATA +484 -0
- mcsmp-0.0.1.dist-info/RECORD +35 -0
- mcsmp-0.0.1.dist-info/WHEEL +4 -0
- mcsmp-0.0.1.dist-info/licenses/LICENSE +21 -0
mcsmp/__init__.py
ADDED
mcsmp/client.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
import ssl
|
|
5
|
+
import logging
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any, AsyncContextManager, Final
|
|
8
|
+
from asyncio import Future, Task, Lock
|
|
9
|
+
from websockets import connect, ClientConnection
|
|
10
|
+
from mcsmp.jsonrpc import Request, Response
|
|
11
|
+
from mcsmp.endpoints import *
|
|
12
|
+
from pyeventic import event
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("mcsmp.client")
|
|
15
|
+
|
|
16
|
+
class Client(AsyncContextManager["Client"]):
|
|
17
|
+
"""
|
|
18
|
+
An asynchronous client for interacting with a Minecraft server using the Minecraft Server Management Protocol (MCSMP) over WebSocket.
|
|
19
|
+
|
|
20
|
+
See `MC SMP Documentation <https://github.com/TechnoBro03/MC-SMP>`_ for more information.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, host: str, port: int, secret: str, *, tls: bool = False, cert: str | None = None, check_hostname: bool = True, timeout: float = 10.0) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initialize an MCSMP client.
|
|
26
|
+
|
|
27
|
+
:param host: The hostname or IP address of the Minecraft server.
|
|
28
|
+
:type host: str
|
|
29
|
+
:param port: The port number of the Minecraft server.
|
|
30
|
+
:type port: int
|
|
31
|
+
:param secret: The secret key for authentication.
|
|
32
|
+
:type secret: str
|
|
33
|
+
:param tls: Whether to use TLS for the connection.
|
|
34
|
+
:type tls: bool
|
|
35
|
+
:param cert: Path to a file of concatenated CA certificates in PEM format for verifying the server's certificate.
|
|
36
|
+
:type cert: str | None
|
|
37
|
+
:param check_hostname: Whether to verify the server's hostname in the TLS certificate.
|
|
38
|
+
:type check_hostname: bool
|
|
39
|
+
:param timeout: The timeout duration for requests in seconds.
|
|
40
|
+
:type timeout: float
|
|
41
|
+
"""
|
|
42
|
+
self.host: Final[str] = host
|
|
43
|
+
self.port: Final[int] = port
|
|
44
|
+
self.tls: Final[bool] = tls or cert is not None
|
|
45
|
+
|
|
46
|
+
self._secret: Final[str] = secret
|
|
47
|
+
self._cert: Final[str | None] = cert
|
|
48
|
+
self._check_hostname: Final[bool] = check_hostname
|
|
49
|
+
self._timeout: Final[float] = timeout
|
|
50
|
+
self._lock = Lock()
|
|
51
|
+
self._ws: ClientConnection | None = None
|
|
52
|
+
self._recv_task: Task[None] | None = None
|
|
53
|
+
|
|
54
|
+
self._requests: dict[str, Future[Any]] = {}
|
|
55
|
+
|
|
56
|
+
# Build endpoints
|
|
57
|
+
self.allow_list = AllowList(self)
|
|
58
|
+
"""The AllowList endpoint for managing the server's allow list."""
|
|
59
|
+
self.bans = Bans(self)
|
|
60
|
+
"""The Bans endpoint for managing player bans."""
|
|
61
|
+
self.gamerules = Gamerules(self)
|
|
62
|
+
"""The Gamerules endpoint for managing server game rules."""
|
|
63
|
+
self.ip_bans = IPBans(self)
|
|
64
|
+
"""The IPBans endpoint for managing IP bans."""
|
|
65
|
+
self.operators = Operators(self)
|
|
66
|
+
"""The Operators endpoint for managing server operators."""
|
|
67
|
+
self.players = Players(self)
|
|
68
|
+
"""The Players endpoint for managing players."""
|
|
69
|
+
self.server_settings = ServerSettings(self)
|
|
70
|
+
"""The ServerSettings endpoint for managing server settings."""
|
|
71
|
+
self.server = Server(self)
|
|
72
|
+
"""The Server endpoint for server-related operations and notifications."""
|
|
73
|
+
|
|
74
|
+
async def connect(self) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Connect to the Minecraft server via WebSocket.
|
|
77
|
+
|
|
78
|
+
:raises SSLCertVerificationError: If SSL certificate verification fails. The server's certificate may be invalid or untrusted.
|
|
79
|
+
:raises SSLError: If an SSL error occurs.
|
|
80
|
+
:raises ConnectionRefusedError: If the connection is refused. The server may be offline or unreachable.
|
|
81
|
+
:raises RuntimeError: If any other connection error occurs. A 401 error means the provided secret is invalid.
|
|
82
|
+
"""
|
|
83
|
+
if self._ws: return
|
|
84
|
+
header = {
|
|
85
|
+
"Authorization": f"Bearer {self._secret}",
|
|
86
|
+
"Origin": "mcsmp-client"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if self.tls:
|
|
90
|
+
uri = f"wss://{self.host}:{self.port}" # Secure WebSocket
|
|
91
|
+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) # Does not verify cert or hostname by default
|
|
92
|
+
if self._cert:
|
|
93
|
+
ssl_context.load_verify_locations(cafile=self._cert)
|
|
94
|
+
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
|
95
|
+
ssl_context.check_hostname = self._check_hostname
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
uri = f"ws://{self.host}:{self.port}"
|
|
99
|
+
ssl_context = None
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
self._ws = await connect(uri, additional_headers=header, ssl=ssl_context)
|
|
103
|
+
logger.info(f"Connected to {uri}.")
|
|
104
|
+
self._recv_task = asyncio.create_task(self._recv_loop(), name="mcsmp-jsonrpc-recv")
|
|
105
|
+
except ssl.SSLCertVerificationError as e:
|
|
106
|
+
raise RuntimeError(f"SSL certificate verification failed: {e}") from e
|
|
107
|
+
except ssl.SSLError as e:
|
|
108
|
+
raise RuntimeError(f"SSL error occurred: {e}") from e
|
|
109
|
+
except ConnectionRefusedError as e:
|
|
110
|
+
raise RuntimeError(f"Connection to {uri} was refused: {e}") from e
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise RuntimeError(f"Failed to connect to {uri}: {e}") from e
|
|
113
|
+
|
|
114
|
+
async def close(self) -> None:
|
|
115
|
+
"""Close the WebSocket connection to the Minecraft server."""
|
|
116
|
+
if self._recv_task:
|
|
117
|
+
self._recv_task.cancel()
|
|
118
|
+
try: await self._recv_task
|
|
119
|
+
except asyncio.CancelledError: ...
|
|
120
|
+
finally: self._recv_task = None
|
|
121
|
+
logger.debug("Stopped receive loop.")
|
|
122
|
+
|
|
123
|
+
if self._ws:
|
|
124
|
+
try: await self._ws.close()
|
|
125
|
+
finally: self._ws = None
|
|
126
|
+
logger.info("Closed connection.")
|
|
127
|
+
|
|
128
|
+
# Wait for all notification handlers to complete
|
|
129
|
+
await self.on_notification.wait()
|
|
130
|
+
|
|
131
|
+
async def request(self, method: str, params: Any = None) -> Any:
|
|
132
|
+
"""
|
|
133
|
+
Send a JSON-RPC request to the server and await the response.
|
|
134
|
+
|
|
135
|
+
:param method: The JSON-RPC method to call.
|
|
136
|
+
:type method: str
|
|
137
|
+
:param params: The parameters for the JSON-RPC method.
|
|
138
|
+
:type params: Any
|
|
139
|
+
:return: The result of the JSON-RPC call.
|
|
140
|
+
:rtype: Any
|
|
141
|
+
"""
|
|
142
|
+
if not self._ws: raise RuntimeError("WebSocket is not connected.")
|
|
143
|
+
|
|
144
|
+
request_id = uuid.uuid4().hex
|
|
145
|
+
request = Request(id=request_id, method=method, params=params)
|
|
146
|
+
logger.debug(f"Sending request: {method} (id: {request_id})")
|
|
147
|
+
|
|
148
|
+
future = asyncio.get_running_loop().create_future()
|
|
149
|
+
async with self._lock: self._requests[request_id] = future
|
|
150
|
+
|
|
151
|
+
# Wait for response (future will be set in the recv loop)
|
|
152
|
+
try:
|
|
153
|
+
await self._ws.send(json.dumps(dict(request)))
|
|
154
|
+
result = await asyncio.wait_for(future, timeout=self._timeout)
|
|
155
|
+
logger.debug(f"Received response for request: {method} (id: {request_id})")
|
|
156
|
+
return result
|
|
157
|
+
except asyncio.TimeoutError:
|
|
158
|
+
logger.exception(f"Request {method} (id: {request_id}) timed out after {self._timeout} seconds.")
|
|
159
|
+
raise RuntimeError(f"Request {method} timed out after {self._timeout} seconds.")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.exception(f"An error occurred while sending request {method} (id: {request_id}): {e}")
|
|
162
|
+
raise RuntimeError(f"An error occurred while sending request {method}.") from e
|
|
163
|
+
finally:
|
|
164
|
+
async with self._lock: self._requests.pop(request_id, None)
|
|
165
|
+
|
|
166
|
+
async def discover(self) -> dict[str, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Returns an API schema containing supported methods and notifications of the currently running server.
|
|
169
|
+
|
|
170
|
+
:return: A JSON string representing the API schema.
|
|
171
|
+
:rtype: str
|
|
172
|
+
"""
|
|
173
|
+
if not self._ws: raise RuntimeError("WebSocket is not connected.")
|
|
174
|
+
|
|
175
|
+
response = await self.request("rpc.discover")
|
|
176
|
+
return response
|
|
177
|
+
|
|
178
|
+
@event
|
|
179
|
+
async def on_notification(self, notification: Request) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Event triggered when a notification is received from the server.
|
|
182
|
+
|
|
183
|
+
:param notification: The received JSON-RPC notification.
|
|
184
|
+
:type notification: Request
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
async def _recv_loop(self) -> None:
|
|
188
|
+
if not self._ws: return
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
logger.debug("Starting receive loop...")
|
|
192
|
+
async for message in self._ws:
|
|
193
|
+
try: response: dict[Any, Any] | list[dict[Any, Any]] = json.loads(message)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"Failed to decode JSON message: {e}")
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# It's possible JSON RPC messages are sent in batches
|
|
199
|
+
if isinstance(response, list):
|
|
200
|
+
for item in response:
|
|
201
|
+
await self._handle_item(item)
|
|
202
|
+
else:
|
|
203
|
+
await self._handle_item(response)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.exception(f"An error occurred in the receive loop.")
|
|
206
|
+
async with self._lock:
|
|
207
|
+
for future in self._requests.values():
|
|
208
|
+
if not future.done():
|
|
209
|
+
future.set_exception(RuntimeError(f"An error occurred: {e}"))
|
|
210
|
+
self._requests.clear()
|
|
211
|
+
finally:
|
|
212
|
+
await self.close()
|
|
213
|
+
|
|
214
|
+
async def _handle_item(self, item: dict[str, Any]):
|
|
215
|
+
try:
|
|
216
|
+
request_id = item.get("id")
|
|
217
|
+
|
|
218
|
+
# Response
|
|
219
|
+
if request_id is not None:
|
|
220
|
+
logger.debug(f"Handling response for request id: {request_id}")
|
|
221
|
+
async with self._lock:
|
|
222
|
+
future = self._requests.pop(request_id, None)
|
|
223
|
+
|
|
224
|
+
if future and not future.done():
|
|
225
|
+
response = Response(**item)
|
|
226
|
+
|
|
227
|
+
if response.error:
|
|
228
|
+
data = f" ({response.error.data})" if response.error.data else ""
|
|
229
|
+
future.set_exception(RuntimeError(f"[{response.error.code}] {response.error.message}{data}"))
|
|
230
|
+
else:
|
|
231
|
+
future.set_result(response.result)
|
|
232
|
+
|
|
233
|
+
# Notification
|
|
234
|
+
else:
|
|
235
|
+
logger.debug(f"Handling notification: {item.get('method')}")
|
|
236
|
+
notification = Request(**item)
|
|
237
|
+
self.on_notification.fire(notification) # Schedule notification handlers in background
|
|
238
|
+
except Exception:
|
|
239
|
+
logger.exception(f"An error occurred while handling item.")
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
async def __aenter__(self) -> "Client":
|
|
243
|
+
await self.connect()
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
async def __aexit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
|
247
|
+
await self.close()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .allow_list import AllowList
|
|
2
|
+
from .bans import Bans
|
|
3
|
+
from .gamerules import Gamerules
|
|
4
|
+
from .ip_bans import IPBans
|
|
5
|
+
from .operators import Operators
|
|
6
|
+
from .players import Players
|
|
7
|
+
from .server_settings import ServerSettings
|
|
8
|
+
from .server import Server
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AllowList",
|
|
12
|
+
"Bans",
|
|
13
|
+
"Gamerules",
|
|
14
|
+
"IPBans",
|
|
15
|
+
"Operators",
|
|
16
|
+
"Players",
|
|
17
|
+
"ServerSettings",
|
|
18
|
+
"Server"
|
|
19
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from typing import Awaitable, Callable, TYPE_CHECKING, Final
|
|
3
|
+
from pyeventic import event
|
|
4
|
+
from mcsmp.jsonrpc import Request
|
|
5
|
+
from mcsmp.schemas import Player
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from mcsmp import Client
|
|
8
|
+
|
|
9
|
+
class AllowList:
|
|
10
|
+
"""Allow list endpoints."""
|
|
11
|
+
|
|
12
|
+
method: Final[str] = "minecraft:allowlist"
|
|
13
|
+
notification: Final[str] = "minecraft:notification/allowlist"
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: "Client") -> None:
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
self._endpoints: dict[str, Callable[[Request], Awaitable[None]]] = {
|
|
19
|
+
f"{self.notification}/added": lambda n: self.added(Player(**n.params[0])),
|
|
20
|
+
f"{self.notification}/removed": lambda n: self.removed(Player(**n.params[0])),
|
|
21
|
+
}
|
|
22
|
+
client.on_notification.subscribe(lambda n: evt(n) if (evt := self._endpoints.get(n.method)) else None) # type: ignore
|
|
23
|
+
|
|
24
|
+
async def get(self) -> list[Player]:
|
|
25
|
+
"""
|
|
26
|
+
Get the allow list.
|
|
27
|
+
|
|
28
|
+
:return: A list of Players representing the current allow list.
|
|
29
|
+
:rtype: list[Player]
|
|
30
|
+
"""
|
|
31
|
+
response = await self._client.request(self.method)
|
|
32
|
+
return [Player(**p) for p in response]
|
|
33
|
+
|
|
34
|
+
async def set(self, players: list[Player]) -> list[Player]:
|
|
35
|
+
"""
|
|
36
|
+
Set the allow list to the provided list of players.
|
|
37
|
+
|
|
38
|
+
:param players: A list of Players to set as the allow list.
|
|
39
|
+
:type players: list[Player]
|
|
40
|
+
:return: A list of Players representing the updated allow list.
|
|
41
|
+
:rtype: list[Player]
|
|
42
|
+
"""
|
|
43
|
+
params = {"players": [asdict(p) for p in players]}
|
|
44
|
+
response = await self._client.request(f"{self.method}/set", params=params)
|
|
45
|
+
return [Player(**p) for p in response]
|
|
46
|
+
|
|
47
|
+
async def add(self, players: list[Player]) -> list[Player]:
|
|
48
|
+
"""
|
|
49
|
+
Add players to the allow list.
|
|
50
|
+
|
|
51
|
+
:param players: A list of Players to add to the allow list.
|
|
52
|
+
:type players: list[Player]
|
|
53
|
+
:return: A list of Players representing the updated allow list.
|
|
54
|
+
:rtype: list[Player]
|
|
55
|
+
"""
|
|
56
|
+
params = {"add": [asdict(p) for p in players]}
|
|
57
|
+
response = await self._client.request(f"{self.method}/add", params=params)
|
|
58
|
+
return [Player(**p) for p in response]
|
|
59
|
+
|
|
60
|
+
async def remove(self, players: list[Player]) -> list[Player]:
|
|
61
|
+
"""
|
|
62
|
+
Remove players from the allow list.
|
|
63
|
+
|
|
64
|
+
:param players: A list of Players to remove from the allow list.
|
|
65
|
+
:type players: list[Player]
|
|
66
|
+
:return: A list of Players representing the updated allow list.
|
|
67
|
+
:rtype: list[Player]
|
|
68
|
+
"""
|
|
69
|
+
params = {"remove": [asdict(p) for p in players]}
|
|
70
|
+
response = await self._client.request(f"{self.method}/remove", params=params)
|
|
71
|
+
return [Player(**p) for p in response]
|
|
72
|
+
|
|
73
|
+
async def clear(self) -> list[Player]:
|
|
74
|
+
"""
|
|
75
|
+
Clear all players in the allow list.
|
|
76
|
+
|
|
77
|
+
:return: A list of Players representing the updated (empty) allow list.
|
|
78
|
+
:rtype: list[Player]
|
|
79
|
+
"""
|
|
80
|
+
response = await self._client.request(f"{self.method}/clear")
|
|
81
|
+
return [Player(**p) for p in response]
|
|
82
|
+
|
|
83
|
+
@event
|
|
84
|
+
async def added(self, player: Player) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Called when a player is added to the allowlist.
|
|
87
|
+
|
|
88
|
+
:param player: The player who was added to the allowlist.
|
|
89
|
+
:type player: Player
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@event
|
|
93
|
+
async def removed(self, player: Player) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Called when a player is removed from the allowlist.
|
|
96
|
+
|
|
97
|
+
:param player: The player who was removed from the allowlist.
|
|
98
|
+
:type player: Player
|
|
99
|
+
"""
|
mcsmp/endpoints/bans.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from typing import Awaitable, Callable, TYPE_CHECKING, Final
|
|
3
|
+
from pyeventic import event
|
|
4
|
+
from mcsmp.schemas import Player, UserBan
|
|
5
|
+
from mcsmp.jsonrpc import Request
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from mcsmp import Client
|
|
8
|
+
|
|
9
|
+
class Bans:
|
|
10
|
+
"""Ban endpoints."""
|
|
11
|
+
|
|
12
|
+
method: Final[str] = "minecraft:bans"
|
|
13
|
+
notification: Final[str] = "minecraft:notification/bans"
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: "Client"):
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
self._endpoints: dict[str, Callable[[Request], Awaitable[None]]] = {
|
|
19
|
+
f"{self.notification}/added": lambda n: self.added(UserBan(**n.params[0])),
|
|
20
|
+
f"{self.notification}/removed": lambda n: self.removed(Player(**n.params[0])),
|
|
21
|
+
}
|
|
22
|
+
client.on_notification.subscribe(lambda n: evt(n) if (evt := self._endpoints.get(n.method)) else None) # type: ignore
|
|
23
|
+
|
|
24
|
+
async def get(self) -> list[UserBan]:
|
|
25
|
+
"""
|
|
26
|
+
Get the ban list.
|
|
27
|
+
|
|
28
|
+
:return: A list of UserBans representing the current ban list.
|
|
29
|
+
:rtype: list[UserBan]
|
|
30
|
+
"""
|
|
31
|
+
response = await self._client.request(self.method)
|
|
32
|
+
return [UserBan(**u) for u in response]
|
|
33
|
+
|
|
34
|
+
async def set(self, bans: list[UserBan]) -> list[UserBan]:
|
|
35
|
+
"""
|
|
36
|
+
Set the ban list to the provided list of UserBans.
|
|
37
|
+
|
|
38
|
+
:param bans: A list of UserBans to set as the ban list.
|
|
39
|
+
:type bans: list[UserBan]
|
|
40
|
+
:return: A list of UserBans representing the updated ban list.
|
|
41
|
+
:rtype: list[UserBan]
|
|
42
|
+
"""
|
|
43
|
+
params = {"bans": [asdict(b) for b in bans]}
|
|
44
|
+
response = await self._client.request(f"{self.method}/set", params=params)
|
|
45
|
+
return [UserBan(**u) for u in response]
|
|
46
|
+
|
|
47
|
+
async def add(self, bans: list[UserBan]) -> list[UserBan]:
|
|
48
|
+
"""
|
|
49
|
+
Add players to the ban list.
|
|
50
|
+
|
|
51
|
+
:param bans: A list of UserBans to add to the ban list.
|
|
52
|
+
:type bans: list[UserBan]
|
|
53
|
+
:return: A list of UserBans representing the updated ban list.
|
|
54
|
+
:rtype: list[UserBan]
|
|
55
|
+
"""
|
|
56
|
+
params = {"add": [asdict(b) for b in bans]}
|
|
57
|
+
response = await self._client.request(f"{self.method}/add", params=params)
|
|
58
|
+
return [UserBan(**u) for u in response]
|
|
59
|
+
|
|
60
|
+
async def remove(self, bans: list[Player]) -> list[UserBan]:
|
|
61
|
+
"""
|
|
62
|
+
Remove players from the ban list.
|
|
63
|
+
|
|
64
|
+
:param bans: A list of Players to remove from the ban list.
|
|
65
|
+
:type bans: list[Player]
|
|
66
|
+
:return: A list of UserBans representing the updated ban list.
|
|
67
|
+
:rtype: list[UserBan]
|
|
68
|
+
"""
|
|
69
|
+
params = {"remove": [asdict(b) for b in bans]}
|
|
70
|
+
response = await self._client.request(f"{self.method}/remove", params=params)
|
|
71
|
+
return [UserBan(**u) for u in response]
|
|
72
|
+
|
|
73
|
+
async def clear(self) -> list[UserBan]:
|
|
74
|
+
"""
|
|
75
|
+
Clear all players in the ban list.
|
|
76
|
+
|
|
77
|
+
:return: A list of UserBans representing the updated (empty) ban list.
|
|
78
|
+
:rtype: list[UserBan]
|
|
79
|
+
"""
|
|
80
|
+
response = await self._client.request(f"{self.method}/clear")
|
|
81
|
+
return [UserBan(**u) for u in response]
|
|
82
|
+
|
|
83
|
+
@event
|
|
84
|
+
async def added(self, player: UserBan) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Called when a player is added to the ban list.
|
|
87
|
+
|
|
88
|
+
:param player: The player who was added to the ban list.
|
|
89
|
+
:type player: UserBan
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@event
|
|
93
|
+
async def removed(self, player: Player) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Called when a player is removed from the ban list.
|
|
96
|
+
|
|
97
|
+
:param player: The player who was removed from the ban list.
|
|
98
|
+
:type player: Player
|
|
99
|
+
"""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from typing import TYPE_CHECKING, Awaitable, Callable, Final
|
|
3
|
+
from pyeventic import event
|
|
4
|
+
from mcsmp.jsonrpc.request import Request
|
|
5
|
+
from mcsmp.schemas import TypedGameRule, UntypedGameRule
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from mcsmp.client import Client
|
|
8
|
+
|
|
9
|
+
class Gamerules:
|
|
10
|
+
"""Gamerule endpoints."""
|
|
11
|
+
|
|
12
|
+
method: Final[str] = "minecraft:gamerules"
|
|
13
|
+
notification: Final[str] = "minecraft:notification/gamerules"
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: "Client"):
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
self._endpoints: dict[str, Callable[[Request], Awaitable[None]]] = {
|
|
19
|
+
f"{self.notification}/updated": lambda n: self.updated(TypedGameRule(**n.params[0]))
|
|
20
|
+
}
|
|
21
|
+
client.on_notification.subscribe(lambda n: evt(n) if (evt := self._endpoints.get(n.method)) else None) # type: ignore
|
|
22
|
+
|
|
23
|
+
async def get(self) -> list[TypedGameRule]:
|
|
24
|
+
"""
|
|
25
|
+
Get the available game rule keys and their current values.
|
|
26
|
+
|
|
27
|
+
:return: A list of TypedGameRules representing the current game rules.
|
|
28
|
+
:rtype: list[TypedGameRule]
|
|
29
|
+
"""
|
|
30
|
+
response = await self._client.request(self.method)
|
|
31
|
+
return [TypedGameRule(**r) for r in response]
|
|
32
|
+
|
|
33
|
+
async def update(self, gamerule: UntypedGameRule) -> TypedGameRule:
|
|
34
|
+
"""
|
|
35
|
+
Update a game rule to the provided value.
|
|
36
|
+
|
|
37
|
+
:param gamerule: An UntypedGameRule representing the game rule to update.
|
|
38
|
+
:type gamerule: UntypedGameRule
|
|
39
|
+
:return: A TypedGameRule representing the updated game rule.
|
|
40
|
+
:rtype: TypedGameRule
|
|
41
|
+
"""
|
|
42
|
+
params = {"gamerule": asdict(gamerule)}
|
|
43
|
+
response = await self._client.request(f"{self.method}/update", params=params)
|
|
44
|
+
return TypedGameRule(**response)
|
|
45
|
+
|
|
46
|
+
@event
|
|
47
|
+
async def updated(self, gamerule: TypedGameRule) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Called when a gamerule is updated.
|
|
50
|
+
|
|
51
|
+
:param gamerule: The updated gamerule.
|
|
52
|
+
:type gamerule: TypedGameRule
|
|
53
|
+
"""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from typing import TYPE_CHECKING, Awaitable, Callable, Final
|
|
3
|
+
from pyeventic import event
|
|
4
|
+
from mcsmp.jsonrpc.request import Request
|
|
5
|
+
from mcsmp.schemas import IPBan, IncomingIPBan
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from mcsmp.client import Client
|
|
8
|
+
|
|
9
|
+
class IPBans:
|
|
10
|
+
"""IP ban endpoints."""
|
|
11
|
+
|
|
12
|
+
method: Final[str] = "minecraft:ip_bans"
|
|
13
|
+
notification: Final[str] = "minecraft:notification/ip_bans"
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: "Client"):
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
self._endpoints: dict[str, Callable[[Request], Awaitable[None]]] = {
|
|
19
|
+
f"{self.notification}/added": lambda n: self.added(IPBan(**n.params[0])),
|
|
20
|
+
f"{self.notification}/removed": lambda n: self.removed(n.params[0]),
|
|
21
|
+
}
|
|
22
|
+
client.on_notification.subscribe(lambda n: evt(n) if (evt := self._endpoints.get(n.method)) else None) # type: ignore
|
|
23
|
+
|
|
24
|
+
async def get(self) -> list[IPBan]:
|
|
25
|
+
"""
|
|
26
|
+
Get the IP ban list.
|
|
27
|
+
|
|
28
|
+
:return: A list of IPBans representing the current IP ban list.
|
|
29
|
+
:rtype: list[IPBan]
|
|
30
|
+
"""
|
|
31
|
+
response = await self._client.request(self.method)
|
|
32
|
+
return [IPBan(**b) for b in response]
|
|
33
|
+
|
|
34
|
+
async def set(self, bans: list[IPBan]) -> list[IPBan]:
|
|
35
|
+
"""
|
|
36
|
+
Set the IP ban list to the provided list of IPBans.
|
|
37
|
+
|
|
38
|
+
:param bans: A list of IPBans to set as the IP ban list.
|
|
39
|
+
:type bans: list[IPBan]
|
|
40
|
+
:return: A list of IPBans representing the updated IP ban list.
|
|
41
|
+
:rtype: list[IPBan]
|
|
42
|
+
"""
|
|
43
|
+
params = {"banlist": [asdict(b) for b in bans]}
|
|
44
|
+
response = await self._client.request(f"{self.method}/set", params=params)
|
|
45
|
+
return [IPBan(**b) for b in response]
|
|
46
|
+
|
|
47
|
+
async def add(self, bans: list[IncomingIPBan]) -> list[IPBan]:
|
|
48
|
+
"""
|
|
49
|
+
Add IPs to the IP ban list.
|
|
50
|
+
|
|
51
|
+
:param bans: A list of IncomingIPBans to add to the IP ban list.
|
|
52
|
+
:type bans: list[IncomingIPBan]
|
|
53
|
+
:return: A list of IPBans representing the updated IP ban list.
|
|
54
|
+
:rtype: list[IPBan]
|
|
55
|
+
"""
|
|
56
|
+
params = {"add": [asdict(b) for b in bans]}
|
|
57
|
+
response = await self._client.request(f"{self.method}/add", params=params)
|
|
58
|
+
return [IPBan(**b) for b in response]
|
|
59
|
+
|
|
60
|
+
async def remove(self, bans: list[str]) -> list[IPBan]:
|
|
61
|
+
"""
|
|
62
|
+
Remove IPs from the IP ban list.
|
|
63
|
+
|
|
64
|
+
:param bans: A list of IP addresses (as strings) to remove from the IP ban list.
|
|
65
|
+
:type bans: list[str]
|
|
66
|
+
:return: A list of IPBans representing the updated IP ban list.
|
|
67
|
+
:rtype: list[IPBan]
|
|
68
|
+
"""
|
|
69
|
+
params = {"ip": bans}
|
|
70
|
+
response = await self._client.request(f"{self.method}/remove", params=params)
|
|
71
|
+
return [IPBan(**b) for b in response]
|
|
72
|
+
|
|
73
|
+
async def clear(self) -> list[IPBan]:
|
|
74
|
+
"""
|
|
75
|
+
Clear all IPs in the IP ban list.
|
|
76
|
+
|
|
77
|
+
:return: A list of IPBans representing the updated IP ban list.
|
|
78
|
+
:rtype: list[IPBan]
|
|
79
|
+
"""
|
|
80
|
+
response = await self._client.request(f"{self.method}/clear")
|
|
81
|
+
return [IPBan(**b) for b in response]
|
|
82
|
+
|
|
83
|
+
@event
|
|
84
|
+
async def added(self, player: IPBan) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Called when an IP is added to the ip ban list.
|
|
87
|
+
|
|
88
|
+
:param player: The IP that was added to the ip ban list.
|
|
89
|
+
:type player: IPBan
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@event
|
|
93
|
+
async def removed(self, player: str) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Called when an IP is removed from the ip ban list.
|
|
96
|
+
|
|
97
|
+
:param player: The IP that was removed from the ip ban list.
|
|
98
|
+
:type player: str
|
|
99
|
+
"""
|