From 61f62c146808838e0e721c898888fbab179bb2fb Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 17 Oct 2015 06:21:51 -0400 Subject: [PATCH] Add Channel.permissions_for and PrivateChannel.permissions_for. These functions handle permission resolution for a specific member. Aids with #18. --- discord/channel.py | 117 ++++++++++++++++++++++++++++++++++++----- discord/permissions.py | 25 +++++++++ 2 files changed, 128 insertions(+), 14 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index ee276f58b..3f8b566b2 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -25,6 +25,10 @@ DEALINGS IN THE SOFTWARE. from copy import deepcopy from . import utils +from .permissions import Permissions +from collections import namedtuple + +MemberOverwrite = namedtuple('MemberOverwrite', ['id', 'allow', 'deny']) class Channel(object): """Represents a Discord server channel. @@ -75,7 +79,13 @@ class Channel(object): self.position = kwargs.get('position') self.type = kwargs.get('type') self.changed_roles = [] + self._user_permissions = [] for overridden in kwargs.get('permission_overwrites', []): + if overridden.get('type') == 'member': + del overridden['type'] + self._user_permissions.append(MemberOverwrite(**overridden)) + continue + # this is pretty inefficient due to the deep nested loops unfortunately role = utils.find(lambda r: r.id == overridden['id'], self.server.roles) if role is None: @@ -84,22 +94,77 @@ class Channel(object): denied = overridden.get('deny', 0) allowed = overridden.get('allow', 0) override = deepcopy(role) - - # 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) & ~denied. - # The OP2 is base | allowed. - override.permissions.value = ((override.permissions.value ^ denied) & ~denied) | allowed + override.permissions.handle_overwrite(allowed, denied) self.changed_roles.append(override) + def is_default_channel(self): + """Checks if this is the default channel for the :class:`Server` it belongs to.""" + return self.server.id == self.id + + def permissions_for(self, member): + """Handles permission resolution for the current :class:`Member`. + + This function takes into consideration the following cases: + + - Server owner + - Server roles + - Channel overrides + - Member overrides + - Whether the channel is the default channel. + + :param member: The :class:`Member` to resolve permissions for. + :return: The resolved :class:`Permissions` for the :class:`Member`. + """ + + # The current cases can be explained as: + # Server owner get all permissions -- no questions asked. Otherwise... + # The @everyone role gets the first application. + # After that, the applied roles that the user has in the channel + # (or otherwise) are then OR'd together. + # After the role permissions are resolved, the member permissions + # have to take into effect. + # After all that is done.. you have to do the following: + + # If manage permissions is True, then all permissions are set to + # True. If the channel is the default channel then everyone gets + # read permissions regardless. + + # The operation first takes into consideration the denied + # and then the allowed. + + if member.id == self.server.owner.id: + return Permissions.ALL + + base = self.server.get_default_role().permissions + + # Apply server roles that the member has. + for role in member.roles: + denied = ~role.permissions.value + base.handle_overwrite(allow=role.permissions.value, deny=denied) + + # Server-wide Manage Roles -> True for everything + if base.can_manage_roles: + base = Permissions.ALL + + # Apply channel specific permission overwrites + for role in self.changed_roles: + denied = ~role.permissions.value + base.handle_overwrite(allow=role.permissions.value, deny=denied) + + # Apply member specific permission overwrites + for overwrite in self._user_permissions: + if overwrite.id == member.id: + base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny) + + if base.can_manage_roles: + # This point is essentially Channel-specific Manage Roles. + base.value |= Permissions.ALL_CHANNEL.value + + if self.is_default_channel(): + base.can_read_messages = True + + return base + class PrivateChannel(object): """Represents a Discord private channel. @@ -121,3 +186,27 @@ class PrivateChannel(object): self.id = id self.is_private = True + def permissions_for(user): + """Handles permission resolution for a :class:`User`. + + This function is there for compatibility with :class:`Channel`. + + Actual private messages do not really have the concept of permissions. + + This returns all the Text related permissions set to true except: + + - can_send_tts_messages: You cannot send TTS messages in a PM. + - can_manage_messages: You cannot delete others messages in a PM. + - can_mention_everyone: There is no one to mention in a PM. + + :param user: The :class:`User` to check permissions for. + :return: A :class:`Permission` with the resolved permission value. + """ + + base = Permissions.TEXT + base.can_send_tts_messages = False + base.can_manage_messages = False + base.can_mention_everyone = False + return base + + diff --git a/discord/permissions.py b/discord/permissions.py index 43cfcef3f..1b58bf544 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -24,6 +24,16 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +def create_permission_masks(cls): + cls.NONE = cls(0) + cls.ALL = cls(0b00000011111100111111110000111111) + cls.ALL_CHANNEL = cls(0b00000011111100111111110000011001) + cls.GENERAL = cls(0b00000000000000000000000000111111) + cls.TEXT = cls(0b00000000000000111111110000000000) + cls.VOICE = cls(0b00000011111100000000000000000000) + return cls + +@create_permission_masks class Permissions(object): """Wraps up the Discord permission value. @@ -53,6 +63,21 @@ class Permissions(object): else: raise TypeError('Value to set for Permissions must be a bool.') + 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) & ~denied. + # The OP2 is base | allowed. + self.value = ((self.value ^ deny) & ~deny) | allow + @property def can_create_instant_invite(self): """Returns True if the user can create instant invites."""