From 6d33ea748a1f4655350cbb6a5dc545498501d548 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 9 Jan 2017 18:51:24 -0500 Subject: [PATCH] Implement rate limit headers. --- discord/http.py | 316 ++++++++++++++++++++++++++++-------------------- 1 file changed, 185 insertions(+), 131 deletions(-) diff --git a/discord/http.py b/discord/http.py index 4e5410d08..146706328 100644 --- a/discord/http.py +++ b/discord/http.py @@ -29,8 +29,9 @@ import asyncio import json import sys import logging -import inspect import weakref +import datetime +from email.utils import parsedate_to_datetime from random import randint as random_integer log = logging.getLogger(__name__) @@ -45,9 +46,41 @@ def json_or_text(response): return json.loads(text) return text -def _func_(): - # emulate __func__ from C++ - return inspect.currentframe().f_back.f_code.co_name +class Route: + BASE = 'https://discordapp.com/api/v6' + + def __init__(self, method, path, **parameters): + self.path = path + self.method = method + url = (self.BASE + self.path) + if parameters: + self.url = url.format(**parameters) + else: + self.url = url + + # major parameters: + self.channel_id = parameters.get('channel_id') + self.guild_id = parameters.get('guild_id') + + @property + def bucket(self): + # the bucket is just method + path w/ major parameters + return '{0.method}:{0.channel_id}:{0.guild_id}:{0.path}'.format(self) + +class MaybeUnlock: + def __init__(self, lock): + self.lock = lock + self._unlock = True + + def __enter__(self): + return self + + def defer(self): + self._unlock = False + + def __exit__(self, type, value, traceback): + if self._unlock: + self.lock.release() class HTTPClient: """Represents an HTTP client sending HTTP requests to the Discord API.""" @@ -72,6 +105,7 @@ class HTTPClient: self.connector = connector self.session = aiohttp.ClientSession(connector=connector, loop=self.loop) self._locks = weakref.WeakValueDictionary() + self._global_lock = asyncio.Lock(loop=self.loop) self.token = None self.bot_token = False @@ -79,7 +113,11 @@ class HTTPClient: self.user_agent = user_agent.format(__version__, sys.version_info, aiohttp.__version__) @asyncio.coroutine - def request(self, method, url, *, bucket=None, **kwargs): + def request(self, route, *, header_bypass_delay=None, **kwargs): + bucket = route.bucket + method = route.method + url = route.url + lock = self._locks.get(bucket) if lock is None: lock = asyncio.Lock(loop=self.loop) @@ -100,7 +138,13 @@ class HTTPClient: kwargs['data'] = utils.to_json(kwargs.pop('json')) kwargs['headers'] = headers - with (yield from lock): + + if self._global_lock.locked(): + # wait until the global lock is complete + yield from self._global_lock + + yield from lock + with MaybeUnlock(lock) as maybe_lock: for tries in range(5): r = yield from self.session.request(method, url, **kwargs) log.debug(self.REQUEST_LOG.format(method=method, url=url, status=r.status, json=kwargs.get('data'))) @@ -108,6 +152,22 @@ class HTTPClient: # even errors have text involved in them so this is safe to call data = yield from json_or_text(r) + # check if we have rate limit header information + remaining = r.headers.get('X-Ratelimit-Remaining') + if remaining == '0': + # we've depleted our current bucket + if header_bypass_delay is None: + now = parsedate_to_datetime(r.headers['Date']) + reset = datetime.datetime.fromtimestamp(int(r.headers['X-Ratelimit-Reset']), datetime.timezone.utc) + delta = (reset - now).total_seconds() + else: + delta = header_bypass_delay + + fmt = 'A rate limit bucket has been exhausted (bucket: {bucket}, retry: {delta}).' + log.info(fmt.format(bucket=bucket, delta=delta)) + maybe_lock.defer() + self.loop.call_later(delta, lock.release) + # the request was successful so just return the text/json if 300 > r.status >= 200: log.debug(self.SUCCESS_LOG.format(method=method, url=url, text=data)) @@ -120,7 +180,21 @@ class HTTPClient: # sleep a bit retry_after = data['retry_after'] / 1000.0 log.info(fmt.format(retry_after, bucket)) + + # check if it's a global rate limit + is_global = data.get('global', False) + if is_global: + log.info('Global rate limit has been hit. Retrying in {:.2} seconds.'.format(retry_after)) + # acquire the global lock and block all processing + yield from self._global_lock + yield from asyncio.sleep(retry_after, loop=self.loop) + + # release the global lock now that the + # global rate limit has passed + if is_global: + self._global_lock.release() + continue # we've received a 502, unconditional retry @@ -177,7 +251,7 @@ class HTTPClient: } try: - data = yield from self.post(self.LOGIN, json=payload, bucket=_func_()) + data = yield from self.request(Route('POST', '/auth/login'), json=payload) except HTTPException as e: if e.response.status == 400: raise LoginFailure('Improper credentials have been passed.') from e @@ -192,7 +266,7 @@ class HTTPClient: self._token(token, bot=bot) try: - data = yield from self.get(self.ME) + data = yield from self.request(Route('GET', '/users/@me')) except HTTPException as e: self._token(old_token, bot=old_bot) if e.response.status == 401: @@ -202,7 +276,7 @@ class HTTPClient: return data def logout(self): - return self.post(self.LOGOUT, bucket=_func_()) + return self.request(Route('POST', '/auth/logout')) # Message management @@ -211,10 +285,12 @@ class HTTPClient: 'recipient_id': user_id } - return self.post(self.ME + '/channels', json=payload, bucket=_func_()) + return self.request(Route('POST', '/users/@me/channels'), json=payload) + + # TODO: remove guild_id parameters here def send_message(self, channel_id, content, *, guild_id=None, tts=False, embed=None): - url = '{0.CHANNELS}/{1}/messages'.format(self, channel_id) + r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) payload = { 'nonce': random_integer(-2**63, 2**63 - 1) } @@ -228,14 +304,13 @@ class HTTPClient: if embed: payload['embed'] = embed - return self.post(url, json=payload, bucket='messages:' + str(guild_id)) + return self.request(r, json=payload) def send_typing(self, channel_id): - url = '{0.CHANNELS}/{1}/typing'.format(self, channel_id) - return self.post(url, bucket=_func_()) + return self.request(Route('POST', '/channels/{channel_id}/typing', channel_id=channel_id)) def send_file(self, channel_id, buffer, *, guild_id=None, filename=None, content=None, tts=False, embed=None): - url = '{0.CHANNELS}/{1}/messages'.format(self, channel_id) + r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) form = aiohttp.FormData() payload = {'tts': tts} @@ -247,23 +322,24 @@ class HTTPClient: form.add_field('payload_json', utils.to_json(payload)) form.add_field('file', buffer, filename=filename, content_type='application/octet-stream') - return self.post(url, data=form, bucket='messages:' + str(guild_id)) + return self.request(r, data=form) def delete_message(self, channel_id, message_id, guild_id=None): - url = '{0.CHANNELS}/{1}/messages/{2}'.format(self, channel_id, message_id) - bucket = '{}:{}'.format(_func_(), guild_id) - return self.delete(url, bucket=bucket) + r = Route('DELETE', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, + message_id=message_id) + return self.request(r) def delete_messages(self, channel_id, message_ids, guild_id=None): - url = '{0.CHANNELS}/{1}/messages/bulk_delete'.format(self, channel_id) + r = Route('POST', '/channels/{channel_id}/messages/bulk_delete', channel_id=channel_id) payload = { 'messages': message_ids } - bucket = '{}:{}'.format(_func_(), guild_id) - return self.post(url, json=payload, bucket=bucket) + + return self.request(r, json=payload) def edit_message(self, message_id, channel_id, content, *, guild_id=None, embed=None): - url = '{0.CHANNELS}/{1}/messages/{2}'.format(self, channel_id, message_id) + r = Route('PATCH', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, + message_id=message_id) payload = {} if content: @@ -272,33 +348,36 @@ class HTTPClient: if embed: payload['embed'] = embed - return self.patch(url, json=payload, bucket='messages:' + str(guild_id)) + return self.request(r, json=payload) def add_reaction(self, message_id, channel_id, emoji): - url = '{0.CHANNELS}/{1}/messages/{2}/reactions/{3}/@me'.format( - self, channel_id, message_id, emoji) - return self.put(url, bucket='%s:%s' % (_func_(), channel_id)) + r = Route('PUT', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', + channel_id=channel_id, message_id=message_id, emoji=emoji) + return self.request(r, header_bypass_delay=0.25) def remove_reaction(self, message_id, channel_id, emoji, member_id): - url = '{0.CHANNELS}/{1}/messages/{2}/reactions/{3}/{4}'.format( - self, channel_id, message_id, emoji, member_id) - return self.delete(url, bucket='%s:%s' % (_func_(), channel_id)) + r = Route('DELETE', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{member_id}', + channel_id=channel_id, message_id=message_id, member_id=member_id, emoji=emoji) + return self.request(r, header_bypass_delay=0.25) def get_reaction_users(self, message_id, channel_id, emoji, limit, after=None): - url = '{0.CHANNELS}/{1}/messages/{2}/reactions/{3}'.format( - self, channel_id, message_id, emoji) + r = Route('GET', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}', + channel_id=channel_id, message_id=message_id, emoji=emoji) + params = {'limit': limit} if after: params['after'] = after - return self.get(url, params=params, bucket='%s:%s' % (_func_(), channel_id)) + return self.request(r, params=params) def clear_reactions(self, message_id, channel_id): - url = '{0.CHANNELS}/{1}/messages/{2}/reactions'.format(self, channel_id, message_id) - return self.delete(url, bucket='%s:%s' % (_func_(), channel_id)) + r = Route('DELETE', '/channels/{channel_id}/messages/{message_id}/reactions', + channel_id=channel_id, message_id=message_id) + + return self.request(r) def get_message(self, channel_id, message_id): - url = '{0.CHANNELS}/{1}/messages/{2}'.format(self, channel_id, message_id) - return self.get(url, bucket=_func_()) + r = Route('GET', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, message_id=message_id) + return self.request(r) def logs_from(self, channel_id, limit, before=None, after=None, around=None): url = '{0.CHANNELS}/{1}/messages'.format(self, channel_id) @@ -313,39 +392,38 @@ class HTTPClient: if around: params['around'] = around - return self.get(url, params=params, bucket=_func_()) + return self.request(Route('GET', '/channels/{channel_id}/messages', channel_id=channel_id), params=params) def pin_message(self, channel_id, message_id): - url = '{0.CHANNELS}/{1}/pins/{2}'.format(self, channel_id, message_id) - return self.put(url, bucket=_func_()) + return self.request(Route('PUT', '/channels/{channel_id}/pins/{message_id}', + channel_id=channel_id, message_id=message_id)) def unpin_message(self, channel_id, message_id): - url = '{0.CHANNELS}/{1}/pins/{2}'.format(self, channel_id, message_id) - return self.delete(url, bucket=_func_()) + return self.request(Route('DELETE', '/channels/{channel_id}/pins/{message_id}', + channel_id=channel_id, message_id=message_id)) def pins_from(self, channel_id): - url = '{0.CHANNELS}/{1}/pins'.format(self, channel_id) - return self.get(url, bucket=_func_()) + return self.request(Route('GET', '/channels/{channel_id}/pins/', channel_id=channel_id)) # Member management def kick(self, user_id, guild_id): - url = '{0.GUILDS}/{1}/members/{2}'.format(self, guild_id, user_id) - return self.delete(url, bucket=_func_()) + r = Route('DELETE', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) + return self.request(r) def ban(self, user_id, guild_id, delete_message_days=1): - url = '{0.GUILDS}/{1}/bans/{2}'.format(self, guild_id, user_id) + r = Route('PUT', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) params = { 'delete-message-days': delete_message_days } - return self.put(url, params=params, bucket=_func_()) + return self.request(r, params=params) def unban(self, user_id, guild_id): - url = '{0.GUILDS}/{1}/bans/{2}'.format(self, guild_id, user_id) - return self.delete(url, bucket=_func_()) + r = Route('DELETE', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) + return self.request(r) def guild_voice_state(self, user_id, guild_id, *, mute=None, deafen=None): - url = '{0.GUILDS}/{1}/members/{2}'.format(self, guild_id, user_id) + r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) payload = {} if mute is not None: payload['mute'] = mute @@ -353,7 +431,7 @@ class HTTPClient: if deafen is not None: payload['deaf'] = deafen - return self.patch(url, json=payload, bucket='members:' + str(guild_id)) + return self.request(r, json=payload) def edit_profile(self, password, username, avatar, **fields): payload = { @@ -368,28 +446,24 @@ class HTTPClient: if 'new_password' in fields: payload['new_password'] = fields['new_password'] - return self.patch(self.ME, json=payload, bucket=_func_()) + return self.request(Route('PATCH', '/users/@me'), json=payload) def change_my_nickname(self, guild_id, nickname): - url = '{0.GUILDS}/{1}/members/@me/nick'.format(self, guild_id) payload = { 'nick': nickname } - bucket = '{}:{}'.format(_func_(), guild_id) - return self.patch(url, json=payload, bucket=bucket) + return self.request(Route('PATCH', '/guilds/{guild_id}/members/@me/nick', guild_id=guild_id), json=payload) def change_nickname(self, guild_id, user_id, nickname): - url = '{0.GUILDS}/{1}/members/{2}'.format(self, guild_id, user_id) + r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) payload = { 'nick': nickname } - bucket = 'members:{}'.format(guild_id) - return self.patch(url, json=payload, bucket=bucket) + return self.request(r, json=payload) def edit_member(self, guild_id, user_id, **fields): - url = '{0.GUILDS}/{1}/members/{2}'.format(self, guild_id, user_id) - bucket = 'members:%s' % guild_id - return self.patch(url, json=fields, bucket=bucket) + r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) + return self.request(r, json=fields, bucket=bucket) # Channel management @@ -401,10 +475,9 @@ class HTTPClient: k: v for k, v in options.items() if k in valid_keys } - return self.patch(url, json=payload, bucket=_func_()) + return self.request(Route('PATCH', '/channels/{channel_id}', channel_id=channel_id), json=payload) def create_channel(self, guild_id, name, channe_type, permission_overwrites=None): - url = '{0.GUILDS}/{1}/channels'.format(self, guild_id) payload = { 'name': name, 'type': channe_type @@ -413,21 +486,18 @@ class HTTPClient: if permission_overwrites is not None: payload['permission_overwrites'] = permission_overwrites - return self.post(url, json=payload, bucket=_func_()) + return self.request(Route('POST', '/guilds/{guild_id}/channels', guild_id=guild_id), json=payload) def delete_channel(self, channel_id): - url = '{0.CHANNELS}/{1}'.format(self, channel_id) - return self.delete(url, bucket=_func_()) + return self.request(Route('DELETE', '/channels/{channel_id}', channel_id=channel_id)) # Guild management def leave_guild(self, guild_id): - url = '{0.USERS}/@me/guilds/{1}'.format(self, guild_id) - return self.delete(url, bucket=_func_()) + return self.request(Route('DELETE', '/users/@me/guilds/{guild_id}', guild_id=guild_id)) def delete_guild(self, guild_id): - url = '{0.GUILDS}/{1}'.format(self, guild_id) - return self.delete(url, bucket=_func_()) + return self.request(Route('DELETE', '/guilds/{guild_id}', guild_id=guild_id)) def create_guild(self, name, region, icon): payload = { @@ -436,7 +506,7 @@ class HTTPClient: 'region': region } - return self.post(self.GUILDS, json=payload, bucket=_func_()) + return self.request(Route('POST', '/guilds'), json=payload) def edit_guild(self, guild_id, **fields): valid_keys = ('name', 'region', 'icon', 'afk_timeout', 'owner_id', @@ -446,26 +516,22 @@ class HTTPClient: k: v for k, v in fields.items() if k in valid_keys } - url = '{0.GUILDS}/{1}'.format(self, guild_id) - return self.patch(url, json=payload, bucket=_func_()) + return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload) def get_bans(self, guild_id): - url = '{0.GUILDS}/{1}/bans'.format(self, guild_id) - return self.get(url, bucket=_func_()) + return self.request(Route('GET', '/guilds/{guild_id}/bans', guild_id=guild_id)) def prune_members(self, guild_id, days): - url = '{0.GUILDS}/{1}/prune'.format(self, guild_id) params = { 'days': days } - return self.post(url, params=params, bucket=_func_()) + return self.request(Route('POST', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params) def estimate_pruned_members(self, guild_id, days): - url = '{0.GUILDS}/{1}/prune'.format(self, guild_id) params = { 'days': days } - return self.get(url, params=params, bucket=_func_()) + return self.request(Route('GET', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params) def create_custom_emoji(self, guild_id, name, image): payload = { @@ -473,117 +539,105 @@ class HTTPClient: 'image': image } - bucket = '%s:%s' % (_func_(), guild_id) - return self.post('{0.GUILDS}/{1}/emojis'.format(self, guild_id), json=payload, bucket=bucket) + r = Route('POST', '/guilds/{guild_id}/emojis', guild_id=guild_id) + return self.request(r, json=payload) def delete_custom_emoji(self, guild_id, emoji_id): - url = '{0.GUILDS}/{1}/emojis/{2}'.format(self, guild_id, emoji_id) - bucket = '%s:%s' % (_func_(), guild_id) - return self.delete(url, bucket=bucket) + return self.request(Route('DELETE', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id)) def edit_custom_emoji(self, guild_id, emoji_id, *, name): payload = { 'name': name } - url = '{0.GUILDS}/{1}/emojis/{2}'.format(self, guild_id, emoji_id) - bucket = '%s:%s' % (_func_(), guild_id) - return self.patch(url, bucket=bucket, json=payload) + r = Route('PATCH', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) + return self.request(r, json=payload) # Invite management def create_invite(self, channel_id, **options): - url = '{0.CHANNELS}/{1}/invites'.format(self, channel_id) + r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) payload = { 'max_age': options.get('max_age', 0), 'max_uses': options.get('max_uses', 0), - 'temporary': options.get('temporary', False), - 'xkcdpass': options.get('xkcd', False) + 'temporary': options.get('temporary', False) } - return self.post(url, json=payload, bucket=_func_()) + return self.request(r, json=payload) def get_invite(self, invite_id): - url = '{0.API_BASE}/invite/{1}'.format(self, invite_id) - return self.get(url, bucket=_func_()) + return self.request(Route('GET', '/invite/{invite_id}', invite_id=invite_id)) def invites_from(self, guild_id): - url = '{0.GUILDS}/{1}/invites'.format(self, guild_id) - return self.get(url, bucket=_func_()) + return self.request(Route('GET', '/guilds/{guild_id}/invites', guild_id=guild_id)) + + def invites_from_channel(self, channel_id): + return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id)) def accept_invite(self, invite_id): - url = '{0.API_BASE}/invite/{1}'.format(self, invite_id) - return self.post(url, bucket=_func_()) + return self.request(Route('POST', '/invite/{invite_id}', invite_id=invite_id)) def delete_invite(self, invite_id): - url = '{0.API_BASE}/invite/{1}'.format(self, invite_id) - return self.delete(url, bucket=_func_()) + return self.request(Route('DELETE', '/invite/{invite_id}', invite_id=invite_id)) # Role management def edit_role(self, guild_id, role_id, **fields): - url = '{0.GUILDS}/{1}/roles/{2}'.format(self, guild_id, role_id) + r = Route('PATCH', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable') payload = { k: v for k, v in fields.items() if k in valid_keys } - return self.patch(url, json=payload, bucket='roles:' + str(guild_id)) + return self.request(r, json=payload) def delete_role(self, guild_id, role_id): - url = '{0.GUILDS}/{1}/roles/{2}'.format(self, guild_id, role_id) - return self.delete(url, bucket=_func_()) + r = Route('DELETE', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) + return self.request(r) def replace_roles(self, user_id, guild_id, role_ids): - url = '{0.GUILDS}/{1}/members/{2}'.format(self, guild_id, user_id) - payload = { - 'roles': role_ids - } - return self.patch(url, json=payload, bucket='members:' + str(guild_id)) + return self.edit_member(guild_id=guild_id, user_id=user_id, roles=role_ids) def create_role(self, guild_id): - url = '{0.GUILDS}/{1}/roles'.format(self, guild_id) - return self.post(url, bucket=_func_()) + r = Route('POST', '/guilds/{guild_id}/roles/', guild_id=guild_id) + return self.request(r) - def add_role(self, guild_id, member_id, role_id): - url = '{0.GUILDS}/{1}/members/{2}/{3}'.format(self, guild_id, member_id, role_id) - return self.put(url, bucket='%s:%s' % (_func_(), guild_id)) + def add_role(self, guild_id, user_id, role_id): + r = Route('PUT', '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', + guild_id=guild_id, user_id=user_id, role_id=role_id) + return self.request(r) - def remove_role(self, guild_id, member_id, role_id): - url = '{0.GUILDS}/{1}/members/{2}/{3}'.format(self, guild_id, member_id, role_id) - return self.delete(url, bucket='%s:%s' % (_func_(), guild_id)) + def remove_role(self, guild_id, user_id, role_id): + r = Route('DELETE', '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', + guild_id=guild_id, user_id=user_id, role_id=role_id) + return self.request(r) def edit_channel_permissions(self, channel_id, target, allow, deny, type): - url = '{0.CHANNELS}/{1}/permissions/{2}'.format(self, channel_id, target) payload = { 'id': target, 'allow': allow, 'deny': deny, 'type': type } - return self.put(url, json=payload, bucket=_func_()) + r = Route('PUT', '/channels/{channel_id}/permissions/{target}', channel_id=channel_id, target=target) + return self.request(r, json=payload) def delete_channel_permissions(self, channel_id, target): - url = '{0.CHANNELS}/{1}/permissions/{2}'.format(self, channel_id, target) - return self.delete(url, bucket=_func_()) + r = Route('DELETE', '/channels/{channel_id}/permissions/{target}', channel_id=channel_id, target=target) + return self.request(r) # Voice management def move_member(self, user_id, guild_id, channel_id): - url = '{0.GUILDS}/{1}/members/{2}'.format(self, guild_id, user_id) - payload = { - 'channel_id': channel_id - } - return self.patch(url, json=payload, bucket='members:' + str(guild_id)) + return self.edit_member(guild_id=guild_id, user_id=user_id, channel_id=channel_id) # Misc def application_info(self): - url = '{0.APPLICATIONS}/@me'.format(self) - return self.get(url, bucket=_func_()) + return self.request(Route('GET', '/oauth/applications/@me')) @asyncio.coroutine def get_gateway(self): try: - data = yield from self.get(self.GATEWAY, bucket=_func_()) + data = yield from self.request(Route('GET', '/gateway')) except HTTPException as e: raise GatewayNotFound() from e return data.get('url') + '?encoding=json&v=6' @@ -591,11 +645,11 @@ class HTTPClient: @asyncio.coroutine def get_bot_gateway(self): try: - data = yield from self.get(self.GATEWAY + '/bot', bucket=_func_()) + data = yield from self.request(Route('GET', '/gateway/bot')) except HTTPException as e: raise GatewayNotFound() from e else: return data['shards'], data['url'] + '?encoding=json&v=6' def get_user_info(self, user_id): - return self.get('{0.USERS}/{1}'.format(self, user_id), bucket=_func_()) + return self.request(Route('GET', '/users/{user_id}', user_id=user_id))