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 ADDED
@@ -0,0 +1,5 @@
1
+ from .client import Client
2
+
3
+ __all__ = [
4
+ "Client",
5
+ ]
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
+ """
@@ -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
+ """