diff --git a/.gitignore b/.gitignore index 872c981..c2b2915 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,7 @@ dmypy.json .pyre/ .idea + +logs/* +Lavalink.jar + diff --git a/interactions/ext/lavalink/base.py b/interactions/ext/lavalink/base.py index 1a24b5b..6d6c81c 100644 --- a/interactions/ext/lavalink/base.py +++ b/interactions/ext/lavalink/base.py @@ -2,7 +2,7 @@ from interactions.ext.version import Version, VersionAuthor __all__ = ["version", "base"] -__version__ = "0.1.0" +__version__ = "0.1.1" version = Version( version=__version__, author=VersionAuthor(name="Damego", email="damego.dev@gmail.com") diff --git a/interactions/ext/lavalink/client.py b/interactions/ext/lavalink/client.py index bf896b2..43355ed 100644 --- a/interactions/ext/lavalink/client.py +++ b/interactions/ext/lavalink/client.py @@ -3,7 +3,7 @@ from lavalink import Client as LavalinkClient -from interactions import Client, LibraryException, Snowflake +from interactions import Client, Snowflake from .models import VoiceState from .player import Player @@ -16,8 +16,8 @@ class VoiceClient(Client): def __init__(self, token: str, **kwargs): super().__init__(token, **kwargs) - self._websocket = VoiceWebSocketClient(token, self._intents) - self.lavalink_client = LavalinkClient(int(self.me.id), player=Player) + self._websocket = VoiceWebSocketClient(self, token, self._intents) + self.lavalink_client: LavalinkClient = None self._websocket._dispatch.register( self.__raw_voice_state_update, "on_raw_voice_state_update" @@ -26,8 +26,13 @@ def __init__(self, token: str, **kwargs): self.__raw_voice_server_update, "on_raw_voice_server_update" ) - self._websocket._http._bot_var = self + async def _login(self) -> None: self._http._bot_var = self + self.lavalink_client = LavalinkClient(int(self.me.id), player=Player) + + self.__register_lavalink_listeners() + + await super()._login() async def __raw_voice_state_update(self, data: dict): lavalink_data = {"t": "VOICE_STATE_UPDATE", "d": data} @@ -47,24 +52,19 @@ async def connect( """ Connects to voice channel and creates player. - :param guild_id: The guild id to connect. - :type guild_id: Union[Snowflake, int, str] - :param channel_id: The channel id to connect. - :type channel_id: Union[Snowflake, int, str] - :param self_deaf: Whether bot is self deafened - :type self_deaf: bool - :param self_mute: Whether bot is self muted - :type self_mute: bool + :param Union[Snowflake, int, str] guild_id: The guild id to connect. + :param Union[Snowflake, int, str] channel_id: The channel id to connect. + :param bool self_deaf: Whether bot is self deafened + :param bool self_mute: Whether bot is self muted :return: Created guild player. :rtype: Player """ - # Discord will fire INVALID_SESSION if channel_id is None if guild_id is None: - raise LibraryException(message="Missed requirement argument: guild_id") + raise TypeError("guild_id cannot be NoneType") if channel_id is None: - raise LibraryException(message="Missed requirement argument: channel_id") + raise TypeError("channel_id cannot be NoneType for connect method") - await self._websocket.connect_voice_channel(guild_id, channel_id, self_deaf, self_mute) + await self._websocket.update_voice_state(guild_id, channel_id, self_deaf, self_mute) player = self.lavalink_client.player_manager.get(int(guild_id)) if player is None: player = self.lavalink_client.player_manager.create(int(guild_id)) @@ -72,17 +72,16 @@ async def connect( async def disconnect(self, guild_id: Union[Snowflake, int]): if guild_id is None: - raise LibraryException(message="Missed requirement argument: guild_id") + raise TypeError("guild_id cannot be NoneType") - await self._websocket.disconnect_voice_channel(int(guild_id)) + await self._websocket.update_voice_state(int(guild_id)) await self.lavalink_client.player_manager.destroy(int(guild_id)) def get_player(self, guild_id: Union[Snowflake, int]) -> Player: """ Returns current player in guild. - :param guild_id: The guild id - :type guild_id: Union[Snowflake, int] + :param Union[Snowflake, int] guild_id: The guild id :return: Guild player :rtype: Player """ @@ -97,8 +96,7 @@ def get_user_voice_state(self, user_id: Union[Snowflake, int]) -> Optional[Voice """ Returns user voice state. - :param user_id: The user id - :type user_id: Union[Snowflake, int] + :param Union[Snowflake, int] user_id: The user id :return: Founded user voice state else nothing :rtype: Optional[VoiceState] """ @@ -110,8 +108,7 @@ def get_guild_voice_states(self, guild_id: Union[Snowflake, int]) -> Optional[Li """ Returns guild voice states. - :param guild_id: The channel id - :type guild_id: Union[Snowflake, int] + :param Union[Snowflake, int] guild_id: The channel id :return: Founded channel voice states else nothing :rtype: Optional[List[VoiceState]] """ @@ -129,8 +126,7 @@ def get_channel_voice_states( """ Returns channel voice states. - :param channel_id: The channel id - :type channel_id: Union[Snowflake, int] + :param Union[Snowflake, int] channel_id: The channel id :return: Founded channel voice states else nothing :rtype: Optional[List[VoiceState]] """ @@ -145,16 +141,14 @@ def get_channel_voice_states( def __register_lavalink_listeners(self): for extension in self._extensions.values(): for name, func in getmembers(extension): - if hasattr(func, "__lavalink__"): - name = func.__lavalink__[3:] - event_name = "".join(word.capitalize() for word in name.split("_")) + "Event" - if event_name not in self.lavalink_client._event_hooks: - self.lavalink_client._event_hooks[event_name] = [] - self.lavalink_client._event_hooks[event_name].append(func) - - async def _ready(self) -> None: - self.__register_lavalink_listeners() - await super()._ready() + if not hasattr(func, "__lavalink__"): + continue + name = func.__lavalink__[3:] + event_name = "".join(word.capitalize() for word in name.split("_")) + "Event" + event_hooks = self.lavalink_client._event_hooks + if event_name not in event_hooks: + event_hooks[event_name] = [] + event_hooks[event_name].append(func) def listener(func=None, *, name: str = None): diff --git a/interactions/ext/lavalink/models.py b/interactions/ext/lavalink/models.py index 69f28b1..4a890bc 100644 --- a/interactions/ext/lavalink/models.py +++ b/interactions/ext/lavalink/models.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Optional -from interactions.api.models.attrs_utils import ClientSerializerMixin, define, field +from interactions.utils.attrs_utils import ClientSerializerMixin, define, field from interactions import Channel, Guild, LibraryException, Member, Snowflake @@ -20,37 +20,19 @@ class VoiceState(ClientSerializerMixin): This class creates an object every time the event ``VOICE_STATE_UPDATE`` is received from the discord API. It contains information about the user's update voice information. - Attributes: - ----------- - _json : dict - All data of the object stored as dictionary - member : Member - The member whose VoiceState was updated - user_id : int - The id of the user whose VoiceState was updated. This is technically the same as the "member id", - but it is called `user_id` because of API terminology. - suppress : bool - Whether the user is muted by the current user(-> bot) - session_id : int - The id of the session - self_video : bool - Whether the user's camera is enabled. - self_mute : bool - Whether the user is muted by themselves - self_deaf : bool - Whether the user is deafened by themselves - self_stream : bool - Whether the user is streaming in the current channel - request_to_speak_timestamp : datetime - Only for stage-channels; when the user requested permissions to speak in the stage channel - mute : bool - Whether the user's microphone is muted by the server - guild_id : int - The id of the guild in what the update took action - deaf : bool - Whether the user is deafened by the guild - channel_id : int - The id of the channel the update took action + :ivar Member member: The member whose VoiceState was updated + :ivar int user_id: The id of the user whose VoiceState was updated. + :ivar bool suppress: Whether the user is muted by the current user(-> bot) + :ivar int session_id: The id of the session + :ivar bool self_video: Whether the user's camera is enabled. + :ivar bool self_mute: Whether the user is muted by themselves + :ivar bool self_deaf: Whether the user is deafened by themselves + :ivar bool self_stream: Whether the user is streaming in the current channel + :ivar datetime request_to_speak_timestamp: Only for stage-channels; when the user requested permissions to speak in the stage channel + :ivar bool mute: Whether the user's microphone is muted by the server + :ivar int guild_id: The id of the guild in what the update took action + :ivar bool deaf: Whether the user is deafened by the guild + :ivar int channel_id: The id of the channel the update took action """ guild_id: Optional[Snowflake] = field(converter=Snowflake, default=None) @@ -78,7 +60,7 @@ def joined(self) -> bool: """ return self.channel_id is not None - async def mute_member(self, reason: Optional[str]) -> Member: + async def mute_member(self, reason: Optional[str] = None) -> Member: """ Mutes the current member. @@ -89,7 +71,7 @@ async def mute_member(self, reason: Optional[str]) -> Member: """ return await self.member.modify(guild_id=int(self.guild_id), mute=True, reason=reason) - async def deafen_member(self, reason: Optional[str]) -> Member: + async def deafen_member(self, reason: Optional[str] = None) -> Member: """ Deafens the current member. @@ -100,7 +82,7 @@ async def deafen_member(self, reason: Optional[str]) -> Member: """ return await self.member.modify(guild_id=int(self.guild_id), deaf=True, reason=reason) - async def move_member(self, channel_id: int, *, reason: Optional[str]) -> Member: + async def move_member(self, channel_id: int, *, reason: Optional[str] = None) -> Member: """ Moves the member to another channel. @@ -141,7 +123,7 @@ async def connect(self, self_deaf: bool = False, self_mute: bool = False) -> "Pl if not self.channel_id: raise LibraryException(message="User not connected to the voice channel!") - await self._client._bot_var._websocket.connect_voice_channel( + await self._client._bot_var._websocket.update_voice_state( self.guild_id, self.channel_id, self_deaf, self_mute ) player = self._client._bot_var.lavalink_client.player_manager.get(int(self.guild_id)) @@ -154,7 +136,10 @@ async def connect(self, self_deaf: bool = False, self_mute: bool = False) -> "Pl class VoiceServer(ClientSerializerMixin): """ A class object representing the gateway event ``VOICE_SERVER_UPDATE``. - This class creates an object every time the event ``VOICE_SERVER_UPDATE`` is received from the discord API. + + :ivar str endpoint: Voice connection token + :ivar Snowflake guild_id: Guild this voice server update is for + :ivar str token: Voice server host """ endpoint: str = field() diff --git a/interactions/ext/lavalink/websocket.py b/interactions/ext/lavalink/websocket.py index d37512e..dad4ec3 100644 --- a/interactions/ext/lavalink/websocket.py +++ b/interactions/ext/lavalink/websocket.py @@ -1,20 +1,44 @@ -from interactions import OpCodeType, Storage, WebSocketClient +from typing import TYPE_CHECKING + +from interactions import OpCodeType, Storage, WebSocketClient, HTTPClient from .models import VoiceServer, VoiceState +if TYPE_CHECKING: + from .client import VoiceClient + __all__ = ["VoiceWebSocketClient"] class VoiceWebSocketClient(WebSocketClient): - def __init__(self, *args): + def __init__(self, bot_var: "VoiceClient", *args): + self._bot_var: "VoiceClient" = bot_var super().__init__(*args) + async def run(self) -> None: + """ + Handles the client's connection with the Gateway. + """ + + if isinstance(self._http, str): + self._http = HTTPClient(self._http) + self._http._bot_var = self._bot_var + + await super().run() + def _dispatch_event(self, event: str, data: dict) -> None: + """ + Dispatches VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE events from the Gateway. + + :param str event: The name of the event. + :param dict data: The data for the event. + """ if event not in ("VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"): return super()._dispatch_event(event, data) self._dispatch.dispatch(f"on_raw_{event.lower()}", data) _cache: Storage = self._http.cache[VoiceState] + if event == "VOICE_SERVER_UPDATE": model = VoiceServer(**data, _client=self._http) self._dispatch.dispatch("on_voice_server_update", model) @@ -24,44 +48,32 @@ def _dispatch_event(self, event: str, data: dict) -> None: self._dispatch.dispatch("on_voice_state_update", old, model) _cache.add(model, model.user_id) - async def connect_voice_channel( - self, guild_id: int, channel_id: int, self_deaf: bool, self_mute: bool + async def update_voice_state( + self, + guild_id: int, + channel_id: int = None, + self_deaf: bool = None, + self_mute: bool = None, ): """ - Sends packet to websocket for connection to voice channel. - - :param guild_id: The guild id to connect. - :type guild_id: int - :param channel_id: The channel id to connect. - :type channel_id: int - :param self_deaf: Whether bot is self deafened - :type self_deaf: bool - :param self_mute: Whether bot is self muted - :type self_mute: bool + Sends VOICE_STATE packet to websocket. + + :param int guild_id: The guild id. + :param int channel_id: The channel id. + :param bool self_deaf: Whether bot is self deafened + :param bool self_mute: Whether bot is self muted """ + payload = { "op": OpCodeType.VOICE_STATE, "d": { - "channel_id": str(channel_id), "guild_id": str(guild_id), - "self_deaf": self_deaf, - "self_mute": self_mute, + "channel_id": str(channel_id) if channel_id is not None else None, }, } - - await self._send_packet(payload) - - async def disconnect_voice_channel(self, guild_id: int): - """ - Sends packet to websocket for disconnecting from voice channel. - - :param guild_id: The guild id - :type guild_id: int - - """ - payload = { - "op": OpCodeType.VOICE_STATE, - "d": {"channel_id": None, "guild_id": str(guild_id)}, - } + if self_deaf is not None: + payload["d"]["self_deaf"] = self_deaf + if self_mute is not None: + payload["d"]["self_mute"] = self_mute await self._send_packet(payload) diff --git a/requirements.txt b/requirements.txt index 18216b0..fb05259 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -discord-py-interactions>=4.3.0, <4.3.2 +discord-py-interactions>=4.3.2 lavalink~=4.0.1 diff --git a/setup.py b/setup.py index 2da0f3a..cc30fcd 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,6 @@ with open("README.md", "r", encoding="utf-8") as f: README = f.read() -# with open("requirements.txt", "r", encoding="utf-8") as f: -# requirements = f.read() - with open("interactions/ext/lavalink/base.py") as fp: VERSION = re.search('__version__ = "([^"]+)"', fp.read())[1] @@ -27,7 +24,7 @@ author_email=AUTHOR_EMAIL, description=DESCRIPTION, include_package_data=True, - install_requires=["discord-py-interactions>=4.3.0, <4.3.2", "lavalink~=4.0.1"], + install_requires=["discord-py-interactions>=4.3.2", "lavalink~=4.0.1"], license="GPL-3.0 License", long_description=README, long_description_content_type="text/markdown",