Beispiel #1
0
    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
Beispiel #2
0
    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
Beispiel #3
0
    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()
Beispiel #4
0
 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
Beispiel #5
0
 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()
Beispiel #6
0
 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
Beispiel #7
0
    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
Beispiel #8
0
 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)
Beispiel #9
0
    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