Merge remote-tracking branch 'quart-discord/master' into quart-discord

# Conflicts:
#	README.md
#	docs/introduction.rst
#	quart_discord/__init__.py
#	quart_discord/_http.py
This commit is contained in:
thec0sm0s
2020-10-01 00:54:09 +05:30
21 changed files with 241 additions and 297 deletions

2
.gitignore vendored
View File

@@ -57,7 +57,7 @@ coverage.xml
local_settings.py
db.sqlite3
# Flask stuff:
# Quart stuff:
instance/
.webassets-cache

View File

@@ -1,27 +1,24 @@
# Flask-Discord
[![PyPI](https://img.shields.io/pypi/v/Flask-Discord?style=for-the-badge)](https://pypi.org/project/Flask-Discord/) [![Read the Docs](https://img.shields.io/readthedocs/flask-discord?style=for-the-badge)](https://flask-discord.readthedocs.io/en/latest/) [![Discord](https://img.shields.io/discord/690878977920729177?label=Discord%20Community&logo=Discord&style=for-the-badge)](https://discord.gg/7CrQEyP)
# Quart-Discord
[![PyPI](https://img.shields.io/pypi/v/Quart-Discord?style=for-the-badge)](https://pypi.org/project/Quart-Discord/) [![Read the Docs](https://img.shields.io/readthedocs/quart-discord?style=for-the-badge)](https://quart-discord.readthedocs.io/en/latest/)
Discord OAuth2 extension for Flask.
Discord OAuth2 extension for Quart.
### Installation
To install current latest release you can use following command:
```sh
python3 -m pip install Flask-Discord
python3 -m pip install Quart-Discord
```
### Basic Example
```python
import os
from quart import Quart, redirect, url_for
from quart_discord import DiscordOAuth2Session, requires_authorization, Unauthorized
from flask import Flask, redirect, url_for
from flask_discord import DiscordOAuth2Session, requires_authorization, Unauthorized
app = Quart(__name__)
app = Flask(__name__)
app.secret_key = b"random bytes representing flask secret key"
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" # !! Only in development environment.
app.secret_key = b"random bytes representing quart secret key"
app.config["DISCORD_CLIENT_ID"] = 490732332240863233 # Discord client ID.
app.config["DISCORD_CLIENT_SECRET"] = "" # Discord client secret.
@@ -32,25 +29,25 @@ discord = DiscordOAuth2Session(app)
@app.route("/login/")
def login():
return discord.create_session()
async def login():
return await discord.create_session()
@app.route("/callback/")
def callback():
discord.callback()
async def callback():
await discord.callback()
return redirect(url_for(".me"))
@app.errorhandler(Unauthorized)
def redirect_unauthorized(e):
async def redirect_unauthorized(e):
return redirect(url_for("login"))
@app.route("/me/")
@requires_authorization
def me():
user = discord.fetch_user()
async def me():
user = await discord.fetch_user()
return f"""
<html>
<head>
@@ -70,8 +67,8 @@ For an example to the working application, check [`test_app.py`](tests/test_app.
### Requirements
* Flask
* requests_oauthlib
* Quart
* Async-OAuthlib
* cachetools
* discord.py
@@ -80,4 +77,4 @@ For an example to the working application, check [`test_app.py`](tests/test_app.
Head over to [documentation] for full API reference.
[documentation]: https://flask-discord.readthedocs.io/en/latest/
[documentation]: https://quart-discord.readthedocs.io/en/latest/

View File

@@ -8,11 +8,11 @@ attributes and available methods.
Discord OAuth2 Client
---------------------
.. autoclass:: flask_discord.DiscordOAuth2Session
.. autoclass:: quart_discord.DiscordOAuth2Session
:members:
:inherited-members:
.. autoclass:: flask_discord._http.DiscordOAuth2HttpClient
.. autoclass:: quart_discord._http.DiscordOAuth2HttpClient
:members:
:inherited-members:
@@ -20,23 +20,23 @@ Discord OAuth2 Client
Models
------
.. autoclass:: flask_discord.models.Guild
.. autoclass:: quart_discord.models.Guild
:members:
:inherited-members:
.. autoclass:: flask_discord.models.User
.. autoclass:: quart_discord.models.User
:members:
:inherited-members:
.. autoclass:: flask_discord.models.Bot
.. autoclass:: quart_discord.models.Bot
:members:
:inherited-members:
.. autoclass:: flask_discord.models.Integration
.. autoclass:: quart_discord.models.Integration
:members:
:inherited-members:
.. autoclass:: flask_discord.models.UserConnection
.. autoclass:: quart_discord.models.UserConnection
:members:
:inherited-members:
@@ -44,20 +44,20 @@ Models
Utilities
---------
.. autodecorator:: flask_discord.requires_authorization
.. autodecorator:: quart_discord.requires_authorization
Exceptions
----------
.. autoclass:: flask_discord.HttpException
.. autoclass:: quart_discord.HttpException
:members:
.. autoclass:: flask_discord.RateLimited
.. autoclass:: quart_discord.RateLimited
:members:
.. autoclass:: flask_discord.Unauthorized
.. autoclass:: quart_discord.Unauthorized
:members:
.. autoclass:: flask_discord.AccessDenied
.. autoclass:: quart_discord.AccessDenied
:members:

View File

@@ -14,16 +14,16 @@ import os
import re
import sys
sys.path.insert(0, os.path.abspath('..'))
sys.path.append('../flask_discord/')
sys.path.append('../quart_discord/')
# -- Project information -----------------------------------------------------
project = 'Flask-Discord'
copyright = '2019, □ | The Cosmos'
author = '□ | The Cosmos'
project = 'Quart-Discord'
copyright = '2020, Philip Dowie'
author = 'Philip Dowie'
with open('../flask_discord/__init__.py') as f:
with open('../quart_discord/__init__.py') as f:
ver = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1)
# The short X.Y version
version = ver
@@ -48,9 +48,9 @@ extensions = [
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'discord': ('https://discordpy.readthedocs.io/en/latest/', None),
'flask': ('https://flask.palletsprojects.com/en/1.1.x/', None),
'quart': ('https://pgjones.gitlab.io/quart/', None),
'cachetools': ('https://cachetools.readthedocs.io/en/stable/', None),
'requests_oauthlib': ('https://requests-oauthlib.readthedocs.io/en/latest/', None)
'async_oauthlib': ('https://async-oauthlib.readthedocs.io/en/latest/', None)
}
# Add any paths that contain templates here, relative to this directory.

View File

@@ -1,14 +1,14 @@
.. Flask-Discord documentation master file, created by
.. Quart-Discord documentation master file, created by
sphinx-quickstart on Wed May 8 08:29:45 2019.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Flask-Discord's documentation!
Welcome to Quart-Discord's documentation!
=========================================
.. image:: /images/background.jpg
Flask-Discord is an extension made for Flask which makes implementation of
Quart-Discord is an extension made for Quart which makes implementation of
Discord's OAuth2 API easier.
**Features**

View File

@@ -5,7 +5,7 @@
Introduction
============
Flask-Discord is an extension for Flask - Python web framework which
Quart-Discord is an extension for Quart - Python web framework which
makes easy implementation of Discord OAuth2 API. After creating a discord
client object, one can easily request authorization and hence any of the
resources provided by the discord OAuth2 API under the available scope
@@ -14,14 +14,14 @@ permissions.
Requirements
------------
- **Flask**
This is an Flask extension.
- **Quart**
This is a Quart extension.
- **requests_oauthlib**
It also requires requests_oauthlib to make OAuth2 sessions with discord.
- **cachetools**
Flask Discord supports caching discord objects to boost the performance when page loads.
Quart Discord supports caching discord objects to boost the performance when page loads.
- **discord.py**
Makes use of discord.py for re-using many Discord models.
@@ -29,15 +29,15 @@ Requirements
Installing
----------
You can install Flask-Discord directly from PyPI using PIP and following command
You can install Quart-Discord directly from PyPI using PIP and following command
in shell or command prompt: ::
python3 -m pip install -U Flask-Discord
python3 -m pip install -U Quart-Discord
You can also install the latest development version (**maybe unstable/broken**) by
using following command: ::
python3 -m pip install -U git+https://github.com/thec0sm0s/Flask-Discord.git@dev
python3 -m pip install -U git+https://github.com/jnawk/Quart-Discord.git@dev
Basic Usage
@@ -48,14 +48,12 @@ in exchange for fetching user's details and display them on web page.
.. code-block:: python3
from flask import Flask, redirect, url_for
from flask_discord import DiscordOAuth2Session, requires_authorization, Unauthorized
from quart import Quart, redirect, url_for
from quart_discord import DiscordOAuth2Session, requires_authorization, Unauthorized
app = Flask(__name__)
app = Quart(__name__)
app.secret_key = b"random bytes representing flask secret key"
# OAuth2 must make use of HTTPS in production environment.
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" # !! Only in development environment.
app.secret_key = b"random bytes representing quart secret key"
app.config["DISCORD_CLIENT_ID"] = 490732332240863233 # Discord client ID.
app.config["DISCORD_CLIENT_SECRET"] = "" # Discord client secret.
@@ -67,25 +65,25 @@ in exchange for fetching user's details and display them on web page.
@app.route("/login/")
def login():
return discord.create_session()
async def login():
return await discord.create_session()
@app.route("/callback/")
def callback():
discord.callback()
async def callback():
await discord.callback()
return redirect(url_for(".me"))
@app.errorhandler(Unauthorized)
def redirect_unauthorized(e):
async def redirect_unauthorized(e):
return redirect(url_for("login"))
@app.route("/me/")
@requires_authorization
def me():
user = discord.fetch_user()
async def me():
user = await discord.fetch_user()
return f"""
<html>
<head>
@@ -99,26 +97,3 @@ in exchange for fetching user's details and display them on web page.
if __name__ == "__main__":
app.run()
**Lazy initialization with flask factory pattern**
.. code-block:: python3
from flask_discord import DiscordOAuth2Session
discord = DiscordOAuth2Session()
def get_app():
app = Flask(__name__)
app.secret_key = b"random bytes representing flask secret key"
# OAuth2 must make use of HTTPS in production environment.
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" # !! Only in development environment.
app.config["DISCORD_CLIENT_ID"] = 490732332240863233 # Discord client ID.
app.config["DISCORD_CLIENT_SECRET"] = "" # Discord client secret.
app.config["DISCORD_REDIRECT_URI"] = "" # URL to your callback endpoint.
app.config["DISCORD_BOT_TOKEN"] = "" # Required to access BOT resources.
discord.init_app(app)
return app

View File

@@ -14,4 +14,4 @@ __all__ = [
]
__version__ = "0.1.61"
__version__ = "0.3.0"

View File

@@ -1,20 +1,20 @@
import cachetools
import requests
import aiohttp
import typing
import json
import os
import abc
from . import configs
from . import exceptions
from flask import session, request
from quart import session, request
from collections.abc import Mapping
from requests_oauthlib import OAuth2Session
from async_oauthlib import OAuth2Session
class DiscordOAuth2HttpClient(abc.ABC):
"""An OAuth2 http abstract base class providing some factory methods.
This class is meant to be overridden by :py:class:`flask_discord.DiscordOAuth2Session` and should not be
This class is meant to be overridden by :py:class:`quart_discord.DiscordOAuth2Session` and should not be
used directly.
"""
@@ -25,78 +25,53 @@ class DiscordOAuth2HttpClient(abc.ABC):
"DISCORD_OAUTH2_TOKEN",
]
def __init__(
self, app=None,
client_id=None, client_secret=None, redirect_uri=None,
bot_token=None, users_cache=None, proxy=None, proxy_auth=None
):
self.client_id = client_id
self.__client_secret = client_secret
self.redirect_uri = redirect_uri
self.__bot_token = bot_token
self.users_cache = users_cache
self.proxy = proxy
self.proxy_auth = proxy_auth
if app is not None:
self.init_app(app)
def init_app(self, app):
"""A method to lazily initialize the application.
Use this when you're using flask factory pattern to create your instances of your flask application.
Parameters
----------
app : Flask
An instance of your `flask application <http://flask.pocoo.org/docs/1.0/api/#flask.Flask>`_.
"""
self.client_id = self.client_id or app.config["DISCORD_CLIENT_ID"]
self.__client_secret = self.__client_secret or app.config["DISCORD_CLIENT_SECRET"]
self.redirect_uri = self.redirect_uri or app.config["DISCORD_REDIRECT_URI"]
self.__bot_token = self.__bot_token or app.config.get("DISCORD_BOT_TOKEN", str())
def __init__(self, app, client_id=None, client_secret=None, redirect_uri=None, bot_token=None, users_cache=None):
self.client_id = client_id or app.config["DISCORD_CLIENT_ID"]
self.__client_secret = client_secret or app.config["DISCORD_CLIENT_SECRET"]
self.redirect_uri = redirect_uri or app.config["DISCORD_REDIRECT_URI"]
self.__bot_token = bot_token or app.config.get("DISCORD_BOT_TOKEN", str())
self.users_cache = cachetools.LFUCache(
app.config.get("DISCORD_USERS_CACHE_MAX_LIMIT", configs.DISCORD_USERS_CACHE_DEFAULT_MAX_LIMIT)
) if self.users_cache is None else self.users_cache
) if users_cache is None else users_cache
if not issubclass(self.users_cache.__class__, Mapping):
raise ValueError("Instance users_cache must be a mapping like object.")
self.proxy = self.proxy or app.config.get("DISCORD_PROXY_SETTINGS")
self.proxy_auth = self.proxy_auth or app.config.get("DISCORD_PROXY_AUTH_SETTINGS")
if "http://" in self.redirect_uri:
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true"
app.discord = self
@property
def user_id(self) -> typing.Union[int, None]:
"""A property which returns Discord user ID if it exists in flask :py:attr:`flask.session` object.
"""A property which returns Discord user ID if it exists in quart :py:attr:`quart.session` object.
Returns
-------
int
The Discord user ID of current user.
None
If the user ID doesn't exists in flask :py:attr:`flask.session`.
If the user ID doesn't exists in quart :py:attr:`quart.session`.
"""
return session.get("DISCORD_USER_ID")
@staticmethod
@abc.abstractmethod
def save_authorization_token(token: dict):
async def save_authorization_token(token: dict):
raise NotImplementedError
@staticmethod
@abc.abstractmethod
def get_authorization_token() -> dict:
async def get_authorization_token() -> dict:
raise NotImplementedError
def _fetch_token(self, state):
discord = self._make_session(state=state)
return discord.fetch_token(
async def _fetch_token(self, state):
discord = await self._make_session(state=state)
return await discord.fetch_token(
configs.DISCORD_TOKEN_URL,
client_secret=self.__client_secret,
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.
Parameters
@@ -117,7 +92,7 @@ class DiscordOAuth2HttpClient(abc.ABC):
"""
return OAuth2Session(
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"),
scope=scope,
redirect_uri=self.redirect_uri,
@@ -128,7 +103,7 @@ class DiscordOAuth2HttpClient(abc.ABC):
auto_refresh_url=configs.DISCORD_TOKEN_URL,
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.
Note
@@ -154,33 +129,29 @@ class DiscordOAuth2HttpClient(abc.ABC):
Raises
------
flask_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized.
flask_discord.RateLimited
Raises an instance of :py:class:`flask_discord.RateLimited` if application is being rate limited by Discord.
quart_discord.Unauthorized
Raises :py:class:`quart_discord.Unauthorized` if current user is not authorized.
quart_discord.RateLimited
Raises an instance of :py:class:`quart_discord.RateLimited` if application is being rate limited by Discord.
"""
route = configs.DISCORD_API_BASE_URL + route
discord = await self._make_session()
async with (await discord.request(
method, route, data, **kwargs
) if oauth else aiohttp.request(method, route, data=data, **kwargs)) as response:
if self.proxy is not None:
kwargs["proxy"] = self.proxy
if self.proxy_auth is not None:
kwargs["proxy_auth"] = self.proxy_auth
if response.status == 401:
raise exceptions.Unauthorized
if response.status == 429:
raise exceptions.RateLimited(response)
response = self._make_session(
).request(method, route, data, **kwargs) if oauth else requests.request(method, route, data=data, **kwargs)
try:
return await response.json()
except aiohttp.ContentTypeError:
return await response.text()
if response.status_code == 401:
raise exceptions.Unauthorized()
if response.status_code == 429:
raise exceptions.RateLimited(response)
try:
return response.json()
except json.JSONDecodeError:
return 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.
Parameters
@@ -198,11 +169,11 @@ class DiscordOAuth2HttpClient(abc.ABC):
Raises
------
flask_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized.
flask_discord.RateLimited
Raises an instance of :py:class:`flask_discord.RateLimited` if application is being rate limited by Discord.
quart_discord.Unauthorized
Raises :py:class:`quart_discord.Unauthorized` if current user is not authorized.
quart_discord.RateLimited
Raises an instance of :py:class:`quart_discord.RateLimited` if application is being rate limited by Discord.
"""
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

@@ -5,31 +5,31 @@ import discord
from . import configs, _http, models, utils, exceptions
from oauthlib.common import add_params_to_uri
from flask import request, session, redirect, current_app
from quart import request, session, redirect, current_app
class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
"""Main client class representing hypothetical OAuth2 session with discord.
It uses Flask `session <http://flask.pocoo.org/docs/1.0/api/#flask.session>`_ local proxy object
to save state, authorization token and keeps record of users sessions across different requests.
This class inherits :py:class:`flask_discord._http.DiscordOAuth2HttpClient` class.
It uses Quart `session <https://pgjones.gitlab.io/quart/reference/source/quart.sessions.html#quart.sessions.Session>`_
local proxy object to save state, authorization token and keeps record of users sessions across different requests.
This class inherits :py:class:`quart_discord._http.DiscordOAuth2HttpClient` class.
Parameters
----------
app : Flask
An instance of your `flask application <http://flask.pocoo.org/docs/1.0/api/#flask.Flask>`_.
app : Quart
An instance of your `quart application <https://pgjones.gitlab.io/quart/reference/source/quart.app.html#quart.app.Quart>`_.
client_id : int, optional
The client ID of discord application provided. Can be also set to flask config
The client ID of discord application provided. Can be also set to quart config
with key ``DISCORD_CLIENT_ID``.
client_secret : str, optional
The client secret of discord application provided. Can be also set to flask config
The client secret of discord application provided. Can be also set to quart config
with key ``DISCORD_CLIENT_SECRET``.
redirect_uri : str, optional
The default URL to use to redirect user to after authorization. Can be also set to flask config
The default URL to use to redirect user to after authorization. Can be also set to quart config
with key ``DISCORD_REDIRECT_URI``.
bot_token : str, optional
The bot token of the application. This is required when you also need to access bot scope resources
beyond the normal resources provided by the OAuth. Can be also set to flask config with
beyond the normal resources provided by the OAuth. Can be also set to quart config with
key ``DISCORD_BOT_TOKEN``.
users_cache : cachetools.LFUCache, optional
Any dict like mapping to internally cache the authorized users. Preferably an instance of
@@ -57,7 +57,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
def __get_state():
return session.pop("DISCORD_OAUTH2_STATE", str())
def create_session(
async def create_session(
self, scope: list = None, *, data: dict = None, prompt: bool = True,
permissions: typing.Union[discord.Permissions, int] = 0, **params
):
@@ -71,7 +71,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
<https://discordapp.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes>`_.
data : dict, optional
A mapping of your any custom data which you want to access after authorization grant. Use
`:py:meth:flask_discord.DiscordOAuth2Session.callback` to retrieve this data in your callback view.
`:py:meth:quart_discord.DiscordOAuth2Session.callback` to retrieve this data in your callback view.
prompt : bool, optional
Determines if the OAuth2 grant should be explicitly prompted and re-approved. Defaults to True.
Specify False for implicit grant which will skip the authorization screen and redirect to redirect URI.
@@ -85,7 +85,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
Returns
-------
redirect
Flask redirect to discord authorization servers to complete authorization code grant process.
Quart redirect to discord authorization servers to complete authorization code grant process.
"""
scope = scope or request.args.get("scope", str()).split() or configs.DISCORD_OAUTH_DEFAULT_SCOPES
@@ -95,7 +95,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
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)
self.__save_state(state)
@@ -117,50 +117,50 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
return redirect(authorization_url)
@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.
Meaning by default, it uses client side session handling.
Override this method if you want to handle the user's session server side. If this method is overridden then,
you must also override :py:meth:`flask_discord.DiscordOAuth2Session.get_authorization_token`.
you must also override :py:meth:`quart_discord.DiscordOAuth2Session.get_authorization_token`.
"""
session["DISCORD_OAUTH2_TOKEN"] = token
@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
previously by `:py:meth:`flask_discord.DiscordOAuth2Session.save_authorization_token` from user's cookies.
previously by `:py:meth:`quart_discord.DiscordOAuth2Session.save_authorization_token` from user's cookies.
You must override this method if you are implementing server side session handling.
"""
return session.get("DISCORD_OAUTH2_TOKEN")
def callback(self):
async def callback(self):
"""A method which should be always called after completing authorization code grant process
usually in callback view.
It fetches the authorization token and saves it flask
`session <http://flask.pocoo.org/docs/1.0/api/#flask.session>`_ object.
It fetches the authorization token and saves it quart
`session <https://pgjones.gitlab.io/quart/reference/source/quart.sessions.html#quart.sessions.Session>`_ object.
"""
error = request.values.get("error")
error = (await request.values).get("error")
if error:
if error == "access_denied":
raise exceptions.AccessDenied()
raise exceptions.HttpException(error)
state = self.__get_state()
token = self._fetch_token(state)
self.save_authorization_token(token)
token = await self._fetch_token(state)
await self.save_authorization_token(token)
return jwt.decode(state, current_app.config["SECRET_KEY"])
def revoke(self):
"""This method clears current discord token, state and all session data from flask
`session <http://flask.pocoo.org/docs/1.0/api/#flask.session>`_. Which means user will have
to go through discord authorization token grant flow again. Also tries to remove the user from internal
cache if they exist.
"""This method clears current discord token, state and all session data from quart
`session <https://pgjones.gitlab.io/quart/reference/source/quart.sessions.html#quart.sessions.Session>`_. Which
means user will have to go through discord authorization token grant flow again. Also tries to remove the user
from internal cache if they exist.
"""
@@ -172,31 +172,30 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
except KeyError:
pass
@property
def authorized(self):
async def authorized(self):
"""A boolean indicating whether current session has authorization token or not."""
return self._make_session().authorized
return (await self._make_session()).authorized
@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.
Returns
-------
flask_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
def fetch_connections() -> list:
async def fetch_connections() -> list:
"""This method returns list of user connection objects from internal cache if it exists otherwise
makes an API call to do so.
Returns
-------
list
List of :py:class:`flask_discord.models.UserConnection` objects.
List of :py:class:`quart_discord.models.UserConnection` objects.
"""
user = models.User.get_from_cache()
@@ -206,17 +205,17 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
except AttributeError:
pass
return models.UserConnection.fetch_from_api()
return await models.UserConnection.fetch_from_api()
@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
call to do so.
Returns
-------
list
List of :py:class:`flask_discord.models.Guild` objects.
List of :py:class:`quart_discord.models.Guild` objects.
"""
user = models.User.get_from_cache()
@@ -226,4 +225,4 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
except AttributeError:
pass
return models.Guild.fetch_from_api()
return await models.Guild.fetch_from_api()

View File

@@ -1,4 +1,4 @@
DISCORD_API_BASE_URL = "https://discordapp.com/api"
DISCORD_API_BASE_URL = "https://discord.com/api"
DISCORD_AUTHORIZATION_BASE_URL = DISCORD_API_BASE_URL + "/oauth2/authorize"
DISCORD_TOKEN_URL = DISCORD_API_BASE_URL + "/oauth2/token"

View File

@@ -1,4 +1,4 @@
from flask import current_app
from quart import current_app
from abc import ABCMeta, abstractmethod
@@ -22,20 +22,20 @@ class DiscordModelsBase(metaclass=DiscordModelsMeta):
self._payload = payload
@staticmethod
def _request(*args, **kwargs):
"""A shorthand to :py:func:flask_discord.request`. It uses Flask current_app local proxy to get the
Flask-Discord client.
async def _request(*args, **kwargs):
"""A shorthand to :py:func:quart_discord.request`. It uses Quart current_app local proxy to get the
Quart-Discord client.
"""
return current_app.discord.request(*args, **kwargs)
return await current_app.discord.request(*args, **kwargs)
@staticmethod
def _bot_request(*args, **kwargs):
"""A shorthand to :py:func:flask_discord.bot_request`."""
return current_app.discord.bot_request(*args, **kwargs)
async def _bot_request(*args, **kwargs):
"""A shorthand to :py:func:quart_discord.bot_request`."""
return await current_app.discord.bot_request(*args, **kwargs)
@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
API call to Discord.
@@ -48,7 +48,7 @@ class DiscordModelsBase(metaclass=DiscordModelsMeta):
"""
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:
return [cls(_) for _ in payload]
return cls(payload)

View File

@@ -1,4 +1,4 @@
from flask import current_app
from quart import current_app
from .base import DiscordModelsBase
from .integration import Integration
@@ -57,24 +57,24 @@ class UserConnection(DiscordModelsBase):
return bool(self.visibility)
@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
API call to Discord. If an instance of :py:class:`flask_discord.User` exists in the users internal cache
who are attached to these connections then, the cached property :py:attr:`flask_discord.User.connections`
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`
is updated.
Parameters
----------
cache : bool
Determines if the :py:attr:`flask_discord.User.guilds` cache should be updated with the new guilds.
Determines if the :py:attr:`quart_discord.User.guilds` cache should be updated with the new guilds.
Returns
-------
list[flask_discord.UserConnection, ...]
List of instances of :py:class:`flask_discord.UserConnection` to which this user belongs.
list[quart_discord.UserConnection, ...]
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:
user = current_app.discord.users_cache.get(current_app.discord.user_id)

View File

@@ -1,5 +1,5 @@
from .base import DiscordModelsBase
from flask import current_app
from quart import current_app
import discord
from .. import configs
@@ -66,23 +66,23 @@ class Guild(DiscordModelsBase):
return configs.DISCORD_GUILD_ICON_BASE_URL.format(guild_id=self.id, icon_hash=self.icon_hash)
@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
API call to Discord. If an instance of :py:class:`flask_discord.User` exists in the users internal cache
who belongs to these guilds then, the cached property :py:attr:`flask_discord.User.guilds` is updated.
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.
Parameters
----------
cache : bool
Determines if the :py:attr:`flask_discord.User.guilds` cache should be updated with the new guilds.
Determines if the :py:attr:`quart_discord.User.guilds` cache should be updated with the new guilds.
Returns
-------
list[flask_discord.Guild, ...]
List of instances of :py:class:`flask_discord.Guild` to which this user belongs.
list[quart_discord.Guild, ...]
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:
user = current_app.discord.users_cache.get(current_app.discord.user_id)

View File

@@ -5,7 +5,7 @@ from .. import exceptions
from .base import DiscordModelsBase
from .connections import UserConnection
from flask import current_app, session
from quart import current_app, session
class User(DiscordModelsBase):
@@ -48,7 +48,7 @@ class User(DiscordModelsBase):
An integer representing the
`type of nitro subscription <https://discordapp.com/developers/docs/resources/user#user-object-premium-types>`_.
connections : list
A list of :py:class:`flask_discord.UserConnection` instances. These are cached and this list might be empty.
A list of :py:class:`quart_discord.UserConnection` instances. These are cached and this list might be empty.
"""
@@ -69,12 +69,12 @@ class User(DiscordModelsBase):
self.premium_type = self._payload.get("premium_type")
# Few properties which are intended to be cached.
self._guilds = None # Mapping of guild ID to flask_discord.models.Guild(...).
self.connections = None # List of flask_discord.models.UserConnection(...).
self._guilds = None # Mapping of guild ID to quart_discord.models.Guild(...).
self.connections = None # List of quart_discord.models.UserConnection(...).
@property
def guilds(self):
"""A cached mapping of user's guild ID to :py:class:`flask_discord.Guild`. The guilds are cached when the first
"""A cached mapping of user's guild ID to :py:class:`quart_discord.Guild`. The guilds are cached when the first
API call for guilds is requested so it might be an empty dict.
"""
@@ -125,7 +125,7 @@ class User(DiscordModelsBase):
return False
@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
API call to Discord. The user returned from API will always be cached and update in internal cache.
@@ -133,10 +133,10 @@ class User(DiscordModelsBase):
----------
guilds : bool
A boolean indicating if user's guilds should be cached or not. Defaults to ``False``. If chose to not
cache, user's guilds can always be obtained from :py:func:`flask_discord.Guilds.fetch_from_api()`.
cache, user's guilds can always be obtained from :py:func:`quart_discord.Guilds.fetch_from_api()`.
connections : bool
A boolean indicating if user's connections should be cached or not. Defaults to ``False``. If chose to not
cache, user's connections can always be obtained from :py:func:`flask_discord.Connections.fetch_from_api()`.
cache, user's connections can always be obtained from :py:func:`quart_discord.Connections.fetch_from_api()`.
Returns
-------
@@ -144,14 +144,14 @@ class User(DiscordModelsBase):
An instance of this model itself.
[cls, ...]
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})
session["DISCORD_USER_ID"] = self.id
if guilds:
self.fetch_guilds()
await self.fetch_guilds()
if connections:
self.fetch_connections()
await self.fetch_connections()
return self
@@ -161,7 +161,7 @@ class User(DiscordModelsBase):
Returns
-------
flask_discord.User
quart_discord.User
An user instance if it exists in internal cache.
None
If the current doesn't exists in internal cache.
@@ -169,7 +169,7 @@ class User(DiscordModelsBase):
"""
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.
Parameters
@@ -184,40 +184,40 @@ class User(DiscordModelsBase):
Raises
------
flask_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized.
quart_discord.Unauthorized
Raises :py:class:`quart_discord.Unauthorized` if current user is not authorized.
"""
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:
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
and returns list of all guilds the user is member of.
Returns
-------
list
List of :py:class:`flask_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
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
cache and returns list of all connection instances.
Returns
-------
list
A list of :py:class:`flask_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

View File

@@ -3,7 +3,7 @@
import functools
from . import exceptions
from flask import current_app
from quart import current_app
class JSONBool(object):
@@ -35,7 +35,7 @@ def json_bool(value):
# Decorators.
def requires_authorization(view):
"""A decorator for flask views which raises exception :py:class:`flask_discord.Unauthorized` if the user
"""A decorator for quart views which raises exception :py:class:`quart_discord.Unauthorized` if the user
is not authorized from Discord OAuth2.
"""
@@ -43,9 +43,9 @@ def requires_authorization(view):
# TODO: Add support to validate scopes.
@functools.wraps(view)
def wrapper(*args, **kwargs):
if not current_app.discord.authorized:
async def wrapper(*args, **kwargs):
if not await current_app.discord.authorized():
raise exceptions.Unauthorized
return view(*args, **kwargs)
return await view(*args, **kwargs)
return wrapper

View File

@@ -1,8 +1,9 @@
Flask
Quart
pyjwt
requests
aiohttp
oauthlib
discord.py
cachetools
setuptools
requests_oauthlib
Async-OAuthlib

View File

@@ -1,8 +1,8 @@
"""
Flask-Discord
Quart-Discord
-------------
An Discord OAuth2 flask extension.
An Discord OAuth2 quart extension.
"""
import re
@@ -12,15 +12,15 @@ from setuptools import setup, find_packages
def __get_version():
with open("flask_discord/__init__.py") as package_init_file:
with open("quart_discord/__init__.py") as package_init_file:
return re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', package_init_file.read(), re.MULTILINE).group(1)
requirements = [
'Flask',
'Quart',
'pyjwt',
'oauthlib',
'requests_oauthlib',
'Async-OAuthlib',
'cachetools',
'requests',
'discord.py',
@@ -40,13 +40,13 @@ extra_requirements = {
setup(
name='Flask-Discord',
name='Quart-Discord',
version=__get_version(),
url='https://github.com/thec0sm0s/Flask-Discord',
url='https://github.com/jnawk/Quart-Discord',
license='MIT',
author='□ | The Cosmos',
author_email='deepakrajko14@gmail.com',
description='Discord OAuth2 extension for Flask.',
author='Philip Dowie',
author_email='philip@jnawk.nz',
description='Discord OAuth2 extension for Quart.',
long_description=__doc__,
packages=find_packages(),
zip_safe=False,
@@ -55,7 +55,6 @@ setup(
install_requires=requirements,
extra_requirements=extra_requirements,
classifiers=[
'Framework :: Flask',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',

View File

@@ -1,10 +1,10 @@
import os
from flask import Flask, redirect, url_for
from flask_discord import DiscordOAuth2Session, requires_authorization
from quart import Quart, redirect, url_for
from quart_discord import DiscordOAuth2Session, requires_authorization
app = Flask(__name__)
app = Quart(__name__)
app.secret_key = b"%\xe0'\x01\xdeH\x8e\x85m|\xb3\xffCN\xc9g"
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" # !! Only in development environment.
@@ -21,8 +21,8 @@ HYPERLINK = '<a href="{}">{}</a>'
@app.route("/")
def index():
if not discord.authorized:
async def index():
if not await discord.authorized():
return f"""
{HYPERLINK.format(url_for(".login"), "Login")} <br />
{HYPERLINK.format(url_for(".login_with_data"), "Login with custom data")} <br />
@@ -39,35 +39,37 @@ def index():
@app.route("/login/")
def login():
return discord.create_session()
async def login():
return await discord.create_session()
@app.route("/login-data/")
def login_with_data():
return discord.create_session(data=dict(redirect="/me/", coupon="15off", number=15, zero=0, status=False))
async def login_with_data():
return await discord.create_session(data=dict(redirect="/me/", coupon="15off", number=15, zero=0, status=False))
@app.route("/invite-bot/")
def invite_bot():
return discord.create_session(scope=["bot"], permissions=8, guild_id=464488012328468480, disable_guild_select=True)
async def invite_bot():
return await discord.create_session(
scope=["bot"], permissions=8, guild_id=464488012328468480, disable_guild_select=True
)
@app.route("/invite-oauth/")
def invite_oauth():
return discord.create_session(scope=["bot", "identify"], permissions=8)
async def invite_oauth():
return await discord.create_session(scope=["bot", "identify"], permissions=8)
@app.route("/callback/")
def callback():
data = discord.callback()
async def callback():
data = await discord.callback()
redirect_to = data.get("redirect", "/")
return redirect(redirect_to)
@app.route("/me/")
def me():
user = discord.fetch_user()
async def me():
user = await discord.fetch_user()
return f"""
<html>
<head>
@@ -84,21 +86,21 @@ def me():
@app.route("/me/guilds/")
def user_guilds():
guilds = discord.fetch_guilds()
async def user_guilds():
guilds = await discord.fetch_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>/")
def add_to_guild(guild_id):
user = discord.fetch_user()
return user.add_to_guild(guild_id)
async def add_to_guild(guild_id):
user = await discord.fetch_user()
return await user.add_to_guild(guild_id)
@app.route("/me/connections/")
def my_connections():
user = discord.fetch_user()
connections = discord.fetch_connections()
async def my_connections():
user = await discord.fetch_user()
connections = await discord.fetch_connections()
return f"""
<html>
<head>
@@ -113,14 +115,14 @@ def my_connections():
@app.route("/logout/")
def logout():
async def logout():
discord.revoke()
return redirect(url_for(".index"))
@app.route("/secret/")
@requires_authorization
def secret():
async def secret():
return os.urandom(16)