Browse Source

Fix disconnect when trying to move to another voice channel.

Not overly proud of this implementation but this allows the library
to differentiate between a 4014 that means "move to another channel" or
"move nowhere". Sometimes the VOICE_STATE_UPDATE comes before the
actual websocket disconnect so special care had to be taken in that
case.

Fix #5904
v1.5.x
Rapptz 5 years ago
parent
commit
21ed9f61d2
  1. 3
      discord/gateway.py
  2. 79
      discord/voice_client.py

3
discord/gateway.py

@ -719,6 +719,7 @@ class DiscordVoiceWebSocket:
self.loop = loop self.loop = loop
self._keep_alive = None self._keep_alive = None
self._close_code = None self._close_code = None
self.secret_key = None
async def send_as_json(self, data): async def send_as_json(self, data):
log.debug('Sending voice websocket frame: %s.', data) log.debug('Sending voice websocket frame: %s.', data)
@ -872,7 +873,7 @@ class DiscordVoiceWebSocket:
async def load_secret_key(self, data): async def load_secret_key(self, data):
log.info('received secret key for voice connection') log.info('received secret key for voice connection')
self._connection.secret_key = data.get('secret_key') self.secret_key = self._connection.secret_key = data.get('secret_key')
await self.speak() await self.speak()
await self.speak(False) await self.speak(False)

79
discord/voice_client.py

@ -208,6 +208,7 @@ class VoiceClient(VoiceProtocol):
self._connected = threading.Event() self._connected = threading.Event()
self._handshaking = False self._handshaking = False
self._potentially_reconnecting = False
self._voice_state_complete = asyncio.Event() self._voice_state_complete = asyncio.Event()
self._voice_server_complete = asyncio.Event() self._voice_server_complete = asyncio.Event()
@ -250,8 +251,10 @@ class VoiceClient(VoiceProtocol):
self.session_id = data['session_id'] self.session_id = data['session_id']
channel_id = data['channel_id'] channel_id = data['channel_id']
if not self._handshaking: if not self._handshaking or self._potentially_reconnecting:
# If we're done handshaking then we just need to update ourselves # If we're done handshaking then we just need to update ourselves
# If we're potentially reconnecting due to a 4014, then we need to differentiate
# a channel move and an actual force disconnect
if channel_id is None: if channel_id is None:
# We're being disconnected so cleanup # We're being disconnected so cleanup
await self.disconnect() await self.disconnect()
@ -294,26 +297,39 @@ class VoiceClient(VoiceProtocol):
self._voice_server_complete.set() self._voice_server_complete.set()
async def voice_connect(self): async def voice_connect(self):
self._connections += 1
await self.channel.guild.change_voice_state(channel=self.channel) await self.channel.guild.change_voice_state(channel=self.channel)
async def voice_disconnect(self): async def voice_disconnect(self):
log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', self.channel.id, self.guild.id) log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', self.channel.id, self.guild.id)
await self.channel.guild.change_voice_state(channel=None) await self.channel.guild.change_voice_state(channel=None)
def prepare_handshake(self):
self._voice_state_complete.clear()
self._voice_server_complete.clear()
self._handshaking = True
log.info('Starting voice handshake... (connection attempt %d)', self._connections + 1)
self._connections += 1
def finish_handshake(self):
log.info('Voice handshake complete. Endpoint found %s', self.endpoint)
self._handshaking = False
self._voice_server_complete.clear()
self._voice_state_complete.clear()
async def connect_websocket(self):
ws = await DiscordVoiceWebSocket.from_client(self)
self._connected.clear()
while ws.secret_key is None:
await ws.poll_event()
self._connected.set()
return ws
async def connect(self, *, reconnect, timeout): async def connect(self, *, reconnect, timeout):
log.info('Connecting to voice...') log.info('Connecting to voice...')
self.timeout = timeout self.timeout = timeout
try:
del self.secret_key
except AttributeError:
pass
for i in range(5): for i in range(5):
self._voice_state_complete.clear() self.prepare_handshake()
self._voice_server_complete.clear()
self._handshaking = True
# This has to be created before we start the flow. # This has to be created before we start the flow.
futures = [ futures = [
@ -322,7 +338,6 @@ class VoiceClient(VoiceProtocol):
] ]
# Start the connection flow # Start the connection flow
log.info('Starting voice handshake... (connection attempt %d)', self._connections + 1)
await self.voice_connect() await self.voice_connect()
try: try:
@ -331,17 +346,10 @@ class VoiceClient(VoiceProtocol):
await self.disconnect(force=True) await self.disconnect(force=True)
raise raise
log.info('Voice handshake complete. Endpoint found %s', self.endpoint) self.finish_handshake()
self._handshaking = False
self._voice_server_complete.clear()
self._voice_state_complete.clear()
try: try:
self.ws = await DiscordVoiceWebSocket.from_client(self) self.ws = await self.connect_websocket()
self._connected.clear()
while not hasattr(self, 'secret_key'):
await self.ws.poll_event()
self._connected.set()
break break
except (ConnectionClosed, asyncio.TimeoutError): except (ConnectionClosed, asyncio.TimeoutError):
if reconnect: if reconnect:
@ -355,6 +363,26 @@ class VoiceClient(VoiceProtocol):
if self._runner is None: if self._runner is None:
self._runner = self.loop.create_task(self.poll_voice_ws(reconnect)) self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
async def potential_reconnect(self):
self.prepare_handshake()
self._potentially_reconnecting = True
try:
# We only care about VOICE_SERVER_UPDATE since VOICE_STATE_UPDATE can come before we get disconnected
await asyncio.wait_for(self._voice_server_complete.wait(), timeout=self.timeout)
except asyncio.TimeoutError:
self._potentially_reconnecting = False
await self.disconnect(force=True)
return False
self.finish_handshake()
self._potentially_reconnecting = False
try:
self.ws = await self.connect_websocket()
except (ConnectionClosed, asyncio.TimeoutError):
return False
else:
return True
@property @property
def latency(self): def latency(self):
""":class:`float`: Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. """:class:`float`: Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
@ -387,10 +415,19 @@ class VoiceClient(VoiceProtocol):
# 1000 - normal closure (obviously) # 1000 - normal closure (obviously)
# 4014 - voice channel has been deleted. # 4014 - voice channel has been deleted.
# 4015 - voice server has crashed # 4015 - voice server has crashed
if exc.code in (1000, 4014, 4015): if exc.code in (1000, 4015):
log.info('Disconnecting from voice normally, close code %d.', exc.code) log.info('Disconnecting from voice normally, close code %d.', exc.code)
await self.disconnect() await self.disconnect()
break break
if exc.code == 4014:
log.info('Disconnected from voice by force... potentially reconnecting.')
successful = await self.potential_reconnect()
if not successful:
log.info('Reconnect was unsuccessful, disconnecting from voice normally...')
await self.disconnect()
break
else:
continue
if not reconnect: if not reconnect:
await self.disconnect() await self.disconnect()

Loading…
Cancel
Save