Asynchronize the world

This commit is contained in:
thec0sm0s
2020-09-30 13:12:53 +05:30
parent 256d6cae81
commit bc15de5119
9 changed files with 74 additions and 70 deletions

View File

@@ -1,5 +1,5 @@
import cachetools import cachetools
import requests import aiohttp
import typing import typing
import json import json
import os import os
@@ -56,23 +56,23 @@ class DiscordOAuth2HttpClient(abc.ABC):
@staticmethod @staticmethod
@abc.abstractmethod @abc.abstractmethod
def save_authorization_token(token: dict): async def save_authorization_token(token: dict):
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
@abc.abstractmethod @abc.abstractmethod
def get_authorization_token() -> dict: async def get_authorization_token() -> dict:
raise NotImplementedError raise NotImplementedError
def _fetch_token(self, state): async def _fetch_token(self, state):
discord = self._make_session(state=state) discord = await self._make_session(state=state)
return discord.fetch_token( return await discord.fetch_token(
configs.DISCORD_TOKEN_URL, configs.DISCORD_TOKEN_URL,
client_secret=self.__client_secret, client_secret=self.__client_secret,
authorization_response=request.url authorization_response=request.url
) )
def _make_session(self, token: str = None, state: str = None, scope: list = None) -> OAuth2Session: async def _make_session(self, token: str = None, state: str = None, scope: list = None) -> OAuth2Session:
"""A low level method used for creating OAuth2 session. """A low level method used for creating OAuth2 session.
Parameters Parameters
@@ -93,7 +93,7 @@ class DiscordOAuth2HttpClient(abc.ABC):
""" """
return OAuth2Session( return OAuth2Session(
client_id=self.client_id, client_id=self.client_id,
token=token or self.get_authorization_token(), token=token or await self.get_authorization_token(),
state=state or session.get("DISCORD_OAUTH2_STATE"), state=state or session.get("DISCORD_OAUTH2_STATE"),
scope=scope, scope=scope,
redirect_uri=self.redirect_uri, redirect_uri=self.redirect_uri,
@@ -104,7 +104,7 @@ class DiscordOAuth2HttpClient(abc.ABC):
auto_refresh_url=configs.DISCORD_TOKEN_URL, auto_refresh_url=configs.DISCORD_TOKEN_URL,
token_updater=self.save_authorization_token) token_updater=self.save_authorization_token)
def request(self, route: str, method="GET", data=None, oauth=True, **kwargs) -> typing.Union[dict, str]: async def request(self, route: str, method="GET", data=None, oauth=True, **kwargs) -> typing.Union[dict, str]:
"""Sends HTTP request to provided route or discord endpoint. """Sends HTTP request to provided route or discord endpoint.
Note Note
@@ -137,20 +137,22 @@ class DiscordOAuth2HttpClient(abc.ABC):
""" """
route = configs.DISCORD_API_BASE_URL + route route = configs.DISCORD_API_BASE_URL + route
response = self._make_session( discord = await self._make_session()
).request(method, route, data, **kwargs) if oauth else requests.request(method, route, data=data, **kwargs) async with (discord.request(
method, route, data, **kwargs
) if oauth else aiohttp.request(method, route, data=data, **kwargs)) as response:
if response.status_code == 401: if response.status == 401:
raise exceptions.Unauthorized raise exceptions.Unauthorized
if response.status_code == 429: if response.status == 429:
raise exceptions.RateLimited(response) raise exceptions.RateLimited(response)
try: try:
return response.json() return await response.json()
except json.JSONDecodeError: except json.JSONDecodeError:
return response.text return await response.text()
def bot_request(self, route: str, method="GET", **kwargs) -> typing.Union[dict, str]: async def bot_request(self, route: str, method="GET", **kwargs) -> typing.Union[dict, str]:
"""Make HTTP request to specified endpoint with bot token as authorization headers. """Make HTTP request to specified endpoint with bot token as authorization headers.
Parameters Parameters
@@ -175,4 +177,4 @@ class DiscordOAuth2HttpClient(abc.ABC):
""" """
headers = {"Authorization": f"Bot {self.__bot_token}"} headers = {"Authorization": f"Bot {self.__bot_token}"}
return self.request(route, method=method, oauth=False, headers=headers, **kwargs) return await self.request(route, method=method, oauth=False, headers=headers, **kwargs)

View File

@@ -57,7 +57,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
def __get_state(): def __get_state():
return session.pop("DISCORD_OAUTH2_STATE", str()) return session.pop("DISCORD_OAUTH2_STATE", str())
def create_session( async def create_session(
self, scope: list = None, *, data: dict = None, prompt: bool = True, self, scope: list = None, *, data: dict = None, prompt: bool = True,
permissions: typing.Union[discord.Permissions, int] = 0, **params permissions: typing.Union[discord.Permissions, int] = 0, **params
): ):
@@ -95,7 +95,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
state = jwt.encode(data or dict(), current_app.config["SECRET_KEY"]).decode(encoding="utf-8") state = jwt.encode(data or dict(), current_app.config["SECRET_KEY"]).decode(encoding="utf-8")
discord_session = self._make_session(scope=scope, state=state) discord_session = await self._make_session(scope=scope, state=state)
authorization_url, state = discord_session.authorization_url(configs.DISCORD_AUTHORIZATION_BASE_URL) authorization_url, state = discord_session.authorization_url(configs.DISCORD_AUTHORIZATION_BASE_URL)
self.__save_state(state) self.__save_state(state)
@@ -117,7 +117,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
return redirect(authorization_url) return redirect(authorization_url)
@staticmethod @staticmethod
def save_authorization_token(token: dict): async def save_authorization_token(token: dict):
"""A staticmethod which saves a dict containing Discord OAuth2 token and other secrets to the user's cookies. """A staticmethod which saves a dict containing Discord OAuth2 token and other secrets to the user's cookies.
Meaning by default, it uses client side session handling. Meaning by default, it uses client side session handling.
@@ -128,7 +128,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
session["DISCORD_OAUTH2_TOKEN"] = token session["DISCORD_OAUTH2_TOKEN"] = token
@staticmethod @staticmethod
def get_authorization_token() -> dict: async def get_authorization_token() -> dict:
"""A static method which returns a dict containing Discord OAuth2 token and other secrets which was saved """A static method which returns a dict containing Discord OAuth2 token and other secrets which was saved
previously by `:py:meth:`quart_discord.DiscordOAuth2Session.save_authorization_token` from user's cookies. previously by `:py:meth:`quart_discord.DiscordOAuth2Session.save_authorization_token` from user's cookies.
@@ -151,8 +151,8 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
raise exceptions.HttpException(error) raise exceptions.HttpException(error)
state = self.__get_state() state = self.__get_state()
token = self._fetch_token(state) token = await self._fetch_token(state)
self.save_authorization_token(token) await self.save_authorization_token(token)
return jwt.decode(state, current_app.config["SECRET_KEY"]) return jwt.decode(state, current_app.config["SECRET_KEY"])
@@ -172,13 +172,12 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
except KeyError: except KeyError:
pass pass
@property async def authorized(self):
def authorized(self):
"""A boolean indicating whether current session has authorization token or not.""" """A boolean indicating whether current session has authorization token or not."""
return self._make_session().authorized return (await self._make_session()).authorized
@staticmethod @staticmethod
def fetch_user() -> models.User: async def fetch_user() -> models.User:
"""This method returns user object from the internal cache if it exists otherwise makes an API call to do so. """This method returns user object from the internal cache if it exists otherwise makes an API call to do so.
Returns Returns
@@ -186,10 +185,10 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
quart_discord.models.User quart_discord.models.User
""" """
return models.User.get_from_cache() or models.User.fetch_from_api() return models.User.get_from_cache() or await models.User.fetch_from_api()
@staticmethod @staticmethod
def fetch_connections() -> list: async def fetch_connections() -> list:
"""This method returns list of user connection objects from internal cache if it exists otherwise """This method returns list of user connection objects from internal cache if it exists otherwise
makes an API call to do so. makes an API call to do so.
@@ -206,10 +205,10 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
except AttributeError: except AttributeError:
pass pass
return models.UserConnection.fetch_from_api() return await models.UserConnection.fetch_from_api()
@staticmethod @staticmethod
def fetch_guilds() -> list: async def fetch_guilds() -> list:
"""This method returns list of guild objects from internal cache if it exists otherwise makes an API """This method returns list of guild objects from internal cache if it exists otherwise makes an API
call to do so. call to do so.
@@ -226,4 +225,4 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
except AttributeError: except AttributeError:
pass pass
return models.Guild.fetch_from_api() return await models.Guild.fetch_from_api()

View File

@@ -22,20 +22,20 @@ class DiscordModelsBase(metaclass=DiscordModelsMeta):
self._payload = payload self._payload = payload
@staticmethod @staticmethod
def _request(*args, **kwargs): async def _request(*args, **kwargs):
"""A shorthand to :py:func:quart_discord.request`. It uses Quart current_app local proxy to get the """A shorthand to :py:func:quart_discord.request`. It uses Quart current_app local proxy to get the
Quart-Discord client. Quart-Discord client.
""" """
return current_app.discord.request(*args, **kwargs) return await current_app.discord.request(*args, **kwargs)
@staticmethod @staticmethod
def _bot_request(*args, **kwargs): async def _bot_request(*args, **kwargs):
"""A shorthand to :py:func:quart_discord.bot_request`.""" """A shorthand to :py:func:quart_discord.bot_request`."""
return current_app.discord.bot_request(*args, **kwargs) return await current_app.discord.bot_request(*args, **kwargs)
@classmethod @classmethod
def fetch_from_api(cls): async def fetch_from_api(cls):
"""A class method which returns an instance or list of instances of this model by implicitly making an """A class method which returns an instance or list of instances of this model by implicitly making an
API call to Discord. API call to Discord.
@@ -48,7 +48,7 @@ class DiscordModelsBase(metaclass=DiscordModelsMeta):
""" """
request_method = cls._bot_request if cls.BOT else cls._request request_method = cls._bot_request if cls.BOT else cls._request
payload = request_method(cls.ROUTE) payload = await request_method(cls.ROUTE)
if cls.MANY: if cls.MANY:
return [cls(_) for _ in payload] return [cls(_) for _ in payload]
return cls(payload) return cls(payload)

View File

@@ -57,7 +57,7 @@ class UserConnection(DiscordModelsBase):
return bool(self.visibility) return bool(self.visibility)
@classmethod @classmethod
def fetch_from_api(cls, cache=True): async def fetch_from_api(cls, cache=True):
"""A class method which returns an instance or list of instances of this model by implicitly making an """A class method which returns an instance or list of instances of this model by implicitly making an
API call to Discord. If an instance of :py:class:`quart_discord.User` exists in the users internal cache API call to Discord. If an instance of :py:class:`quart_discord.User` exists in the users internal cache
who are attached to these connections then, the cached property :py:attr:`quart_discord.User.connections` who are attached to these connections then, the cached property :py:attr:`quart_discord.User.connections`
@@ -74,7 +74,7 @@ class UserConnection(DiscordModelsBase):
List of instances of :py:class:`quart_discord.UserConnection` to which this user belongs. List of instances of :py:class:`quart_discord.UserConnection` to which this user belongs.
""" """
connections = super().fetch_from_api() connections = await super().fetch_from_api()
if cache: if cache:
user = current_app.discord.users_cache.get(current_app.discord.user_id) user = current_app.discord.users_cache.get(current_app.discord.user_id)

View File

@@ -66,7 +66,7 @@ class Guild(DiscordModelsBase):
return configs.DISCORD_GUILD_ICON_BASE_URL.format(guild_id=self.id, icon_hash=self.icon_hash) return configs.DISCORD_GUILD_ICON_BASE_URL.format(guild_id=self.id, icon_hash=self.icon_hash)
@classmethod @classmethod
def fetch_from_api(cls, cache=True): async def fetch_from_api(cls, cache=True):
"""A class method which returns an instance or list of instances of this model by implicitly making an """A class method which returns an instance or list of instances of this model by implicitly making an
API call to Discord. If an instance of :py:class:`quart_discord.User` exists in the users internal cache API call to Discord. If an instance of :py:class:`quart_discord.User` exists in the users internal cache
who belongs to these guilds then, the cached property :py:attr:`quart_discord.User.guilds` is updated. who belongs to these guilds then, the cached property :py:attr:`quart_discord.User.guilds` is updated.
@@ -82,7 +82,7 @@ class Guild(DiscordModelsBase):
List of instances of :py:class:`quart_discord.Guild` to which this user belongs. List of instances of :py:class:`quart_discord.Guild` to which this user belongs.
""" """
guilds = super().fetch_from_api() guilds = await super().fetch_from_api()
if cache: if cache:
user = current_app.discord.users_cache.get(current_app.discord.user_id) user = current_app.discord.users_cache.get(current_app.discord.user_id)

View File

@@ -125,7 +125,7 @@ class User(DiscordModelsBase):
return False return False
@classmethod @classmethod
def fetch_from_api(cls, guilds=False, connections=False): async def fetch_from_api(cls, guilds=False, connections=False):
"""A class method which returns an instance of this model by implicitly making an """A class method which returns an instance of this model by implicitly making an
API call to Discord. The user returned from API will always be cached and update in internal cache. API call to Discord. The user returned from API will always be cached and update in internal cache.
@@ -144,14 +144,14 @@ class User(DiscordModelsBase):
An instance of this model itself. An instance of this model itself.
[cls, ...] [cls, ...]
List of instances of this model when many of these models exist.""" List of instances of this model when many of these models exist."""
self = super().fetch_from_api() self = await super().fetch_from_api()
current_app.discord.users_cache.update({self.id: self}) current_app.discord.users_cache.update({self.id: self})
session["DISCORD_USER_ID"] = self.id session["DISCORD_USER_ID"] = self.id
if guilds: if guilds:
self.fetch_guilds() await self.fetch_guilds()
if connections: if connections:
self.fetch_connections() await self.fetch_connections()
return self return self
@@ -169,7 +169,7 @@ class User(DiscordModelsBase):
""" """
return current_app.discord.users_cache.get(current_app.discord.user_id) return current_app.discord.users_cache.get(current_app.discord.user_id)
def add_to_guild(self, guild_id) -> dict: async def add_to_guild(self, guild_id) -> dict:
"""Method to add user to the guild, provided OAuth2 session has already been created with ``guilds.join`` scope. """Method to add user to the guild, provided OAuth2 session has already been created with ``guilds.join`` scope.
Parameters Parameters
@@ -189,12 +189,12 @@ class User(DiscordModelsBase):
""" """
try: try:
data = {"access_token": current_app.discord.get_authorization_token()["access_token"]} data = {"access_token": await current_app.discord.get_authorization_token()["access_token"]}
except KeyError: except KeyError:
raise exceptions.Unauthorized raise exceptions.Unauthorized
return self._bot_request(f"/guilds/{guild_id}/members/{self.id}", method="PUT", json=data) or dict() return await self._bot_request(f"/guilds/{guild_id}/members/{self.id}", method="PUT", json=data) or dict()
def fetch_guilds(self) -> list: async def fetch_guilds(self) -> list:
"""A method which makes an API call to Discord to get user's guilds. It prepares the internal guilds cache """A method which makes an API call to Discord to get user's guilds. It prepares the internal guilds cache
and returns list of all guilds the user is member of. and returns list of all guilds the user is member of.
@@ -204,10 +204,10 @@ class User(DiscordModelsBase):
List of :py:class:`quart_discord.Guilds` instances. List of :py:class:`quart_discord.Guilds` instances.
""" """
self._guilds = {guild.id: guild for guild in Guild.fetch_from_api(cache=False)} self._guilds = {guild.id: guild for guild in await Guild.fetch_from_api(cache=False)}
return self.guilds return self.guilds
def fetch_connections(self) -> list: async def fetch_connections(self) -> list:
"""A method which makes an API call to Discord to get user's connections. It prepares the internal connection """A method which makes an API call to Discord to get user's connections. It prepares the internal connection
cache and returns list of all connection instances. cache and returns list of all connection instances.
@@ -217,7 +217,7 @@ class User(DiscordModelsBase):
A list of :py:class:`quart_discord.UserConnection` instances. A list of :py:class:`quart_discord.UserConnection` instances.
""" """
self.connections = UserConnection.fetch_from_api(cache=False) self.connections = await UserConnection.fetch_from_api(cache=False)
return self.connections return self.connections

View File

@@ -44,7 +44,7 @@ def requires_authorization(view):
@functools.wraps(view) @functools.wraps(view)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
if not current_app.discord.authorized: if not await current_app.discord.authorized():
raise exceptions.Unauthorized raise exceptions.Unauthorized
return await view(*args, **kwargs) return await view(*args, **kwargs)

View File

@@ -1,8 +1,9 @@
Quart Quart
pyjwt pyjwt
requests requests
aiohttp
oauthlib oauthlib
discord.py discord.py
cachetools cachetools
setuptools setuptools
requests_oauthlib git+https://github.com/thec0sm0s/requests-oauthlib.git@async

View File

@@ -21,7 +21,7 @@ HYPERLINK = '<a href="{}">{}</a>'
@app.route("/") @app.route("/")
async def index(): async def index():
if not discord.authorized: if not await discord.authorized():
return f""" return f"""
{HYPERLINK.format(url_for(".login"), "Login")} <br /> {HYPERLINK.format(url_for(".login"), "Login")} <br />
{HYPERLINK.format(url_for(".login_with_data"), "Login with custom data")} <br /> {HYPERLINK.format(url_for(".login_with_data"), "Login with custom data")} <br />
@@ -39,22 +39,24 @@ async def index():
@app.route("/login/") @app.route("/login/")
async def login(): async def login():
return discord.create_session() return await discord.create_session()
@app.route("/login-data/") @app.route("/login-data/")
async def login_with_data(): async def login_with_data():
return discord.create_session(data=dict(redirect="/me/", coupon="15off", number=15, zero=0, status=False)) return await discord.create_session(data=dict(redirect="/me/", coupon="15off", number=15, zero=0, status=False))
@app.route("/invite-bot/") @app.route("/invite-bot/")
async def invite_bot(): async def invite_bot():
return discord.create_session(scope=["bot"], permissions=8, guild_id=464488012328468480, disable_guild_select=True) return await discord.create_session(
scope=["bot"], permissions=8, guild_id=464488012328468480, disable_guild_select=True
)
@app.route("/invite-oauth/") @app.route("/invite-oauth/")
async def invite_oauth(): async def invite_oauth():
return discord.create_session(scope=["bot", "identify"], permissions=8) return await discord.create_session(scope=["bot", "identify"], permissions=8)
@app.route("/callback/") @app.route("/callback/")
@@ -66,7 +68,7 @@ async def callback():
@app.route("/me/") @app.route("/me/")
async def me(): async def me():
user = discord.fetch_user() user = await discord.fetch_user()
return f""" return f"""
<html> <html>
<head> <head>
@@ -84,20 +86,20 @@ async def me():
@app.route("/me/guilds/") @app.route("/me/guilds/")
async def user_guilds(): async def user_guilds():
guilds = discord.fetch_guilds() guilds = await discord.fetch_guilds()
return "<br />".join([f"[ADMIN] {g.name}" if g.permissions.administrator else g.name for g in guilds]) return "<br />".join([f"[ADMIN] {g.name}" if g.permissions.administrator else g.name for g in guilds])
@app.route("/add_to/<int:guild_id>/") @app.route("/add_to/<int:guild_id>/")
async def add_to_guild(guild_id): async def add_to_guild(guild_id):
user = discord.fetch_user() user = await discord.fetch_user()
return user.add_to_guild(guild_id) return await user.add_to_guild(guild_id)
@app.route("/me/connections/") @app.route("/me/connections/")
async def my_connections(): async def my_connections():
user = discord.fetch_user() user = await discord.fetch_user()
connections = discord.fetch_connections() connections = await discord.fetch_connections()
return f""" return f"""
<html> <html>
<head> <head>