diff --git a/discord/guild.py b/discord/guild.py index ad713bb65..6e37c3ab8 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -24,6 +24,9 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import copy +import asyncio + from . import utils from .role import Role from .member import Member, VoiceState @@ -32,8 +35,8 @@ from .game import Game from .channel import * from .enums import GuildRegion, Status, ChannelType, try_enum, VerificationLevel from .mixins import Hashable - -import copy +from .user import User +from .invite import Invite class Guild(Hashable): """Represents a Discord guild. @@ -375,3 +378,305 @@ class Guild(Hashable): return m.nick == name or m.name == name return utils.find(pred, members) + + + @asyncio.coroutine + def leave(self): + """|coro| + + Leaves the guild. + + Note + -------- + You cannot leave the guild that you own, you must delete it instead + via :meth:`delete`. + + Raises + -------- + HTTPException + Leaving the guild failed. + """ + yield from self._state.http.leave_guild(self.id) + + @asyncio.coroutine + def delete(self): + """|coro| + + Deletes the guild. You must be the guild owner to delete the + guild. + + Raises + -------- + HTTPException + Deleting the guild failed. + Forbidden + You do not have permissions to delete the guild. + """ + + yield from self._state.http.delete_guild(self.id) + + @asyncio.coroutine + def edit(self, **fields): + """|coro| + + Edits the guild. + + You must have the :attr:`Permissions.manage_guild` permission + to edit the guild. + + Parameters + ---------- + name: str + The new name of the guild. + icon: bytes + A *bytes-like* object representing the icon. Only PNG/JPEG supported. + Could be ``None`` to denote removal of the icon. + region: :class:`GuildRegion` + The new region for the guild's voice communication. + afk_channel: :class:`VoiceChannel` + The new channel that is the AFK channel. Could be ``None`` for no AFK channel. + afk_timeout: int + The number of seconds until someone is moved to the AFK channel. + owner: :class:`Member` + The new owner of the guild to transfer ownership to. Note that you must + be owner of the guild to do this. + verification_level: :class:`VerificationLevel` + The new verification level for the guild. + + Raises + ------- + Forbidden + You do not have permissions to edit the guild. + HTTPException + Editing the guild failed. + InvalidArgument + The image format passed in to ``icon`` is invalid. It must be + PNG or JPG. This is also raised if you are not the owner of the + guild and request an ownership transfer. + """ + + try: + icon_bytes = fields['icon'] + except KeyError: + icon = self.icon + else: + if icon_bytes is not None: + icon = utils._bytes_to_base64_data(icon_bytes) + else: + icon = None + + fields['icon'] = icon + if 'afk_channel' in fields: + fields['afk_channel_id'] = fields['afk_channel'].id + + if 'owner' in fields: + if self.owner != self.me: + raise InvalidArgument('To transfer ownership you must be the owner of the guild.') + + fields['owner_id'] = fields['owner'].id + + if 'region' in fields: + fields['region'] = str(fields['region']) + + level = fields.get('verification_level', self.verification_level) + if not isinstance(level, VerificationLevel): + raise InvalidArgument('verification_level field must of type VerificationLevel') + + fields['verification_level'] = level.value + yield from self._state.http.edit_guild(self.id, **fields) + + + @asyncio.coroutine + def bans(self): + """|coro| + + Retrieves all the :class:`User`\s that are banned from the guild. + + You must have :attr:`Permissions.ban_members` permission + to get this information. + + Raises + ------- + Forbidden + You do not have proper permissions to get the information. + HTTPException + An error occurred while fetching the information. + + Returns + -------- + list + A list of :class:`User` that have been banned. + """ + + data = yield from self._state.http.get_bans(self.id) + return [User(state=self._state, data=user) for user in data] + + @asyncio.coroutine + def prune_members(self, *, days): + """|coro| + + Prunes the guild from its inactive members. + + The inactive members are denoted if they have not logged on in + ``days`` number of days and they have no roles. + + You must have the :attr:`Permissions.kick_members` permission + to use this. + + To check how many members you would prune without actually pruning, + see the :meth:`estimate_pruned_members` function. + + Parameters + ----------- + days: int + The number of days before counting as inactive. + + Raises + ------- + Forbidden + You do not have permissions to prune members. + HTTPException + An error occurred while pruning members. + InvalidArgument + An integer was not passed for ``days``. + + Returns + --------- + int + The number of members pruned. + """ + + if not isinstance(days, int): + raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days)) + + data = yield from self._state.http.prune_members(self.id, days) + return data['pruned'] + + @asyncio.coroutine + def estimate_pruned_members(self, *, days): + """|coro| + + Similar to :meth:`prune_members` except instead of actually + pruning members, it returns how many members it would prune + from the guild had it been called. + + Parameters + ----------- + days: int + The number of days before counting as inactive. + + Raises + ------- + Forbidden + You do not have permissions to prune members. + HTTPException + An error occurred while fetching the prune members estimate. + InvalidArgument + An integer was not passed for ``days``. + + Returns + --------- + int + The number of members estimated to be pruned. + """ + + if not isinstance(days, int): + raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days)) + + data = yield from self._state.http.estimate_pruned_members(self.id, days) + return data['pruned'] + + @asyncio.coroutine + def invites(self): + """|coro| + + Returns a list of all active instant invites from the guild. + + You must have :attr:`Permissions.manage_guild` to get this information. + + Raises + ------- + Forbidden + You do not have proper permissions to get the information. + HTTPException + An error occurred while fetching the information. + + Returns + ------- + list of :class:`Invite` + The list of invites that are currently active. + """ + + data = yield from self._state.http.invites_from(guild.id) + result = [] + for invite in data: + channel = self.get_channel(int(invite['channel']['id'])) + invite['channel'] = channel + invite['guild'] = self + result.append(Invite(state=self._state, data=invite)) + + return result + + @asyncio.coroutine + def create_custom_emoji(self, *, name, image): + """|coro| + + Creates a custom :class:`Emoji` for the guild. + + This endpoint is only allowed for user bots or white listed + bots. If this is done by a user bot then this is a local + emoji that can only be used inside the guild. If done by + a whitelisted bot, then this emoji is "global". + + There is currently a limit of 50 local emotes per guild. + + Parameters + ----------- + name: str + The emoji name. Must be at least 2 characters. + image: bytes + The *bytes-like* object representing the image data to use. + Only JPG and PNG images are supported. + + Returns + -------- + :class:`Emoji` + The created emoji. + + Raises + ------- + Forbidden + You are not allowed to create emojis. + HTTPException + An error occurred creating an emoji. + """ + + img = utils._bytes_to_base64_data(image) + data = yield from self._state.http.create_custom_emoji(self.id, name, img) + return Emoji(guild=self, data=data, state=self._state) + + @asyncio.coroutine + def create_role(self, **fields): + """|coro| + + Creates a :class:`Role` for the guild. + + This function is similar to :meth:`Role.edit` in both + the fields taken and exceptions thrown. + + Returns + -------- + :class:`Role` + The newly created role. + """ + + data = yield from self._state.http.create_role(self.id) + role = Role(guild=self, data=data, state=self._state) + + if fields: + # we have to call edit because you can't pass a payload to the + # http request currently. + yield from role.edit(**fields) + + # TODO: add to cache + return role diff --git a/discord/role.py b/discord/role.py index 945748d54..7ea93a98b 100644 --- a/discord/role.py +++ b/discord/role.py @@ -24,6 +24,8 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import asyncio + from .permissions import Permissions from .colour import Colour from .mixins import Hashable @@ -144,3 +146,109 @@ class Role(Hashable): def mention(self): """Returns a string that allows you to mention a role.""" return '<@&{}>'.format(self.id) + + @asyncio.coroutine + def _move(self, position): + if position <= 0: + raise InvalidArgument("Cannot move role to position 0 or below") + + if self.is_everyone: + raise InvalidArgument("Cannot move default role") + + if self.position == position: + return # Save discord the extra request. + + http = self._state.http + url = '{0}/{1}/roles'.format(http.GUILDS, self.guild.id) + + change_range = range(min(self.position, position), max(self.position, position) + 1) + sorted_roles = sorted((x for x in self.guild.roles if x.position in change_range and x.id != self.id), + key=lambda x: x.position) + + roles = [r.id for r in sorted_roles] + + if self.position > position: + roles.insert(0, self.id) + else: + roles.append(self.id) + + payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)] + yield from http.patch(url, json=payload, bucket='move_role') + + @asyncio.coroutine + def edit(self, **fields): + """|coro| + + Edits the role. + + You must have the :attr:`Permissions.manage_roles` permission to + use this. + + All fields are optional. + + Parameters + ----------- + name: str + The new role name to change to. + permissions: :class:`Permissions` + The new permissions to change to. + colour: :class:`Colour` + The new colour to change to. (aliased to color as well) + hoist: bool + Indicates if the role should be shown separately in the member list. + mentionable: bool + Indicates if the role should be mentionable by others. + position: int + The new role's position. This must be below your top role's + position or it will fail. + + Raises + ------- + Forbidden + You do not have permissions to change the role. + HTTPException + Editing the role failed. + InvalidArgument + An invalid position was given or the default + role was asked to be moved. + """ + + position = fields.get('position') + if position is not None: + yield from self._move(position) + self.position = position + + try: + colour = fields['colour'] + except KeyError: + colour = fields.get('color', self.colour) + + payload = { + 'name': fields.get('name', self.name), + 'permissions': fields.get('permissions', self.permissions).value, + 'color': colour.value, + 'hoist': fields.get('hoist', self.hoist), + 'mentionable': fields.get('mentionable', self.mentionable) + } + + data = yield from self._state.http.edit_role(self.guild.id, self.id, **payload) + self._update(data) + + @asyncio.coroutine + def delete(self): + """|coro| + + Deletes the role. + + You must have the :attr:`Permissions.manage_roles` permission to + use this. + + Raises + -------- + Forbidden + You do not have permissions to delete the role. + HTTPException + Deleting the role failed. + """ + + yield from self._state.http.delete_role(self.guild.id, self.id) diff --git a/discord/state.py b/discord/state.py index 4aa8d8381..0cf0e9559 100644 --- a/discord/state.py +++ b/discord/state.py @@ -589,7 +589,7 @@ class ConnectionState: def parse_guild_role_delete(self, data): guild = self._get_guild(int(data['guild_id'])) if guild is not None: - role_id = data.get('role_id') + role_id = int(data['role_id']) role = utils.find(lambda r: r.id == role_id, guild.roles) try: guild._remove_role(role) @@ -602,7 +602,7 @@ class ConnectionState: guild = self._get_guild(int(data['guild_id'])) if guild is not None: role_data = data['role'] - role_id = role_data['id'] + role_id = int(role_data['id']) role = utils.find(lambda r: r.id == role_id, guild.roles) if role is not None: old_role = copy.copy(role)