From 79bdf2a72174a634aad0a752cad60d96ee8336f6 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 4 Dec 2015 22:13:17 -0500 Subject: [PATCH] Add enumerators instead of strings. Changes channel type, status and server region into 3.4 enums. --- discord/channel.py | 89 +++++++++++++++++++++++++--------------------- discord/client.py | 13 ++++--- discord/enums.py | 54 ++++++++++++++++++++++++++++ discord/member.py | 57 +++++++++++++---------------- discord/server.py | 60 +++++++++++++++---------------- discord/state.py | 5 +++ docs/api.rst | 61 +++++++++++++++++++++++++++++++ 7 files changed, 231 insertions(+), 108 deletions(-) create mode 100644 discord/enums.py diff --git a/discord/channel.py b/discord/channel.py index 32b98fda1..7204d31ff 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -26,44 +26,37 @@ DEALINGS IN THE SOFTWARE. from copy import deepcopy from . import utils from .permissions import Permissions +from .enums import ChannelType from collections import namedtuple Overwrites = namedtuple('Overwrites', 'id allow deny type') -class Channel(object): +class Channel: """Represents a Discord server channel. - Instance attributes: - - .. attribute:: name - + Attributes + ----------- + name : str The channel name. - .. attribute:: server - - The :class:`Server` the channel belongs to. - .. attribute:: id - + server : :class:`Server` + The server the channel belongs to. + id : str The channel ID. - .. attribute:: topic - + topic : Optional[str] The channel's topic. None if it doesn't exist. - .. attribute:: is_private - + is_private : bool ``True`` if the channel is a private channel (i.e. PM). ``False`` in this case. - .. attribute:: position - + position : int The position in the channel list. - .. attribute:: type - - The channel type. Usually ``'voice'`` or ``'text'``. - .. attribute:: changed_roles - + 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. + changed_roles A list of :class:`Roles` that have been overridden from their default values in the :attr:`Server.roles` attribute. - .. attribute:: voice_members - + voice_members A list of :class:`Members` that are currently inside this voice channel. - If :attr:`type` is not ``'voice'`` then this is always an empty array. + If :attr:`type` is not :attr:`ChannelType.voice` then this is always an empty array. """ def __init__(self, **kwargs): @@ -78,6 +71,11 @@ class Channel(object): self.is_private = False self.position = kwargs.get('position') self.type = kwargs.get('type') + try: + self.type = ChannelType(self.type) + except: + pass + self.changed_roles = [] self._permission_overwrites = [] for overridden in kwargs.get('permission_overwrites', []): @@ -98,11 +96,11 @@ class Channel(object): self.changed_roles.append(override) def is_default_channel(self): - """Checks if this is the default channel for the :class:`Server` it belongs to.""" + """bool : Indicates if this is the default channel for the :class:`Server` it belongs to.""" return self.server.id == self.id def mention(self): - """Returns a string that allows you to mention the channel.""" + """str : The string that allows you to mention the channel.""" return '<#{0.id}>'.format(self) def permissions_for(self, member): @@ -116,8 +114,15 @@ class Channel(object): - 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`. + 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: @@ -173,19 +178,16 @@ class Channel(object): return base -class PrivateChannel(object): +class PrivateChannel: """Represents a Discord private channel. - Instance attributes: - - .. attribute:: user - - The :class:`User` in the private channel. - .. attribute:: id - + Attributes + ---------- + user : :class:`User` + The user you are participating with in the private channel. + id : str The private channel ID. - .. attribute:: is_private - + is_private : bool ``True`` if the channel is a private channel (i.e. PM). ``True`` in this case. """ @@ -207,8 +209,15 @@ class PrivateChannel(object): - 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. + Parameters + ----------- + user : :class:`User` + The user to check permissions for. + + Returns + -------- + :class:`Permission` + The resolved permissions for the user. """ base = Permissions.TEXT diff --git a/discord/client.py b/discord/client.py index 9f675ed3d..881bab6bb 100644 --- a/discord/client.py +++ b/discord/client.py @@ -34,6 +34,7 @@ from .object import Object from .errors import * from .state import ConnectionState from . import utils +from .enums import ChannelType, ServerRegion import asyncio import aiohttp @@ -1085,7 +1086,7 @@ class Client: log.debug(request_success_log.format(response=r, json=payload, data=data)) @asyncio.coroutine - def create_channel(self, server, name, type='text'): + def create_channel(self, server, name, type=None): """|coro| Creates a :class:`Channel` in the specified :class:`Server`. @@ -1098,8 +1099,8 @@ class Client: The server to create the channel in. name : str The channel's name. - type : str - The type of channel to create. 'text' or 'voice'. + type : :class:`ChannelType` + The type of channel to create. Defaults to :attr:`ChannelType.text`. Raises ------- @@ -1117,9 +1118,12 @@ class Client: different than the one that will be added in cache. """ + if type is None: + type = ChannelType.text + payload = { 'name': name, - 'type': type + 'type': str(type) } url = '{0}/{1.id}/channels'.format(endpoints.SERVERS, server) @@ -1160,4 +1164,3 @@ class Client: response = yield from self.session.delete(url, headers=self.headers) log.debug(request_logging_format.format(method='DELETE', response=response)) yield from utils._verify_successful_response(response) - diff --git a/discord/enums.py b/discord/enums.py new file mode 100644 index 000000000..0d2d93900 --- /dev/null +++ b/discord/enums.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +""" +The MIT License (MIT) + +Copyright (c) 2015 Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from enum import Enum + +class ChannelType(Enum): + text = 'text' + voice = 'voice' + + def __str__(self): + return self.value + +class ServerRegion(Enum): + us_west = 'us-west' + us_east = 'us-east' + singapore = 'singapore' + london = 'london' + sydney = 'sydney' + amsterdam = 'amsterdam' + frankfurt = 'frankfurt' + + def __str__(self): + return self.value + +class Status(Enum): + online = 'online' + offline = 'offline' + idle = 'idle' + + def __str__(self): + return self.value diff --git a/discord/member.py b/discord/member.py index 2fadc664d..5c151e9fe 100644 --- a/discord/member.py +++ b/discord/member.py @@ -26,6 +26,7 @@ DEALINGS IN THE SOFTWARE. from .user import User from .utils import parse_time +from .enums import Status class Member(User): """Represents a Discord member to a :class:`Server`. @@ -33,44 +34,34 @@ class Member(User): This is a subclass of :class:`User` that extends more functionality that server members have such as roles and permissions. - Instance attributes: - - .. attribute:: deaf - - A boolean that specifies if the member is currently deafened by the server. - .. attribute:: mute - - A boolean that specifies if the member is currently muted by the server. - .. attribute:: self_mute - - A boolean that specifies if the member is currently muted by their own accord. - .. attribute:: self_deaf - - A boolean that specifies if the member is currently deafened by their own accord. - .. attribute:: is_afk - - A boolean that specifies if the member is currently in the AFK channel in the server. - .. attribute:: voice_channel - - A voice :class:`Channel` that the member is currently connected to. None if the member + Attributes + ---------- + deaf : bool + Indicates if the member is currently deafened by the server. + mute : bool + Indicates if the member is currently muted by the server. + self_mute : bool + Indicates if the member is currently muted by their own accord. + self_deaf : bool + Indicates if the member is currently deafened by their own accord. + is_afk : bool + Indicates if the member is currently in the AFK channel in the server. + voice_channel : :class:`Channel` + The voice channel that the member is currently connected to. None if the member is not currently in a voice channel. - .. attribute:: roles - + roles A list of :class:`Role` that the member belongs to. Note that the first element of this list is always the default '@everyone' role. - .. attribute:: joined_at - + joined_at : `datetime.datetime` A datetime object that specifies the date and time in UTC that the member joined the server for the first time. - .. attribute:: status - - A string that denotes the user's status. Can be 'online', 'offline' or 'idle'. - .. attribute:: game_id - + status : :class:`Status` + The member's status. There is a chance that the status will be a ``str`` + if it is a value that is not recognised by the enumerator. + game_id : int The game ID that the user is currently playing. Could be None if no game is being played. - .. attribute:: server - - The :class:`Server` that the member belongs to. + server : :class:`Server` + The server that the member belongs to. """ def __init__(self, deaf, joined_at, user, roles, mute, **kwargs): @@ -79,7 +70,7 @@ class Member(User): self.mute = mute self.joined_at = parse_time(joined_at) self.roles = roles - self.status = 'offline' + self.status = Status.offline self.game_id = kwargs.get('game_id', None) self.server = kwargs.get('server', None) self.update_voice_state(mute=mute, deaf=deaf) diff --git a/discord/server.py b/discord/server.py index 0c2eca5a2..8309f0522 100644 --- a/discord/server.py +++ b/discord/server.py @@ -28,45 +28,36 @@ from . import utils from .role import Role from .member import Member from .channel import Channel +from .enums import ServerRegion, Status -class Server(object): +class Server: """Represents a Discord server. - Instance attributes: - - .. attribute:: name - + Attributes + ---------- + name : str The server name. - .. attribute:: roles - + roles A list of :class:`Role` that the server has available. - .. attribute:: region - - The region the server belongs on. - .. attribute:: afk_timeout - + region : :class:`ServerRegion` + The region the server belongs on. There is a chance that the region + will be a ``str`` if the value is not recognised by the enumerator. + afk_timeout : int The timeout to get sent to the AFK channel. - .. attribute:: afk_channel - - The :class:`Channel` that denotes the AFK channel. None if it doesn't exist. - .. attribute:: members - + afk_channel : :class:`Channel` + The channel that denotes the AFK channel. None if it doesn't exist. + members A list of :class:`Member` that are currently on the server. - .. attribute:: channels - + channels A list of :class:`Channel` that are currently on the server. - .. attribute:: icon - + icon : str The server's icon. - .. attribute:: id - + id : str The server's ID. - .. attribute:: owner - - The :class:`Member` who owns the server. - .. attribute:: unavailable - - A boolean indicating if the server is unavailable. If this is ``True`` then the + owner : :class:`Member` + The member who owns the server. + unavailable : bool + Indicates if the server is unavailable. If this is ``True`` then the reliability of other attributes outside of :meth:`Server.id` is slim and they might all be None. It is best to not do anything with the server if it is unavailable. @@ -88,6 +79,11 @@ class Server(object): def _from_data(self, guild): self.name = guild.get('name') self.region = guild.get('region') + try: + self.region = ServerRegion(self.region) + except: + pass + self.afk_timeout = guild.get('afk_timeout') self.icon = guild.get('icon') self.unavailable = guild.get('unavailable', False) @@ -119,6 +115,10 @@ class Server(object): member = utils.find(lambda m: m.id == user_id, self.members) if member is not None: member.status = presence['status'] + try: + member.status = Status(member.status) + except: + pass member.game_id = presence['game_id'] self.channels = [Channel(server=self, **c) for c in guild['channels']] @@ -137,7 +137,7 @@ class Server(object): return utils.find(lambda c: c.is_default_channel(), self.channels) def icon_url(self): - """Returns the URL version of the server's icon. Returns None if it has no icon.""" + """Returns the URL version of the server's icon. Returns an empty string if it has no icon.""" if self.icon is None: return '' return 'https://cdn.discordapp.com/icons/{0.id}/{0.icon}.jpg'.format(self) diff --git a/discord/state.py b/discord/state.py index b62817a74..d8b38a432 100644 --- a/discord/state.py +++ b/discord/state.py @@ -31,6 +31,7 @@ from .channel import Channel, PrivateChannel from .member import Member from .role import Role from . import utils +from .enums import Status from collections import deque import copy @@ -113,6 +114,10 @@ class ConnectionState: if member is not None: old_member = copy.copy(member) member.status = data.get('status') + try: + member.status = Status(member.status) + except: + pass member.game_id = data.get('game_id') member.name = user.get('username', member.name) member.avatar = user.get('avatar', member.avatar) diff --git a/docs/api.rst b/docs/api.rst index 59efac785..93b2c239e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -297,6 +297,67 @@ Utility Functions .. autofunction:: discord.utils.find +Enumerators +------------ + +The API provides some enumerators for certain types of strings to avoid the API +from being stringly typed in case the strings change in the future. + +All enumerators are subclasses of `enum`_. + +.. _enum: https://docs.python.org/3/library/enum.html + +.. class:: ChannelType + + Specifies the type of :class:`Channel`. + + .. attribute:: text + + A text channel. + .. attribute:: voice + + A voice channel. + +.. class:: ServerRegion + + Specifies the region a :class:`Server`'s voice server belongs to. + + .. attribute:: us_west + + The US West region. + .. attribute:: us_east + + The US East region. + .. attribute:: singapore + + The Singapore region. + .. attribute:: london + + The London region. + .. attribute:: sydney + + The Sydney region. + .. attribute:: amsterdam + + The Amsterdam region. + .. attribute:: frankfurt + + The Frankfurt region. + +.. class:: Status + + Specifies a :class:`Member` 's status. + + .. attribute:: online + + The member is online. + .. attribute:: offline + + The member is offline. + .. attribute:: idle + + The member is idle. + Data Classes --------------