# This file is part of curious.
#
# curious is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# curious is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with curious. If not, see <http://www.gnu.org/licenses/>.
"""
Wrappers for Permission objects.
This class uses some automatic generation to create the objects.
.. currentmodule:: curious.dataclasses.permissions
"""
import typing
from curious.dataclasses import channel as dt_channel, member as dt_member, role as dt_role
from curious.exc import PermissionsError
# I'm far too lazy to type out each permission bit manually.
# So here's a helper method.
[docs]def build_permissions_class(name: str = "Permissions") -> type:
"""
Builds the permissions class automagically.
This should ***not*** be used by normal user code - it is designed for internal usage by
curious.
:param name: The name of the class.
:return: A new type representing the permissions class.
"""
# Closure methods.
__doc__ = """
Represents the permissions a user can have.
This type is automatically generated based upon a set of constant permission bits.
Every permission is accessible via a property getter and setter. The raw permissions value is
accessible via ``bitfield``.
"""
def __init__(self, value: int = 0,
**kwargs):
"""
Creates a new Permissions object.
:param value: The bitfield value of the permissions object.
"""
self.bitfield = value
for perm, value in kwargs.items():
if perm not in permissions:
raise ValueError("Unknown permission", perm)
setattr(self, perm, value)
def __new__(cls, value: int = 0,
**kwargs):
if isinstance(value, cls):
return value
return super(Permissions, cls).__new__(cls)
def _get_bit(self, bit: int) -> bool:
"""
Gets a bit from the internal bitfield of the permissions.
"""
return bool((self.bitfield >> bit) & 1)
def _set_bit(self, bit: int, value: bool):
if value:
self.bitfield |= (1 << bit)
else:
self.bitfield &= ~(1 << bit)
# Operator overloads.
def __eq__(self, other):
return self.bitfield == other.bitfield
# This is a dict because discord skips some permissions.
permissions = {
"create_instant_invite": 0,
"kick_members": 1,
"ban_members": 2,
"administrator": 3,
"manage_channels": 4,
"manage_server": 5,
"add_reactions": 6,
"view_audit_log": 7,
"read_messages": 10,
"send_messages": 11,
"send_tts_messages": 12,
"manage_messages": 13,
"embed_links": 14,
"attach_files": 15,
"read_message_history": 16,
"mention_everyone": 17,
"use_external_emojis": 18,
"voice_connect": 20,
"voice_speak": 21,
"voice_mute_members": 22,
"voice_deafen_members": 23,
"voice_move_members": 24,
"voice_use_voice_activation": 25,
"change_nickname": 26,
"manage_nicknames": 27,
"manage_roles": 28,
"manage_webhooks": 29,
"manage_emojis": 30,
# rest are unused
}
# Create a bunch of property objects for each permission.
def _get_permission_getter(name: str, bit: int):
def _junk_function(self) -> bool:
return self._get_bit(bit)
_junk_function.__name__ = name
return _junk_function
def _get_permission_setter(name: str, bit: int):
def _junk_function(self, value: bool):
return self._set_bit(bit, value)
_junk_function.__name__ = name
return _junk_function
_doc_base = ":return: If this member has the {} permission (bit {})."
properties = {
name: property(fget=_get_permission_getter(name, bit),
fset=_get_permission_setter(name, bit),
doc=_doc_base.format(name, bit)) for (name, bit) in permissions.items()
}
def raise_for_permission(self, permission: typing.Union[str]) -> None:
"""
Raises :class:`.PermissionsError` if this permission does not have the required bit.
"""
if not getattr(self, permission):
raise PermissionsError(permission)
# Create some useful classmethods.
@classmethod
def all(cls):
"""
:return: A new Permissions object with all permissions.
"""
return cls(9007199254740991)
@classmethod
def none(cls):
"""
:return: A new permissions object with no permissions.
"""
return cls(0)
# Create the namespace dict to use in the type declaration.
namespace = {
"__init__": __init__,
"__new__": __new__,
"_set_bit": _set_bit,
"_get_bit": _get_bit,
"__eq__": __eq__,
"__repr__": lambda self: "<Permissions value={}>".format(self.bitfield),
"all": all,
"none": none,
"raise_for_permission": raise_for_permission,
"__slots__": ("bitfield",),
**properties
}
new_class = type(name, (object,), namespace)
new_class.__doc__ = __doc__
return new_class
Permissions = build_permissions_class("Permissions")
[docs]class Overwrite(object):
"""
Represents a permission overwrite.
This has all properties that the base Permissions object, but it takes into accounts the
overwrites for the channels. It is always recommended to use this over the server permissions,
as it will fall back to the default permissions for the role if it can't find specific
overwrites.
The overwrite has a permission marked as ``True`` if the object has a) an overwrite on the
channel OR b) the object has that permission and no overwrite. The overwrite is marked as
``False`` if the object has a) an overwrite on the channel OR b) the object does not have that
permission and no overwrite/a deny overwrite.
You can set an attribute to None to clear the overwrite, True to set an allow overwrite, and
False to set a deny overwrite.
"""
__slots__ = "target", "channel_id", "allow", "deny"
def __init__(self, allow: typing.Union[int, Permissions], deny: typing.Union[int, Permissions],
obb: 'typing.Union[dt_member.Member, dt_role.Role]' = None,
channel_id: int = None):
"""
:param allow: A :class:`.Permissions` that this overwrite allows.
:param deny: A :class:`.Permissions` that this overwrite denies.
:param obb: Optional: The :class:`.Member` or :class:`.Role` that this overwrite is for.
:param channel_id: Optional: The channel ID this overwrite is in.
"""
self.target = obb
self.channel_id = channel_id
if isinstance(allow, Permissions):
allow = allow.bitfield
self.allow = Permissions(value=allow if allow is not None else 0)
if isinstance(deny, Permissions):
deny = deny.bitfield
self.deny = Permissions(value=deny if deny is not None else 0)
@property
def channel(self) -> 'typing.Union[dt_channel.Channel, None]':
"""
:return: The :class:`.Channel` this overwrite represents.
"""
def __repr__(self) -> str:
return "<Overwrites for object={} channel={} allow={} deny={}>".format(self.target,
self.channel,
self.allow,
self.deny)
def __getattr__(self, item) -> bool:
"""
Attribute getter helper.
This will check allow first, the deny, then finally the role permissions.
"""
if isinstance(self.target, dt_member.Member):
permissions = self.target.guild_permissions
elif isinstance(self.target, dt_role.Role):
permissions = self.target.permissions
else:
raise TypeError("Target must be a member or a role")
if permissions.administrator:
# short-circuit to always return True if they have administrator
# this is because those overrides are useless
# if the user wants to get the override, they can access `allow/deny` directly.
return True
if not hasattr(self.allow, item):
raise AttributeError(item)
if getattr(self.allow, item, None) is True:
return True
if getattr(self.deny, item, None) is True:
# Return False because it's denied.
return False
return getattr(permissions, item, False)
def __setattr__(self, key: str, value: object) -> object:
"""
Attribute setter helper.
"""
if not hasattr(Permissions, key):
super().__setattr__(key, value)
return
if value is False:
setattr(self.deny, key, True)
elif value is True:
setattr(self.allow, key, True)
elif value is None:
setattr(self.allow, key, False)
setattr(self.deny, key, False)