From 2cf793d79eb3d517eb8cd652c6e6743fc0562a7b Mon Sep 17 00:00:00 2001 From: Mysty Date: Fri, 8 Dec 2023 02:38:45 +1000 Subject: [PATCH] Add public node endpoints with payloads. (#260) * Update response payload type. * Add new fetch endpoint payloads * Add public endpoints * Update docs for payloads. * Bump version * Add warning to docs. * Add some version ifo and migrating. * versionadd should be versionadded * Run black, --- docs/migrating.rst | 5 + docs/wavelink.rst | 40 +++++++ pyproject.toml | 2 +- wavelink/__init__.py | 2 +- wavelink/node.py | 146 +++++++++++++++++++++++ wavelink/payloads.py | 234 ++++++++++++++++++++++++++++++++++++- wavelink/types/response.py | 10 +- 7 files changed, 430 insertions(+), 9 deletions(-) diff --git a/docs/migrating.rst b/docs/migrating.rst index e2c7a09b..b445fe46 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -80,6 +80,11 @@ Added - :meth:`wavelink.Node.send` - :class:`wavelink.Search` - LFU (Least Frequently Used) Cache for request caching. +- :meth:`wavelink.Node.fetch_info` +- :meth:`wavelink.Node.fetch_stats` +- :meth:`wavelink.Node.fetch_version` +- :meth:`wavelink.Node.fetch_player_info` +- :meth:`wavelink.Node.fetch_players` Connecting diff --git a/docs/wavelink.rst b/docs/wavelink.rst index caca9279..15018dee 100644 --- a/docs/wavelink.rst +++ b/docs/wavelink.rst @@ -150,6 +150,46 @@ Payloads .. autoclass:: StatsEventFrames :members: +.. attributetable:: StatsResponsePayload + +.. autoclass:: StatsResponsePayload + :members: + +.. attributetable:: PlayerStatePayload + +.. autoclass:: PlayerStatePayload + :members: + +.. attributetable:: VoiceStatePayload + +.. autoclass:: VoiceStatePayload + :members: + +.. attributetable:: PlayerResponsePayload + +.. autoclass:: PlayerResponsePayload + :members: + +.. attributetable:: GitResponsePayload + +.. autoclass:: GitResponsePayload + :members: + +.. attributetable:: VersionResponsePayload + +.. autoclass:: VersionResponsePayload + :members: + +.. attributetable:: PluginResponsePayload + +.. autoclass:: PluginResponsePayload + :members: + +.. attributetable:: InfoResponsePayload + +.. autoclass:: InfoResponsePayload + :members: + Enums ----- diff --git a/pyproject.toml b/pyproject.toml index 3d6e3a1e..6531dcae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wavelink" -version = "3.0.0" +version = "3.1.0" authors = [ { name="PythonistaGuild, EvieePy", email="evieepy@gmail.com" }, ] diff --git a/wavelink/__init__.py b/wavelink/__init__.py index a92a252e..b97ebf3a 100644 --- a/wavelink/__init__.py +++ b/wavelink/__init__.py @@ -25,7 +25,7 @@ __author__ = "PythonistaGuild, EvieePy" __license__ = "MIT" __copyright__ = "Copyright 2019-Present (c) PythonistaGuild, EvieePy" -__version__ = "3.0.0" +__version__ = "3.1.0" from .enums import * diff --git a/wavelink/node.py b/wavelink/node.py index bfe79cd7..58533780 100644 --- a/wavelink/node.py +++ b/wavelink/node.py @@ -44,6 +44,7 @@ NodeException, ) from .lfu import LFUCache +from .payloads import * from .tracks import Playable, Playlist from .websocket import Websocket @@ -338,6 +339,9 @@ async def send( NodeException An error occured while making this request to Lavalink, and Lavalink was unable to send any error information. + + + .. versionadded:: 3.0.0 """ clean_path: str = path.removesuffix("/") uri: str = f"{self.uri}/{clean_path}" @@ -391,6 +395,36 @@ async def _fetch_players(self) -> list[PlayerResponse]: raise LavalinkException(data=exc_data) + async def fetch_players(self) -> list[PlayerResponsePayload]: + """Method to fetch the player information Lavalink holds for every connected player on this node. + + .. warning:: + + This payload is not the same as the :class:`wavelink.Player` class. This is the data received from + Lavalink about the players. + + + Returns + ------- + list[:class:`PlayerResponsePayload`] + A list of :class:`PlayerResponsePayload` representing each player connected to this node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: list[PlayerResponse] = await self._fetch_players() + + payload: list[PlayerResponsePayload] = [PlayerResponsePayload(p) for p in data] + return payload + async def _fetch_player(self, guild_id: int, /) -> PlayerResponse: uri: str = f"{self.uri}/v4/sessions/{self.session_id}/players/{guild_id}" @@ -408,6 +442,48 @@ async def _fetch_player(self, guild_id: int, /) -> PlayerResponse: raise LavalinkException(data=exc_data) + async def fetch_player_info(self, guild_id: int, /) -> PlayerResponsePayload | None: + """Method to fetch the player information Lavalink holds for the specific guild. + + .. warning:: + + This payload is not the same as the :class:`wavelink.Player` class. This is the data received from + Lavalink about the player. See: :meth:`~wavelink.Node.get_player` + + + Parameters + ---------- + guild_id: int + The ID of the guild you want to fetch info for. + + Returns + ------- + :class:`PlayerResponsePayload` | None + The :class:`PlayerResponsePayload` representing the player info for the guild ID connected to this node. + Could be ``None`` if no player is found with the given guild ID. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + try: + data: PlayerResponse = await self._fetch_player(guild_id) + except LavalinkException as e: + if e.status == 404: + return None + + raise e + + payload: PlayerResponsePayload = PlayerResponsePayload(data) + return payload + async def _update_player(self, guild_id: int, /, *, data: Request, replace: bool = False) -> PlayerResponse: no_replace: bool = not replace @@ -499,6 +575,30 @@ async def _fetch_info(self) -> InfoResponse: raise LavalinkException(data=exc_data) + async def fetch_info(self) -> InfoResponsePayload: + """Method to fetch this Lavalink Nodes info response data. + + Returns + ------- + :class:`InfoResponsePayload` + The :class:`InfoResponsePayload` associated with this Node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: InfoResponse = await self._fetch_info() + + payload: InfoResponsePayload = InfoResponsePayload(data) + return payload + async def _fetch_stats(self) -> StatsResponse: uri: str = f"{self.uri}/v4/stats" @@ -516,6 +616,30 @@ async def _fetch_stats(self) -> StatsResponse: raise LavalinkException(data=exc_data) + async def fetch_stats(self) -> StatsResponsePayload: + """Method to fetch this Lavalink Nodes stats response data. + + Returns + ------- + :class:`StatsResponsePayload` + The :class:`StatsResponsePayload` associated with this Node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: StatsResponse = await self._fetch_stats() + + payload: StatsResponsePayload = StatsResponsePayload(data) + return payload + async def _fetch_version(self) -> str: uri: str = f"{self.uri}/version" @@ -531,6 +655,28 @@ async def _fetch_version(self) -> str: raise LavalinkException(data=exc_data) + async def fetch_version(self) -> str: + """Method to fetch this Lavalink version string. + + Returns + ------- + str + The version string associated with this Lavalink node. + + Raises + ------ + LavalinkException + An error occurred while making this request to Lavalink. + NodeException + An error occured while making this request to Lavalink, + and Lavalink was unable to send any error information. + + + .. versionadded:: 3.1.0 + """ + data: str = await self._fetch_version() + return data + def get_player(self, guild_id: int, /) -> Player | None: """Return a :class:`~wavelink.Player` associated with the provided :attr:`discord.Guild.id`. diff --git a/wavelink/payloads.py b/wavelink/payloads.py index 272c3cda..2edc46f2 100644 --- a/wavelink/payloads.py +++ b/wavelink/payloads.py @@ -23,16 +23,20 @@ """ from __future__ import annotations +import datetime from typing import TYPE_CHECKING, cast import wavelink from .enums import DiscordVoiceCloseType +from .filters import Filters +from .tracks import Playable if TYPE_CHECKING: from .node import Node from .player import Player - from .tracks import Playable + from .types.filters import * + from .types.response import * from .types.state import PlayerState from .types.stats import CPUStats, FrameStats, MemoryStats from .types.websocket import StatsOP, TrackExceptionPayload @@ -50,6 +54,14 @@ "StatsEventMemory", "StatsEventCPU", "StatsEventFrames", + "StatsResponsePayload", + "GitResponsePayload", + "VersionResponsePayload", + "PluginResponsePayload", + "InfoResponsePayload", + "PlayerStatePayload", + "VoiceStatePayload", + "PlayerResponsePayload", ) @@ -281,8 +293,8 @@ class StatsEventPayload: See Also: :class:`wavelink.StatsEventMemory` cpu: :class:`wavelink.StatsEventCPU` See Also: :class:`wavelink.StatsEventCPU` - frames: :class:`wavelink.StatsEventFrames` - See Also: :class:`wavelink.StatsEventFrames` + frames: :class:`wavelink.StatsEventFrames` | None + See Also: :class:`wavelink.StatsEventFrames`. This could be ``None``. """ def __init__(self, data: StatsOP) -> None: @@ -294,5 +306,217 @@ def __init__(self, data: StatsOP) -> None: self.cpu: StatsEventCPU = StatsEventCPU(data=data["cpu"]) self.frames: StatsEventFrames | None = None - if data["frameStats"]: - self.frames = StatsEventFrames(data=data["frameStats"]) + if frames := data.get("frameStats", None): + self.frames = StatsEventFrames(frames) + + +class StatsResponsePayload: + """Payload received when using :meth:`~wavelink.Node.fetch_stats` + + Attributes + ---------- + players: int + The amount of players connected to the node (Lavalink). + playing: int + The amount of players playing a track. + uptime: int + The uptime of the node in milliseconds. + memory: :class:`wavelink.StatsEventMemory` + See Also: :class:`wavelink.StatsEventMemory` + cpu: :class:`wavelink.StatsEventCPU` + See Also: :class:`wavelink.StatsEventCPU` + frames: :class:`wavelink.StatsEventFrames` | None + See Also: :class:`wavelink.StatsEventFrames`. This could be ``None``. + """ + + def __init__(self, data: StatsResponse) -> None: + self.players: int = data["players"] + self.playing: int = data["playingPlayers"] + self.uptime: int = data["uptime"] + + self.memory: StatsEventMemory = StatsEventMemory(data=data["memory"]) + self.cpu: StatsEventCPU = StatsEventCPU(data=data["cpu"]) + self.frames: StatsEventFrames | None = None + + if frames := data.get("frameStats", None): + self.frames = StatsEventFrames(frames) + + +class PlayerStatePayload: + """Represents the PlayerState information received via :meth:`~wavelink.Node.fetch_player_info` or + :meth:`~wavelink.Node.fetch_players` + + Attributes + ---------- + time: int + Unix timestamp in milliseconds received from Lavalink. + position: int + The position of the track in milliseconds received from Lavalink. + connected: bool + Whether Lavalink is connected to the voice gateway. + ping: int + The ping of the node to the Discord voice server in milliseconds (-1 if not connected). + """ + + def __init__(self, data: PlayerState) -> None: + self.time: int = data["time"] + self.position: int = data["position"] + self.connected: bool = data["connected"] + self.ping: int = data["ping"] + + +class VoiceStatePayload: + """Represents the VoiceState information received via :meth:`~wavelink.Node.fetch_player_info` or + :meth:`~wavelink.Node.fetch_players`. This is the voice state information received via Discord and sent to your + Lavalink node. + + Attributes + ---------- + token: str | None + The Discord voice token authenticated with. This is not the same as your bots token. Could be ``None``. + endpoint: str | None + The Discord voice endpoint connected to. Could be ``None``. + session_id: str | None + The Discord voice session ID autheticated with. Could be ``None``. + """ + + def __init__(self, data: VoiceStateResponse) -> None: + self.token: str | None = data.get("token") + self.endpoint: str | None = data.get("endpoint") + self.session_id: str | None = data.get("sessionId") + + +class PlayerResponsePayload: + """Payload received when using :meth:`~wavelink.Node.fetch_player_info` or :meth:`~wavelink.Node.fetch_players` + + Attributes + ---------- + guild_id: int + The guild ID as an int that this player is connected to. + track: :class:`wavelink.Playable` | None + The current track playing on Lavalink. Could be ``None`` if no track is playing. + volume: int + The current volume of the player. + paused: bool + A bool indicating whether the player is paused. + state: :class:`wavelink.PlayerStatePayload` + The current state of the player. See: :class:`wavelink.PlayerStatePayload`. + voice_state: :class:`wavelink.VoiceStatePayload` + The voice state infomration received via Discord and sent to Lavalink. See: :class:`wavelink.VoiceStatePayload`. + filters: :class:`wavelink.Filters` + The :class:`wavelink.Filters` currently associated with this player. + """ + + def __init__(self, data: PlayerResponse) -> None: + self.guild_id: int = int(data["guildId"]) + self.track: Playable | None = None + + if track := data.get("track"): + self.track = Playable(track) + + self.volume: int = data["volume"] + self.paused: bool = data["paused"] + self.state: PlayerStatePayload = PlayerStatePayload(data["state"]) + self.voice_state: VoiceStatePayload = VoiceStatePayload(data["voice"]) + self.filters: Filters = Filters(data=data["filters"]) + + +class GitResponsePayload: + """Represents Git information received via :meth:`wavelink.Node.fetch_info` + + Attributes + ---------- + branch: str + The branch this Lavalink server was built on. + commit: str + The commit this Lavalink server was built on. + commit_time: :class:`datetime.datetime` + The timestamp for when the commit was created. + """ + + def __init__(self, data: GitPayload) -> None: + self.branch: str = data["branch"] + self.commit: str = data["commit"] + self.commit_time: datetime.datetime = datetime.datetime.fromtimestamp( + data["commitTime"] / 1000, tz=datetime.timezone.utc + ) + + +class VersionResponsePayload: + """Represents Version information received via :meth:`wavelink.Node.fetch_info` + + Attributes + ---------- + semver: str + The full version string of this Lavalink server. + major: int + The major version of this Lavalink server. + minor: int + The minor version of this Lavalink server. + patch: int + The patch version of this Lavalink server. + pre_release: str + The pre-release version according to semver as a ``.`` separated list of identifiers. + build: str | None + The build metadata according to semver as a ``.`` separated list of identifiers. Could be ``None``. + """ + + def __init__(self, data: VersionPayload) -> None: + self.semver: str = data["semver"] + self.major: int = data["major"] + self.minor: int = data["minor"] + self.patch: int = data["patch"] + self.pre_release: str | None = data.get("preRelease") + self.build: str | None = data.get("build") + + +class PluginResponsePayload: + """Represents Plugin information received via :meth:`wavelink.Node.fetch_info` + + Attributes + ---------- + name: str + The plugin name. + version: str + The plugin version. + """ + + def __init__(self, data: PluginPayload) -> None: + self.name: str = data["name"] + self.version: str = data["version"] + + +class InfoResponsePayload: + """Payload received when using :meth:`~wavelink.Node.fetch_info` + + Attributes + ---------- + version: :class:`VersionResponsePayload` + The version info payload for this Lavalink node in the :class:`VersionResponsePayload` object. + build_time: :class:`datetime.datetime` + The timestamp when this Lavalink jar was built. + git: :class:`GitResponsePayload` + The git info payload for this Lavalink node in the :class:`GitResponsePayload` object. + jvm: str + The JVM version this Lavalink node runs on. + lavaplayer: str + The Lavaplayer version being used by this Lavalink node. + source_managers: list[str] + The enabled source managers for this node. + filters: list[str] + The enabled filters for this node. + plugins: list[:class:`PluginResponsePayload`] + The enabled plugins for this node. + """ + + def __init__(self, data: InfoResponse) -> None: + self.version: VersionResponsePayload = VersionResponsePayload(data["version"]) + self.build_time: datetime.datetime = datetime.datetime.fromtimestamp( + data["buildTime"] / 1000, tz=datetime.timezone.utc + ) + self.git: GitResponsePayload = GitResponsePayload(data["git"]) + self.jvm: str = data["jvm"] + self.lavaplayer: str = data["lavaplayer"] + self.source_managers: list[str] = data["sourceManagers"] + self.filters: list[str] = data["filters"] + self.plugins: list[PluginResponsePayload] = [PluginResponsePayload(p) for p in data["plugins"]] diff --git a/wavelink/types/response.py b/wavelink/types/response.py index a2e73ba6..4ef88e21 100644 --- a/wavelink/types/response.py +++ b/wavelink/types/response.py @@ -27,7 +27,7 @@ from typing_extensions import Never, NotRequired from .filters import FilterPayload - from .state import PlayerState, VoiceState + from .state import PlayerState from .stats import CPUStats, FrameStats, MemoryStats from .tracks import PlaylistPayload, TrackPayload @@ -47,13 +47,19 @@ class LoadedErrorPayload(TypedDict): cause: str +class VoiceStateResponse(TypedDict, total=False): + token: str + endpoint: str | None + sessionId: str + + class PlayerResponse(TypedDict): guildId: str track: NotRequired[TrackPayload] volume: int paused: bool state: PlayerState - voice: VoiceState + voice: VoiceStateResponse filters: FilterPayload