Merge pull request #10 from thec0sm0s/dev

An internal caching layer.
This commit is contained in:
thec0sm0s
2020-05-16 20:19:36 +05:30
committed by GitHub
16 changed files with 411 additions and 90 deletions

View File

@@ -53,5 +53,8 @@ Exceptions
.. autoclass:: flask_discord.HttpException .. autoclass:: flask_discord.HttpException
:members: :members:
.. autoclass:: flask_discord.RateLimited
:members:
.. autoclass:: flask_discord.Unauthorized .. autoclass:: flask_discord.Unauthorized
:members: :members:

View File

@@ -45,6 +45,13 @@ extensions = [
'pallets_sphinx_themes', 'pallets_sphinx_themes',
] ]
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'flask': ('https://flask.palletsprojects.com/en/1.1.x/', None),
'cachetools': ('https://cachetools.readthedocs.io/en/stable/', None),
'requests_oauthlib': ('https://requests-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.
templates_path = ['_templates'] templates_path = ['_templates']

View File

@@ -16,6 +16,7 @@ Discord's OAuth2 API easier.
- Clean object-oriented design. - Clean object-oriented design.
- Covers most of the scopes provided by the API. - Covers most of the scopes provided by the API.
- Supports various discord models and objects. - Supports various discord models and objects.
- An internal smart caching layer to increase the performance.
Contents Contents

View File

@@ -9,8 +9,9 @@ __all__ = [
"requires_authorization", "requires_authorization",
"HttpException", "HttpException",
"RateLimited",
"Unauthorized", "Unauthorized",
] ]
__version__ = "0.1.11" __version__ = "0.1.50"

View File

@@ -1,3 +1,7 @@
import cachetools
import requests
import typing
import json
import os import os
import abc import abc
@@ -5,6 +9,7 @@ from . import configs
from . import exceptions from . import exceptions
from flask import session from flask import session
from collections.abc import Mapping
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
@@ -21,22 +26,46 @@ class DiscordOAuth2HttpClient(abc.ABC):
The client secret of discord application provided. The client secret of discord application provided.
redirect_uri : str redirect_uri : str
The default URL to use to redirect user to after authorization. The default URL to use to redirect user to after authorization.
users_cache : cachetools.LFUCache
Any dict like mapping to internally cache the authorized users. Preferably an instance of
cachetools.LFUCache or cachetools.TTLCache. If not specified, default cachetools.LFUCache is used.
Uses the default max limit for cache if ``DISCORD_USERS_CACHE_MAX_LIMIT`` isn't specified in app config.
""" """
SESSION_KEYS = [ SESSION_KEYS = [
"DISCORD_USER_ID",
"DISCORD_OAUTH2_STATE", "DISCORD_OAUTH2_STATE",
"DISCORD_OAUTH2_TOKEN", "DISCORD_OAUTH2_TOKEN",
] ]
def __init__(self, app): def __init__(self, app, users_cache=None):
self.client_id = app.config["DISCORD_CLIENT_ID"] self.client_id = app.config["DISCORD_CLIENT_ID"]
self.client_secret = app.config["DISCORD_CLIENT_SECRET"] self.client_secret = app.config["DISCORD_CLIENT_SECRET"]
self.redirect_uri = app.config["DISCORD_REDIRECT_URI"] self.redirect_uri = app.config["DISCORD_REDIRECT_URI"]
self.users_cache = cachetools.LFUCache(
app.config.get("DISCORD_USERS_CACHE_MAX_LIMIT", configs.DISCORD_USERS_CACHE_DEFAULT_MAX_LIMIT)
) 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.")
if "http://" in self.redirect_uri: if "http://" in self.redirect_uri:
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true"
app.discord = self 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.
Returns
-------
int
The Discord user ID of current user.
None
If the user ID doesn't exists in flask :py:attr:`flask.session`.
"""
return session.get("DISCORD_USER_ID")
@staticmethod @staticmethod
def _token_updater(token): def _token_updater(token):
session["DISCORD_OAUTH2_TOKEN"] = token session["DISCORD_OAUTH2_TOKEN"] = token
@@ -63,7 +92,7 @@ class DiscordOAuth2HttpClient(abc.ABC):
return OAuth2Session( return OAuth2Session(
client_id=self.client_id, client_id=self.client_id,
token=token or session.get("DISCORD_OAUTH2_TOKEN"), token=token or session.get("DISCORD_OAUTH2_TOKEN"),
state=state, state=state or session.get("DISCORD_OAUTH2_STATE"),
scope=scope, scope=scope,
redirect_uri=self.redirect_uri, redirect_uri=self.redirect_uri,
auto_refresh_kwargs={ auto_refresh_kwargs={
@@ -73,7 +102,7 @@ class DiscordOAuth2HttpClient(abc.ABC):
auto_refresh_url=configs.DISCORD_TOKEN_URL, auto_refresh_url=configs.DISCORD_TOKEN_URL,
token_updater=self._token_updater) token_updater=self._token_updater)
def request(self, route: str, method="GET", data=None, **kwargs) -> dict: 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
@@ -88,21 +117,33 @@ class DiscordOAuth2HttpClient(abc.ABC):
Specify the HTTP method to use to perform this request. Specify the HTTP method to use to perform this request.
data : dict, optional data : dict, optional
The optional payload the include with the request. The optional payload the include with the request.
oauth : bool
A boolean determining if this should be Discord OAuth2 session request or any standard request.
Returns Returns
------- -------
dict dict, str
Dictionary containing received from sent HTTP GET request. Dictionary containing received from sent HTTP GET request if content-type is ``application/json``
otherwise returns raw text content of the response.
Raises Raises
------ ------
flask_discord.Unauthorized flask_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. 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.
""" """
response = self._make_session().request(method, configs.DISCORD_API_BASE_URL + route, data, **kwargs) route = configs.DISCORD_API_BASE_URL + route
response = self._make_session(
).request(method, route, data, **kwargs) if oauth else requests.request(method, route, data=data, **kwargs)
if response.status_code == 401: if response.status_code == 401:
raise exceptions.Unauthorized raise exceptions.Unauthorized
if response.status_code == 429:
raise exceptions.RateLimited(response)
return response.json() try:
return response.json()
except json.JSONDecodeError:
return response.text

View File

@@ -58,9 +58,13 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
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 flask
`session <http://flask.pocoo.org/docs/1.0/api/#flask.session>`_. Which means user will have `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. to go through discord authorization token grant flow again. Also tries to remove the user from internal
cache if they exist.
""" """
self.users_cache.pop(self.user_id, None)
for session_key in self.SESSION_KEYS: for session_key in self.SESSION_KEYS:
try: try:
session.pop(session_key) session.pop(session_key)
@@ -72,18 +76,21 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
"""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 self._make_session().authorized
def fetch_user(self) -> models.User: @staticmethod
"""This method requests for data of current user from discord and returns user object. 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 Returns
------- -------
flask_discord.models.User flask_discord.models.User
""" """
return models.User(self.request("/users/@me")) return models.User.get_from_cache() or models.User.fetch_from_api()
def fetch_connections(self) -> list: @staticmethod
"""Requests and returns connections of current user from discord. 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 Returns
------- -------
@@ -91,11 +98,19 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
List of :py:class:`flask_discord.models.UserConnection` objects. List of :py:class:`flask_discord.models.UserConnection` objects.
""" """
connections_payload = self.request("/users/@me/connections") user = models.User.get_from_cache()
return [models.UserConnection(payload) for payload in connections_payload] try:
if user.connections is not None:
return user.connections
except AttributeError:
pass
def fetch_guilds(self) -> list: return models.UserConnection.fetch_from_api()
"""Requests and returns guilds of current user from discord.
@staticmethod
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 Returns
------- -------
@@ -103,5 +118,11 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient):
List of :py:class:`flask_discord.models.Guild` objects. List of :py:class:`flask_discord.models.Guild` objects.
""" """
guilds_payload = self.request("/users/@me/guilds") user = models.User.get_from_cache()
return [models.Guild(payload) for payload in guilds_payload] try:
if user.guilds is not None:
return user.guilds
except AttributeError:
pass
return models.Guild.fetch_from_api()

View File

@@ -19,3 +19,5 @@ DISCORD_IMAGE_FORMAT = "png"
DISCORD_ANIMATED_IMAGE_FORMAT = "gif" DISCORD_ANIMATED_IMAGE_FORMAT = "gif"
DISCORD_USER_AVATAR_BASE_URL = DISCORD_IMAGE_BASE_URL + "avatars/{user_id}/{avatar_hash}.{format}" DISCORD_USER_AVATAR_BASE_URL = DISCORD_IMAGE_BASE_URL + "avatars/{user_id}/{avatar_hash}.{format}"
DISCORD_GUILD_ICON_BASE_URL = DISCORD_IMAGE_BASE_URL + "icons/{guild_id}/{icon_hash}.png" DISCORD_GUILD_ICON_BASE_URL = DISCORD_IMAGE_BASE_URL + "icons/{guild_id}/{icon_hash}.png"
DISCORD_USERS_CACHE_DEFAULT_MAX_LIMIT = 100

View File

@@ -1,6 +1,44 @@
import json
class HttpException(Exception): class HttpException(Exception):
"""Base Exception class representing a HTTP exception.""" """Base Exception class representing a HTTP exception."""
class RateLimited(HttpException):
"""A HTTP Exception raised when the application is being rate limited.
It provides the ``response`` attribute which can be used to get more details of the actual response from
the Discord API with few more shorthands to ``response.json()``.
Attributes
----------
response : requests.Response
The actual response object received from Discord API.
json : dict
The actual JSON data received. Shorthand to ``response.json()``.
message : str
A message saying you are being rate limited.
retry_after : int
The number of milliseconds to wait before submitting another request.
is_global : bool
A value indicating if you are being globally rate limited or not
"""
def __init__(self, response):
self.response = response
try:
self.json = self.response.json()
except json.JSONDecodeError:
self.json = dict()
self.message = self.response.text
else:
self.message = self.json["message"]
self.is_global = self.json["global"]
self.retry_after = self.json["retry_after"]
finally:
super().__init__(self.message)
class Unauthorized(HttpException): class Unauthorized(HttpException):
"""A HTTP Exception raised when user is not authorized.""" """A HTTP Exception raised when user is not authorized."""

View File

@@ -1,6 +1,7 @@
from .guild import Guild from .connections import UserConnection
from .integration import Integration
from .user import User, Bot from .user import User, Bot
from .connections import UserConnection, Integration from .guild import Guild
__all__ = [ __all__ = [
@@ -8,5 +9,4 @@ __all__ = [
"User", "User",
"Bot", "Bot",
"UserConnection", "UserConnection",
"Integration",
] ]

View File

@@ -1,7 +1,50 @@
from abc import ABC from flask import current_app
from abc import ABCMeta, abstractmethod
class DiscordModelsBase(ABC): class DiscordModelsMeta(ABCMeta):
ROUTE = str()
def __init__(cls, name, *args, **kwargs):
if not cls.ROUTE and name != "DiscordModelsBase":
raise NotImplementedError(f"ROUTE must be specified in a Discord model: {name}.")
super().__init__(name, *args, **kwargs)
class DiscordModelsBase(metaclass=DiscordModelsMeta):
MANY = False
@abstractmethod
def __init__(self, payload):
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.
"""
return current_app.discord.request(*args, **kwargs)
@classmethod
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.
Returns
-------
cls
An instance of this model itself.
[cls, ...]
List of instances of this model when many of these models exist.
"""
payload = cls._request(cls.ROUTE)
if cls.MANY:
return [cls(_) for _ in payload]
return cls(payload)
def to_json(self): def to_json(self):
"""A utility method which returns raw payload object as it was received from discord. """A utility method which returns raw payload object as it was received from discord.

View File

@@ -1,55 +1,10 @@
from flask import current_app
from .base import DiscordModelsBase from .base import DiscordModelsBase
from .integration import Integration
from .user import User
class Integration(DiscordModelsBase): class UserConnection(DiscordModelsBase):
""""Class representing discord server integrations.
Attributes
----------
id : int
Integration ID.
name : str
Name of integration.
type : str
Integration type (twitch, youtube, etc).
enabled : bool
A boolean representing if this integration is enabled.
syncing : bool
A boolean representing if this integration is syncing.
role_id : int
ID that this integration uses for subscribers.
expire_behaviour : int
An integer representing the behaviour of expiring subscribers.
expire_grace_period : int
An integer representing the grace period before expiring subscribers.
user : User
Object representing user of this integration.
account : dict
A dictionary representing raw
`account <https://discordapp.com/developers/docs/resources/guild#integration-account-object>`_ object.
synced_at : ISO8601 timestamp
Representing when this integration was last synced.
"""
def __init__(self, payload):
self._payload = payload
self.id = int(self._payload.get("id", 0))
self.name = self._payload.get("name")
self.type = self._payload.get("type")
self.enabled = self._payload.get("enabled")
self.syncing = self._payload.get("syncing")
self.role_id = int(self._payload.get("role_id", 0))
self.expire_behaviour = self._payload.get("expire_behaviour")
self.expire_grace_period = self._payload.get("expire_grace_period")
self.user = User(self._payload.get("user", dict()))
self.account = self._payload.get("account")
self.synced_at = self._payload.get("synced_at")
class UserConnection(object):
"""Class representing connections in discord account of the user. """Class representing connections in discord account of the user.
Attributes Attributes
@@ -78,8 +33,11 @@ class UserConnection(object):
""" """
MANY = True
ROUTE = "/users/@me/connections"
def __init__(self, payload): def __init__(self, payload):
self._payload = payload super().__init__(payload)
self.id = self._payload["id"] self.id = self._payload["id"]
self.name = self._payload.get("name") self.name = self._payload.get("name")
self.type = self._payload.get("type") self.type = self._payload.get("type")
@@ -97,3 +55,32 @@ class UserConnection(object):
def is_visible(self): def is_visible(self):
"""A property returning bool if this integration is visible to everyone.""" """A property returning bool if this integration is visible to everyone."""
return bool(self.visibility) return bool(self.visibility)
@classmethod
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`
is updated.
Parameters
----------
cache : bool
Determines if the :py:attr:`flask_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.
"""
connections = super().fetch_from_api()
if cache:
user = current_app.discord.users_cache.get(current_app.discord.user_id)
try:
user.connections = connections
except AttributeError:
pass
return connections

View File

@@ -1,5 +1,5 @@
from .base import DiscordModelsBase from .base import DiscordModelsBase
from flask import current_app
from .. import configs from .. import configs
@@ -21,8 +21,11 @@ class Guild(DiscordModelsBase):
""" """
MANY = True
ROUTE = "/users/@me/guilds"
def __init__(self, payload): def __init__(self, payload):
self._payload = payload super().__init__(payload)
self.id = int(self._payload["id"]) self.id = int(self._payload["id"])
self.name = self._payload["name"] self.name = self._payload["name"]
self.icon_hash = self._payload.get("icon") self.icon_hash = self._payload.get("icon")
@@ -38,3 +41,31 @@ class Guild(DiscordModelsBase):
if not self.icon_hash: if not self.icon_hash:
return return
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
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.
Parameters
----------
cache : bool
Determines if the :py:attr:`flask_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.
"""
guilds = super().fetch_from_api()
if cache:
user = current_app.discord.users_cache.get(current_app.discord.user_id)
try:
user.guilds = {guild.id: guild for guild in guilds}
except AttributeError:
pass
return guilds

View File

@@ -0,0 +1,42 @@
class Integration(object):
""""Class representing discord server integrations.
Attributes
----------
id : int
Integration ID.
name : str
Name of integration.
type : str
Integration type (twitch, youtube, etc).
enabled : bool
A boolean representing if this integration is enabled.
syncing : bool
A boolean representing if this integration is syncing.
role_id : int
ID that this integration uses for subscribers.
expire_behaviour : int
An integer representing the behaviour of expiring subscribers.
expire_grace_period : int
An integer representing the grace period before expiring subscribers.
account : dict
A dictionary representing raw
`account <https://discordapp.com/developers/docs/resources/guild#integration-account-object>`_ object.
synced_at : ISO8601 timestamp
Representing when this integration was last synced.
"""
def __init__(self, payload):
self._payload = payload
self.id = int(self._payload.get("id", 0))
self.name = self._payload.get("name")
self.type = self._payload.get("type")
self.enabled = self._payload.get("enabled")
self.syncing = self._payload.get("syncing")
self.role_id = int(self._payload.get("role_id", 0))
self.expire_behaviour = self._payload.get("expire_behaviour")
self.expire_grace_period = self._payload.get("expire_grace_period")
# self.user = User(self._payload.get("user", dict()))
self.account = self._payload.get("account")
self.synced_at = self._payload.get("synced_at")

View File

@@ -1,9 +1,9 @@
from flask import current_app, session from .. import configs
import requests
from .guild import Guild
from .base import DiscordModelsBase from .base import DiscordModelsBase
from .. import configs, exceptions from flask import current_app, session
from .connections import UserConnection
class User(DiscordModelsBase): class User(DiscordModelsBase):
@@ -35,11 +35,15 @@ class User(DiscordModelsBase):
premium_type : int premium_type : int
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
A list of :py:class:`flask_discord.UserConnection` instances. These are cached and this list might be empty.
""" """
ROUTE = "/users/@me"
def __init__(self, payload): def __init__(self, payload):
self._payload = payload super().__init__(payload)
self.id = int(self._payload["id"]) self.id = int(self._payload["id"])
self.username = self._payload["username"] self.username = self._payload["username"]
self.discriminator = self._payload["discriminator"] self.discriminator = self._payload["discriminator"]
@@ -52,6 +56,25 @@ class User(DiscordModelsBase):
self.flags = self._payload.get("flags") self.flags = self._payload.get("flags")
self.premium_type = self._payload.get("premium_type") 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(...).
@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
API call for guilds is requested so it might be an empty dict.
"""
try:
return list(self._guilds.values())
except AttributeError:
pass
@guilds.setter
def guilds(self, value):
self._guilds = value
def __str__(self): def __str__(self):
return f"{self.name}#{self.discriminator}" return f"{self.name}#{self.discriminator}"
@@ -73,6 +96,51 @@ class User(DiscordModelsBase):
"""A boolean representing if avatar of user is animated. Meaning user has GIF avatar.""" """A boolean representing if avatar of user is animated. Meaning user has GIF avatar."""
return self.avatar_hash.startswith("a_") return self.avatar_hash.startswith("a_")
@classmethod
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.
Parameters
----------
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()`.
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()`.
Returns
-------
cls
An instance of this model itself.
[cls, ...]
List of instances of this model when many of these models exist."""
self = super().fetch_from_api()
current_app.discord.users_cache.update({self.id: self})
session["DISCORD_USER_ID"] = self.id
if guilds:
self.fetch_guilds()
if connections:
self.fetch_connections()
return self
@classmethod
def get_from_cache(cls):
"""A class method which returns an instance of this model if it exists in internal cache.
Returns
-------
flask_discord.User
An user instance if it exists in internal cache.
None
If the current doesn't exists in internal cache.
"""
return current_app.discord.users_cache.get(current_app.discord.user_id)
def add_to_guild(self, guild_id) -> dict: 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.
@@ -92,19 +160,39 @@ class User(DiscordModelsBase):
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized. Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized.
""" """
route = configs.DISCORD_API_BASE_URL + f"/guilds/{guild_id}/members/{self.id}"
data = {"access_token": session["DISCORD_OAUTH2_TOKEN"]["access_token"]} data = {"access_token": session["DISCORD_OAUTH2_TOKEN"]["access_token"]}
headers = {"Authorization": f"Bot {current_app.config['DISCORD_BOT_TOKEN']}"} headers = {"Authorization": f"Bot {current_app.config['DISCORD_BOT_TOKEN']}"}
response = requests.put(route, json=data, headers=headers) return self._request(
f"/guilds/{guild_id}/members/{self.id}", method="PUT", oauth=False, json=data, headers=headers
) or dict()
if response.status_code == 401: def fetch_guilds(self) -> list:
raise exceptions.Unauthorized """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.
if response.status_code == 204: Returns
return dict() -------
list
List of :py:class:`flask_discord.Guilds` instances.
return response.json() """
self._guilds = {guild.id: guild for guild in Guild.fetch_from_api(cache=False)}
return self.guilds
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.
"""
self.connections = UserConnection.fetch_from_api(cache=False)
return self.connections
class Bot(User): class Bot(User):
"""Class representing the client user itself.""" """Class representing the client user itself."""
# TODO: What is this?

View File

@@ -1,2 +1,3 @@
Flask Flask
cachetools
requests_oauthlib requests_oauthlib

View File

@@ -16,15 +16,30 @@ app.config["DISCORD_REDIRECT_URI"] = "http://127.0.0.1:5000/callback"
discord = DiscordOAuth2Session(app) discord = DiscordOAuth2Session(app)
HYPERLINK = '<a href="{}">{}</a>'
@app.route("/") @app.route("/")
def index(): def index():
if not discord.authorized:
return HYPERLINK.format(url_for(".login"), "Login")
return f"""
{HYPERLINK.format(url_for(".me"), "@ME")}<br />
{HYPERLINK.format(url_for(".logout"), "Logout")}<br />
{HYPERLINK.format(url_for(".user_guilds"), "My Servers")}<br />
{HYPERLINK.format(url_for(".add_to_guild", guild_id=475549041741135881), "Add me to 475549041741135881.")}
"""
@app.route("/login/")
def login():
return discord.create_session() return discord.create_session()
@app.route("/callback/") @app.route("/callback/")
def callback(): def callback():
discord.callback() discord.callback()
return redirect(url_for(".me")) return redirect(url_for(".index"))
@app.route("/me/") @app.route("/me/")