Split channel types.
This splits them into the following: * DMChannel * GroupChannel * VoiceChannel * TextChannel This also makes the channels "stateful".
This commit is contained in:
@ -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.
|
||||
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
|
||||
__slots__ = ( 'voice_members', 'name', 'id', 'server', 'topic',
|
||||
'type', 'bitrate', 'user_limit', '_state', 'position',
|
||||
'_permission_overwrites' )
|
||||
|
||||
def __init__(self, *, state, server, data):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self._update(server, data)
|
||||
self.voice_members = []
|
||||
class CommonGuildChannel(Hashable):
|
||||
__slots__ = ()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
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 = []
|
||||
@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.server.id}/channels'.format(http.GUILDS, self)
|
||||
channels = [c for c in self.server.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.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
|
||||
@ -146,10 +117,6 @@ class Channel(Hashable):
|
||||
"""bool : Indicates if this is the default channel for the :class:`Server` it belongs to."""
|
||||
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."""
|
||||
@ -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 _get_destination(self):
|
||||
return self.id, None
|
||||
|
||||
def __str__(self):
|
||||
if self.type is ChannelType.private:
|
||||
return 'Direct Message with {0.name}'.format(self.user)
|
||||
|
||||
if self.name:
|
||||
return self.name
|
||||
|
||||
@ -385,15 +616,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."""
|
||||
@ -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
|
||||
|
Reference in New Issue
Block a user