diff --git a/docs/api.rst b/docs/api.rst index b57face..27d88b8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -47,6 +47,14 @@ Utilities .. autodecorator:: flask_discord.requires_authorization +Types +----- + +.. autoclass:: flask_discord.types.Permissions + :members: + :inherited-members: + + Exceptions ---------- diff --git a/flask_discord/__init__.py b/flask_discord/__init__.py index 9e1a2db..917a723 100644 --- a/flask_discord/__init__.py +++ b/flask_discord/__init__.py @@ -15,4 +15,4 @@ __all__ = [ ] -__version__ = "0.1.65" +__version__ = "0.1.66" diff --git a/flask_discord/client.py b/flask_discord/client.py index f09d3d7..b09f485 100644 --- a/flask_discord/client.py +++ b/flask_discord/client.py @@ -1,8 +1,7 @@ import jwt import typing -import discord -from . import configs, _http, models, utils, exceptions +from . import configs, _http, models, utils, exceptions, types from flask import request, session, redirect, current_app from oauthlib.common import add_params_to_uri, generate_token @@ -59,7 +58,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): def create_session( self, scope: list = None, *, data: dict = None, prompt: bool = True, - permissions: typing.Union[discord.Permissions, int] = 0, **params + permissions: typing.Union[types.Permissions, int] = 0, **params ): """Primary method used to create OAuth2 session and redirect users for authorization code grant. @@ -75,7 +74,7 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): prompt : bool, optional Determines if the OAuth2 grant should be explicitly prompted and re-approved. Defaults to True. Specify False for implicit grant which will skip the authorization screen and redirect to redirect URI. - permissions: typing.Union[discord.Permissions, int], optional + permissions: typing.Union[flask_discord.types.Permissions, int], optional An optional parameter determining guild permissions of the bot while adding it to a guild using discord OAuth2 with `bot` scope. It is same as generating so called *bot invite link* which redirects to your callback endpoint after bot authorization flow. Defaults to 0 or no permissions. @@ -106,9 +105,9 @@ class DiscordOAuth2Session(_http.DiscordOAuth2HttpClient): params = params or dict() params["prompt"] = "consent" if prompt else "none" if "bot" in scope: - if not isinstance(permissions, (discord.Permissions, int)): + if not isinstance(permissions, (types.Permissions, int)): raise ValueError(f"Passed permissions must be an int or discord.Permissions, not {type(permissions)}.") - if isinstance(permissions, discord.Permissions): + if isinstance(permissions, types.Permissions): permissions = permissions.value params["permissions"] = permissions try: diff --git a/flask_discord/models/guild.py b/flask_discord/models/guild.py index 0885550..f6ae593 100644 --- a/flask_discord/models/guild.py +++ b/flask_discord/models/guild.py @@ -1,7 +1,7 @@ from .base import DiscordModelsBase from flask import current_app -import discord +from .. import types from .. import configs @@ -47,7 +47,7 @@ class Guild(DiscordModelsBase): def __get_permissions(permissions_value): if permissions_value is None: return - return discord.Permissions(int(permissions_value)) + return types.Permissions(int(permissions_value)) def __str__(self): return self.name diff --git a/flask_discord/types/__init__.py b/flask_discord/types/__init__.py new file mode 100644 index 0000000..3349f3b --- /dev/null +++ b/flask_discord/types/__init__.py @@ -0,0 +1,6 @@ +from .permissions import Permissions + + +__all__ = [ + "Permissions", +] diff --git a/flask_discord/types/permissions.py b/flask_discord/types/permissions.py new file mode 100644 index 0000000..b49848b --- /dev/null +++ b/flask_discord/types/permissions.py @@ -0,0 +1,563 @@ +""" +Blatantly copied from: +- https://github.com/Rapptz/discord.py/blob/45d498c1b76deaf3b394d17ccf56112fa691d160/discord/permissions.py +- https://github.com/Rapptz/discord.py/blob/45d498c1b76deaf3b394d17ccf56112fa691d160/discord/flags.py + +The MIT License (MIT) +Copyright (c) 2015-present Rapptz +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + + +# noinspection PyProtectedMember +# noinspection PyPep8Naming +class flag_value: + def __init__(self, func): + self.flag = func(None) + self.__doc__ = func.__doc__ + + def __get__(self, instance, owner): + if instance is None: + return self + return instance._has_flag(self.flag) + + def __set__(self, instance, value): + instance._set_flag(self.flag, value) + + def __repr__(self): + return ''.format(self) + + +def fill_with_flags(*, inverted=False): + def decorator(cls): + cls.VALID_FLAGS = { + name: value.flag + for name, value in cls.__dict__.items() + if isinstance(value, flag_value) + } + + if inverted: + max_bits = max(cls.VALID_FLAGS.values()).bit_length() + cls.DEFAULT_VALUE = -1 + (2 ** max_bits) + else: + cls.DEFAULT_VALUE = 0 + + return cls + return decorator + + +# n.b. flags must inherit from this and use the decorator above +# noinspection PyUnresolvedReferences +class BaseFlags: + __slots__ = ('value',) + + def __init__(self, **kwargs): + self.value = self.DEFAULT_VALUE + for key, value in kwargs.items(): + if key not in self.VALID_FLAGS: + raise TypeError('%r is not a valid flag name.' % key) + setattr(self, key, value) + + @classmethod + def _from_value(cls, value): + self = cls.__new__(cls) + self.value = value + return self + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.value == other.value + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.value) + + def __repr__(self): + return '<%s value=%s>' % (self.__class__.__name__, self.value) + + def __iter__(self): + for name, value in self.__class__.__dict__.items(): + if isinstance(value, alias_flag_value): + continue + + if isinstance(value, flag_value): + # noinspection PyRedundantParentheses + yield (name, self._has_flag(value.flag)) + + def _has_flag(self, o): + return (self.value & o) == o + + def _set_flag(self, o, toggle): + if toggle is True: + self.value |= o + elif toggle is False: + self.value &= ~o + else: + raise TypeError('Value to set for %s must be a bool.' % self.__class__.__name__) + + +# noinspection PyPep8Naming +class alias_flag_value(flag_value): + pass + + +# A permission alias works like a regular flag but is marked +# So the PermissionOverwrite knows to work with it +# noinspection PyPep8Naming +class permission_alias(alias_flag_value): + pass + + +def make_permission_alias(alias): + def decorator(func): + ret = permission_alias(func) + ret.alias = alias + return ret + return decorator + + +@fill_with_flags() +class Permissions(BaseFlags): + """Wraps up the Discord permission value. + + The properties provided are two way. You can set and retrieve individual + bits using the properties as if they were regular bools. This allows + you to edit permissions. + + .. versionchanged:: 1.3 + You can now use keyword arguments to initialize :class:`Permissions` + similar to :meth:`update`. + + .. container:: operations + + .. describe:: x == y + + Checks if two permissions are equal. + .. describe:: x != y + + Checks if two permissions are not equal. + .. describe:: x <= y + + Checks if a permission is a subset of another permission. + .. describe:: x >= y + + Checks if a permission is a superset of another permission. + .. describe:: x < y + + Checks if a permission is a strict subset of another permission. + .. describe:: x > y + + Checks if a permission is a strict superset of another permission. + .. describe:: hash(x) + + Return the permission's hash. + .. describe:: iter(x) + + Returns an iterator of ``(perm, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + Attributes + ----------- + value: :class:`int` + The raw value. This value is a bit array field of a 53-bit integer + representing the currently available permissions. You should query + permissions via the properties rather than using this raw value. + """ + + __slots__ = () + + # noinspection PyMissingConstructor + def __init__(self, permissions=0, **kwargs): + if not isinstance(permissions, int): + raise TypeError('Expected int parameter, received %s instead.' % permissions.__class__.__name__) + + self.value = permissions + for key, value in kwargs.items(): + if key not in self.VALID_FLAGS: + raise TypeError('%r is not a valid permission name.' % key) + setattr(self, key, value) + + def is_subset(self, other): + """Returns ``True`` if self has the same or fewer permissions as other.""" + if isinstance(other, Permissions): + return (self.value & other.value) == self.value + else: + raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__)) + + def is_superset(self, other): + """Returns ``True`` if self has the same or more permissions as other.""" + if isinstance(other, Permissions): + return (self.value | other.value) == self.value + else: + raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__.__name__)) + + def is_strict_subset(self, other): + """Returns ``True`` if the permissions on other are a strict subset of those on self.""" + return self.is_subset(other) and self != other + + def is_strict_superset(self, other): + """Returns ``True`` if the permissions on other are a strict superset of those on self.""" + return self.is_superset(other) and self != other + + __le__ = is_subset + __ge__ = is_superset + __lt__ = is_strict_subset + __gt__ = is_strict_superset + + @classmethod + def none(cls): + """A factory method that creates a :class:`Permissions` with all + permissions set to ``False``.""" + return cls(0) + + @classmethod + def all(cls): + """A factory method that creates a :class:`Permissions` with all + permissions set to ``True``. + """ + return cls(0b111111111111111111111111111111111) + + @classmethod + def all_channel(cls): + """A :class:`Permissions` with all channel-specific permissions set to + ``True`` and the guild-specific ones set to ``False``. The guild-specific + permissions are currently: + + - :attr:`manage_emojis` + - :attr:`view_audit_log` + - :attr:`view_guild_insights` + - :attr:`manage_guild` + - :attr:`change_nickname` + - :attr:`manage_nicknames` + - :attr:`kick_members` + - :attr:`ban_members` + - :attr:`administrator` + + .. versionchanged:: 1.7 + Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_slash_commands` permissions. + """ + return cls(0b10110011111101111111111101010001) + + @classmethod + def general(cls): + """A factory method that creates a :class:`Permissions` with all + "General" permissions from the official Discord UI set to ``True``. + + .. versionchanged:: 1.7 + Permission :attr:`read_messages` is now included in the general permissions, but + permissions :attr:`administrator`, :attr:`create_instant_invite`, :attr:`kick_members`, + :attr:`ban_members`, :attr:`change_nickname` and :attr:`manage_nicknames` are + no longer part of the general permissions. + """ + return cls(0b01110000000010000000010010110000) + + @classmethod + def membership(cls): + """A factory method that creates a :class:`Permissions` with all + "Membership" permissions from the official Discord UI set to ``True``. + + .. versionadded:: 1.7 + """ + return cls(0b00001100000000000000000000000111) + + @classmethod + def text(cls): + """A factory method that creates a :class:`Permissions` with all + "Text" permissions from the official Discord UI set to ``True``. + + .. versionchanged:: 1.7 + Permission :attr:`read_messages` is no longer part of the text permissions. + Added :attr:`use_slash_commands` permission. + """ + return cls(0b10000000000001111111100001000000) + + @classmethod + def voice(cls): + """A factory method that creates a :class:`Permissions` with all + "Voice" permissions from the official Discord UI set to ``True``.""" + return cls(0b00000011111100000000001100000000) + + @classmethod + def stage(cls): + """A factory method that creates a :class:`Permissions` with all + "Stage Channel" permissions from the official Discord UI set to ``True``. + + .. versionadded:: 1.7 + """ + return cls(1 << 32) + + @classmethod + def stage_moderator(cls): + """A factory method that creates a :class:`Permissions` with all + "Stage Moderator" permissions from the official Discord UI set to ``True``. + + .. versionadded:: 1.7 + """ + return cls(0b100000001010000000000000000000000) + + @classmethod + def advanced(cls): + """A factory method that creates a :class:`Permissions` with all + "Advanced" permissions from the official Discord UI set to ``True``. + + .. versionadded:: 1.7 + """ + return cls(1 << 3) + + def update(self, **kwargs): + r"""Bulk updates this permission object. + + Allows you to set multiple attributes by using keyword + arguments. The names must be equivalent to the properties + listed. Extraneous key/value pairs will be silently ignored. + + Parameters + ------------ + \*\*kwargs + A list of key/value pairs to bulk update permissions with. + """ + for key, value in kwargs.items(): + if key in self.VALID_FLAGS: + setattr(self, key, value) + + def handle_overwrite(self, allow, deny): + # Basically this is what's happening here. + # We have an original bit array, e.g. 1010 + # Then we have another bit array that is 'denied', e.g. 1111 + # And then we have the last one which is 'allowed', e.g. 0101 + # We want original OP denied to end up resulting in + # whatever is in denied to be set to 0. + # So 1010 OP 1111 -> 0000 + # Then we take this value and look at the allowed values. + # And whatever is allowed is set to 1. + # So 0000 OP2 0101 -> 0101 + # The OP is base & ~denied. + # The OP2 is base | allowed. + self.value = (self.value & ~deny) | allow + + @flag_value + def create_instant_invite(self): + """:class:`bool`: Returns ``True`` if the user can create instant invites.""" + return 1 << 0 + + @flag_value + def kick_members(self): + """:class:`bool`: Returns ``True`` if the user can kick users from the guild.""" + return 1 << 1 + + @flag_value + def ban_members(self): + """:class:`bool`: Returns ``True`` if a user can ban users from the guild.""" + return 1 << 2 + + @flag_value + def administrator(self): + """:class:`bool`: Returns ``True`` if a user is an administrator. This role overrides all other permissions. + + This also bypasses all channel-specific overrides. + """ + return 1 << 3 + + @flag_value + def manage_channels(self): + """:class:`bool`: Returns ``True`` if a user can edit, delete, or create channels in the guild. + + This also corresponds to the "Manage Channel" channel-specific override.""" + return 1 << 4 + + @flag_value + def manage_guild(self): + """:class:`bool`: Returns ``True`` if a user can edit guild properties.""" + return 1 << 5 + + @flag_value + def add_reactions(self): + """:class:`bool`: Returns ``True`` if a user can add reactions to messages.""" + return 1 << 6 + + @flag_value + def view_audit_log(self): + """:class:`bool`: Returns ``True`` if a user can view the guild's audit log.""" + return 1 << 7 + + @flag_value + def priority_speaker(self): + """:class:`bool`: Returns ``True`` if a user can be more easily heard while talking.""" + return 1 << 8 + + @flag_value + def stream(self): + """:class:`bool`: Returns ``True`` if a user can stream in a voice channel.""" + return 1 << 9 + + @flag_value + def read_messages(self): + """:class:`bool`: Returns ``True`` if a user can read messages from all or specific text channels.""" + return 1 << 10 + + @make_permission_alias('read_messages') + def view_channel(self): + """:class:`bool`: An alias for :attr:`read_messages`. + + .. versionadded:: 1.3 + """ + return 1 << 10 + + @flag_value + def send_messages(self): + """:class:`bool`: Returns ``True`` if a user can send messages from all or specific text channels.""" + return 1 << 11 + + @flag_value + def send_tts_messages(self): + """:class:`bool`: Returns ``True`` if a user can send TTS messages from all or specific text channels.""" + return 1 << 12 + + @flag_value + def manage_messages(self): + """:class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel. + + .. note:: + + Note that there are currently no ways to edit other people's messages. + """ + return 1 << 13 + + @flag_value + def embed_links(self): + """:class:`bool`: Returns ``True`` if a user's messages will automatically be embedded by Discord.""" + return 1 << 14 + + @flag_value + def attach_files(self): + """:class:`bool`: Returns ``True`` if a user can send files in their messages.""" + return 1 << 15 + + @flag_value + def read_message_history(self): + """:class:`bool`: Returns ``True`` if a user can read a text channel's previous messages.""" + return 1 << 16 + + @flag_value + def mention_everyone(self): + """:class:`bool`: Returns ``True`` if a user's @everyone or @here will mention everyone in the text channel.""" + return 1 << 17 + + @flag_value + def external_emojis(self): + """:class:`bool`: Returns ``True`` if a user can use emojis from other guilds.""" + return 1 << 18 + + @make_permission_alias('external_emojis') + def use_external_emojis(self): + """:class:`bool`: An alias for :attr:`external_emojis`. + + .. versionadded:: 1.3 + """ + return 1 << 18 + + @flag_value + def view_guild_insights(self): + """:class:`bool`: Returns ``True`` if a user can view the guild's insights. + + .. versionadded:: 1.3 + """ + return 1 << 19 + + @flag_value + def connect(self): + """:class:`bool`: Returns ``True`` if a user can connect to a voice channel.""" + return 1 << 20 + + @flag_value + def speak(self): + """:class:`bool`: Returns ``True`` if a user can speak in a voice channel.""" + return 1 << 21 + + @flag_value + def mute_members(self): + """:class:`bool`: Returns ``True`` if a user can mute other users.""" + return 1 << 22 + + @flag_value + def deafen_members(self): + """:class:`bool`: Returns ``True`` if a user can deafen other users.""" + return 1 << 23 + + @flag_value + def move_members(self): + """:class:`bool`: Returns ``True`` if a user can move users between other voice channels.""" + return 1 << 24 + + @flag_value + def use_voice_activation(self): + """:class:`bool`: Returns ``True`` if a user can use voice activation in voice channels.""" + return 1 << 25 + + @flag_value + def change_nickname(self): + """:class:`bool`: Returns ``True`` if a user can change their nickname in the guild.""" + return 1 << 26 + + @flag_value + def manage_nicknames(self): + """:class:`bool`: Returns ``True`` if a user can change other user's nickname in the guild.""" + return 1 << 27 + + @flag_value + def manage_roles(self): + """:class:`bool`: Returns ``True`` if a user can create or edit roles less than their role's position. + + This also corresponds to the "Manage Permissions" channel-specific override. + """ + return 1 << 28 + + @make_permission_alias('manage_roles') + def manage_permissions(self): + """:class:`bool`: An alias for :attr:`manage_roles`. + + .. versionadded:: 1.3 + """ + return 1 << 28 + + @flag_value + def manage_webhooks(self): + """:class:`bool`: Returns ``True`` if a user can create, edit, or delete webhooks.""" + return 1 << 29 + + @flag_value + def manage_emojis(self): + """:class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis.""" + return 1 << 30 + + @flag_value + def use_slash_commands(self): + """:class:`bool`: Returns ``True`` if a user can use slash commands. + + .. versionadded:: 1.7 + """ + return 1 << 31 + + @flag_value + def request_to_speak(self): + """:class:`bool`: Returns ``True`` if a user can request to speak in a stage channel. + + .. versionadded:: 1.7 + """ + return 1 << 32 diff --git a/requirements.lock.txt b/requirements.lock.txt index 28294b6..abdc4ac 100644 --- a/requirements.lock.txt +++ b/requirements.lock.txt @@ -6,7 +6,6 @@ certifi==2021.10.8 chardet==4.0.0 charset-normalizer==2.0.11 click==8.0.3 -discord.py==1.7.3 Flask==2.0.2 idna==3.3 itsdangerous==2.0.1 diff --git a/requirements.txt b/requirements.txt index 1c2c830..aeb68e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ Flask pyjwt requests oauthlib -discord.py cachetools setuptools requests_oauthlib diff --git a/setup.py b/setup.py index d647058..4599d0a 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,6 @@ requirements = [ "chardet==4.0.0", "charset-normalizer==2.0.11", "click==8.0.3", - "discord.py==1.7.3", "Flask==2.0.2", "idna==3.3", "itsdangerous==2.0.1",