def __init__(self): """ Creates a new mass user chunker. """ self.waiter = Future(KOKORO) self.last = now = LOOP_TIME() self.timer = KOKORO.call_at(now + USER_CHUNK_TIMEOUT, type(self)._cancel, self)
def __new__(cls, client, message, check, timeout): """ Creates a new ``ComponentInteractionWaiter`` with the given parameters. Parameters ---------- client : ``Client`` The client who will wait for component interaction. message : ``Message`` The waited interaction component's message. check : `None`, `callable` The check to call to validate whether the response is sufficient. timeout : `None`, `float` The timeout till the waiting is done. If expires, `TimeoutError` is raised to ``._future``. """ self = object.__new__(cls) self._finished = False self._future = Future(KOKORO) self._message = message self._check = check self._timeouter = None if (timeout is not None): self._timeouter = Timeouter(self, timeout) client.slasher.add_component_interaction_waiter(message, self) return self
def __iter__(self): """ Awaits the rate limit handler. This method is a generator. Should be used with `await` expression. Returns ------- cancelled : `bool` Whether the respective gateway was closed. """ now = LOOP_TIME() if now >= self.resets_at: self.resets_at = now + GATEWAY_RATE_LIMIT_RESET remaining = GATEWAY_RATE_LIMIT_LIMIT else: remaining = self.remaining if remaining: self.remaining = remaining - 1 return False if self.wake_upper is None: self.wake_upper = KOKORO.call_at(self.resets_at, type(self).wake_up, self) future = Future(KOKORO) self.queue.append(future) return (yield from future)
def __await__(self): """Awaits the iterator's next result.""" future = self._future if (future is None): # As it should be queue = self._queue if queue is None: # Check finished here :KoishiWink: if self._finished: exception = self._exception if (exception is None): return None else: raise exception future = self._future = Future(KOKORO) else: result = queue.popleft() if not queue: self._queue = None return result try: return (yield from future) finally: self._future = None
def __new__(cls, node, guild_id, channel_id): """ Creates a new solar player instance. Parameters ---------- node : ``SolarNode`` The node of the player. guild_id : `int` The guild's identifier, where the node will connect to. channel_id : `int` The channel's identifier, where the node will connect to. Returns ------- self : ``SolarPlayerBase`` The player. waiter : ``Future`` A future, which can be awaited to wait till the player is connected. """ self = object.__new__(cls) self._filters = {} self._position = 0.0 self._position_update = 0.0 self._forward_data = None self.guild_id = guild_id self.channel_id = channel_id self.node = node waiter = Future(KOKORO) Task(self._connect(waiter=waiter), KOKORO) return self, waiter
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 enter(self): """ Waits till a respective request can be started. Should be called before the rate limit handler is used inside of it's context manager. This method is a coroutine. """ size = self.parent.size if size < 1: if size == UNLIMITED_SIZE_VALUE: return if size == 0: size = 1 else: size = -size queue = self.queue if queue is None: self.queue = queue = deque() active = self.active left = size - active if left <= 0: future = Future(KOKORO) queue.append(future) await future self.active += 1 return left -= self.count_drops() if left > 0: self.active = active + 1 return future = Future(KOKORO) queue.append(future) await future self.active += 1
def _set_payload_reader(self, payload_reader): """ Sets payload reader to the stream. Parameters ---------- payload_reader : `GeneratorType` A generator, what gets control, every time a chunk is received, till it returns or raises. Returns ------- payload_waiter : ``Future`` Waiter, to what the result of the `payload_reader` is set. Raises ------ ValueError I/O operation on closed or on a detached file. """ assert self._payload_reader is None, 'Payload reader already set!' if self._closed and (not self._chunks): raise ValueError('I/O operation on closed or on a detached file.') payload_waiter = Future(KOKORO) try: payload_reader.send(None) except StopIteration as err: args = err.args if not args: result = None elif len(args) == 1: result = args[0] else: result = args payload_waiter.set_result_if_pending(result) except GeneratorExit as err: exception = CancelledError() exception.__cause__ = err payload_waiter.set_exception_if_pending(exception) except BaseException as err: payload_waiter.set_exception_if_pending(err) else: self._payload_waiter = payload_waiter self._payload_reader = payload_reader return payload_waiter
async def start(self): """ Starts the lavalink node. This method is a coroutine. Raises ------ BaseException Any exception raised when trying to connect. """ waiter = Future(KOKORO) Task(self.run(waiter=waiter), KOKORO) return await waiter
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 _start(self): """ The main keep alive task of kokoro. The control flow of the method is the following: - Set `.ws_waiter` and wait till it is done. If it is done, means that kokoro's gateway is connected. If it was cancelled meanwhile, means the beat task should stop. - Sets `.beater` to ``._keep_beating`` and awaits it. This is the main beater of kokoro and runs meanwhile it's gateway is connected. This task can be cancelled, but we ignore that case. - If `.running` is still `True`, repeat. This method is a coroutine. """ try: self.running = True while True: #wait for start try: waiter = Future(KOKORO) self.ws_waiter = waiter await waiter except CancelledError: #kokoro cancelled, client shuts down break finally: self.ws_waiter = None #keep beating try: beater = Task(self._keep_beating(), KOKORO) self.beater = beater await beater except CancelledError: #connection cancelled, lets wait for it pass finally: #make sure self.beater = None if self.running: continue break finally: if self.task is KOKORO.current_task: self.task = None
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()
def wait_for_reaction(client, message, check, timeout): """ Executes waiting for reaction on a message with a ``Future``. Parameters ---------- client : ``Client`` The client who's `reaction_add` event will be used. message : ``Message`` The target message on what new reactions will be checked. check : `callable` The check what is called with the received parameters whenever an event is received. timeout : `float` The timeout after `TimeoutError` will be raised to the waiter future. Returns ------- future : ``Future`` The waiter future, what should be awaited. """ future = Future(KOKORO) WaitAndContinue(future, check, message, client.events.reaction_add, timeout) return future
async def wait_till_limits_expire(self): """ Waits till the represented rate limits expire. This method is a coroutine. Raises ------ RuntimeError If the method is called meanwhile `keep_alive` is `True`. Notes ----- The waiting is implemented with weakreference callback, so the coroutine returns when the source callback is garbage collected. This also means waiting on the exact same limit multiple times causes misbehaviour. """ handler = self._handler while True: if (handler is not None): handler = handler() if (handler is not None): break handler = self.client.http.handlers.get(self._key) if handler is None: return break if handler is self._key: raise RuntimeError('Cannot use `.wait_till_limits_expire` meanwhile `keep_alive` is `True`.') future = Future(current_thread()) self._handler = WeakReferer(handler, self._wait_till_limits_expire_callback(future)) await future
def wait_for_message(client, channel, check, timeout): """ Executes waiting for messages at a channel with a ``Future``. Parameters ---------- client : ``Client`` The client who's `message_create` event will be used. channel : ``ChannelBase`` The target channel where the new messages will be checked. check : `callable` The check what is called with the received parameters whenever an event is received. timeout : `float` The timeout after `TimeoutError` will be raised to the waiter future. Returns ------- future : ``Future`` The waiter future, what should be awaited. """ future = Future(KOKORO) WaitAndContinue(future, check, channel, client.events.message_create, timeout) return future
def __new__(cls, client, guild_id, channel_id): """ Creates a ``VoiceClient``. If any of the required libraries are not present, raises `RuntimeError`. If the voice client was successfully created, returns a ``Future``, what is a waiter for it's ``._connect`` method. If connecting failed, then the future will raise `TimeoutError`. Parameters ---------- client : ``Client`` The parent client. guild_id : `int` the guild's identifier, where the the client will connect to. channel_id : `int` The channel's identifier where the client will connect to. Returns ------- waiter : ``Future`` Raises ------ RuntimeError If `PyNaCl` is not loaded. If `Opus` is not loaded. """ # raise error at __new__ if SecretBox is None: raise RuntimeError('PyNaCl is not loaded.') if OpusEncoder is None: raise RuntimeError('Opus is not loaded.') region = try_get_voice_region(guild_id, channel_id) self = object.__new__(cls) self.guild_id = guild_id self.channel_id = channel_id self.region = region self.gateway = DiscordGatewayVoice(self) self._socket = None self._protocol = None self._transport = None self.client = client self.connected = Event(KOKORO) self.queue = [] self.player = None self.call_after = cls._play_next self.speaking = 0 self.lock = Lock(KOKORO) self.reader = None self._handshake_complete = Future(KOKORO) self._encoder = OpusEncoder() self._sequence = 0 self._timestamp = 0 self._audio_source = 0 self._video_source = 0 self._pref_volume = 1.0 self._set_speaking_task = None self._endpoint = None self._port = None self._endpoint_ip = None self._secret_box = None self._audio_port = None self._ip = None self._audio_sources = {} self._video_sources = {} self._audio_streams = None self._reconnecting = True client.voice_clients[guild_id] = self waiter = Future(KOKORO) Task(self._connect(waiter=waiter), KOKORO) return waiter
class ComponentInteractionWaiter: """ Waiter class for button press. Parameters ---------- _check : `None`, `callable` The check to call to validate whether the response is sufficient. _finished : `bool` Whether the waiter finished. _future : ``Future`` The waiter future. _message : ``Message`` The waited interaction component's message. _timeouter : `None`, ``Timeouter`` Executes the timeout feature on the waiter. """ __slots__ = ('_check', '_finished', '_future', '_message', '_timeouter') def __new__(cls, client, message, check, timeout): """ Creates a new ``ComponentInteractionWaiter`` with the given parameters. Parameters ---------- client : ``Client`` The client who will wait for component interaction. message : ``Message`` The waited interaction component's message. check : `None`, `callable` The check to call to validate whether the response is sufficient. timeout : `None`, `float` The timeout till the waiting is done. If expires, `TimeoutError` is raised to ``._future``. """ self = object.__new__(cls) self._finished = False self._future = Future(KOKORO) self._message = message self._check = check self._timeouter = None if (timeout is not None): self._timeouter = Timeouter(self, timeout) client.slasher.add_component_interaction_waiter(message, self) return self async def __call__(self, interaction_event): """ Calls the component interaction waiter checking whether the respective event is sufficient setting the waiter's result. This method is a coroutine. Parameters ---------- interaction_event : ``InteractionEvent`` The received interaction event """ check = self._check if check is None: self._future.set_result_if_pending(interaction_event) should_acknowledge = False else: try: result = check(interaction_event) except BaseException as err: self._future.set_exception_if_pending(err) should_acknowledge = False else: if isinstance(result, bool): if result: self._future.set_result_if_pending(interaction_event) should_acknowledge = False else: should_acknowledge = True else: self._future.set_result_if_pending((interaction_event, result)) should_acknowledge = False if should_acknowledge: await acknowledge_component_interaction(interaction_event) else: self.cancel() def __await__(self): """Awaits the waiter's result.""" return (yield from self._future) def cancel(self, exception=None): """ Cancels the component waiter. Parameters ---------- exception : `None`, ``BaseException`` = `None`, Optional The exception to cancel the waiter with. """ if self._finished: return self._finished = True timeouter = self._timeouter if (timeouter is not None): self._timeouter = None timeouter.cancel() message = self._message client = get_client_from_message(message) client.slasher.remove_component_interaction_waiter(message, self) future = self._future if exception is None: future.set_result_if_pending(None) else: future.set_exception_if_pending(exception)
async def run(self): """ Runs the gateway sharder's gateways. If any of them returns, stops the rest as well. And if any of them returned `True`, then returns `True`, else `False`. This method is a coroutine. Raises ------ DiscordGatewayException The client tries to connect with bad or not acceptable intent or shard value. DiscordException Any exception raised by the discord API when connecting. """ max_concurrency = self.client._gateway_max_concurrency gateways = self.gateways index = 0 limit = len(gateways) # At every step we add up to max_concurrency gateways to launch up. When a gateway is launched up, the waiter # yields a ``Future`` and if the same amount of ``Future`` is yielded as gateway started up, then we do the next # loop. An exception is, when the waiter yielded a ``Task``, because t–en 1 of our gateway stopped with no # internet stop, or it was stopped by the client, so we abort all the launching and return. waiter = WaitContinuously(None, KOKORO) while True: if index == limit: break left_from_batch = 0 while True: future = Future(KOKORO) waiter.add(future) task = Task(gateways[index].run(future), KOKORO) waiter.add(task) index += 1 left_from_batch += 1 if index == limit: break if left_from_batch == max_concurrency: break continue while True: try: result = await waiter except: waiter.cancel() raise waiter.reset() if type(result) is Future: left_from_batch -= 1 if left_from_batch: continue break waiter.cancel() result.result() # We could time gateway connect rate limit more precisely, but this is already fine. We don't need to rush # it, there is many gateway to connect and sync with. await sleep(5.0, KOKORO) continue try: result = await waiter finally: waiter.cancel() result.result()
async def execute(self, client, parameter): """ Executes the request and returns it's result or raises. This method is a coroutine. Parameters ---------- client : ``Client`` The client, who's `.discovery_validate_term` method was called. parameter : `str` The discovery term. Returns ------- result : `Any` Raises ------ ConnectionError If there is no internet connection, or there is no available cached result. TypeError The given `parameter` was not passed as `str`. DiscordException If any exception was received from the Discord API. """ # First check parameter parameter_type = parameter.__class__ if parameter_type is str: pass elif issubclass(parameter_type, str): parameter = str(parameter) else: raise TypeError( f'`parameter` can be `str`, got {parameter_type.__class__}; {parameter!r}.' ) # First check cache try: unit = self.cached[parameter] except KeyError: unit = None else: now = LOOP_TIME() if self.timeout + unit.creation_time > now: unit.last_usage_time = now return unit.result # Second check actual request try: waiter = self._waiters[parameter] except KeyError: pass else: if waiter is None: self._waiters[parameter] = waiter = Future(KOKORO) return await waiter # No actual request is being done, so mark that we are doing a request. self._waiters[parameter] = None # Search client with free rate limits. free_count = RateLimitProxy(client, *self._rate_limit_proxy_parameters).free_count if not free_count: requester = client for client_ in CLIENTS.values(): if client_ is client: continue free_count = RateLimitProxy(client_, *self._rate_limit_proxy_parameters).free_count if free_count: requester = client_ break continue # If there is no client with free count do not care about the reset times, because probably only 1 client # forces requests anyways, so that's rate limits will reset first as well. client = requester # Do the request try: result = await self.func(client, parameter) except ConnectionError as err: if (unit is None): waiter = self._waiters.pop(parameter) if (waiter is not None): waiter.set_exception(err) raise unit.last_usage_time = LOOP_TIME() result = unit.result except BaseException as err: waiter = self._waiters.pop(parameter, None) if (waiter is not None): waiter.set_exception(err) raise else: if unit is None: self.cached[parameter] = unit = TimedCacheUnit() now = LOOP_TIME() unit.last_usage_time = now unit.creation_time = now unit.result = result finally: # Do cleanup if needed now = LOOP_TIME() if self._last_cleanup + self._minimal_cleanup_interval < now: self._last_cleanup = now cleanup_till = now - self.timeout collected = [] cached = self.cached for cached_parameter, cached_unit in cached.items(): if cached_unit.last_usage_time < cleanup_till: collected.append(cached_parameter) for cached_parameter in collected: del cached[cached_parameter] waiter = self._waiters.pop(parameter) if (waiter is not None): waiter.set_result(result) return result
def __init__(self, ): self.waiter = Future(KOKORO) self.timer = KOKORO.call_at(LOOP_TIME() + USER_CHUNK_TIMEOUT, type(self)._cancel, self)
async def execute(self, client): """ Executes the request and returns it's result or raises. This method is a coroutine. Returns ------- result : `Any` Raises ------ ConnectionError If there is no internet connection, or there is no available cached result. DiscordException If any exception was received from the Discord API. """ if (LOOP_TIME() - self.timeout) < self._last_update: if self._active_request: waiter = self._waiter if waiter is None: waiter = self._waiter = Future(KOKORO) result = await waiter else: result = self.cached return result self._active_request = True try: result = await self.func(client) except ConnectionError as err: result = self.cached if (result is ...): waiter = self._waiter if (waiter is not None): self._waiter = None waiter.set_exception(err) raise except BaseException as err: waiter = self._waiter if (waiter is not None): self._waiter = None waiter.set_exception(err) raise else: self._last_update = LOOP_TIME() finally: self._active_request = False waiter = self._waiter if (waiter is not None): self._waiter = None waiter.set_result(result) return result
class SingleUserChunker: """ A user chunk waiter, which yields after the first received chunk. Used at ``Client.request_members``. Attributes ---------- timer : `Handle`, `None` The time-outer of the chunker, what will cancel if the timeout occurs. waiter : ``Future`` The waiter future what will yield, when we receive the response, or when the timeout occurs. """ __slots__ = ('timer', 'waiter',) def __init__(self, ): self.waiter = Future(KOKORO) self.timer = KOKORO.call_at(LOOP_TIME() + USER_CHUNK_TIMEOUT, type(self)._cancel, self) def __call__(self, event): """ Called when a chunk is received with it's respective nonce. Parameters ---------- event : ``GuildUserChunkEvent`` The received guild user chunk's event. Returns ------- is_last : `bool` ``SingleUserChunker`` returns always `True`, because it waits only for one event. """ self.waiter.set_result_if_pending(event.users) timer = self.timer if (timer is not None): self.timer = None timer.cancel() return True def _cancel(self): """ The chunker's timer calls this method. Cancels ``.waiter`` and ``.timer``. After this method was called, the waiting coroutine will remove it's reference from the event handler. """ self.waiter.cancel() timer = self.timer if (timer is not None): self.timer = None timer.cancel() def cancel(self): """ Cancels the chunker. This method should be called when when the chunker is canceller from outside. Before this method is called, it's references should be removed as well from the event handler. """ self.waiter.set_result_if_pending([]) timer = self.timer if (timer is not None): self.timer = None timer.cancel() def __await__(self): """ Awaits the chunker's waiter and returns that's result. Returns ------- users : `list` of ``ClientUserBase`` objects The received users. Can be an empty list. Raises ------ CancelledError If timeout occurred. """ return self.waiter.__await__()
async def _request(self, method, url, rate_limit_handler, data=None, query_parameters=None): """ Does a request towards top.gg API. This method is a coroutine. Parameters ---------- method : `str` Http method. url : `str` Endpoint to do request towards. rate_limit_handler : ``RateLimitHandlerBase` Rate limit handle to handle rate limit as. data : `None`, `Any` = `None`, Optional Json serializable data. query_parameters : `None`, `Any` = `None`, Optional Query parameters. Raises ------ ConnectionError No internet connection. TopGGGloballyRateLimited If the client got globally rate limited by top.gg and `raise_on_top_gg_global_rate_limit` was given as `True`. TopGGHttpException Any exception raised by top.gg api. """ headers = self._headers.copy() if (data is not None): headers[CONTENT_TYPE] = 'application/json' data = to_json(data) try_again = 2 while try_again > 0: global_rate_limit_expires_at = self._global_rate_limit_expires_at if global_rate_limit_expires_at > LOOP_TIME(): if self._raise_on_top_gg_global_rate_limit: raise TopGGGloballyRateLimited(None) future = Future(KOKORO) KOKORO.call_at(global_rate_limit_expires_at, Future.set_result_if_pending, future, None) await future async with rate_limit_handler.ctx(): try: async with RequestContextManager( self.http._request(method, url, headers, data, query_parameters)) as response: response_data = await response.text(encoding='utf-8') except OSError as err: if not try_again: raise ConnectionError( 'Invalid address or no connection with Top.gg.' ) from err await sleep(0.5 / try_again, KOKORO) try_again -= 1 continue response_headers = response.headers status = response.status content_type_headers = response_headers.get(CONTENT_TYPE, None) if (content_type_headers is not None ) and content_type_headers.startswith('application/json'): response_data = from_json(response_data) if 199 < status < 305: return response_data # Are we rate limited? if status == 429: try: retry_after = headers[RETRY_AFTER] except KeyError: retry_after = RATE_LIMIT_GLOBAL_DEFAULT_DURATION else: try: retry_after = float(retry_after) except ValueError: retry_after = RATE_LIMIT_GLOBAL_DEFAULT_DURATION self._global_rate_limit_expires_at = LOOP_TIME( ) + retry_after if self._raise_on_top_gg_global_rate_limit: raise TopGGGloballyRateLimited(None) await sleep(retry_after, KOKORO) continue # Python casts sets to frozensets if (status in {400, 401, 402, 404}): raise TopGGHttpException(response, response_data) if try_again and (status >= 500): await sleep(10.0 / try_again, KOKORO) try_again -= 1 continue raise TopGGHttpException(response, response_data)
class MassUserChunker: """ A user chunk waiter, which yields after the chunks of certain amount of guilds are received. Used at ``Client._request_members`` and at ``Client._request_members``. Attributes ---------- last : `float` The timestamp of the last received chunk. timer : `Handle`, `None` The time-outer of the chunker, what will cancel if the timeout occurs. waiter : ``Future`` The waiter future what will yield, when we receive the response, or when the timeout occurs. """ __slots__ = ('last', 'timer', 'waiter',) def __init__(self): """ Creates a new mass user chunker. """ self.waiter = Future(KOKORO) self.last = now = LOOP_TIME() self.timer = KOKORO.call_at(now + USER_CHUNK_TIMEOUT, type(self)._cancel, self) def __call__(self, event): """ Called when a chunk is received with it's respective nonce. Updates the chunker's last received chunk's time to push out the current timeout. Parameters ---------- event : ``GuildUserChunkEvent`` The received guild user chunk's event. Returns ------- is_last : `bool` Whether the last chunk was received. """ self.last = LOOP_TIME() if event.index + 1 != event.count: return False self.waiter.set_result_if_pending(None) timer = self.timer if (timer is not None): self.timer = None timer.cancel() return True def _cancel(self): """ The chunker's timer calls this method. If the chunker received any chunks since it's ``.timer`` was started, pushes out the timeout. Cancels ``.waiter`` and ``.timer``. After this method was called, the waiting coroutine will remove it's reference from the event handler. """ now = LOOP_TIME() next_ = self.last + USER_CHUNK_TIMEOUT if next_ > now: self.timer = KOKORO.call_at(next_, type(self)._cancel, self) else: self.timer = None self.waiter.cancel() def cancel(self): """ Cancels the chunker. This method should be called when when the chunker is canceller from outside. Before this method is called, it's references should be removed as well from the event handler. """ self.waiter.set_result_if_pending(None) timer = self.timer if (timer is not None): self.timer = None timer.cancel() def __await__(self): """ Awaits the chunker's waiter. Raises ------ CancelledError If timeout occurred. """ return self.waiter.__await__()
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
class VoiceClient: """ Represents a client what is joined to a voice channel. Attributes ---------- _audio_port : `int` The port, where the voice client should send the audio data. _audio_source : `int` An identifier sent by Discord what should be sent back with every voice packet. _audio_sources : `dict` of (`int`, `int`) items `user_id` - `audio_source` mapping used by ``AudioStream``-s. _audio_streams : `None`, `dict` of (`int`, (``AudioStream`` or (`list` of ``AudioStream``)) items `user_id` - ``AudioStream``(s) mapping for linking ``AudioStream`` to their respective user. _encoder : ``OpusEncoder`` Encode not opus encoded audio data. _endpoint : `None`, `str` The endpoint, where the voice client sends the audio data. _endpoint_ip : `None`, `tuple` of `int` The ip version of the `._endpoint` attribute. _handshake_complete : ``Future`` Used for awaiting the connecting handshake with Discord. _ip : `None`, `tuple` of `int` The ip to what the voice client's gateway connects. _port : `None`, `int` The port to what the voice client's gateway connects. _pref_volume : `float` The preferred volume of the voice client. can be between `0.0` and `2.0`. _protocol : `None`, ``DatagramMergerReadProtocol`` Asynchronous protocol of the voice client to communicate with it's socket. _reconnecting : `bool` Whether the voice client plans to reconnect and it's reader and player should not be stopped. _secret_box : `None`, `nacl.secret.SecretBox` Data encoder of the voice client. _sequence : `int` Counter to define the sent data's sequence for Discord. _session_id : `None`, `str` The session id of the voice client's owner client's shard. _set_speaking_task : `None`, ``Task`` Synchronization task for the `.set_speaking` coroutine. _socket : `None`, `socket.socket` The socket through what the ``VoiceClient`` sends the voice data to Discord. Created by the ``._create_socket`` method, when the client's gateway receives response after connecting. If the client leaves the voice channel, then the socket is closed and set back to `None`. _timestamp : `int` A timestamp identifier to tell Discord how much frames we sent to it. _token : `str` Token received by the voice client's owner client's gateway. Used to authorize the voice client. _transport : `None`, ``_SelectorDatagramTransport`` Asynchronous transport of the voice client to communicate with it's socket. _video_source : `int` An identifier sent by Discord what should be sent back with every video packet. _video_sources : `dict` of (`int`, `int`) items `user_id` - `video_source` mapping. Not used for now. call_after : `callable` (`awaitable`) A coroutine function what is awaited, when the voice clients's current audio finishes playing. By default this attribute is set to the ``._play_next`` function of the voice client (plays the next audio at the voice clients's ``.queue`` as expected. This attribute of the client can be modified freely. To it `2` parameters are passed: +------------------+---------------------------+ | Respective name | Type | +==================+===========================+ | client | ``VoiceClient`` | +------------------+---------------------------+ | last_source | `None`, ``AudioSource`` | +------------------+---------------------------+ The ``VoiceClient`` also includes some other predefined function for setting as `call_after`: - ``._play_next`` - ``._loop_actual`` - ``._loop_queue`` channel_id : `int` The channel's identifier where the voice client currently is. client : ``Client`` The voice client's owner client. connected : ``Event`` Used to communicate with the ``AudioPlayer`` thread. gateway : ``DiscordGatewayVoice`` The gateway through what the voice client communicates with Discord. guild_id : `int`` The guild's identifier where the voice client is. lock : `Lock` A lock used meanwhile changing the currently playing audio to not modifying it parallelly. player : ``AudioPlayer`` The actual player of the ``VoiceClient``. If the voice client is not playing nor paused, then set as `None`. queue : `list` of ``AudioSource`` A list of the scheduled audios. reader : `None`, ``AudioReader`` Meanwhile the received audio is collected, this attribute is set to a running ``AudioReader``. region : ``VoiceRegion`` The actual voice region of the voice client. speaking : `int` Whether the client is showed by Discord as `speaking`, then this attribute should is set as `1`. Can be modified, with the ``.set_speaking``, however it is always adjusted to the voice client's current playing state. """ __slots__ = ('_audio_port', '_audio_source', '_audio_sources', '_audio_streams', '_encoder', '_endpoint', '_endpoint_ip', '_handshake_complete', '_ip', '_port', '_pref_volume', '_protocol', '_reconnecting', '_secret_box', '_sequence', '_session_id', '_set_speaking_task', '_socket', '_timestamp', '_token', '_transport', '_video_source', '_video_sources', 'call_after', 'channel_id', 'client', 'connected', 'gateway', 'guild_id', 'lock', 'player', 'queue', 'reader', 'region', 'speaking') def __new__(cls, client, guild_id, channel_id): """ Creates a ``VoiceClient``. If any of the required libraries are not present, raises `RuntimeError`. If the voice client was successfully created, returns a ``Future``, what is a waiter for it's ``._connect`` method. If connecting failed, then the future will raise `TimeoutError`. Parameters ---------- client : ``Client`` The parent client. guild_id : `int` the guild's identifier, where the the client will connect to. channel_id : `int` The channel's identifier where the client will connect to. Returns ------- waiter : ``Future`` Raises ------ RuntimeError If `PyNaCl` is not loaded. If `Opus` is not loaded. """ # raise error at __new__ if SecretBox is None: raise RuntimeError('PyNaCl is not loaded.') if OpusEncoder is None: raise RuntimeError('Opus is not loaded.') region = try_get_voice_region(guild_id, channel_id) self = object.__new__(cls) self.guild_id = guild_id self.channel_id = channel_id self.region = region self.gateway = DiscordGatewayVoice(self) self._socket = None self._protocol = None self._transport = None self.client = client self.connected = Event(KOKORO) self.queue = [] self.player = None self.call_after = cls._play_next self.speaking = 0 self.lock = Lock(KOKORO) self.reader = None self._handshake_complete = Future(KOKORO) self._encoder = OpusEncoder() self._sequence = 0 self._timestamp = 0 self._audio_source = 0 self._video_source = 0 self._pref_volume = 1.0 self._set_speaking_task = None self._endpoint = None self._port = None self._endpoint_ip = None self._secret_box = None self._audio_port = None self._ip = None self._audio_sources = {} self._video_sources = {} self._audio_streams = None self._reconnecting = True client.voice_clients[guild_id] = self waiter = Future(KOKORO) Task(self._connect(waiter=waiter), KOKORO) return waiter # properties def _get_volume(self): return self._pref_volume def _set_volume(self, value): if value < 0.: value = 0. elif value > 2.: value = 2. self._pref_volume = value volume = property(_get_volume, _set_volume) del _get_volume, _set_volume if DOCS_ENABLED: volume.__doc__ = (""" Get-set property for accessing the voice client's volume. Can be between `0.0` and `2.0`. """) @property def source(self): """ Returns the voice client's player's source if applicable. Returns ------- source : `None`, ``AudioSource`` """ player = self.player if player is None: return return player.source # methods async def set_speaking(self, value): """ A coroutine, what is used when changing the ``.speaking`` state of the voice client. By default when audio is played, the speaking state is changed to `True` and meanwhile not, then to `False`. This method is a coroutine. Parameters ---------- value : `int` (`0`, `1`) Notes ----- Tinkering with this method is not recommended. """ task = self._set_speaking_task if (task is not None): await task if self.speaking == value: return self.speaking = value task = Task(self.gateway._set_speaking(value), KOKORO) self._set_speaking_task = task try: await task finally: self._set_speaking_task = None def listen_to(self, user, **kwargs): """ Creates an audio stream for the given user. Parameters ---------- user : ``UserBase`` The user, who's voice will be captured. **kwargs : Keyword parameters Additional keyword parameters. Other Parameters ---------------- auto_decode : `bool` Whether the received packets should be auto decoded. yield_decoded : `bool` Whether the audio stream should yield encoded data. Returns ------- audio_stream : ``AudioStream`` """ stream = AudioStream(self, user, **kwargs) self._link_audio_stream(stream) return stream def _link_audio_stream(self, stream): """ Links the given ``AudioStream`` to self causing to start receiving audio. Parameters ---------- stream : ``AudioStream`` """ voice_client_audio_streams = self._audio_streams if voice_client_audio_streams is None: voice_client_audio_streams = self._audio_streams = {} user_id = stream.user.id try: voice_client_actual_stream = voice_client_audio_streams[user_id] except KeyError: voice_client_audio_streams[user_id] = stream else: if type(voice_client_actual_stream) is list: voice_client_actual_stream.append(stream) else: voice_client_audio_streams[user_id] = [ voice_client_actual_stream, stream ] source = stream.source if (source is not None): reader = self.reader if reader is None: reader = self.reader = AudioReader(self) reader_audio_streams = reader.audio_streams try: reader_actual_stream = reader_audio_streams[source] except KeyError: reader_audio_streams[source] = stream else: if type(reader_actual_stream) is list: reader_actual_stream.append(stream) else: reader_audio_streams[source] = [ reader_actual_stream, stream ] def _unlink_audio_stream(self, audio_stream): """ Un-links the given audio stream from the voice client causing it to stop receiving audio. Parameters ---------- audio_stream : ``AudioStream`` """ voice_client_audio_streams = self._audio_streams if (voice_client_audio_streams is not None): user_id = audio_stream.user.id try: voice_client_actual_stream = voice_client_audio_streams[ user_id] except KeyError: pass else: if type(voice_client_actual_stream) is list: try: voice_client_actual_stream.remove(audio_stream) except ValueError: pass else: if len(voice_client_actual_stream) == 1: voice_client_audio_streams[ user_id] = voice_client_actual_stream[0] else: if voice_client_actual_stream is audio_stream: del voice_client_audio_streams[user_id] reader = self.reader if (reader is not None): source = audio_stream.source if (source is not None): reader_audio_streams = reader.audio_streams try: reader_actual_stream = reader_audio_streams[source] except KeyError: pass else: if type(reader_actual_stream) is list: try: reader_actual_stream.remove(audio_stream) except ValueError: pass else: if len(reader_actual_stream) == 1: reader_audio_streams[ source] = reader_actual_stream[0] else: if reader_actual_stream is audio_stream: del reader_audio_streams[source] def _remove_source(self, user_id): """ Un-links the audio and video streams's source listening to the given user (id), causing the affected audio str-eam(s) to stop receiving audio data at the meanwhile. Parameters ---------- user_id : `int` The respective user's id. """ voice_sources = self._audio_sources try: voice_source = voice_sources.pop(user_id) except KeyError: pass else: audio_streams = self._audio_streams if (audio_streams is not None): try: audio_streams[user_id] except KeyError: pass else: reader = self.reader if (reader is not None): try: del reader.audio_streams[voice_source] except KeyError: pass try: del self._video_sources[user_id] except KeyError: pass def _update_audio_source(self, user_id, audio_source): """ Updates (or adds) an `user-id` - `audio-source` relation to the voice client causing the affected audio streams to listen to their new source. Parameters ---------- user_id : `int` The respective user's id. audio_source : `int` Audio source identifier of the user. """ voice_sources = self._audio_sources try: old_audio_source = voice_sources.pop(user_id) except KeyError: # Should not happen if it is an update, only if it is an add pass else: # Should happen if it is an update if audio_source == old_audio_source: # Should double happen if it is an update return reader = self.reader if (reader is not None): reader_audio_streams = reader.audio_streams try: del reader_audio_streams[old_audio_source] except KeyError: pass voice_sources[user_id] = audio_source streams = self._audio_streams if streams is None: return try: voice_client_actual_stream = streams[user_id] except KeyError: return # Link source if type(voice_client_actual_stream) is list: for stream in voice_client_actual_stream: stream.source = audio_source else: voice_client_actual_stream.source = audio_source # Add the sources to reader reader = self.reader if reader is None: reader = self.reader = AudioReader(self) reader_audio_streams = reader.audio_streams try: reader_actual_stream = reader_audio_streams[audio_source] except KeyError: # This should happen if type(voice_client_actual_stream) is list: reader_new_stream = voice_client_actual_stream.copy() else: reader_new_stream = voice_client_actual_stream reader_audio_streams[audio_source] = reader_new_stream else: # Should not happen if type(reader_actual_stream) is list: if type(voice_client_actual_stream) is list: reader_actual_stream.extend(voice_client_actual_stream) else: reader_actual_stream.append(voice_client_actual_stream) else: reader_new_stream = [reader_actual_stream] if type(voice_client_actual_stream) is list: reader_new_stream.extend(voice_client_actual_stream) else: reader_new_stream.append(voice_client_actual_stream) reader_audio_streams[audio_source] = reader_new_stream def _update_video_source(self, user_id, video_source): """ Updates (or adds) an `user-id` - `video-source` relation to the voice client. Parameters ---------- user_id : `int` The respective user's id. video_source : `int` Video source identifier of the user. """ self._video_sources[user_id] = video_source def get_audio_streams(self): """ Returns the audio streams of the voice client within a `list`. Returns ------- streams : `list` of `tuple` (``ClientUserBase``, ``AudioStream``) Audio streams as a `list` of `tuples` of their respective listened `user` and `stream`. """ streams = [] voice_client_audio_streams = self._audio_streams if (voice_client_audio_streams is not None): for user_id, stream in voice_client_audio_streams.items(): user = User.precreate(user_id) if type(stream) is list: for stream in stream: streams.append((user, stream)) else: streams.append((user, stream)) return streams @property def voice_state(self): """ Returns the voice state of the client. Returns ------- voice_state : `None`, ``VoiceState`` """ try: guild = GUILDS[self.guild_id] except KeyError: pass else: return guild.voice_states.get(self.client.id, None) async def move_to(self, channel): """ Move the voice client to an another voice channel. This method is a coroutine. Parameters --------- channel : ``ChannelVoiceBase``, `int` The channel where the voice client will move to. Returns ------- moved : `bool` Returns `False` if the voice client is already in the channel. Raises ------ TypeError If `channel` was not given as ``ChannelVoiceBase`` not `int`. """ if isinstance(channel, ChannelVoiceBase): channel_id = channel.id else: channel_id = maybe_snowflake(channel) if channel_id is None: raise TypeError( f'`channel` can be `{ChannelVoiceBase.__name__}`, `int`, got ' f'{channel.__class__.__name__}; {channel!r}.') if self.channel_id == channel_id: return False gateway = self.client.gateway_for(self.guild_id) await gateway.change_voice_state(self.guild_id, channel_id) return True async def join_speakers(self, *, request=False): """ Requests to speak at the voice client's voice channel. Only applicable for stage channels. This method is a coroutine. Parameters ---------- request : `bool` = `False`, Optional (Keyword only) Whether the client should only request to speak. Raises ------ ConnectionError No internet connection. DiscordException If any exception was received from the Discord API. """ guild_id = self.guild_id try: guild = GUILDS[guild_id] except KeyError: pass else: try: voice_state = guild.voice_states[self.client.id] except KeyError: return if voice_state.is_speaker: return if request: timestamp = datetime_to_timestamp(datetime.utcnow()) else: timestamp = None data = { 'suppress': False, 'request_to_speak_timestamp': timestamp, 'channel_id': self.channel_id } await self.client.http.voice_state_client_edit(guild_id, data) async def join_audience(self): """ Joins the audience in the voice client's voice channel. Only applicable for stage channels. This method is a coroutine. Raises ------ ConnectionError No internet connection. DiscordException If any exception was received from the Discord API. """ guild_id = self.guild_id try: guild = GUILDS[guild_id] except KeyError: pass else: try: voice_state = guild.voice_states[self.client.id] except KeyError: return if not voice_state.is_speaker: return data = {'suppress': True, 'channel_id': self.channel_id} await self.client.http.voice_state_client_edit(guild_id, data) def append(self, source): """ Starts playing the given audio source. If the voice client is already playing, puts it on it's queue instead. Parameters --------- source : ``AudioSource`` The audio source to put on the queue. Returns ------- started_playing : `bool` Whether the source is started playing and not put on queue. """ if not isinstance(source, AudioSource): raise TypeError( f'Expected `{AudioSource.__name__}`, got {source.__class__.__name__}; {source!r}.' ) player = self.player if player is None: self.player = AudioPlayer( self, source, ) Task(self.set_speaking(1), KOKORO) return True queue = self.queue if queue or (player.source is not None): queue.append(source) return False player.set_source(source) Task(self.set_speaking(1), KOKORO) return True def skip(self, index=0): """ Skips the currently played audio at the given index and returns it. Skipping nothing yields to returning `None`. Parameters ---------- index : `int` = `0`, Optional The index of the audio to skip. Defaults to `0`, what causes the currently playing source to skipped. Returns ------- source : `None`, ``AudioSource`` """ if index == 0: player = self.player if player is None: source = None else: source = player.source # Try playing next even if player is not `None`. Task(self.play_next(), KOKORO) elif index < 0: source = None else: queue = self.queue if index > len(queue): source = None else: source = queue.pop(index - 1) return source def pause(self): """ Pauses the currently played audio if applicable. """ player = self.player if (player is not None): player.pause() Task(self.set_speaking(0), KOKORO) def resume(self): """ Resumes the currently stopped audio if applicable. """ player = self.player if (player is not None): player.resume() Task(self.set_speaking(1), KOKORO) def stop(self): """ Stops the currently playing audio and clears the audio queue. """ self.queue.clear() player = self.player if (player is not None): self.player = None player.stop() def is_connected(self): """ Returns whether the voice client is connected to a ``ChannelVoice``. Returns ------- is_connected : `bool` """ return self.connected.is_set() def is_playing(self): """ Returns whether the voice client is currently playing audio. Returns ------- is_playing : `bool` """ player = self.player if player is None: return False if player.done: return False if not player.resumed_waiter.is_set(): return False return True def is_paused(self): """ Returns whether the voice client is currently paused (or not playing). Returns ------- is_paused : `bool` """ player = self.player if player is None: return True if player.done: return True if not player.resumed_waiter.is_set(): return True return False # connection related 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 async def disconnect(self): """ Disconnects the voice client. This method is a coroutine. """ await self._disconnect() async def _disconnect(self, force=False, terminate=True): """ Disconnects the voice client. If you want to disconnect a voice client, then you should use ``.disconnect``. Passing bad parameters to this method the can cause misbehaviour. This method is a coroutine. Parameters ---------- force : `bool` = `False`, Optional Whether the voice client should disconnect only if it is not connected (for example when it is connecting). terminate : `bool` = `True`, Optional Whether it is an internal disconnect. If the Disconnect comes from Discord's side, then `terminate` is `False`, what means, we do not need to terminate the gateway handshake. """ if not (force or self.connected.is_set()): return self.queue.clear() if not self._reconnecting: player = self.player if (player is not None): self.player = None player.stop() # skip 1 full loop await skip_poll_cycle(KOKORO) reader = self.reader if (reader is not None): self.reader = None reader.stop() self.connected.clear() try: await self.gateway.close() if terminate: await self._terminate_handshake() finally: self._maybe_close_socket() @classmethod async def _kill_ghost(cls, client, voice_state): """ When a client is restarted, it might happen that it will be in still in some voice channels. At this case this function is ensured to kill the ghost connection. This method is a coroutine. Parameters ---------- client : ``Client`` The owner client of the ghost connection. voice_state : ``VoiceState`` The ghost voice client's voice state. """ try: voice_client = await cls(client, voice_state.guild_id, voice_state.channel_id) except (RuntimeError, TimeoutError): return await voice_client._disconnect(force=True) async def play_next(self): """ Skips the currently playing audio. Familiar to `.skip`, but it return when the operation id done. This method is a coroutine. """ async with self.lock: await self._play_next(self, None) @staticmethod async def _play_next(self, last_source): """ Starts to play the next audio object on ``.queue`` and cancels the actual one if applicable. Should be used inside of ``.lock`` to ensure that the voice client is not modified parallelly. This function is a coroutine. Parameters ---------- self : ``VoiceClient`` The respective voice client. last_source : `None`, ``AudioSource`` The audio what was played. """ player = self.player queue = self.queue if player is None: if not queue: return source = queue.pop(0) self.player = AudioPlayer(self, source) if self.connected.is_set(): Task(self.set_speaking(1), KOKORO) return if not queue: player.set_source(None) if self.connected.is_set(): Task(self.set_speaking(0), KOKORO) return source = queue.pop(0) player.set_source(source) if self.connected.is_set(): Task(self.set_speaking(1), KOKORO) @staticmethod async def _loop_actual(self, last_source): """ Repeats the last played audio if applicable. Should be used inside of ``.lock``to ensure that the voice client is not modified parallelly. This function is a coroutine. Parameters ---------- self : ``VoiceClient`` The respective voice client. last_source : `None`, ``AudioSource`` The audio what was played. """ if (last_source is None) or (not last_source.REPEATABLE): await self._play_next(self, None) return player = self.player if player is None: # Should not happen, lol self.player = AudioPlayer(self, last_source) else: # The audio was over. player.set_source(last_source) if self.connected.is_set(): Task(self.set_speaking(1), KOKORO) @staticmethod async def _loop_queue(self, last_source): """ Puts the last played audio back on the voice client's queue. Should be used inside of ``.lock``to ensure that the voice client is not modified parallelly. This function is a coroutine. Parameters ---------- self : ``VoiceClient`` The respective voice client. last_source : `None`, ``AudioSource`` The audio what was played. """ if (last_source is not None) and last_source.REPEATABLE: # The last source was not skipped an we can repeat it. self.queue.append(last_source) await self._play_next(self, None) 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 _terminate_handshake(self): """ Called when connecting to Discord fails. Ensures, that everything is aborted correctly. This method is a coroutine. """ self._handshake_complete.clear() gateway = self.client.gateway_for(self.guild_id) try: await gateway.change_voice_state(self.guild_id, 0, self_mute=True) except ConnectionClosed: pass kokoro = self.gateway.kokoro if (kokoro is not None): kokoro.terminate() async def _create_socket(self, event): """ Called when voice server update data is received from Discord. If full data was received, closes the actual socket if exists and creates a new one connected to the received address. If the voice client is already connected reconnects it, if not then marks it as connected. This method is a coroutine. Parameters ---------- event : ``VoiceServerUpdateEvent`` Voice server update event. """ self.connected.clear() gateway = self.client.gateway_for(self.guild_id) self._session_id = gateway.session_id token = event.token self._token = token endpoint = event.endpoint if (endpoint is None) or (token is None): return self._endpoint = endpoint.replace(':80', '').replace(':443', '') self._maybe_close_socket() socket = module_socket.socket(module_socket.AF_INET, module_socket.SOCK_DGRAM) protocol = await KOKORO.create_datagram_connection_with(partial_func( DatagramMergerReadProtocol, KOKORO), socket=socket) self._transport = protocol.get_transport() self._protocol = protocol self._socket = socket if self.reader is None: self.reader = AudioReader(self) handshake_complete = self._handshake_complete if handshake_complete.is_done(): # terminate the websocket and handle the reconnect loop if necessary. handshake_complete.clear() await self.gateway.terminate() else: handshake_complete.set_result(None) def send_packet(self, packet): """ Sends the given packet to Discord with the voice client's socket. Parameters ---------- packet : `bytes-like` The packet to send. """ transport = self._transport if (transport is not None): transport.send_to(packet, (self._endpoint_ip, self._audio_port)) def __del__(self): """Stops and unallocates the resources by the voice client, if was not done already.""" self.stop() self.connected.set() player = self.player if (player is not None): self.player = None player.stop() reader = self.reader if (reader is not None): self.reader = None reader.stop() self._maybe_close_socket() def _maybe_close_socket(self): """ Closes the voice client's socket and transport if they are set. """ protocol = self._protocol if (protocol is not None): self._protocol = None self._transport = None protocol.close() socket = self._socket if socket is not None: self._socket = None socket.close() def _maybe_change_voice_region(self): """ Resets the voice region of the voice client. Returns ------- changed: `bool` Whether voice region changed. """ region = try_get_voice_region(self.guild_id, self.channel_id) if region is self.region: changed = False else: self.region = region changed = True return changed def __repr__(self): """Returns the voice client's representation.""" repr_parts = [ '<', self.__class__.__name__, ' client=', repr(self.client.full_name), ', channel_id=', repr(self.channel_id), ', guild_id=', repr(self.guild_id), ] repr_parts.append('>') return ''.join(repr_parts) @property def channel(self): """ Returns the voice client's channel. Returns ------- channel : `None`, ``ChannelVoiceBase`` """ return CHANNELS.get(self.channel_id, None) @property def guild(self): """ Returns the voice client's guild. Returns ------- guild : `None`, ``Guild`` """ return GUILDS.get(self.guild_id, None)