Ejemplo n.º 1
0
    def __init__(self, token):
        super(HTTPClient, self).__init__()

        self.limiter = RateLimiter()
        self.headers = {
            'Authorization': 'Bot ' + token,
        }
Ejemplo n.º 2
0
    def __init__(self, token):
        super(HTTPClient, self).__init__()

        py_version = '{}.{}.{}'.format(sys.version_info.major,
                                       sys.version_info.minor,
                                       sys.version_info.micro)

        self.limiter = RateLimiter()
        self.headers = {
            'Authorization':
            'Bot ' + token,
            'User-Agent':
            'DiscordBot (https://github.com/b1naryth1ef/disco {}) Python/{} requests/{}'
            .format(disco_version, py_version, requests_version),
        }
Ejemplo n.º 3
0
class HTTPClient(LoggingClass):
    """
    A simple HTTP client which wraps the requests library, adding support for
    Discords rate-limit headers, authorization, and request/response validation.
    """
    BASE_URL = 'https://discordapp.com/api/v7'
    MAX_RETRIES = 5

    def __init__(self, token):
        super(HTTPClient, self).__init__()

        py_version = '{}.{}.{}'.format(sys.version_info.major,
                                       sys.version_info.minor,
                                       sys.version_info.micro)

        self.limiter = RateLimiter()
        self.headers = {
            'User-Agent':
            'DiscordBot (https://github.com/b1naryth1ef/disco {}) Python/{} requests/{}'
            .format(disco_version, py_version, requests_version),
        }

        if token:
            self.headers['Authorization'] = 'Bot ' + token

    def __call__(self, route, args=None, **kwargs):
        return self.call(route, args, **kwargs)

    def call(self, route, args=None, **kwargs):
        """
        Makes a request to the given route (as specified in
        :class:`disco.api.http.Routes`) with a set of URL arguments, and keyword
        arguments passed to requests.

        Parameters
        ----------
        route : tuple(:class:`HTTPMethod`, str)
            The method.URL combination that when compiled with URL arguments
            creates a requestable route which the HTTPClient will make the
            request too.
        args : dict(str, str)
            A dictionary of URL arguments that will be compiled with the raw URL
            to create the requestable route. The HTTPClient uses this to track
            rate limits as well.
        kwargs : dict
            Keyword arguments that will be passed along to the requests library

        Raises
        ------
        APIException
            Raised when an unrecoverable error occurs, or when we've exhausted
            the number of retries.

        Returns
        -------
        :class:`requests.Response`
            The response object for the request
        """
        args = args or {}
        retry = kwargs.pop('retry_number', 0)

        # Merge or set headers
        if 'headers' in kwargs:
            kwargs['headers'].update(self.headers)
        else:
            kwargs['headers'] = self.headers

        # Build the bucket URL
        args = {k: to_bytes(v) for k, v in six.iteritems(args)}
        filtered = {
            k: (v if k in ('guild', 'channel') else '')
            for k, v in six.iteritems(args)
        }
        bucket = (route[0].value, route[1].format(**filtered))

        # Possibly wait if we're rate limited
        self.limiter.check(bucket)

        self.log.debug('KW: %s', kwargs)

        # Make the actual request
        url = self.BASE_URL + route[1].format(**args)
        self.log.info('%s %s (%s)', route[0].value, url, kwargs.get('params'))
        r = requests.request(route[0].value, url, **kwargs)

        # Update rate limiter
        self.limiter.update(bucket, r)

        # If we got a success status code, just return the data
        if r.status_code < 400:
            return r
        elif r.status_code != 429 and 400 <= r.status_code < 500:
            self.log.warning('Request failed with code %s: %s', r.status_code,
                             r.content)
            raise APIException(r)
        else:
            if r.status_code == 429:
                self.log.warning(
                    'Request responded w/ 429, retrying (but this should not happen, check your clock sync'
                )

            # If we hit the max retries, throw an error
            retry += 1
            if retry > self.MAX_RETRIES:
                self.log.error('Failing request, hit max retries')
                raise APIException(r, retries=self.MAX_RETRIES)

            backoff = self.random_backoff()
            self.log.warning(
                'Request to `{}` failed with code {}, retrying after {}s ({})'.
                format(url, r.status_code, backoff, r.content))
            gevent.sleep(backoff)

            # Otherwise just recurse and try again
            return self(route, args, retry_number=retry, **kwargs)

    @staticmethod
    def random_backoff():
        """
        Returns a random backoff (in milliseconds) to be used for any error the
        client suspects is transient. Will always return a value between 500 and
        5000 milliseconds.

        :returns: a random backoff in milliseconds
        :rtype: float
        """
        return random.randint(500, 5000) / 1000.0