From 2175bd51c0d0c2817e69a708e507108f3bc902bd Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 30 Jun 2025 09:14:23 +0200 Subject: [PATCH] Fix voice connection issues and upgrade to voice v8 --- discord/gateway.py | 16 ++++++++++++++-- discord/voice_state.py | 40 +++++++++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index 44656df03..a2c3da3d2 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -212,6 +212,9 @@ class KeepAliveHandler(threading.Thread): class VoiceKeepAliveHandler(KeepAliveHandler): + if TYPE_CHECKING: + ws: DiscordVoiceWebSocket + def __init__(self, *args: Any, **kwargs: Any) -> None: name: str = kwargs.pop('name', f'voice-keep-alive-handler:{id(self):#x}') super().__init__(*args, name=name, **kwargs) @@ -223,7 +226,10 @@ class VoiceKeepAliveHandler(KeepAliveHandler): def get_payload(self) -> Dict[str, Any]: return { 'op': self.ws.HEARTBEAT, - 'd': int(time.time() * 1000), + 'd': { + 't': int(time.time() * 1000), + 'seq_ack': self.ws.seq_ack, + }, } def ack(self) -> None: @@ -830,6 +836,8 @@ class DiscordVoiceWebSocket: self._keep_alive: Optional[VoiceKeepAliveHandler] = None self._close_code: Optional[int] = None self.secret_key: Optional[List[int]] = None + # defaulting to -1 + self.seq_ack: int = -1 if hook: self._hook = hook # type: ignore @@ -850,6 +858,7 @@ class DiscordVoiceWebSocket: 'token': state.token, 'server_id': str(state.server_id), 'session_id': state.session_id, + 'seq_ack': self.seq_ack, }, } await self.send_as_json(payload) @@ -874,14 +883,16 @@ class DiscordVoiceWebSocket: *, resume: bool = False, hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None, + seq_ack: int = -1, ) -> Self: """Creates a voice websocket for the :class:`VoiceClient`.""" - gateway = f'wss://{state.endpoint}/?v=4' + gateway = f'wss://{state.endpoint}/?v=8' client = state.voice_client http = client._state.http socket = await http.ws_connect(gateway, compress=15) ws = cls(socket, loop=client.loop, hook=hook) ws.gateway = gateway + ws.seq_ack = seq_ack ws._connection = state ws._max_heartbeat_timeout = 60.0 ws.thread_id = threading.get_ident() @@ -934,6 +945,7 @@ class DiscordVoiceWebSocket: _log.debug('Voice websocket frame received: %s', msg) op = msg['op'] data = msg['d'] # According to Discord this key is always given + self.seq_ack = msg.get('seq', self.seq_ack) # this key could not be given if op == self.READY: await self.initial_connection(data) diff --git a/discord/voice_state.py b/discord/voice_state.py index 956f639b8..d2cc0ebc1 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -321,7 +321,7 @@ class VoiceConnectionState: ) return - self.endpoint, _, _ = endpoint.rpartition(':') + self.endpoint = endpoint if self.endpoint.startswith('wss://'): # Just in case, strip it off since we're going to add it later self.endpoint = self.endpoint[6:] @@ -574,7 +574,10 @@ class VoiceConnectionState: self._disconnected.clear() async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket: - ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook) + seq_ack = -1 + if self.ws is not MISSING: + seq_ack = self.ws.seq_ack + ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook, seq_ack=seq_ack) self.state = ConnectionFlowState.websocket_connected return ws @@ -603,15 +606,17 @@ class VoiceConnectionState: # The following close codes are undocumented so I will document them here. # 1000 - normal closure (obviously) # 4014 - we were externally disconnected (voice channel deleted, we were moved, etc) - # 4015 - voice server has crashed - if exc.code in (1000, 4015): + # 4015 - voice server has crashed, we should resume + # 4021 - rate limited, we should not reconnect + # 4022 - call terminated, similar to 4014 + if exc.code == 1000: # Don't call disconnect a second time if the websocket closed from a disconnect call if not self._expecting_disconnect: _log.info('Disconnecting from voice normally, close code %d.', exc.code) await self.disconnect() break - if exc.code == 4014: + if exc.code in (4014, 4022): # We were disconnected by discord # This condition is a race between the main ws event and the voice ws closing if self._disconnected.is_set(): @@ -631,6 +636,31 @@ class VoiceConnectionState: else: continue + if exc.code == 4021: + _log.warning('We are being ratelimited while trying to connect to voice. Disconnecting...') + if self.state is not ConnectionFlowState.disconnected: + await self.disconnect() + break + + if exc.code == 4015: + _log.info('Disconnected from voice, attempting a resume...') + try: + await self._connect( + reconnect=reconnect, + timeout=self.timeout, + self_deaf=(self.self_voice_state or self).self_deaf, + self_mute=(self.self_voice_state or self).self_mute, + resume=True, + ) + except asyncio.TimeoutError: + _log.info('Could not resume the voice connection... Disconnecting...') + if self.state is not ConnectionFlowState.disconnected: + await self.disconnect() + break + else: + _log.info('Successfully resumed voice connection') + continue + _log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason') if not reconnect: