diff --git a/discord/abc.py b/discord/abc.py index 81bb41057..e2ae12e2f 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -29,16 +29,13 @@ import io import os import asyncio +from collections import namedtuple + from .message import Message from .iterators import LogsFromIterator from .context_managers import Typing from .errors import ClientException, NoMoreMessages -import discord.message -import discord.iterators -import discord.context_managers -import discord.errors - class Snowflake(metaclass=abc.ABCMeta): __slots__ = () @@ -89,38 +86,6 @@ class User(metaclass=abc.ABCMeta): return True return NotImplemented -class GuildChannel(metaclass=abc.ABCMeta): - __slots__ = () - - @property - @abc.abstractmethod - def mention(self): - raise NotImplementedError - - @abc.abstractmethod - def overwrites_for(self, obj): - raise NotImplementedError - - @abc.abstractmethod - def permissions_for(self, user): - raise NotImplementedError - - @classmethod - def __subclasshook__(cls, C): - if cls is GuildChannel: - if Snowflake.__subclasshook__(C) is NotImplemented: - return NotImplemented - - mro = C.__mro__ - for attr in ('name', 'guild', 'overwrites_for', 'permissions_for', 'mention'): - for base in mro: - if attr in base.__dict__: - break - else: - return NotImplemented - return True - return NotImplemented - class PrivateChannel(metaclass=abc.ABCMeta): __slots__ = () @@ -137,6 +102,268 @@ class PrivateChannel(metaclass=abc.ABCMeta): return NotImplemented return NotImplemented +_Overwrites = namedtuple('_Overwrites', 'id allow deny type') + +class GuildChannel: + __slots__ = () + + def __str__(self): + return self.name + + @asyncio.coroutine + def _move(self, position): + if position < 0: + raise InvalidArgument('Channel position cannot be less than 0.') + + http = self._state.http + url = '{0}/{1.guild.id}/channels'.format(http.GUILDS, self) + channels = [c for c in self.guild.channels if isinstance(c, type(self))] + + if position >= len(channels): + raise InvalidArgument('Channel position cannot be greater than {}'.format(len(channels) - 1)) + + channels.sort(key=lambda c: c.position) + + try: + # remove ourselves from the channel list + channels.remove(self) + except ValueError: + # not there somehow lol + return + else: + # add ourselves at our designated position + channels.insert(position, self) + + payload = [{'id': c.id, 'position': index } for index, c in enumerate(channels)] + yield from http.patch(url, json=payload, bucket='move_channel') + + def _fill_overwrites(self, data): + self._overwrites = [] + everyone_index = 0 + everyone_id = self.guild.id + + for index, overridden in enumerate(data.get('permission_overwrites', [])): + overridden_id = int(overridden.pop('id')) + self._overwrites.append(_Overwrites(id=overridden_id, **overridden)) + + if overridden['type'] == 'member': + continue + + if overridden_id == everyone_id: + # the @everyone role is not guaranteed to be the first one + # in the list of permission overwrites, however the permission + # resolution code kind of requires that it is the first one in + # the list since it is special. So we need the index so we can + # swap it to be the first one. + everyone_index = index + + # do the swap + tmp = self._overwrites + if tmp: + tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index] + + @property + def changed_roles(self): + """Returns a list of :class:`Roles` that have been overridden from + their default values in the :attr:`Guild.roles` attribute.""" + ret = [] + for overwrite in filter(lambda o: o.type == 'role', self._overwrites): + role = discord.utils.get(self.guild.roles, id=overwrite.id) + if role is None: + continue + + role = copy.copy(role) + role.permissions.handle_overwrite(overwrite.allow, overwrite.deny) + ret.append(role) + return ret + + @property + def is_default(self): + """bool : Indicates if this is the default channel for the :class:`Guild` it belongs to.""" + return self.guild.id == self.id + + @property + def mention(self): + """str : The string that allows you to mention the channel.""" + return '<#{0.id}>'.format(self) + + @property + def created_at(self): + """Returns the channel's creation time in UTC.""" + return discord.utils.snowflake_time(self.id) + + def overwrites_for(self, obj): + """Returns the channel-specific overwrites for a member or a role. + + Parameters + ----------- + obj + The :class:`Role` or :class:`Member` or :class:`Object` denoting + whose overwrite to get. + + Returns + --------- + :class:`PermissionOverwrite` + The permission overwrites for this object. + """ + + if isinstance(obj, Member): + predicate = lambda p: p.type == 'member' + elif isinstance(obj, Role): + predicate = lambda p: p.type == 'role' + else: + predicate = lambda p: True + + for overwrite in filter(predicate, self._overwrites): + if overwrite.id == obj.id: + allow = Permissions(overwrite.allow) + deny = Permissions(overwrite.deny) + return PermissionOverwrite.from_pair(allow, deny) + + return PermissionOverwrite() + + @property + def overwrites(self): + """Returns all of the channel's overwrites. + + This is returned as a list of two-element tuples containing the target, + which can be either a :class:`Role` or a :class:`Member` and the overwrite + as the second element as a :class:`PermissionOverwrite`. + + Returns + -------- + List[Tuple[Union[:class:`Role`, :class:`Member`], :class:`PermissionOverwrite`]]: + The channel's permission overwrites. + """ + ret = [] + for ow in self._permission_overwrites: + allow = Permissions(ow.allow) + deny = Permissions(ow.deny) + overwrite = PermissionOverwrite.from_pair(allow, deny) + + if ow.type == 'role': + # accidentally quadratic + target = discord.utils.find(lambda r: r.id == ow.id, self.server.roles) + elif ow.type == 'member': + target = self.server.get_member(ow.id) + + ret.append((target, overwrite)) + return ret + + def permissions_for(self, member): + """Handles permission resolution for the current :class:`Member`. + + This function takes into consideration the following cases: + + - Guild owner + - Guild roles + - Channel overrides + - Member overrides + - Whether the channel is the default channel. + + Parameters + ---------- + member : :class:`Member` + The member to resolve permissions for. + + Returns + ------- + :class:`Permissions` + The resolved permissions for the member. + """ + + # The current cases can be explained as: + # Guild 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.guild.owner.id: + return Permissions.all() + + default = self.guild.default_role + base = Permissions(default.permissions.value) + + # Apply guild roles that the member has. + for role in member.roles: + base.value |= role.permissions.value + + # Guild-wide Administrator -> True for everything + # Bypass all channel-specific overrides + if base.administrator: + return Permissions.all() + + member_role_ids = set(map(lambda r: r.id, member.roles)) + denies = 0 + allows = 0 + + # Apply channel specific role permission overwrites + for overwrite in self._overwrites: + if overwrite.type == 'role' and overwrite.id in member_role_ids: + denies |= overwrite.deny + allows |= overwrite.allow + + base.handle_overwrite(allow=allows, deny=denies) + + # Apply member specific permission overwrites + for overwrite in self._overwrites: + if overwrite.type == 'member' and overwrite.id == member.id: + base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny) + break + + # default channels can always be read + if self.is_default: + base.read_messages = True + + # if you can't send a message in a channel then you can't have certain + # permissions as well + if not base.send_messages: + base.send_tts_messages = False + base.mention_everyone = False + base.embed_links = False + base.attach_files = False + + # if you can't read a channel then you have no permissions there + if not base.read_messages: + denied = Permissions.all_channel() + base.value &= ~denied.value + + # text channels do not have voice related permissions + if isinstance(self, TextChannel): + denied = Permissions.voice() + base.value &= ~denied.value + + return base + + @asyncio.coroutine + def delete(self): + """|coro| + + Deletes the channel. + + You must have Manage Channel permission to use this. + + Raises + ------- + Forbidden + You do not have proper permissions to delete the channel. + NotFound + The channel was not found or was already deleted. + HTTPException + Deleting the channel failed. + """ + yield from self._state.http.delete_channel(self.id) + class MessageChannel(metaclass=abc.ABCMeta): __slots__ = () diff --git a/discord/channel.py b/discord/channel.py index 75d801ef2..80ecdaabd 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -25,7 +25,6 @@ DEALINGS IN THE SOFTWARE. from .permissions import Permissions, PermissionOverwrite from .enums import ChannelType, try_enum -from collections import namedtuple from .mixins import Hashable from .role import Role from .user import User @@ -39,269 +38,7 @@ import asyncio __all__ = ('TextChannel', 'VoiceChannel', 'DMChannel', 'GroupChannel', '_channel_factory') -Overwrites = namedtuple('Overwrites', 'id allow deny type') - -class CommonGuildChannel(Hashable): - __slots__ = () - - def __str__(self): - return self.name - - @asyncio.coroutine - def _move(self, position): - if position < 0: - raise InvalidArgument('Channel position cannot be less than 0.') - - http = self._state.http - url = '{0}/{1.guild.id}/channels'.format(http.GUILDS, self) - channels = [c for c in self.guild.channels if isinstance(c, type(self))] - - if position >= len(channels): - raise InvalidArgument('Channel position cannot be greater than {}'.format(len(channels) - 1)) - - channels.sort(key=lambda c: c.position) - - try: - # remove ourselves from the channel list - channels.remove(self) - except ValueError: - # not there somehow lol - return - else: - # add ourselves at our designated position - channels.insert(position, self) - - payload = [{'id': c.id, 'position': index } for index, c in enumerate(channels)] - yield from http.patch(url, json=payload, bucket='move_channel') - - def _fill_overwrites(self, data): - self._overwrites = [] - everyone_index = 0 - everyone_id = self.guild.id - - for index, overridden in enumerate(data.get('permission_overwrites', [])): - overridden_id = int(overridden.pop('id')) - self._overwrites.append(Overwrites(id=overridden_id, **overridden)) - - if overridden['type'] == 'member': - continue - - if overridden_id == everyone_id: - # the @everyone role is not guaranteed to be the first one - # in the list of permission overwrites, however the permission - # resolution code kind of requires that it is the first one in - # the list since it is special. So we need the index so we can - # swap it to be the first one. - everyone_index = index - - # do the swap - tmp = self._overwrites - if tmp: - tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index] - - @property - def changed_roles(self): - """Returns a list of :class:`Roles` that have been overridden from - their default values in the :attr:`Guild.roles` attribute.""" - ret = [] - for overwrite in filter(lambda o: o.type == 'role', self._overwrites): - role = discord.utils.get(self.guild.roles, id=overwrite.id) - if role is None: - continue - - role = copy.copy(role) - role.permissions.handle_overwrite(overwrite.allow, overwrite.deny) - ret.append(role) - return ret - - @property - def is_default(self): - """bool : Indicates if this is the default channel for the :class:`Guild` it belongs to.""" - return self.guild.id == self.id - - @property - def mention(self): - """str : The string that allows you to mention the channel.""" - return '<#{0.id}>'.format(self) - - @property - def created_at(self): - """Returns the channel's creation time in UTC.""" - return discord.utils.snowflake_time(self.id) - - def overwrites_for(self, obj): - """Returns the channel-specific overwrites for a member or a role. - - Parameters - ----------- - obj - The :class:`Role` or :class:`Member` or :class:`Object` denoting - whose overwrite to get. - - Returns - --------- - :class:`PermissionOverwrite` - The permission overwrites for this object. - """ - - if isinstance(obj, Member): - predicate = lambda p: p.type == 'member' - elif isinstance(obj, Role): - predicate = lambda p: p.type == 'role' - else: - predicate = lambda p: True - - for overwrite in filter(predicate, self._overwrites): - if overwrite.id == obj.id: - allow = Permissions(overwrite.allow) - deny = Permissions(overwrite.deny) - return PermissionOverwrite.from_pair(allow, deny) - - return PermissionOverwrite() - - @property - def overwrites(self): - """Returns all of the channel's overwrites. - - This is returned as a list of two-element tuples containing the target, - which can be either a :class:`Role` or a :class:`Member` and the overwrite - as the second element as a :class:`PermissionOverwrite`. - - Returns - -------- - List[Tuple[Union[:class:`Role`, :class:`Member`], :class:`PermissionOverwrite`]]: - The channel's permission overwrites. - """ - ret = [] - for ow in self._permission_overwrites: - allow = Permissions(ow.allow) - deny = Permissions(ow.deny) - overwrite = PermissionOverwrite.from_pair(allow, deny) - - if ow.type == 'role': - # accidentally quadratic - target = discord.utils.find(lambda r: r.id == ow.id, self.server.roles) - elif ow.type == 'member': - target = self.server.get_member(ow.id) - - ret.append((target, overwrite)) - return ret - - def permissions_for(self, member): - """Handles permission resolution for the current :class:`Member`. - - This function takes into consideration the following cases: - - - Guild owner - - Guild roles - - Channel overrides - - Member overrides - - Whether the channel is the default channel. - - Parameters - ---------- - member : :class:`Member` - The member to resolve permissions for. - - Returns - ------- - :class:`Permissions` - The resolved permissions for the member. - """ - - # The current cases can be explained as: - # Guild 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.guild.owner.id: - return Permissions.all() - - default = self.guild.default_role - base = Permissions(default.permissions.value) - - # Apply guild roles that the member has. - for role in member.roles: - base.value |= role.permissions.value - - # Guild-wide Administrator -> True for everything - # Bypass all channel-specific overrides - if base.administrator: - return Permissions.all() - - member_role_ids = set(map(lambda r: r.id, member.roles)) - denies = 0 - allows = 0 - - # Apply channel specific role permission overwrites - for overwrite in self._overwrites: - if overwrite.type == 'role' and overwrite.id in member_role_ids: - denies |= overwrite.deny - allows |= overwrite.allow - - base.handle_overwrite(allow=allows, deny=denies) - - # Apply member specific permission overwrites - for overwrite in self._overwrites: - if overwrite.type == 'member' and overwrite.id == member.id: - base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny) - break - - # default channels can always be read - if self.is_default: - base.read_messages = True - - # if you can't send a message in a channel then you can't have certain - # permissions as well - if not base.send_messages: - base.send_tts_messages = False - base.mention_everyone = False - base.embed_links = False - base.attach_files = False - - # if you can't read a channel then you have no permissions there - if not base.read_messages: - denied = Permissions.all_channel() - base.value &= ~denied.value - - # text channels do not have voice related permissions - if isinstance(self, TextChannel): - denied = Permissions.voice() - base.value &= ~denied.value - - return base - - @asyncio.coroutine - def delete(self): - """|coro| - - Deletes the channel. - - You must have Manage Channel permission to use this. - - Raises - ------- - Forbidden - You do not have proper permissions to delete the channel. - NotFound - The channel was not found or was already deleted. - HTTPException - Deleting the channel failed. - """ - yield from self._state.http.delete_channel(self.id) - -class TextChannel(discord.abc.MessageChannel, CommonGuildChannel): +class TextChannel(discord.abc.MessageChannel, discord.abc.GuildChannel, Hashable): """Represents a Discord guild text channel. Supported Operations: @@ -393,7 +130,7 @@ class TextChannel(discord.abc.MessageChannel, CommonGuildChannel): data = yield from self._state.http.edit_channel(self.id, **options) self._update(self.guild, data) -class VoiceChannel(CommonGuildChannel): +class VoiceChannel(discord.abc.GuildChannel, Hashable): """Represents a Discord guild voice channel. Supported Operations: