diff --git a/discord/client.py b/discord/client.py index 77b714669..1c10f2cbc 100644 --- a/discord/client.py +++ b/discord/client.py @@ -411,7 +411,184 @@ class Client: log.info('sending reconnection frame to websocket {}'.format(payload)) yield from self.ws.send(utils.to_json(payload)) - # properties + # login state management + + @asyncio.coroutine + def login(self, email, password): + """|coro| + + Logs in the client with the specified credentials. + + Parameters + ---------- + email : str + The email used to login. + password : str + The password used to login. + + Raises + ------ + LoginFailure + The wrong credentials are passed. + HTTPException + An unknown HTTP related error occurred, + usually when it isn't 200 or the known incorrect credentials + passing status code. + """ + + # attempt to read the token from cache + if self.cache_auth: + yield from self._login_via_cache(email, password) + if self.is_logged_in: + return + + payload = { + 'email': email, + 'password': password + } + + data = utils.to_json(payload) + resp = yield from aiohttp.post(endpoints.LOGIN, data=data, headers=self.headers, loop=self.loop) + log.debug(request_logging_format.format(method='POST', response=resp)) + if resp.status != 200: + yield from resp.release() + if resp.status == 400: + raise LoginFailure('Improper credentials have been passed.') + else: + raise HTTPException(resp, None) + + log.info('logging in returned status code {}'.format(resp.status)) + self.email = email + + body = yield from resp.json() + self.token = body['token'] + self.headers['authorization'] = self.token + self._is_logged_in.set() + + # since we went through all this trouble + # let's make sure we don't have to do it again + if self.cache_auth: + self._update_cache(email, password) + + @asyncio.coroutine + def logout(self): + """|coro| + + Logs out of Discord and closes all connections.""" + response = yield from aiohttp.post(endpoints.LOGOUT, headers=self.headers, loop=self.loop) + yield from response.release() + yield from self.close() + self._is_logged_in.clear() + log.debug(request_logging_format.format(method='POST', response=response)) + + @asyncio.coroutine + def connect(self): + """|coro| + + Creates a websocket connection and connects to the websocket listen + to messages from discord. + + This function is implemented using a while loop in the background. + If you need to run this event listening in another thread then + you should run it in an executor or schedule the coroutine to + be executed later using ``loop.create_task``. + + Raises + ------- + ClientException + If this is called before :meth:`login` was invoked successfully + or when an unexpected closure of the websocket occurs. + GatewayNotFound + If the gateway to connect to discord is not found. Usually if this + is thrown then there is a discord API outage. + """ + self.gateway = yield from self._get_gateway() + yield from self._make_websocket() + + while not self.is_closed: + msg = yield from self.ws.recv() + if msg is None: + if self.ws.close_code == 1012: + yield from self.redirect_websocket(self.gateway) + continue + elif not self._is_ready.is_set(): + raise ClientException('Unexpected websocket closure received') + else: + yield from self.close() + break + + yield from self.received_message(msg) + + @asyncio.coroutine + def close(self): + """Closes the websocket connection. + + To reconnect the websocket connection, :meth:`connect` must be used. + """ + if self.is_closed: + return + + if self.is_voice_connected(): + yield from self.voice.disconnect() + self.voice = None + + if self.ws.open: + yield from self.ws.close() + + self.keep_alive.cancel() + self._closed.set() + self._is_ready.clear() + + @asyncio.coroutine + def start(self, email, password): + """|coro| + + A shorthand coroutine for :meth:`login` + :meth:`connect`. + """ + yield from self.login(email, password) + yield from self.connect() + + def run(self, email, password): + """A blocking call that abstracts away the `event loop`_ + initialisation from you. + + If you want more control over the event loop then this + function should not be used. Use :meth:`start` coroutine + or :meth:`connect` + :meth:`login`. + + Roughly Equivalent to: :: + + try: + loop.run_until_complete(start(email, password)) + except KeyboardInterrupt: + loop.run_until_complete(logout()) + # cancel all tasks lingering + finally: + loop.close() + + Warning + -------- + This function must be the last function to call due to the fact that it + is blocking. That means that registration of events or anything being + called after this function call will not execute until it returns. + """ + + try: + self.loop.run_until_complete(self.start(email, password)) + except KeyboardInterrupt: + self.loop.run_until_complete(self.logout()) + pending = asyncio.Task.all_tasks() + gathered = asyncio.gather(*pending) + try: + gathered.cancel() + self.loop.run_forever() + gathered.exception() + except: + pass + finally: + self.loop.close() + + # properties @property def is_logged_in(self): @@ -605,183 +782,6 @@ class Client: message = None return message - # login state management - - @asyncio.coroutine - def login(self, email, password): - """|coro| - - Logs in the client with the specified credentials. - - Parameters - ---------- - email : str - The email used to login. - password : str - The password used to login. - - Raises - ------ - LoginFailure - The wrong credentials are passed. - HTTPException - An unknown HTTP related error occurred, - usually when it isn't 200 or the known incorrect credentials - passing status code. - """ - - # attempt to read the token from cache - if self.cache_auth: - yield from self._login_via_cache(email, password) - if self.is_logged_in: - return - - payload = { - 'email': email, - 'password': password - } - - data = utils.to_json(payload) - resp = yield from aiohttp.post(endpoints.LOGIN, data=data, headers=self.headers, loop=self.loop) - log.debug(request_logging_format.format(method='POST', response=resp)) - if resp.status != 200: - yield from resp.release() - if resp.status == 400: - raise LoginFailure('Improper credentials have been passed.') - else: - raise HTTPException(resp, None) - - log.info('logging in returned status code {}'.format(resp.status)) - self.email = email - - body = yield from resp.json() - self.token = body['token'] - self.headers['authorization'] = self.token - self._is_logged_in.set() - - # since we went through all this trouble - # let's make sure we don't have to do it again - if self.cache_auth: - self._update_cache(email, password) - - @asyncio.coroutine - def logout(self): - """|coro| - - Logs out of Discord and closes all connections.""" - response = yield from aiohttp.post(endpoints.LOGOUT, headers=self.headers, loop=self.loop) - yield from response.release() - yield from self.close() - self._is_logged_in.clear() - log.debug(request_logging_format.format(method='POST', response=response)) - - @asyncio.coroutine - def connect(self): - """|coro| - - Creates a websocket connection and connects to the websocket listen - to messages from discord. - - This function is implemented using a while loop in the background. - If you need to run this event listening in another thread then - you should run it in an executor or schedule the coroutine to - be executed later using ``loop.create_task``. - - Raises - ------- - ClientException - If this is called before :meth:`login` was invoked successfully - or when an unexpected closure of the websocket occurs. - GatewayNotFound - If the gateway to connect to discord is not found. Usually if this - is thrown then there is a discord API outage. - """ - self.gateway = yield from self._get_gateway() - yield from self._make_websocket() - - while not self.is_closed: - msg = yield from self.ws.recv() - if msg is None: - if self.ws.close_code == 1012: - yield from self.redirect_websocket(self.gateway) - continue - elif not self._is_ready.is_set(): - raise ClientException('Unexpected websocket closure received') - else: - yield from self.close() - break - - yield from self.received_message(msg) - - @asyncio.coroutine - def close(self): - """Closes the websocket connection. - - To reconnect the websocket connection, :meth:`connect` must be used. - """ - if self.is_closed: - return - - if self.is_voice_connected(): - yield from self.voice.disconnect() - self.voice = None - - if self.ws.open: - yield from self.ws.close() - - self.keep_alive.cancel() - self._closed.set() - self._is_ready.clear() - - @asyncio.coroutine - def start(self, email, password): - """|coro| - - A shorthand coroutine for :meth:`login` + :meth:`connect`. - """ - yield from self.login(email, password) - yield from self.connect() - - def run(self, email, password): - """A blocking call that abstracts away the `event loop`_ - initialisation from you. - - If you want more control over the event loop then this - function should not be used. Use :meth:`start` coroutine - or :meth:`connect` + :meth:`login`. - - Roughly Equivalent to: :: - - try: - loop.run_until_complete(start(email, password)) - except KeyboardInterrupt: - loop.run_until_complete(logout()) - # cancel all tasks lingering - finally: - loop.close() - - Warning - -------- - This function must be the last function to call due to the fact that it - is blocking. That means that registration of events or anything being - called after this function call will not execute until it returns. - """ - - try: - self.loop.run_until_complete(self.start(email, password)) - except KeyboardInterrupt: - self.loop.run_until_complete(self.logout()) - pending = asyncio.Task.all_tasks() - gathered = asyncio.gather(*pending) - try: - gathered.cancel() - self.loop.run_forever() - gathered.exception() - except: - pass - finally: - self.loop.close() - # event registration def event(self, coro): diff --git a/docs/conf.py b/docs/conf.py index bb5d585c4..85bdbd894 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,6 +35,8 @@ extensions = [ 'sphinx.ext.napoleon', ] +autodoc_member_order = 'bysource' + extlinks = { 'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'issue '), }