From 499e139f8a951d80553598181899d65e4195c6a7 Mon Sep 17 00:00:00 2001 From: quiktea Date: Sun, 22 Aug 2021 18:37:54 +0100 Subject: [PATCH] added a non async version of the library --- discord/ext/oauth/no_async/client.py | 130 +++++++++++++++++++++++++++ discord/ext/oauth/no_async/errors.py | 21 +++++ discord/ext/oauth/no_async/http.py | 47 ++++++++++ discord/ext/oauth/no_async/user.py | 58 ++++++++++++ requirements.txt | 3 +- setup.py | 2 +- 6 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 discord/ext/oauth/no_async/client.py create mode 100644 discord/ext/oauth/no_async/errors.py create mode 100644 discord/ext/oauth/no_async/http.py create mode 100644 discord/ext/oauth/no_async/user.py diff --git a/discord/ext/oauth/no_async/client.py b/discord/ext/oauth/no_async/client.py new file mode 100644 index 0000000..82fcf0d --- /dev/null +++ b/discord/ext/oauth/no_async/client.py @@ -0,0 +1,130 @@ +import weakref + +from typing import Optional, Union, List + +from .http import Route, HTTPClient +from ..token import AccessTokenResponse +from ..user import User + + +__all__: tuple = ( + "OAuth2Client", +) + + +class NoAsyncOAuth2Client: + """ + A class representing a client interacting with the discord OAuth2 API. + """ + def __init__( + self, + *, + client_id: int, + client_secret: str, + redirect_uri: str, + scopes: Optional[List[str]] = None + ): + """A class representing a client interacting with the discord OAuth2 API. + + :param client_id: The OAuth application's client_id + :type client_id: int + :param client_secret: The OAuth application's client_secret + :type client_secret: str + :param redirect_uri: The OAuth application's redirect_uri. Must be from one of the configured uri's on the developer portal + :type redirect_uri: str + :param scopes: A list of OAuth2 scopes, defaults to None + :type scopes: Optional[List[str]], optional + """ + self._id = client_id + self._auth = client_secret + self._redirect = redirect_uri + self._scopes = " ".join(scopes) if scopes is not None else None + + self.http = HTTPClient() + self.http._state_info.update( + { + "client_id": self._id, + "client_secret": self._auth, + "redirect_uri": self._redirect, + "scopes": self._scopes, + } + ) + + self._user_cache = weakref.WeakValueDictionary() + + def exchange_code(self, code: str) -> AccessTokenResponse: + """Exchanges the code you receive from the OAuth2 redirect. + + :param code: The code you've received from the OAuth2 redirect + :type code: str + :return: A response class containing information about the access token + :rtype: AccessTokenResponse + """ + route = Route("POST", "/oauth2/token") + post_data = { + "client_id": self._id, + "client_secret": self._auth, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": self._redirect, + } + if self._scopes is not None: + post_data["scope"] = self._scopes + request_data = self.http.request(route, data=post_data) + token_resp = AccessTokenResponse(data=request_data) + return token_resp + + def refresh_token(self, refresh_token: Union[str, AccessTokenResponse]) -> AccessTokenResponse: + """Refreshes an access token. Takes either a string or an AccessTokenResponse. + + :param refresh_token: The refresh token you received when exchanging a redirect code + :type refresh_token: Union[str, AccessTokenResponse] + :return: A new access token response containg information about the refreshed access token + :rtype: AccessTokenResponse + """ + refresh_token = ( + refresh_token if isinstance(refresh_token, str) else refresh_token.token + ) + route = Route("POST", "/oauth2/token") + post_data = { + "client_id": self._id, + "client_secret": self._auth, + "grant_type": "refresh_token", + "refresh_token": refresh_token, + } + request_data = self.http.request(route, data=post_data) + token_resp = AccessTokenResponse(data=request_data) + return token_resp + + def fetch_user(self, access_token_response: AccessTokenResponse) -> User: + """Makes an api call to fetch a user using their access token. + + :param access_token_response: A class holding information about an access token + :type access_token_response: AccessTokenResponse + :return: Returns a User object holding information about the select user + :rtype: User + """ + access_token = access_token_response.token + route = Route("GET", "/users/@me") + headers = {"Authorization": "Bearer {}".format(access_token)} + resp = self.http.request(route, headers=headers) + user = User(http=self.http, data=resp, acr=access_token_response) + self._user_cache.update({user.id: user}) + return user + + def get_user(self, id: int) -> Optional[User]: + """Gets a user from the cache. The cache is a WeakValueDictionary, so objects may be removed without notice. + + :param id: The id of the user you want to get + :type id: int + :return: A possible user object. Returns None if no User is found in cache. + :rtype: Optional[User] + """ + user = self._user_cache.get(id) + return user + + def close(self): + """Closes and performs cleanup operations on the client, such as clearing its cache. + """ + self._user_cache.clear() + self.http.close() diff --git a/discord/ext/oauth/no_async/errors.py b/discord/ext/oauth/no_async/errors.py new file mode 100644 index 0000000..236490f --- /dev/null +++ b/discord/ext/oauth/no_async/errors.py @@ -0,0 +1,21 @@ +from requests import Response + + +class ExtOauthException(Exception): + """ + The base exception the library always raises + """ + + +class HTTPException(ExtOauthException): + """ + The error that is raised whenever an http error occurs + """ + + def __init__(self, resp: Response, *, json: dict = {}): + self.resp = resp + self.msg = json.get("error_description") or json.get("message") + + def __str__(self): + fmt = "{0.status_code}: {0.reason}: {1}" + return fmt.format(self.resp, self.msg) diff --git a/discord/ext/oauth/no_async/http.py b/discord/ext/oauth/no_async/http.py new file mode 100644 index 0000000..533bce7 --- /dev/null +++ b/discord/ext/oauth/no_async/http.py @@ -0,0 +1,47 @@ +import requests + +from .errors import HTTPException + + +__all__: tuple = ( + "Route", + "HTTPClient" +) + + +class Route: + BASE = "https://discord.com/api/v9" + + def __init__(self, method: str, endpoint: str, **params): + self.url = self.BASE + endpoint.format(**params) + self.method = method + + +class HTTPClient: + def __init__(self): + self.__session = None # filled in later + self._state_info = {} # client fills this + + def _create_session(self) -> requests.Session: + self.__session = requests.Session() + return self.__session + + def request(self, route: Route, **kwargs) -> dict: + if self.__session is None or self.__session.closed is True: + self._create_session() + + headers = kwargs.pop("headers", {}) + + headers["Content-Type"] = "application/x-www-form-urlencoded" # the discord OAuth2 api requires this header to be set to this + kwargs["headers"] = headers + + resp = self.__session.request(route.method, route.url, **kwargs) + json = resp.json() + if 200 <= resp.status < 300: + return json + else: + raise HTTPException(resp, json=json) + + def close(self): + self.__session.close() + self.__session = None diff --git a/discord/ext/oauth/no_async/user.py b/discord/ext/oauth/no_async/user.py new file mode 100644 index 0000000..b7a343f --- /dev/null +++ b/discord/ext/oauth/no_async/user.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import List, TYPE_CHECKING + +from .http import Route +from ..user import User + +if TYPE_CHECKING: + from ..guild import Guild + from ..token import AccessTokenResponse + + +__all__: tuple = ( + "User", +) + +class NoAsyncUser(User): + def refresh(self) -> AccessTokenResponse: + """Refreshes the access token for the user and returns a fresh access token response. + + :return: A class holding information about the new access token + :rtype: AccessTokenResponse + """ + refresh_token = self.refresh_token + route = Route("POST", "/oauth2/token") + post_data = { + "client_id": self._http._state_info["client_id"], + "client_secret": self._http._state_info["client_secret"], + "grant_type": "refresh_token", + "refresh_token": refresh_token, + } + request_data = self._http.request(route, data=post_data) + token_resp = AccessTokenResponse(data=request_data) + self.refresh_token = token_resp.refresh_token + self.access_token = token_resp.token + self._acr = token_resp + return token_resp + + def fetch_guilds(self, *, refresh: bool = True) -> List[Guild]: + """Makes an api call to fetch the guilds the user is in. Can fill a normal dictionary cache. + + :param refresh: Whether or not to refresh the guild cache attached to this user object. If false, returns the cached guilds, defaults to True + :type refresh: bool, optional + :return: A List of Guild objects either from cache or returned from the api call + :rtype: List[Guild] + """ + if not refresh and self.guilds: + return self.guilds + + route = Route("GET", "/users/@me/guilds") + headers = {"Authorization": "Bearer {}".format(self.access_token)} + resp = self._http.request(route, headers=headers) + self.guilds = [] + for array in resp: + guild = Guild(data=array, user=self) + self.guilds.append(guild) + + return self.guilds \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ce23571..3550c33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -aiohttp \ No newline at end of file +aiohttp +requests \ No newline at end of file diff --git a/setup.py b/setup.py index d9456ff..dd02cee 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ author='moanie', python_requires='>=3.7.0', url='https://github.com/moanie/discord.ext.oauth', - version="0.1.0", + version="0.2.0", packages=[ "discord/ext/oauth" ],