async def _start_handshake(self): """ Requests a gateway handshake from Discord. If we get answer on it, means, we can open the socket to send audio data. This method is a coroutine. Raises ------ TimeoutError We did not receive answer in time. """ client = self.client gateway = client.gateway_for(self.guild_id) # request joining await gateway.change_voice_state(self.guild_id, self.channel_id) future_or_timeout(self._handshake_complete, 60.0) try: await self._handshake_complete except TimeoutError: await self._terminate_handshake() raise
async def wait_for_close(self, timeout=None): """ Waits till the stream is closed. Parameters ---------- timeout : `None`, `float` = `None`, Optional Maximal timeout to wait. Raises ------ TimeoutError Timeout occurred. """ if self._closed: return close_waiter = self._close_waiter if (close_waiter is None): close_waiter = self._close_waiter = Future(KOKORO) waiter = shield(close_waiter, KOKORO) if (timeout is not None): future_or_timeout(waiter, timeout) return waiter
async def _keep_beating(self): """ The beater task of kokoro. The control flow of tze method is the following: - If `.should_beat` is not `True`, break out. - Wait `.interval` time and set the waiting ``Future`` to `self.beat_waiter`. If kokoro is cancelled, or if it should beat now, it is cancelled and we repeat the loop. - If we did not get answer since last beating (what is triggered first from outside), then we stop the gateway. We also break out from the loop to terminate the beating state and we will wait for the websocket to connect again. - We beat one with starting `gateway._beat` as a ``Task`` and setting it to `.beat_task`. If the task is not completed before it's respective timeout, we stop the gateway here as well. We also break out from the loop to terminate the beating state and we will wait for the websocket to connect again. This task can also be cancelled. If cancellation occurs, we repeat the loop. - If the beating task is done, we update `.last_send` to the current `perf_counter` time. Repeat the loop. This method is a coroutine. """ self.last_answer = perf_counter() gateway = self.gateway self.should_beat = True while self.should_beat: waiter = sleep(self.interval, KOKORO) self.beat_waiter = waiter try: await waiter except CancelledError: self.last_send = perf_counter() continue finally: self.beat_waiter = None if (self.last_answer + self.interval + HEARTBEAT_TIMEOUT) - perf_counter() <= 0.0: self.should_beat = False Task(gateway.terminate(), KOKORO) break try: task = Task(gateway._beat(), KOKORO) future_or_timeout(task, HEARTBEAT_TIMEOUT) self.beat_task = task await task except TimeoutError: self.should_beat = False Task(gateway.terminate(), KOKORO) break except CancelledError: continue finally: self.beat_task = None self.last_send = perf_counter()
async def wait_for_response_message(self, *, timeout=None): """ Waits for response message. Applicable for application command interactions. This method is a coroutine. Parameters ---------- timeout : `None`, `float` = `None`, Optional (Keyword only) The maximal time to wait for message before `TimeoutError` is raised. Returns ------- message : ``Message`` The received message. Raises ------ RuntimeError The interaction was acknowledged with `show_for_invoking_user_only=True` (as ephemeral). Response `message` cannot be detected. TimeoutError Message was not received before timeout. """ message = self.message if (message is not None): return message if self._response_flag & RESPONSE_FLAG_EPHEMERAL: raise RuntimeError( f'The interaction was acknowledged with `show_for_invoking_user_only=True` ' f'(as ephemeral). Response `message` cannot be detected.' ) try: waiter = INTERACTION_EVENT_MESSAGE_WAITERS[self] except KeyError: waiter = Future(KOKORO) INTERACTION_EVENT_MESSAGE_WAITERS[self] = waiter waiter = shield(waiter, KOKORO) if (timeout is not None): future_or_timeout(waiter, timeout) await waiter return self.message
async def _runner(self): """ Runner task of the ready state waiting for all guild users to be requested. This method is a coroutine. """ try: shard_count = self.shard_count if not shard_count: shard_count = 1 while True: tasks = None done_tasks = 0 for shard_user_requester in self.shard_user_requesters.values(): if shard_user_requester.state == USER_REQUEST_STATE_NONE: if tasks is None: tasks = [] tasks.append(shard_user_requester.task) else: done_tasks += 1 if (tasks is not None): await WaitTillAll(tasks, KOKORO) continue if done_tasks == shard_count: break shard_ready_waiter = Future(KOKORO) future_or_timeout(shard_ready_waiter, SHARD_CONNECT_TIMEOUT) self.shard_ready_waiter = None try: await shard_ready_waiter except TimeoutError: return finally: self.shard_ready_waiter = None finally: self.task = None self.call_ready()
async def _runner(self): """ Requests the users of the represented shard's guilds. This method is a coroutine. """ try: guild_ids = self.guild_ids received_guild_ids = self.received_guild_ids can_request_users = self.can_request_users if can_request_users: sub_data = { 'guild_id': 0, 'query': '', 'limit': 0, 'presences': CACHE_PRESENCE, } data = { 'op': GATEWAY_OPERATION_CODE_REQUEST_MEMBERS, 'd': sub_data } while guild_ids: if not received_guild_ids: guild_create_waiter = Future(KOKORO) future_or_timeout(guild_create_waiter, GUILD_RECEIVE_TIMEOUT) self.guild_create_waiter = guild_create_waiter try: await guild_create_waiter except TimeoutError: self.state = USER_REQUEST_STATE_TIMEOUT return finally: self.guild_create_waiter = None guild_id, should_request_users = received_guild_ids.popleft() guild_ids.discard(guild_id) if not can_request_users: continue try: READY_STATE_TO_DO_GUILD_IDS.remove(guild_id) except KeyError: continue if not should_request_users: continue sub_data['guild_id'] = guild_id await self.gateway.send_as_json(data) await sleep(0.6, KOKORO) except (CancelledError, GeneratorExit): self.state = USER_REQUEST_STATE_CANCELLED raise except: self.state = USER_REQUEST_STATE_ERROR raise else: self.state = USER_REQUEST_STATE_DONE finally: self.task = None
async def run(self, waiter=None): """ Keeps the node's gateway open. This method is a coroutine. """ client = self.client reconnect_attempts = self.reconnect_attempts try: while True: try: task = Task(self._connect(), KOKORO) future_or_timeout(task, 30.0) await task if (waiter is not None): waiter.set_result_if_pending(True) waiter = None while True: try: with repeat_timeout(60.0) as loop: for _ in loop: should_reconnect = await self._poll_event() if should_reconnect: break except TimeoutError: # timeout, no internet probably; try to reconnect # Make sure we are still online if not client.running: return # Try to reconnect later. await sleep(5.0, KOKORO) break if not client.running: return reconnect_attempts = self.reconnect_attempts task = Task(self._connect(), KOKORO) future_or_timeout(task, 30.0) await task except (OSError, TimeoutError, ConnectionError, ConnectionClosed, WebSocketProtocolError, InvalidHandshake, ValueError) as err: if not client.running: return reconnect_attempts -= 1 if isinstance(err, InvalidHandshake): if err.response.status in (401, 403): raise SolarAuthenticationError( self, err.response) from None sleep_amount = 0.0 elif isinstance(err, ConnectionClosed): sleep_amount = 0.0 elif isinstance(err, TimeoutError): sleep_amount = 0.0 elif isinstance(err, ConnectionError): # no internet sleep_amount = 5.0 else: sleep_amount = 1.0 if not reconnect_attempts: await KOKORO.render_exception_async( err, [ 'Failed to connect to lavalink node ', repr(self), '.run:\n', ], ) if (waiter is not None): waiter.set_result_if_pending(False) waiter = None return if sleep_amount: await sleep(sleep_amount, KOKORO) continue except GeneratorExit: if (waiter is not None): waiter.set_result_if_pending(False) waiter = None raise except BaseException as err: if (waiter is not None): waiter.set_exception_if_pending(err) waiter = None raise finally: await client.solarlink._node_disconnected(self) if (waiter is not None): waiter.set_result_if_pending(False) waiter = None
async def run(self, waiter=None): """ Keeps the gateway receiving message and processing it. If the gateway needs to be reconnected, reconnects itself. If connecting cannot succeed, because there is no internet returns `True`. If the `.client` is stopped, then returns `False`. If `True` is returned the respective client stops all other gateways as well and tries to reconnect. When the internet is back the client will launch back the gateway. This method is a coroutine. Parameters ----------- waiter : `None`, ``Future`` = `None`, Optional A waiter future what is set, when the gateway finished connecting and started polling events. Raises ------ DiscordGatewayException The client tries to connect with bad or not acceptable intent or shard value. DiscordException """ client = self.client while True: try: task = Task(self._connect(), KOKORO) future_or_timeout(task, TIMEOUT_GATEWAY_CONNECT) await task if (waiter is not None): waiter.set_result(None) waiter = None while True: try: with repeat_timeout(TIMEOUT_POLL) as loop: for _ in loop: should_reconnect = await self._poll_event() if should_reconnect: if client.running: break return except TimeoutError: # timeout, no internet probably return task = Task(self._connect(resume=True,), KOKORO) future_or_timeout(task, TIMEOUT_GATEWAY_CONNECT) await task except (OSError, TimeoutError, ConnectionError, ConnectionClosed, WebSocketProtocolError, InvalidHandshake, ValueError) as err: if not client.running: return if isinstance(err, ConnectionClosed): code = err.code if code in (1000, 1006): continue if code in GATEWAY_EXCEPTION_CODE_TABLE: raise DiscordGatewayException(code) from err if isinstance(err, TimeoutError): continue if isinstance(err, ConnectionError): # no internet return await sleep(1.0, KOKORO)
async def _connect(self, waiter=None): """ Connects the voice client to Discord and keeps receiving the gateway events. This method is a coroutine. Parameters ---------- waiter : `None`, ``Future`` = `None`, Optional A Waiter what's result is set (or is raised to), when the voice client connects (or failed to connect). """ try: await self.gateway.start() tries = 0 while True: if tries == 5: if (waiter is not None): waiter.set_exception(TimeoutError()) return self._secret_box = None try: await self._start_handshake() except TimeoutError: tries += 1 continue except: await self._disconnect(force=True) raise try: task = Task(self.gateway.connect(), KOKORO) future_or_timeout( task, 30., ) await task self.connected.clear() while True: task = Task(self.gateway._poll_event(), KOKORO) future_or_timeout(task, 60.) await task if self._secret_box is not None: break self.connected.set() except (OSError, TimeoutError, ConnectionError, ConnectionClosed, WebSocketProtocolError, InvalidHandshake, ValueError) as err: self._maybe_close_socket() if isinstance(err, ConnectionClosed) and ( err.code == VOICE_CLIENT_DISCONNECT_CLOSE_CODE): # If we are getting disconnected and voice region changed, then Discord disconnects us, not # user nor us, so reconnect. if not self._maybe_change_voice_region(): self._reconnecting = False await self._disconnect(force=False) return if not (isinstance(err, ConnectionClosed) and (err.code == VOICE_CLIENT_RECONNECT_CLOSE_CODE)): await sleep(1 + (tries << 1), KOKORO) self._maybe_change_voice_region() tries += 1 await self._terminate_handshake() continue except: await self._disconnect(force=True) raise if (waiter is not None): waiter.set_result(self) waiter = None tries = 0 while True: try: task = Task(self.gateway._poll_event(), KOKORO) future_or_timeout(task, 60.) await task except ( OSError, TimeoutError, ConnectionClosed, WebSocketProtocolError, ) as err: self._maybe_close_socket() if isinstance(err, ConnectionClosed): # If we are getting disconnected and voice region changed, then Discord disconnects us, not # user nor us, so reconnect. code = err.code if code == 1000 or ( (code == VOICE_CLIENT_DISCONNECT_CLOSE_CODE) and (not self._maybe_change_voice_region())): self._reconnecting = False await self._disconnect(force=False) return self.connected.clear() if not (isinstance(err, ConnectionClosed) and (err.code == VOICE_CLIENT_RECONNECT_CLOSE_CODE)): await sleep(5., KOKORO) self._maybe_change_voice_region() await self._terminate_handshake() break except: self._reconnecting = False await self._disconnect(force=True) raise finally: self._reconnecting = False try: del self.client.voice_clients[self.guild_id] except KeyError: pass