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 local_settings.py
db.sqlite3 db.sqlite3
# Flask stuff: # Quart stuff:
instance/ instance/
.webassets-cache .webassets-cache

View File

@@ -1,27 +1,24 @@
# Flask-Discord # Quart-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) [![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 ### Installation
To install current latest release you can use following command: To install current latest release you can use following command:
```sh ```sh
python3 -m pip install Flask-Discord python3 -m pip install Quart-Discord
``` ```
### Basic Example ### Basic Example
```python ```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 app = Quart(__name__)
from flask_discord import DiscordOAuth2Session, requires_authorization, Unauthorized
app = Flask(__name__) app.secret_key = b"random bytes representing quart secret key"
app.secret_key = b"random bytes representing flask secret key"
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" # !! Only in development environment.
app.config["DISCORD_CLIENT_ID"] = 490732332240863233 # Discord client ID. app.config["DISCORD_CLIENT_ID"] = 490732332240863233 # Discord client ID.
app.config["DISCORD_CLIENT_SECRET"] = "" # Discord client secret. app.config["DISCORD_CLIENT_SECRET"] = "" # Discord client secret.
@@ -32,25 +29,25 @@ discord = DiscordOAuth2Session(app)
@app.route("/login/") @app.route("/login/")
def login(): async def login():
return discord.create_session() return await discord.create_session()
@app.route("/callback/") @app.route("/callback/")
def callback(): async def callback():
discord.callback() await discord.callback()
return redirect(url_for(".me")) return redirect(url_for(".me"))
@app.errorhandler(Unauthorized) @app.errorhandler(Unauthorized)
def redirect_unauthorized(e): async def redirect_unauthorized(e):
return redirect(url_for("login")) return redirect(url_for("login"))
@app.route("/me/") @app.route("/me/")
@requires_authorization @requires_authorization
def me(): async def me():
user = discord.fetch_user() user = await discord.fetch_user()
return f""" return f"""
<html> <html>
<head> <head>
@@ -70,8 +67,8 @@ For an example to the working application, check [`test_app.py`](tests/test_app.
### Requirements ### Requirements
* Flask * Quart
* requests_oauthlib * Async-OAuthlib
* cachetools * cachetools
* discord.py * 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. 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 Discord OAuth2 Client
--------------------- ---------------------
.. autoclass:: flask_discord.DiscordOAuth2Session .. autoclass:: quart_discord.DiscordOAuth2Session
:members: :members:
:inherited-members: :inherited-members:
.. autoclass:: flask_discord._http.DiscordOAuth2HttpClient .. autoclass:: quart_discord._http.DiscordOAuth2HttpClient
:members: :members:
:inherited-members: :inherited-members:
@@ -20,23 +20,23 @@ Discord OAuth2 Client
Models Models
------ ------
.. autoclass:: flask_discord.models.Guild .. autoclass:: quart_discord.models.Guild
:members: :members:
:inherited-members: :inherited-members:
.. autoclass:: flask_discord.models.User .. autoclass:: quart_discord.models.User
:members: :members:
:inherited-members: :inherited-members:
.. autoclass:: flask_discord.models.Bot .. autoclass:: quart_discord.models.Bot
:members: :members:
:inherited-members: :inherited-members:
.. autoclass:: flask_discord.models.Integration .. autoclass:: quart_discord.models.Integration
:members: :members:
:inherited-members: :inherited-members:
.. autoclass:: flask_discord.models.UserConnection .. autoclass:: quart_discord.models.UserConnection
:members: :members:
:inherited-members: :inherited-members:
@@ -44,20 +44,20 @@ Models
Utilities Utilities
--------- ---------
.. autodecorator:: flask_discord.requires_authorization .. autodecorator:: quart_discord.requires_authorization
Exceptions Exceptions
---------- ----------
.. autoclass:: flask_discord.HttpException .. autoclass:: quart_discord.HttpException
:members: :members:
.. autoclass:: flask_discord.RateLimited .. autoclass:: quart_discord.RateLimited
:members: :members:
.. autoclass:: flask_discord.Unauthorized .. autoclass:: quart_discord.Unauthorized
:members: :members:
.. autoclass:: flask_discord.AccessDenied .. autoclass:: quart_discord.AccessDenied
:members: :members:

View File

@@ -14,16 +14,16 @@ import os
import re import re
import sys import sys
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('..'))
sys.path.append('../flask_discord/') sys.path.append('../quart_discord/')
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'Flask-Discord' project = 'Quart-Discord'
copyright = '2019, □ | The Cosmos' copyright = '2020, Philip Dowie'
author = '□ | The Cosmos' 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) ver = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1)
# The short X.Y version # The short X.Y version
version = ver version = ver
@@ -48,9 +48,9 @@ extensions = [
intersphinx_mapping = { intersphinx_mapping = {
'python': ('https://docs.python.org/3', None), 'python': ('https://docs.python.org/3', None),
'discord': ('https://discordpy.readthedocs.io/en/latest/', 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), '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. # 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. sphinx-quickstart on Wed May 8 08:29:45 2019.
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
Welcome to Flask-Discord's documentation! Welcome to Quart-Discord's documentation!
========================================= =========================================
.. image:: /images/background.jpg .. 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. Discord's OAuth2 API easier.
**Features** **Features**

View File

@@ -5,7 +5,7 @@
Introduction 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 makes easy implementation of Discord OAuth2 API. After creating a discord
client object, one can easily request authorization and hence any of the client object, one can easily request authorization and hence any of the
resources provided by the discord OAuth2 API under the available scope resources provided by the discord OAuth2 API under the available scope
@@ -14,14 +14,14 @@ permissions.
Requirements Requirements
------------ ------------
- **Flask** - **Quart**
This is an Flask extension. This is a Quart extension.
- **requests_oauthlib** - **requests_oauthlib**
It also requires requests_oauthlib to make OAuth2 sessions with discord. It also requires requests_oauthlib to make OAuth2 sessions with discord.
- **cachetools** - **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** - **discord.py**
Makes use of discord.py for re-using many Discord models. Makes use of discord.py for re-using many Discord models.
@@ -29,15 +29,15 @@ Requirements
Installing 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: :: 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 You can also install the latest development version (**maybe unstable/broken**) by
using following command: :: 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 Basic Usage
@@ -48,14 +48,12 @@ in exchange for fetching user's details and display them on web page.
.. code-block:: python3 .. code-block:: python3
from flask import Flask, redirect, url_for from quart import Quart, redirect, url_for
from flask_discord import DiscordOAuth2Session, requires_authorization, Unauthorized from quart_discord import DiscordOAuth2Session, requires_authorization, Unauthorized
app = Flask(__name__) app = Quart(__name__)
app.secret_key = b"random bytes representing flask secret key" app.secret_key = b"random bytes representing quart 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_ID"] = 490732332240863233 # Discord client ID.
app.config["DISCORD_CLIENT_SECRET"] = "" # Discord client secret. 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/") @app.route("/login/")
def login(): async def login():
return discord.create_session() return await discord.create_session()
@app.route("/callback/") @app.route("/callback/")
def callback(): async def callback():
discord.callback() await discord.callback()
return redirect(url_for(".me")) return redirect(url_for(".me"))
@app.errorhandler(Unauthorized) @app.errorhandler(Unauthorized)
def redirect_unauthorized(e): async def redirect_unauthorized(e):
return redirect(url_for("login")) return redirect(url_for("login"))
@app.route("/me/") @app.route("/me/")
@requires_authorization @requires_authorization
def me(): async def me():
user = discord.fetch_user() user = await discord.fetch_user()
return f""" return f"""
<html> <html>
<head> <head>
@@ -99,26 +97,3 @@ in exchange for fetching user's details and display them on web page.
if __name__ == "__main__": if __name__ == "__main__":
app.run() 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 cachetools
import requests import aiohttp
import typing import typing
import json import os
import abc import abc
from . import configs from . import configs
from . import exceptions from . import exceptions
from flask import session, request from quart import session, request
from collections.abc import Mapping from collections.abc import Mapping
from requests_oauthlib import OAuth2Session from async_oauthlib import OAuth2Session
class DiscordOAuth2HttpClient(abc.ABC): class DiscordOAuth2HttpClient(abc.ABC):
"""An OAuth2 http abstract base class providing some factory methods. """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. used directly.
""" """
@@ -25,78 +25,53 @@ class DiscordOAuth2HttpClient(abc.ABC):
"DISCORD_OAUTH2_TOKEN", "DISCORD_OAUTH2_TOKEN",
] ]
def __init__( def __init__(self, app, client_id=None, client_secret=None, redirect_uri=None, bot_token=None, users_cache=None):
self, app=None, self.client_id = client_id or app.config["DISCORD_CLIENT_ID"]
client_id=None, client_secret=None, redirect_uri=None, self.__client_secret = client_secret or app.config["DISCORD_CLIENT_SECRET"]
bot_token=None, users_cache=None, proxy=None, proxy_auth=None 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.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())
self.users_cache = cachetools.LFUCache( self.users_cache = cachetools.LFUCache(
app.config.get("DISCORD_USERS_CACHE_MAX_LIMIT", configs.DISCORD_USERS_CACHE_DEFAULT_MAX_LIMIT) 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): if not issubclass(self.users_cache.__class__, Mapping):
raise ValueError("Instance users_cache must be a mapping like object.") raise ValueError("Instance users_cache must be a mapping like object.")
self.proxy = self.proxy or app.config.get("DISCORD_PROXY_SETTINGS") if "http://" in self.redirect_uri:
self.proxy_auth = self.proxy_auth or app.config.get("DISCORD_PROXY_AUTH_SETTINGS") os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true"
app.discord = self app.discord = self
@property @property
def user_id(self) -> typing.Union[int, None]: 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 Returns
------- -------
int int
The Discord user ID of current user. The Discord user ID of current user.
None 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") return session.get("DISCORD_USER_ID")
@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
@@ -117,7 +92,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,
@@ -128,7 +103,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
@@ -154,33 +129,29 @@ class DiscordOAuth2HttpClient(abc.ABC):
Raises Raises
------ ------
flask_discord.Unauthorized quart_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. Raises :py:class:`quart_discord.Unauthorized` if current user is not authorized.
flask_discord.RateLimited quart_discord.RateLimited
Raises an instance of :py:class:`flask_discord.RateLimited` if application is being rate limited by Discord. Raises an instance of :py:class:`quart_discord.RateLimited` if application is being rate limited by Discord.
""" """
route = configs.DISCORD_API_BASE_URL + route 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: if response.status == 401:
kwargs["proxy"] = self.proxy raise exceptions.Unauthorized
if self.proxy_auth is not None: if response.status == 429:
kwargs["proxy_auth"] = self.proxy_auth raise exceptions.RateLimited(response)
response = self._make_session( try:
).request(method, route, data, **kwargs) if oauth else requests.request(method, route, data=data, **kwargs) return await response.json()
except aiohttp.ContentTypeError:
return await response.text()
if response.status_code == 401: async def bot_request(self, route: str, method="GET", **kwargs) -> typing.Union[dict, str]:
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]:
"""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
@@ -198,11 +169,11 @@ class DiscordOAuth2HttpClient(abc.ABC):
Raises Raises
------ ------
flask_discord.Unauthorized quart_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. Raises :py:class:`quart_discord.Unauthorized` if current user is not authorized.
flask_discord.RateLimited quart_discord.RateLimited
Raises an instance of :py:class:`flask_discord.RateLimited` if application is being rate limited by Discord. Raises an instance of :py:class:`quart_discord.RateLimited` if application is being rate limited by Discord.
""" """
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

@@ -5,31 +5,31 @@ import discord
from . import configs, _http, models, utils, exceptions from . import configs, _http, models, utils, exceptions
from oauthlib.common import add_params_to_uri 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): class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
"""Main client class representing hypothetical OAuth2 session with discord. """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 It uses Quart `session <https://pgjones.gitlab.io/quart/reference/source/quart.sessions.html#quart.sessions.Session>`_
to save state, authorization token and keeps record of users sessions across different requests. 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. This class inherits :py:class:`quart_discord._http.DiscordOAuth2HttpClient` class.
Parameters Parameters
---------- ----------
app : Flask app : Quart
An instance of your `flask application <http://flask.pocoo.org/docs/1.0/api/#flask.Flask>`_. An instance of your `quart application <https://pgjones.gitlab.io/quart/reference/source/quart.app.html#quart.app.Quart>`_.
client_id : int, optional 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``. with key ``DISCORD_CLIENT_ID``.
client_secret : str, optional 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``. with key ``DISCORD_CLIENT_SECRET``.
redirect_uri : str, optional 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``. with key ``DISCORD_REDIRECT_URI``.
bot_token : str, optional bot_token : str, optional
The bot token of the application. This is required when you also need to access bot scope resources 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``. key ``DISCORD_BOT_TOKEN``.
users_cache : cachetools.LFUCache, optional users_cache : cachetools.LFUCache, optional
Any dict like mapping to internally cache the authorized users. Preferably an instance of 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(): 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
): ):
@@ -71,7 +71,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
<https://discordapp.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes>`_. <https://discordapp.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes>`_.
data : dict, optional data : dict, optional
A mapping of your any custom data which you want to access after authorization grant. Use 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 prompt : bool, optional
Determines if the OAuth2 grant should be explicitly prompted and re-approved. Defaults to True. 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. 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 Returns
------- -------
redirect 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 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") 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,50 +117,50 @@ 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.
Override this method if you want to handle the user's session server side. If this method is overridden then, 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 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:`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. You must override this method if you are implementing server side session handling.
""" """
return session.get("DISCORD_OAUTH2_TOKEN") 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 """A method which should be always called after completing authorization code grant process
usually in callback view. usually in callback view.
It fetches the authorization token and saves it flask It fetches the authorization token and saves it quart
`session <http://flask.pocoo.org/docs/1.0/api/#flask.session>`_ object. `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:
if error == "access_denied": if error == "access_denied":
raise exceptions.AccessDenied() raise exceptions.AccessDenied()
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"])
def revoke(self): def revoke(self):
"""This method clears current discord token, state and all session data from flask """This method clears current discord token, state and all session data from quart
`session <http://flask.pocoo.org/docs/1.0/api/#flask.session>`_. Which means user will have `session <https://pgjones.gitlab.io/quart/reference/source/quart.sessions.html#quart.sessions.Session>`_. Which
to go through discord authorization token grant flow again. Also tries to remove the user from internal means user will have to go through discord authorization token grant flow again. Also tries to remove the user
cache if they exist. from internal cache if they exist.
""" """
@@ -172,31 +172,30 @@ 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
------- -------
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 @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.
Returns Returns
------- -------
list 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() user = models.User.get_from_cache()
@@ -206,17 +205,17 @@ 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.
Returns Returns
------- -------
list 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() user = models.User.get_from_cache()
@@ -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

@@ -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_AUTHORIZATION_BASE_URL = DISCORD_API_BASE_URL + "/oauth2/authorize"
DISCORD_TOKEN_URL = DISCORD_API_BASE_URL + "/oauth2/token" 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 from abc import ABCMeta, abstractmethod
@@ -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:flask_discord.request`. It uses Flask 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
Flask-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:flask_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

@@ -1,4 +1,4 @@
from flask import current_app from quart import current_app
from .base import DiscordModelsBase from .base import DiscordModelsBase
from .integration import Integration from .integration import Integration
@@ -57,24 +57,24 @@ 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:`flask_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:`flask_discord.User.connections` who are attached to these connections then, the cached property :py:attr:`quart_discord.User.connections`
is updated. is updated.
Parameters Parameters
---------- ----------
cache : bool 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 Returns
------- -------
list[flask_discord.UserConnection, ...] list[quart_discord.UserConnection, ...]
List of instances of :py:class:`flask_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

@@ -1,5 +1,5 @@
from .base import DiscordModelsBase from .base import DiscordModelsBase
from flask import current_app from quart import current_app
import discord import discord
from .. import configs 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) 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:`flask_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:`flask_discord.User.guilds` is updated. who belongs to these guilds then, the cached property :py:attr:`quart_discord.User.guilds` is updated.
Parameters Parameters
---------- ----------
cache : bool 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 Returns
------- -------
list[flask_discord.Guild, ...] list[quart_discord.Guild, ...]
List of instances of :py:class:`flask_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

@@ -5,7 +5,7 @@ from .. import exceptions
from .base import DiscordModelsBase from .base import DiscordModelsBase
from .connections import UserConnection from .connections import UserConnection
from flask import current_app, session from quart import current_app, session
class User(DiscordModelsBase): class User(DiscordModelsBase):
@@ -48,7 +48,7 @@ class User(DiscordModelsBase):
An integer representing the An integer representing the
`type of nitro subscription <https://discordapp.com/developers/docs/resources/user#user-object-premium-types>`_. `type of nitro subscription <https://discordapp.com/developers/docs/resources/user#user-object-premium-types>`_.
connections : list 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") self.premium_type = self._payload.get("premium_type")
# Few properties which are intended to be cached. # Few properties which are intended to be cached.
self._guilds = None # Mapping of guild ID to flask_discord.models.Guild(...). self._guilds = None # Mapping of guild ID to quart_discord.models.Guild(...).
self.connections = None # List of flask_discord.models.UserConnection(...). self.connections = None # List of quart_discord.models.UserConnection(...).
@property @property
def guilds(self): 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. API call for guilds is requested so it might be an empty dict.
""" """
@@ -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.
@@ -133,10 +133,10 @@ class User(DiscordModelsBase):
---------- ----------
guilds : bool guilds : bool
A boolean indicating if user's guilds should be cached or not. Defaults to ``False``. If chose to not 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 connections : bool
A boolean indicating if user's connections should be cached or not. Defaults to ``False``. If chose to not 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 Returns
------- -------
@@ -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
@@ -161,7 +161,7 @@ class User(DiscordModelsBase):
Returns Returns
------- -------
flask_discord.User quart_discord.User
An user instance if it exists in internal cache. An user instance if it exists in internal cache.
None None
If the current doesn't exists in internal cache. 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) 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
@@ -184,40 +184,40 @@ class User(DiscordModelsBase):
Raises Raises
------ ------
flask_discord.Unauthorized quart_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. Raises :py:class:`quart_discord.Unauthorized` if current user is not authorized.
""" """
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.
Returns Returns
------- -------
list 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 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.
Returns Returns
------- -------
list 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 return self.connections

View File

@@ -3,7 +3,7 @@
import functools import functools
from . import exceptions from . import exceptions
from flask import current_app from quart import current_app
class JSONBool(object): class JSONBool(object):
@@ -35,7 +35,7 @@ def json_bool(value):
# Decorators. # Decorators.
def requires_authorization(view): 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. is not authorized from Discord OAuth2.
""" """
@@ -43,9 +43,9 @@ def requires_authorization(view):
# TODO: Add support to validate scopes. # TODO: Add support to validate scopes.
@functools.wraps(view) @functools.wraps(view)
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 view(*args, **kwargs) return await view(*args, **kwargs)
return wrapper return wrapper

View File

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

View File

@@ -1,10 +1,10 @@
import os import os
from flask import Flask, redirect, url_for from quart import Quart, redirect, url_for
from flask_discord import DiscordOAuth2Session, requires_authorization 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" app.secret_key = b"%\xe0'\x01\xdeH\x8e\x85m|\xb3\xffCN\xc9g"
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" # !! Only in development environment. os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" # !! Only in development environment.
@@ -21,8 +21,8 @@ HYPERLINK = '<a href="{}">{}</a>'
@app.route("/") @app.route("/")
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,35 +39,37 @@ def index():
@app.route("/login/") @app.route("/login/")
def login(): async def login():
return discord.create_session() return await discord.create_session()
@app.route("/login-data/") @app.route("/login-data/")
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/")
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/")
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/")
def callback(): async def callback():
data = discord.callback() data = await discord.callback()
redirect_to = data.get("redirect", "/") redirect_to = data.get("redirect", "/")
return redirect(redirect_to) return redirect(redirect_to)
@app.route("/me/") @app.route("/me/")
def me(): async def me():
user = discord.fetch_user() user = await discord.fetch_user()
return f""" return f"""
<html> <html>
<head> <head>
@@ -84,21 +86,21 @@ def me():
@app.route("/me/guilds/") @app.route("/me/guilds/")
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>/")
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/")
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>
@@ -113,14 +115,14 @@ def my_connections():
@app.route("/logout/") @app.route("/logout/")
def logout(): async def logout():
discord.revoke() discord.revoke()
return redirect(url_for(".index")) return redirect(url_for(".index"))
@app.route("/secret/") @app.route("/secret/")
@requires_authorization @requires_authorization
def secret(): async def secret():
return os.urandom(16) return os.urandom(16)