noxaeapi-sdk 0.4.0__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.
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import List, Optional
5
+
6
+ from ..http_engine import HttpEngine
7
+ from ..types import LeaderboardEntry, LeaderboardSourceInfo
8
+
9
+
10
+ def _q(value: str) -> str:
11
+ return urllib.parse.quote(value, safe="")
12
+
13
+
14
+ class LeaderboardModule:
15
+ """Wraps the generic, pluggable ``/v1/leaderboards/*`` routes.
16
+
17
+ This is a unified view over every ranking source registered on the
18
+ server — economy currencies, mcMMO power level, AuraSkills power
19
+ level, etc — so you don't need to know ahead of time which plugins
20
+ are installed.
21
+
22
+ Use :meth:`list` to discover available source IDs, then pass one to
23
+ :meth:`get_top`. Sources that are registered but currently
24
+ unavailable (e.g. the backing plugin isn't loaded) raise
25
+ ``NoxAeApiError`` with the source's own unavailable status (424 for
26
+ economy currencies, 503 for mcMMO/AuraSkills) when you call
27
+ :meth:`get_top`.
28
+ """
29
+
30
+ def __init__(self, http: HttpEngine) -> None:
31
+ self._http = http
32
+
33
+ def list(self) -> List[LeaderboardSourceInfo]:
34
+ """List every registered leaderboard source and its availability."""
35
+ return self._http.request("GET", "leaderboards")
36
+
37
+ def get_top(self, id: str, limit: Optional[int] = None) -> List[LeaderboardEntry]:
38
+ """Get ranked entries for one leaderboard source (see :meth:`list` for valid IDs)."""
39
+ return self._http.request("GET", f"leaderboards/{_q(id)}/top", query={"limit": limit})
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import Dict, List
5
+
6
+ from ..http_engine import HttpEngine
7
+ from ..types import GroupInfo, PermissionNode
8
+
9
+
10
+ def _q(value: str) -> str:
11
+ return urllib.parse.quote(value, safe="")
12
+
13
+
14
+ class LuckPermsModule:
15
+ """Wraps the ``/v1/luckperms/*`` routes.
16
+
17
+ These only exist on the server when the LuckPerms mod is loaded —
18
+ calling any method here against a server without it will fail
19
+ (typically a 404). There's no separate "is this available" flag from
20
+ the SDK's side; check ``client.plugins.list()`` for LuckPerms if you
21
+ need to branch on it ahead of time.
22
+
23
+ Unlike most other modules, these POST/DELETE bodies are sent as real
24
+ JSON (the server reads them with ``ctx.bodyAsClass(...)``, not
25
+ ``ctx.formParam(...)``) — none of these calls pass ``form=True``.
26
+ """
27
+
28
+ def __init__(self, http: HttpEngine) -> None:
29
+ self._http = http
30
+
31
+ def get_player_groups(self, uuid: str) -> List[str]:
32
+ """Get the groups a player belongs to."""
33
+ return self._http.request("GET", f"luckperms/player/{_q(uuid)}/groups")
34
+
35
+ def get_player_permissions(self, uuid: str) -> List[PermissionNode]:
36
+ """Get a player's effective permission nodes."""
37
+ return self._http.request("GET", f"luckperms/player/{_q(uuid)}/permissions")
38
+
39
+ def add_player_permission(self, uuid: str, permission: str, value: bool = True) -> None:
40
+ """Add a permission node to a player."""
41
+ return self._http.request(
42
+ "POST", f"luckperms/player/{_q(uuid)}/permission", body={"permission": permission, "value": value}
43
+ )
44
+
45
+ def check_player_permission(self, uuid: str, permission: str) -> Dict[str, object]:
46
+ """Check whether a player has a given permission."""
47
+ return self._http.request(
48
+ "POST", f"luckperms/player/{_q(uuid)}/check-permission", body={"permission": permission}
49
+ )
50
+
51
+ def remove_player_permission(self, uuid: str, permission: str) -> None:
52
+ """Remove a permission node from a player."""
53
+ return self._http.request(
54
+ "DELETE", f"luckperms/player/{_q(uuid)}/permission", body={"permission": permission}
55
+ )
56
+
57
+ def set_player_group(self, uuid: str, group: str) -> None:
58
+ """Set a player's primary group."""
59
+ return self._http.request("POST", f"luckperms/player/{_q(uuid)}/group", body={"group": group})
60
+
61
+ def remove_player_group(self, uuid: str, group_name: str) -> None:
62
+ """Remove a group from a player."""
63
+ return self._http.request("DELETE", f"luckperms/player/{_q(uuid)}/group/{_q(group_name)}")
64
+
65
+ def get_groups(self) -> List[str]:
66
+ """List all known groups."""
67
+ return self._http.request("GET", "luckperms/groups")
68
+
69
+ def create_group(self, name: str) -> Dict[str, str]:
70
+ """Create a new LuckPerms group. The name is lowercased server-side.
71
+
72
+ Raises ``NoxAeApiError`` with a 409 status if the group already exists.
73
+ """
74
+ return self._http.request("POST", "luckperms/groups", body={"name": name})
75
+
76
+ def delete_group(self, name: str) -> None:
77
+ """Delete a LuckPerms group by name.
78
+
79
+ The ``default`` group can't be deleted (400 — every user without
80
+ an explicit group inherits from it) and a 404 is raised if it
81
+ doesn't exist.
82
+ """
83
+ return self._http.request("DELETE", f"luckperms/group/{_q(name)}")
84
+
85
+ def get_group_permissions(self, name: str) -> GroupInfo:
86
+ """Get the permissions attached to a specific group."""
87
+ return self._http.request("GET", f"luckperms/group/{_q(name)}/permissions")
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from ..http_engine import HttpEngine
6
+ from ..types import Advancement
7
+
8
+
9
+ class AdvancementsModule:
10
+ def __init__(self, http: HttpEngine) -> None:
11
+ self._http = http
12
+
13
+ def list(self) -> List[Advancement]:
14
+ """List all advancements known to the server."""
15
+ return self._http.request("GET", "advancements")
16
+
17
+
18
+ class PlaceholdersModule:
19
+ def __init__(self, http: HttpEngine) -> None:
20
+ self._http = http
21
+
22
+ def replace(self, uuid: str, message: str) -> str:
23
+ """Replace PlaceholderAPI-style placeholders (e.g. "%player_name%") in ``message`` for a player.
24
+
25
+ Server-side this is ``PAPIApi.replacePlaceholders``, which reads
26
+ form fields ``message`` and ``uuid`` — the field is literally
27
+ named ``message``, not ``text``.
28
+ """
29
+ return self._http.request("POST", "placeholders/replace", body={"uuid": uuid, "message": message}, form=True)
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import Any, Dict, Optional
5
+
6
+ from ..http_engine import HttpEngine, Method, QueryValue
7
+ from ..types import (
8
+ NetworkBroadcastResponse,
9
+ NetworkFindPlayerResponse,
10
+ NetworkHealthResponse,
11
+ NetworkPlayersResponse,
12
+ NetworkServerStatus,
13
+ )
14
+
15
+
16
+ def _q(value: str) -> str:
17
+ return urllib.parse.quote(value, safe="")
18
+
19
+
20
+ class NetworkModule:
21
+ """Wraps the ``/v1/network/*`` routes.
22
+
23
+ These only exist when ``network.enabled: true`` is set in the
24
+ server's config with at least one backend server configured —
25
+ calling any method here against a server without the network
26
+ aggregator enabled will 404.
27
+
28
+ The aggregator fans requests out to every configured backend server
29
+ (each with its own base URL + key) and merges the results, so a
30
+ single call here can reflect the state of an entire network rather
31
+ than just the server you connected to.
32
+ """
33
+
34
+ def __init__(self, http: HttpEngine) -> None:
35
+ self._http = http
36
+
37
+ def status_all(self) -> Dict[str, list]:
38
+ """Get status (server info + online players) for every configured network server."""
39
+ return self._http.request("GET", "network/status")
40
+
41
+ def status_by_id(self, id: str) -> NetworkServerStatus:
42
+ """Get status for a single network server by its configured ID."""
43
+ return self._http.request("GET", f"network/status/{_q(id)}")
44
+
45
+ def players(self) -> NetworkPlayersResponse:
46
+ """Aggregate online players across every network server."""
47
+ return self._http.request("GET", "network/players")
48
+
49
+ def find_player(self, uuid: str) -> NetworkFindPlayerResponse:
50
+ """Find which network server a player is currently on by UUID."""
51
+ return self._http.request("GET", f"network/players/{_q(uuid)}")
52
+
53
+ def health(self) -> NetworkHealthResponse:
54
+ """Get aggregate health (TPS/memory) from every network server."""
55
+ return self._http.request("GET", "network/health")
56
+
57
+ def broadcast(self, message: str) -> NetworkBroadcastResponse:
58
+ """Broadcast a message to every server on the network.
59
+
60
+ Returns a map of server ID -> "success" | "error" (per-server
61
+ delivery result; a network-wide failure is only raised for a
62
+ malformed request).
63
+ """
64
+ return self._http.request("POST", "network/broadcast", body={"message": message}, form=True)
65
+
66
+ def forward(
67
+ self,
68
+ id: str,
69
+ method: Method,
70
+ path: str,
71
+ *,
72
+ body: Any = None,
73
+ query: Optional[Dict[str, QueryValue]] = None,
74
+ form: bool = False,
75
+ ) -> Any:
76
+ """Forward an arbitrary request to a specific network server's own REST API.
77
+
78
+ E.g. ``network.forward("survival", "POST", "server/exec",
79
+ body={"command": "say hi"}, form=True)`` reaches that server's
80
+ ``POST /v1/server/exec`` directly. Useful for endpoints the
81
+ aggregator doesn't have a dedicated method for (economy, worlds,
82
+ etc) on a specific server without instantiating a second client
83
+ pointed at it.
84
+
85
+ Note the target server responds according to its own route's
86
+ expected encoding (form vs JSON) — pass ``form=True`` the same
87
+ way you would for a direct call to that endpoint.
88
+ """
89
+ return self._http.request(
90
+ method, f"network/{_q(id)}/{path.lstrip('/')}", body=body, query=query, form=form
91
+ )
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import Any, Dict, Optional
5
+
6
+ from ..http_engine import HttpEngine, Method, QueryValue
7
+ from ..types import (
8
+ NetworkHubBroadcastResponse,
9
+ NetworkHubFindPlayerResponse,
10
+ NetworkHubNode,
11
+ NetworkHubPlayersResponse,
12
+ NetworkHubStatusResponse,
13
+ )
14
+
15
+
16
+ def _q(value: str) -> str:
17
+ return urllib.parse.quote(value, safe="")
18
+
19
+
20
+ class NetworkHubModule:
21
+ """Wraps the ``/v1/network/*`` routes exposed by the **NoxAeApi-Velocity** network hub.
22
+
23
+ This is a separate plugin/process from NoxAeApi-main, run on the
24
+ Velocity proxy and listening on its own port (``NetworkHubConfig``'s
25
+ ``api-port``, distinct from any individual backend's own REST port).
26
+
27
+ Point a :class:`~noxaeapi_sdk.client.NoxAeApiNetworkHubClient` (not
28
+ the regular :class:`~noxaeapi_sdk.client.NoxAeApiClient`) at that
29
+ port to use this module. Backend Paper/Bukkit servers connect out to
30
+ the hub over WebSocket (``/network/register``) and push register /
31
+ heartbeat / player-join / player-quit events; the hub answers every
32
+ method below from its own in-memory registry, so calls here are
33
+ cheap and don't block on a live round trip to each backend the way
34
+ the older :class:`~noxaeapi_sdk.modules.network.NetworkModule`
35
+ (NoxAeApi-main's built-in aggregator) does.
36
+
37
+ Response shapes differ from ``NetworkModule`` even where the route
38
+ names match — e.g. :meth:`players` returns one flat proxy-wide
39
+ player list here, not a per-server breakdown — so the two modules'
40
+ types aren't interchangeable. There's also no hub equivalent of
41
+ ``/v1/network/health``; each node's last-reported health is embedded
42
+ in :meth:`status_all`/:meth:`status_by_id`'s ``health`` field
43
+ instead.
44
+ """
45
+
46
+ def __init__(self, http: HttpEngine) -> None:
47
+ self._http = http
48
+
49
+ def status_all(self) -> NetworkHubStatusResponse:
50
+ """Get last-known status (from the registry) for every backend node that has ever registered."""
51
+ return self._http.request("GET", "network/status")
52
+
53
+ def status_by_id(self, id: str) -> NetworkHubNode:
54
+ """Get last-known status for a single node by its configured ID."""
55
+ return self._http.request("GET", f"network/status/{_q(id)}")
56
+
57
+ def players(self) -> NetworkHubPlayersResponse:
58
+ """List every player currently connected to the proxy, straight from Velocity's own registry."""
59
+ return self._http.request("GET", "network/players")
60
+
61
+ def find_player(self, uuid: str) -> NetworkHubFindPlayerResponse:
62
+ """Find which backend server a player is currently on by UUID (proxy-authoritative)."""
63
+ return self._http.request("GET", f"network/players/{_q(uuid)}")
64
+
65
+ def broadcast(self, message: str) -> NetworkHubBroadcastResponse:
66
+ """Broadcast a message directly to every player connected to the proxy.
67
+
68
+ Unlike ``NetworkModule.broadcast``, this doesn't forward to each
69
+ backend's ``/v1/chat/broadcast`` — the proxy already has every
70
+ player in hand — so it still delivers even to servers with no
71
+ REST API of their own reachable from the hub.
72
+ """
73
+ return self._http.request("POST", "network/broadcast", body={"message": message}, form=True)
74
+
75
+ def forward(
76
+ self,
77
+ id: str,
78
+ method: Method,
79
+ path: str,
80
+ *,
81
+ body: Any = None,
82
+ query: Optional[Dict[str, QueryValue]] = None,
83
+ form: bool = False,
84
+ ) -> Any:
85
+ """Forward an arbitrary request to a specific backend node's own REST API.
86
+
87
+ E.g. ``hub.forward("survival", "POST", "server/exec",
88
+ body={"command": "say hi"}, form=True)`` reaches that backend's
89
+ ``POST /v1/server/exec`` directly. Useful for endpoints the hub
90
+ doesn't have a dedicated method for (economy, worlds, etc)
91
+ without instantiating a second client pointed at that backend
92
+ directly.
93
+
94
+ The target backend responds according to its own route's
95
+ expected encoding (form vs JSON) — pass ``form=True`` the same
96
+ way you would for a direct call to that endpoint.
97
+ """
98
+ return self._http.request(
99
+ method, f"network/{_q(id)}/{path.lstrip('/')}", body=body, query=query, form=form
100
+ )
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+
5
+ from ..http_engine import HttpEngine
6
+ from ..types import NoxAuthPlayerInfo, PasswordCheckResult
7
+
8
+
9
+ def _q(value: str) -> str:
10
+ return urllib.parse.quote(value, safe="")
11
+
12
+
13
+ class NoxAuthModule:
14
+ """Wraps the ``/v1/noxauth/*`` routes.
15
+
16
+ These only work when ``noxauth.enabled`` is set to ``true`` in the
17
+ server's ``noxaeapi-config.yml`` and the NoxAuth plugin is installed.
18
+
19
+ ``check_password``'s body is sent as real JSON (the server parses it
20
+ with ``Gson...fromJson(ctx.body(), PasswordCheckRequest.class)``, not
21
+ ``ctx.formParam(...)``) — it does not pass ``form=True``.
22
+ """
23
+
24
+ def __init__(self, http: HttpEngine) -> None:
25
+ self._http = http
26
+
27
+ def get_player_auth(self, name: str) -> NoxAuthPlayerInfo:
28
+ """Get NoxAuth registration/auth info for a player by name."""
29
+ return self._http.request("GET", f"noxauth/player/{_q(name)}")
30
+
31
+ def check_password(self, name: str, password: str) -> PasswordCheckResult:
32
+ """Check whether a password matches a player's stored NoxAuth password."""
33
+ return self._http.request(
34
+ "POST", f"noxauth/player/{_q(name)}/check-password", body={"password": password}
35
+ )
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ from ..http_engine import HttpEngine
7
+ from ..types import Gamemode, InventoryItem, OfflinePlayer, OnlinePlayer, PlayerStats
8
+
9
+
10
+ def _q(value: str) -> str:
11
+ return urllib.parse.quote(value, safe="")
12
+
13
+
14
+ class PlayersModule:
15
+ def __init__(self, http: HttpEngine) -> None:
16
+ self._http = http
17
+
18
+ def list(self) -> List[OnlinePlayer]:
19
+ """List currently online players."""
20
+ return self._http.request("GET", "players")
21
+
22
+ def list_all(self) -> List[OfflinePlayer]:
23
+ """List all players the server has ever seen (online + offline)."""
24
+ return self._http.request("GET", "players/all")
25
+
26
+ def get(self, uuid: str) -> Union[OnlinePlayer, OfflinePlayer]:
27
+ """Get a single player by UUID (works for online or offline players)."""
28
+ return self._http.request("GET", f"players/{_q(uuid)}")
29
+
30
+ def get_inventory(self, player_uuid: str, world_uuid: str) -> List[InventoryItem]:
31
+ """Get a player's inventory in a specific world."""
32
+ return self._http.request("GET", f"players/{_q(player_uuid)}/{_q(world_uuid)}/inventory")
33
+
34
+ def kick(self, uuid: str, reason: Optional[str] = None) -> None:
35
+ """Kick an online player, optionally with a reason."""
36
+ body: Optional[Dict[str, Any]] = {"reason": reason} if reason else None
37
+ return self._http.request("POST", f"players/{_q(uuid)}/kick", body=body, form=True)
38
+
39
+ def ban(self, uuid: str, reason: Optional[str] = None, expiry: Optional[str] = None) -> None:
40
+ """Ban a player, optionally with a reason and an ISO-8601 expiry (e.g. "2030-01-01T00:00:00Z").
41
+
42
+ Omit ``expiry`` for a permanent ban.
43
+ """
44
+ body: Optional[Dict[str, Any]] = {"reason": reason, "expiry": expiry} if (reason or expiry) else None
45
+ return self._http.request("POST", f"players/{_q(uuid)}/ban", body=body, form=True)
46
+
47
+ def unban(self, uuid: str) -> None:
48
+ """Remove a player's ban."""
49
+ return self._http.request("DELETE", f"players/{_q(uuid)}/ban")
50
+
51
+ def teleport(self, uuid: str, location: Dict[str, Any]) -> None:
52
+ """Teleport a player to a location, e.g. ``{"x": 0, "y": 64, "z": 0, "world": "world"}``."""
53
+ return self._http.request("POST", f"players/{_q(uuid)}/teleport", body=location, form=True)
54
+
55
+ def set_gamemode(self, uuid: str, gamemode: Gamemode) -> None:
56
+ """Change a player's gamemode."""
57
+ return self._http.request(
58
+ "PUT", f"players/{_q(uuid)}/gamemode", body={"gamemode": gamemode}, form=True
59
+ )
60
+
61
+ def get_stats(self, uuid: str) -> PlayerStats:
62
+ """Get kill/death/playtime/block stats for a player."""
63
+ return self._http.request("GET", f"players/{_q(uuid)}/stats")
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import List
5
+
6
+ from ..http_engine import HttpEngine
7
+ from ..types import Plugin
8
+
9
+
10
+ def _q(value: str) -> str:
11
+ return urllib.parse.quote(value, safe="")
12
+
13
+
14
+ class PluginsModule:
15
+ def __init__(self, http: HttpEngine) -> None:
16
+ self._http = http
17
+
18
+ def list(self) -> List[Plugin]:
19
+ """List all installed plugins/mods."""
20
+ return self._http.request("GET", "plugins")
21
+
22
+ def install(self, download_url: str) -> None:
23
+ """Install a plugin by downloading it from a direct URL.
24
+
25
+ This is a privileged endpoint — requires a write-enabled API key.
26
+ Server-side (``PluginApi.installPlugin``) reads the form field
27
+ ``downloadUrl``, not ``source``.
28
+ """
29
+ return self._http.request("POST", "plugins", body={"downloadUrl": download_url}, form=True)
30
+
31
+ def enable(self, name: str) -> None:
32
+ """Enable a plugin by name."""
33
+ return self._http.request("POST", f"plugins/{_q(name)}/enable")
34
+
35
+ def disable(self, name: str) -> None:
36
+ """Disable a plugin by name."""
37
+ return self._http.request("POST", f"plugins/{_q(name)}/disable")
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from ..http_engine import HttpEngine
7
+ from ..types import ServerInfo, WhitelistEntry
8
+
9
+
10
+ def _q(value: str) -> str:
11
+ return urllib.parse.quote(value, safe="")
12
+
13
+
14
+ class ServerModule:
15
+ def __init__(self, http: HttpEngine) -> None:
16
+ self._http = http
17
+
18
+ def ping(self) -> Dict[str, str]:
19
+ """Basic liveness check."""
20
+ return self._http.request("GET", "ping")
21
+
22
+ def info(self) -> ServerInfo:
23
+ """Get server info: version, MOTD, TPS, health, player counts, etc."""
24
+ return self._http.request("GET", "server")
25
+
26
+ def exec(self, command: str, wait_ms: Optional[int] = None) -> str:
27
+ """Run a console command on the server, returning its console output.
28
+
29
+ This is a privileged endpoint — requires a write-enabled API key.
30
+ ``wait_ms`` is how long to wait for output before returning
31
+ (server default 500ms if omitted). The server returns the joined
32
+ output as a plain JSON string, not ``{"lines": [...]}``.
33
+ """
34
+ return self._http.request("POST", "server/exec", body={"command": command, "time": wait_ms}, form=True)
35
+
36
+ def get_ops(self) -> List[WhitelistEntry]:
37
+ """List server operators."""
38
+ return self._http.request("GET", "server/ops")
39
+
40
+ def op_player(self, uuid: str) -> None:
41
+ """Grant operator status to a player.
42
+
43
+ Server-side (``ServerApi.opPlayer``) reads the form field
44
+ ``playerUuid``, not ``uuid`` — the field name matters here.
45
+ """
46
+ return self._http.request("POST", "server/ops", body={"playerUuid": uuid}, form=True)
47
+
48
+ def deop_player(self, uuid: str) -> None:
49
+ """Revoke operator status from a player.
50
+
51
+ Server-side (``ServerApi.deopPlayer``) reads this from the query
52
+ string (``playerUuid``), not the request body.
53
+ """
54
+ return self._http.request("DELETE", "server/ops", query={"playerUuid": uuid})
55
+
56
+ def get_whitelist(self) -> List[WhitelistEntry]:
57
+ """Get the current whitelist."""
58
+ return self._http.request("GET", "server/whitelist")
59
+
60
+ def add_to_whitelist(self, uuid: str, name: Optional[str] = None) -> None:
61
+ """Add a player to the whitelist."""
62
+ return self._http.request("POST", "server/whitelist", body={"uuid": uuid, "name": name}, form=True)
63
+
64
+ def remove_from_whitelist(self, uuid: str) -> None:
65
+ """Remove a player from the whitelist.
66
+
67
+ Server-side (``ServerApi.whitelistDelete``) reads ``uuid``/``name``
68
+ from the query string, not the request body.
69
+ """
70
+ return self._http.request("DELETE", "server/whitelist", query={"uuid": uuid})
71
+
72
+ def restart(self) -> None:
73
+ """Restart the server. This is a privileged endpoint — requires a write-enabled API key."""
74
+ return self._http.request("POST", "server/restart")
75
+
76
+ def get_logs(self, lines: Optional[int] = None) -> Dict[str, List[str]]:
77
+ """Tail the server console log."""
78
+ return self._http.request("GET", "server/logs", query={"lines": lines})
79
+
80
+ def get_entities(self, world: Optional[str] = None) -> Dict[str, Any]:
81
+ """Get entity counts on the server, optionally scoped to a world."""
82
+ return self._http.request("GET", "server/entities", query={"world": world})
83
+
84
+ def get_chunks(self, world: Optional[str] = None) -> Dict[str, Any]:
85
+ """Get loaded chunk counts, optionally scoped to a world."""
86
+ return self._http.request("GET", "server/chunks", query={"world": world})
87
+
88
+ def ban_ip(self, ip: str, reason: Optional[str] = None) -> None:
89
+ """Ban an IP address."""
90
+ return self._http.request("POST", "server/ban-ip", body={"ip": ip, "reason": reason}, form=True)
91
+
92
+ def get_objective(self, name: str) -> Any:
93
+ """Get a scoreboard objective's scores by objective name."""
94
+ return self._http.request("GET", f"scoreboard/{_q(name)}")
95
+
96
+ def get_scoreboard(self) -> Any:
97
+ """List all scoreboard objectives and tracked entries."""
98
+ return self._http.request("GET", "scoreboard")
99
+
100
+ def set_score(self, objective: str, entry: str, value: int) -> None:
101
+ """Set a score for an entry on an objective."""
102
+ return self._http.request(
103
+ "POST", f"scoreboard/{_q(objective)}/score", body={"entry": entry, "value": value}, form=True
104
+ )
105
+
106
+ def reset_score(self, objective: str, entry: str) -> None:
107
+ """Reset (remove) a score for an entry on an objective.
108
+
109
+ Server-side (``ServerApi.resetScore``) reads ``entry`` from the
110
+ query string, not the request body.
111
+ """
112
+ return self._http.request("DELETE", f"scoreboard/{_q(objective)}/score", query={"entry": entry})
113
+
114
+ def broadcast(self, message: str) -> None:
115
+ """Broadcast a message to every player on the server."""
116
+ return self._http.request("POST", "chat/broadcast", body={"message": message}, form=True)
117
+
118
+ def tell(self, uuid: str, message: str) -> None:
119
+ """Send a private message to a specific player.
120
+
121
+ Server-side (``ServerApi.tellPost``) reads the form field
122
+ ``playerUuid``, not ``uuid``.
123
+ """
124
+ return self._http.request(
125
+ "POST", "chat/tell", body={"playerUuid": uuid, "message": message}, form=True
126
+ )
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+
5
+ from ..http_engine import HttpEngine
6
+ from ..types import SkillInfo
7
+
8
+
9
+ def _q(value: str) -> str:
10
+ return urllib.parse.quote(value, safe="")
11
+
12
+
13
+ class SkillsModule:
14
+ """Wraps the ``/v1/skills/*`` routes (mcMMO / AuraSkills)."""
15
+
16
+ def __init__(self, http: HttpEngine) -> None:
17
+ self._http = http
18
+
19
+ def get_mcmmo_skills(self, uuid: str) -> SkillInfo:
20
+ """Get mcMMO skill levels and power level for a player.
21
+
22
+ Only works for **online** players — mcMMO's public ExperienceAPI
23
+ has no offline lookup, so this raises ``NoxAeApiNotFoundError`` if
24
+ the player isn't currently connected, and ``NoxAeApiServerError``
25
+ (503) if mcMMO isn't loaded on the target server.
26
+ """
27
+ return self._http.request("GET", f"skills/mcmmo/player/{_q(uuid)}")
28
+
29
+ def get_aura_skills(self, uuid: str) -> SkillInfo:
30
+ """Get AuraSkills skill levels and power level for a player.
31
+
32
+ Works for offline players. Raises ``NoxAeApiServerError`` (503)
33
+ if AuraSkills isn't loaded on the target server.
34
+ """
35
+ return self._http.request("GET", f"skills/auraskills/player/{_q(uuid)}")