🤞 Convert DiscordOAuth2Session into its own package

This commit is contained in:
thec0sm0s
2020-08-29 13:44:05 +05:30
parent d23441a070
commit bfe5adcc2c
11 changed files with 55 additions and 48 deletions

View File

@@ -0,0 +1,12 @@
from .connections import UserConnection
from .integration import Integration
from .user import User, Bot
from .guild import Guild
__all__ = [
"Guild",
"User",
"Bot",
"UserConnection",
]

View File

@@ -0,0 +1,65 @@
from flask import current_app
from abc import ABCMeta, abstractmethod
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):
BOT = False
MANY = False
@abstractmethod
def __init__(self, payload):
self._payload = payload
@staticmethod
def _request(*args, **kwargs):
"""A shorthand to :py:func:flask_discord.oauth2.DiscordOAuth2Session.request`.
It uses Flask current_app local proxy to get the instance of Flask-Discord DiscordOAuth2Session.
"""
return current_app.discord.request(*args, **kwargs)
@staticmethod
def _bot_request(*args, **kwargs):
"""A shorthand to :py:func:flask_discord.oauth2.DiscordOAuth2Session.bot_request`."""
return current_app.discord.bot_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.
"""
request_method = cls._bot_request if cls.BOT else cls._request
payload = request_method(cls.ROUTE)
if cls.MANY:
return [cls(_) for _ in payload]
return cls(payload)
def to_json(self):
"""A utility method which returns raw payload object as it was received from discord.
Returns
-------
dict
A dict representing raw payload object received from discord.
"""
return self._payload

View File

@@ -0,0 +1,87 @@
from flask import current_app
from .base import DiscordModelsBase
from .integration import Integration
class UserConnection(DiscordModelsBase):
"""Class representing connections in discord account of the user.
Attributes
----------
id : str
ID of the connection account.
name : str
The username of the connection account.
type : str
The service of connection (twitch, youtube).
revoked : bool
A boolean representing whether the connection is revoked.
integrations : list
A list of server Integration objects.
verified : bool
A boolean representing whether the connection is verified.
friend_sync : bool
A boolean representing whether friend sync is enabled for this connection.
show_activity : bool
A boolean representing whether activities related to this connection will
be shown in presence updates.
visibility : int
An integer representing
`visibility <https://discordapp.com/developers/docs/resources/user#user-object-visibility-types>`_
of this connection.
"""
MANY = True
ROUTE = "/users/@me/connections"
def __init__(self, payload):
super().__init__(payload)
self.id = self._payload["id"]
self.name = self._payload.get("name")
self.type = self._payload.get("type")
self.revoked = self._payload.get("revoked")
self.integrations = self.__get_integrations()
self.verified = self._payload.get("verified")
self.friend_sync = self._payload.get("friend_sync")
self.show_activity = self._payload.get("show_activity")
self.visibility = self._payload.get("visibility")
def __get_integrations(self):
return [Integration(payload) for payload in self._payload.get("integrations", list())]
@property
def is_visible(self):
"""A property returning bool if this integration is visible to everyone."""
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.oauth2.models.User` exists in the
users internal cache who are attached to these connections then, the cached property
:py:attr:`flask_discord.oauth2.models.User.connections` is updated.
Parameters
----------
cache : bool
Determines if the :py:attr:`flask_discord.oauth2.models.User.guilds` cache should be
updated with the new guilds.
Returns
-------
list[flask_discord.oauth2.models.UserConnection, ...]
List of instances of :py:class:`flask_discord.oauth2.models.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

@@ -0,0 +1,97 @@
from .base import DiscordModelsBase
import discord
from flask import current_app
from flask_discord import configs
class Guild(DiscordModelsBase):
"""Class representing discord Guild the user is part of.
Operations
----------
x == y
Checks if two guild's are the same.
x != y
Checks if two guild's are not the same.
str(x)
Returns the guild's name.
Attributes
----------
id : int
Discord ID of the guild.
name : str
Name of the guild.
icon_hash : str
Hash of guild's icon.
is_owner : bool
Boolean determining if current user is owner of the guild or not.
permissions : discord.Permissions
An instance of discord.Permissions representing permissions of current user in the guild.
"""
MANY = True
ROUTE = "/users/@me/guilds"
def __init__(self, payload):
super().__init__(payload)
self.id = int(self._payload["id"])
self.name = self._payload["name"]
self.icon_hash = self._payload.get("icon")
self.is_owner = self._payload.get("owner")
self.permissions = self.__get_permissions(self._payload.get("permissions"))
@staticmethod
def __get_permissions(permissions_value):
if permissions_value is None:
return
return discord.Permissions(int(permissions_value))
def __str__(self):
return self.name
def __eq__(self, guild):
return isinstance(guild, Guild) and guild.id == self.id
def __ne__(self, guild):
return not self.__eq__(guild)
@property
def icon_url(self):
"""A property returning direct URL to the guild's icon. Returns None if guild has no icon set."""
if not self.icon_hash:
return
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.oauth2.models.Guild` exists in the
users internal cache who belongs to these guilds then, the cached property
:py:attr:`flask_discord.oauth2.models.User.guilds` is updated.
Parameters
----------
cache : bool
Determines if the :py:attr:`flask_discord.oauth2.models.User.guilds` cache should be updated with
the new guilds.
Returns
-------
list[flask_discord.oauth2.models.Guild, ...]
List of instances of :py:class:`flask_discord.oauth2.models.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

@@ -0,0 +1,226 @@
from .base import DiscordModelsBase
from .guild import Guild
from .connections import UserConnection
from flask_discord import configs
from flask_discord import exceptions
from flask import current_app, session
class User(DiscordModelsBase):
"""Class representing Discord User.
Operations
----------
x == y
Checks if two user's are the same.
x != y
Checks if two user's are not the same.
str(x)
Returns the user's name with discriminator.
Attributes
----------
id : int
The discord ID of the user.
username : str
The discord username of the user.
discriminator : str
4 length string representing discord tag of the user.
avatar_hash : str
Hash of users avatar.
bot : bool
A boolean representing whether the user belongs to an OAuth2 application.
mfa_enabled : bool
A boolean representing whether the user has two factor enabled on their account.
locale : str
The user's chosen language option.
verified : bool
A boolean representing whether the email on this account has been verified.
email : str
User's email ID.
flags : int
An integer representing the
`user flags <https://discordapp.com/developers/docs/resources/user#user-object-user-flags>`_.
premium_type : int
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.
"""
ROUTE = "/users/@me"
def __init__(self, payload):
super().__init__(payload)
self.id = int(self._payload["id"])
self.username = self._payload["username"]
self.discriminator = self._payload["discriminator"]
self.avatar_hash = self._payload.get("avatar", self.discriminator)
self.bot = self._payload.get("bot", False)
self.mfa_enabled = self._payload.get("mfa_enabled")
self.locale = self._payload.get("locale")
self.verified = self._payload.get("verified")
self.email = self._payload.get("email")
self.flags = self._payload.get("flags")
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.oauth2.models.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):
return f"{self.name}#{self.discriminator}"
def __eq__(self, user):
return isinstance(user, User) and user.id == self.id
def __ne__(self, user):
return not self.__eq__(user)
@property
def name(self):
"""An alias to the username attribute."""
return self.username
@property
def avatar_url(self):
"""A property returning direct URL to user's avatar."""
if not self.avatar_hash:
return
image_format = configs.DISCORD_ANIMATED_IMAGE_FORMAT \
if self.is_avatar_animated else configs.DISCORD_IMAGE_FORMAT
return configs.DISCORD_USER_AVATAR_BASE_URL.format(
user_id=self.id, avatar_hash=self.avatar_hash, format=image_format)
@property
def default_avatar_url(self):
"""A property which returns the default avatar URL as when user doesn't has any avatar set."""
return configs.DISCORD_DEFAULT_USER_AVATAR_BASE_URL.format(modulo5=int(self.discriminator) % 5)
@property
def is_avatar_animated(self):
"""A boolean representing if avatar of user is animated. Meaning user has GIF avatar."""
try:
return self.avatar_hash.startswith("a_")
except AttributeError:
return False
@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 be obtained from :py:func:`flask_discord.oauth2.models.Guilds.fetch_from_api()`.
connections : bool
A boolean indicating if user's connections should be cached or not. Defaults to ``False``.
If chose to notvcache, user's connections can be obtained from
:py:func:`flask_discord.oauth2.models.UserConnection.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.oauth2.models.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:
"""Method to add user to the guild, provided OAuth2 session has already been created with ``guilds.join`` scope.
Parameters
----------
guild_id : int
The ID of the guild you want this user to be added.
Returns
-------
dict
A dict of guild member object. Returns an empty dict if user is already present in the guild.
Raises
------
flask_discord.Unauthorized
Raises :py:class:`flask_discord.Unauthorized` if current user is not authorized.
"""
try:
data = {"access_token": 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()
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.oauth2.models.Guilds` instances.
"""
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.oauth2.models.UserConnection` instances.
"""
self.connections = UserConnection.fetch_from_api(cache=False)
return self.connections
class Bot(User):
"""Class representing the client user itself."""
# TODO: What is this?