aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRapptz <[email protected]>2016-10-18 01:28:34 -0400
committerRapptz <[email protected]>2017-01-03 09:51:55 -0500
commita7a60e433b5272abedfb05e064334b2365de0591 (patch)
treed7526e747901fc31199198ae8cc122849588187a
parentRename Server to Guild everywhere. (diff)
downloaddiscord.py-a7a60e433b5272abedfb05e064334b2365de0591.tar.xz
discord.py-a7a60e433b5272abedfb05e064334b2365de0591.zip
Make roles and guilds stateful.
-rw-r--r--discord/guild.py309
-rw-r--r--discord/role.py108
-rw-r--r--discord/state.py4
3 files changed, 417 insertions, 4 deletions
diff --git a/discord/guild.py b/discord/guild.py
index ad713bb6..6e37c3ab 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 945748d5..7ea93a98 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 4aa8d838..0cf0e955 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)