diff options
| author | Rapptz <[email protected]> | 2016-10-17 01:10:22 -0400 |
|---|---|---|
| committer | Rapptz <[email protected]> | 2017-01-03 09:51:52 -0500 |
| commit | 53ab2631252bf0977446d762f07b3821edb151ee (patch) | |
| tree | abb8a2e7a966aadb22df8a3ca2220b646eae3765 /discord/channel.py | |
| parent | [commands] Bot skip check now works with the new __eq__ changes. (diff) | |
| download | discord.py-53ab2631252bf0977446d762f07b3821edb151ee.tar.xz discord.py-53ab2631252bf0977446d762f07b3821edb151ee.zip | |
Split channel types.
This splits them into the following:
* DMChannel
* GroupChannel
* VoiceChannel
* TextChannel
This also makes the channels "stateful".
Diffstat (limited to 'discord/channel.py')
| -rw-r--r-- | discord/channel.py | 468 |
1 files changed, 350 insertions, 118 deletions
diff --git a/discord/channel.py b/discord/channel.py index f79a2d5d..b1961dd4 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -23,8 +23,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import copy -from . import utils +from . import utils, abc from .permissions import Permissions, PermissionOverwrite from .enums import ChannelType, try_enum from collections import namedtuple @@ -33,82 +32,54 @@ from .role import Role from .user import User from .member import Member +import copy +import asyncio + +__all__ = ('TextChannel', 'VoiceChannel', 'DMChannel', 'GroupChannel', '_channel_factory') + Overwrites = namedtuple('Overwrites', 'id allow deny type') -class Channel(Hashable): - """Represents a Discord server channel. +class CommonGuildChannel(Hashable): + __slots__ = () - Supported Operations: + def __str__(self): + return self.name - +-----------+---------------------------------------+ - | Operation | Description | - +===========+=======================================+ - | x == y | Checks if two channels are equal. | - +-----------+---------------------------------------+ - | x != y | Checks if two channels are not equal. | - +-----------+---------------------------------------+ - | hash(x) | Returns the channel's hash. | - +-----------+---------------------------------------+ - | str(x) | Returns the channel's name. | - +-----------+---------------------------------------+ + @asyncio.coroutine + def _move(self, position): + if position < 0: + raise InvalidArgument('Channel position cannot be less than 0.') - Attributes - ----------- - name: str - The channel name. - server: :class:`Server` - The server the channel belongs to. - id: int - The channel ID. - topic: Optional[str] - The channel's topic. None if it doesn't exist. - is_private: bool - ``True`` if the channel is a private channel (i.e. PM). ``False`` in this case. - position: int - The position in the channel list. This is a number that starts at 0. e.g. the - top channel is position 0. The position varies depending on being a voice channel - or a text channel, so a 0 position voice channel is on top of the voice channel - list. - type: :class:`ChannelType` - The channel type. There is a chance that the type will be ``str`` if - the channel type is not within the ones recognised by the enumerator. - bitrate: int - The channel's preferred audio bitrate in bits per second. - voice_members - A list of :class:`Members` that are currently inside this voice channel. - If :attr:`type` is not :attr:`ChannelType.voice` then this is always an empty array. - user_limit: int - The channel's limit for number of members that can be in a voice channel. - """ + http = self._state.http + url = '{0}/{1.server.id}/channels'.format(http.GUILDS, self) + channels = [c for c in self.server.channels if isinstance(c, type(self))] - __slots__ = ( 'voice_members', 'name', 'id', 'server', 'topic', - 'type', 'bitrate', 'user_limit', '_state', 'position', - '_permission_overwrites' ) + if position >= len(channels): + raise InvalidArgument('Channel position cannot be greater than {}'.format(len(channels) - 1)) - def __init__(self, *, state, server, data): - self._state = state - self.id = int(data['id']) - self._update(server, data) - self.voice_members = [] + channels.sort(key=lambda c: c.position) - def __str__(self): - return self.name + 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) - def _update(self, server, data): - self.server = server - self.name = data['name'] - self.topic = data.get('topic') - self.position = data['position'] - self.bitrate = data.get('bitrate') - self.type = data['type'] - self.user_limit = data.get('user_limit') - self._permission_overwrites = [] + 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.server.id for index, overridden in enumerate(data.get('permission_overwrites', [])): overridden_id = int(overridden.pop('id')) - self._permission_overwrites.append(Overwrites(id=overridden_id, **overridden)) + self._overwrites.append(Overwrites(id=overridden_id, **overridden)) if overridden['type'] == 'member': continue @@ -122,7 +93,7 @@ class Channel(Hashable): everyone_index = index # do the swap - tmp = self._permission_overwrites + tmp = self._overwrites if tmp: tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index] @@ -131,7 +102,7 @@ class Channel(Hashable): """Returns a list of :class:`Roles` that have been overridden from their default values in the :attr:`Server.roles` attribute.""" ret = [] - for overwrite in filter(lambda o: o.type == 'role', self._permission_overwrites): + for overwrite in filter(lambda o: o.type == 'role', self._overwrites): role = utils.get(self.server.roles, id=overwrite.id) if role is None: continue @@ -147,10 +118,6 @@ class Channel(Hashable): return self.server.id == self.id @property - def is_private(self): - return False - - @property def mention(self): """str : The string that allows you to mention the channel.""" return '<#{0.id}>'.format(self) @@ -182,7 +149,7 @@ class Channel(Hashable): else: predicate = lambda p: True - for overwrite in filter(predicate, self._permission_overwrites): + for overwrite in filter(predicate, self._overwrites): if overwrite.id == obj.id: allow = Permissions(overwrite.allow) deny = Permissions(overwrite.deny) @@ -276,7 +243,7 @@ class Channel(Hashable): allows = 0 # Apply channel specific role permission overwrites - for overwrite in self._permission_overwrites: + for overwrite in self._overwrites: if overwrite.type == 'role' and overwrite.id in member_role_ids: denies |= overwrite.deny allows |= overwrite.allow @@ -284,7 +251,7 @@ class Channel(Hashable): base.handle_overwrite(allow=allows, deny=denies) # Apply member specific permission overwrites - for overwrite in self._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 @@ -307,14 +274,286 @@ class Channel(Hashable): base.value &= ~denied.value # text channels do not have voice related permissions - if self.type is ChannelType.text: + if isinstance(self, TextChannel): denied = Permissions.voice() base.value &= ~denied.value return base -class PrivateChannel(Hashable): - """Represents a Discord private channel. + @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(abc.MessageChannel, CommonGuildChannel): + """Represents a Discord server text channel. + + Supported Operations: + + +-----------+---------------------------------------+ + | Operation | Description | + +===========+=======================================+ + | x == y | Checks if two channels are equal. | + +-----------+---------------------------------------+ + | x != y | Checks if two channels are not equal. | + +-----------+---------------------------------------+ + | hash(x) | Returns the channel's hash. | + +-----------+---------------------------------------+ + | str(x) | Returns the channel's name. | + +-----------+---------------------------------------+ + + Attributes + ----------- + name: str + The channel name. + server: :class:`Server` + The server the channel belongs to. + id: int + The channel ID. + topic: Optional[str] + The channel's topic. None if it doesn't exist. + position: int + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + """ + + __slots__ = ( 'name', 'id', 'server', 'topic', '_state', + 'position', '_overwrites' ) + + def __init__(self, *, state, server, data): + self._state = state + self.id = int(data['id']) + self._update(server, data) + + def _update(self, server, data): + self.server = server + self.name = data['name'] + self.topic = data.get('topic') + self.position = data['position'] + self._fill_overwrites(data) + + def _get_destination(self): + return self.id, self.server.id + + @asyncio.coroutine + def edit(self, **options): + """|coro| + + Edits the channel. + + You must have the Manage Channel permission to use this. + + Parameters + ---------- + name: str + The new channel name. + topic: str + The new channel's topic. + position: int + The new channel's position. + + Raises + ------ + InvalidArgument + If position is less than 0 or greater than the number of channels. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + try: + position = options.pop('position') + except KeyError: + pass + else: + yield from self._move(position) + self.position = position + + if options: + data = yield from self._state.http.edit_channel(self.id, **options) + self._update(self.server, data) + +class VoiceChannel(CommonGuildChannel): + """Represents a Discord server voice channel. + + Supported Operations: + + +-----------+---------------------------------------+ + | Operation | Description | + +===========+=======================================+ + | x == y | Checks if two channels are equal. | + +-----------+---------------------------------------+ + | x != y | Checks if two channels are not equal. | + +-----------+---------------------------------------+ + | hash(x) | Returns the channel's hash. | + +-----------+---------------------------------------+ + | str(x) | Returns the channel's name. | + +-----------+---------------------------------------+ + + Attributes + ----------- + name: str + The channel name. + server: :class:`Server` + The server the channel belongs to. + id: int + The channel ID. + position: int + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + bitrate: int + The channel's preferred audio bitrate in bits per second. + voice_members + A list of :class:`Members` that are currently inside this voice channel. + user_limit: int + The channel's limit for number of members that can be in a voice channel. + """ + + __slots__ = ( 'voice_members', 'name', 'id', 'server', 'bitrate', + 'user_limit', '_state', 'position', '_overwrites' ) + + def __init__(self, *, state, server, data): + self._state = state + self.id = int(data['id']) + self._update(server, data) + self.voice_members = [] + + def _update(self, server, data): + self.server = server + self.name = data['name'] + self.position = data['position'] + self.bitrate = data.get('bitrate') + self.user_limit = data.get('user_limit') + self._fill_overwrites(data) + + @asyncio.coroutine + def edit(self, **options): + """|coro| + + Edits the channel. + + You must have the Manage Channel permission to use this. + + Parameters + ---------- + bitrate: int + The new channel's bitrate. + user_limit: int + The new channel's user limit. + position: int + The new channel's position. + + Raises + ------ + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + + try: + position = options.pop('position') + except KeyError: + pass + else: + yield from self._move(position) + self.position = position + + if options: + data = yield from self._state.http.edit_channel(self.id, **options) + self._update(self.server, data) + +class DMChannel(abc.MessageChannel, Hashable): + """Represents a Discord direct message channel. + + Supported Operations: + + +-----------+-------------------------------------------------+ + | Operation | Description | + +===========+=================================================+ + | x == y | Checks if two channels are equal. | + +-----------+-------------------------------------------------+ + | x != y | Checks if two channels are not equal. | + +-----------+-------------------------------------------------+ + | hash(x) | Returns the channel's hash. | + +-----------+-------------------------------------------------+ + | str(x) | Returns a string representation of the channel | + +-----------+-------------------------------------------------+ + + Attributes + ---------- + recipient: :class:`User` + The user you are participating with in the direct message channel. + me: :class:`User` + The user presenting yourself. + id: int + The direct message channel ID. + """ + + __slots__ = ('id', 'recipient', 'me', '_state') + + def __init__(self, *, me, state, data): + self._state = state + self.recipient = state.try_insert_user(data['recipients'][0]) + self.me = me + self.id = int(data['id']) + + def _get_destination(self): + return self.id, None + + def __str__(self): + return 'Direct Message with %s' % self.recipient + + @property + def created_at(self): + """Returns the direct message channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + + def permissions_for(self, user=None): + """Handles permission resolution for a :class:`User`. + + This function is there for compatibility with other channel types. + + Actual direct messages do not really have the concept of permissions. + + This returns all the Text related permissions set to true except: + + - send_tts_messages: You cannot send TTS messages in a DM. + - manage_messages: You cannot delete others messages in a DM. + + Parameters + ----------- + user: :class:`User` + The user to check permissions for. This parameter is ignored + but kept for compatibility. + + Returns + -------- + :class:`Permissions` + The resolved permissions. + """ + + base = Permissions.text() + base.send_tts_messages = False + base.manage_messages = False + return base + +class GroupChannel(abc.MessageChannel, Hashable): + """Represents a Discord group channel. Supported Operations: @@ -333,50 +572,42 @@ class PrivateChannel(Hashable): Attributes ---------- recipients: list of :class:`User` - The users you are participating with in the private channel. + The users you are participating with in the group channel. me: :class:`User` The user presenting yourself. id: int - The private channel ID. - is_private: bool - ``True`` if the channel is a private channel (i.e. PM). ``True`` in this case. - type: :class:`ChannelType` - The type of private channel. - owner: Optional[:class:`User`] - The user that owns the private channel. If the channel type is not - :attr:`ChannelType.group` then this is always ``None``. + The group channel ID. + owner: :class:`User` + The user that owns the group channel. icon: Optional[str] - The private channel's icon hash. If the channel type is not - :attr:`ChannelType.group` then this is always ``None``. + The group channel's icon hash if provided. name: Optional[str] - The private channel's name. If the channel type is not - :attr:`ChannelType.group` then this is always ``None``. + The group channel's name if provided. """ - __slots__ = ('id', 'recipients', 'type', 'owner', 'icon', 'name', 'me', '_state') + __slots__ = ('id', 'recipients', 'owner', 'icon', 'name', 'me', '_state') def __init__(self, *, me, state, data): self._state = state self.recipients = [state.try_insert_user(u) for u in data['recipients']] self.id = int(data['id']) self.me = me - self.type = try_enum(ChannelType, data['type']) self._update_group(data) def _update_group(self, data): owner_id = utils._get_as_snowflake(data, 'owner_id') self.icon = data.get('icon') self.name = data.get('name') - self.owner = utils.find(lambda u: u.id == owner_id, self.recipients) - @property - def is_private(self): - return True + if owner_id == self.me.id: + self.owner = self.me + else: + self.owner = utils.find(lambda u: u.id == owner_id, self.recipients) - def __str__(self): - if self.type is ChannelType.private: - return 'Direct Message with {0.name}'.format(self.user) + def _get_destination(self): + return self.id, None + def __str__(self): if self.name: return self.name @@ -386,15 +617,6 @@ class PrivateChannel(Hashable): return ', '.join(map(lambda x: x.name, self.recipients)) @property - def user(self): - """A property that returns the first recipient of the private channel. - - This is mainly for compatibility and ease of use with old style private - channels that had a single recipient. - """ - return self.recipients[0] - - @property def icon_url(self): """Returns the channel's icon URL if available or an empty string otherwise.""" if self.icon is None: @@ -404,27 +626,26 @@ class PrivateChannel(Hashable): @property def created_at(self): - """Returns the private channel's creation time in UTC.""" + """Returns the channel's creation time in UTC.""" return utils.snowflake_time(self.id) def permissions_for(self, user): """Handles permission resolution for a :class:`User`. - This function is there for compatibility with :class:`Channel`. + This function is there for compatibility with other channel types. - Actual private messages do not really have the concept of permissions. + Actual direct messages do not really have the concept of permissions. This returns all the Text related permissions set to true except: - - send_tts_messages: You cannot send TTS messages in a PM. - - manage_messages: You cannot delete others messages in a PM. + - send_tts_messages: You cannot send TTS messages in a DM. + - manage_messages: You cannot delete others messages in a DM. - This also handles permissions for :attr:`ChannelType.group` channels - such as kicking or mentioning everyone. + This also checks the kick_members permission if the user is the owner. Parameters ----------- - user : :class:`User` + user: :class:`User` The user to check permissions for. Returns @@ -436,11 +657,22 @@ class PrivateChannel(Hashable): base = Permissions.text() base.send_tts_messages = False base.manage_messages = False - base.mention_everyone = self.type is ChannelType.group + base.mention_everyone = True - if user == self.owner: + if user.id == self.owner.id: base.kick_members = True return base - +def _channel_factory(channel_type): + value = try_enum(ChannelType, channel_type) + if value is ChannelType.text: + return TextChannel, value + elif value is ChannelType.voice: + return VoiceChannel, value + elif value is ChannelType.private: + return DMChannel, value + elif value is ChannelType.group: + return GroupChannel, value + else: + return None, value |