diff --git a/discord/__init__.py b/discord/__init__.py index a982cadd7..b4e749c2b 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -35,7 +35,7 @@ from .permissions import Permissions, PermissionOverwrite from .role import Role from .file import File from .colour import Color, Colour -from .invite import Invite +from .invite import Invite, PartialInviteChannel, PartialInviteGuild from .object import Object from .reaction import Reaction from . import utils, opus, abc diff --git a/discord/client.py b/discord/client.py index f832efcc2..26e22df98 100644 --- a/discord/client.py +++ b/discord/client.py @@ -882,7 +882,7 @@ class Client: # Invite management - async def get_invite(self, url): + async def get_invite(self, url, *, with_counts=True): """|coro| Gets an :class:`Invite` from a discord.gg URL or ID. @@ -890,13 +890,17 @@ class Client: Note ------ If the invite is for a guild you have not joined, the guild and channel - attributes of the returned invite will be :class:`Object` with the names - patched in. + attributes of the returned :class:`Invite` will be :class:`PartialInviteGuild` and + :class:`PartialInviteChannel` respectively. Parameters ----------- - url : str + url: :class:`str` The discord invite ID or URL (must be a discord.gg URL). + with_counts: :class:`bool` + Whether to include count information in the invite. This fills the + :attr:`Invite.approximate_member_count` and :attr:`Invite.approximate_presence_count` + fields. Raises ------- @@ -912,7 +916,7 @@ class Client: """ invite_id = self._resolve_invite(url) - data = await self.http.get_invite(invite_id) + data = await self.http.get_invite(invite_id, with_counts=with_counts) return Invite.from_incomplete(state=self._connection, data=data) async def delete_invite(self, invite): diff --git a/discord/guild.py b/discord/guild.py index 97247666e..7777f0eb9 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -83,7 +83,7 @@ class Guild(Hashable): The timeout to get sent to the AFK channel. afk_channel: Optional[:class:`VoiceChannel`] The channel that denotes the AFK channel. None if it doesn't exist. - icon: :class:`str` + icon: Optional[:class:`str`] The guild's icon. id: :class:`int` The guild's ID. @@ -114,7 +114,7 @@ class Guild(Hashable): - ``VERIFIED``: Guild is a "verified" server. - ``MORE_EMOJI``: Guild is allowed to have more than 50 custom emoji. - splash: :class:`str` + splash: Optional[:class:`str`] The guild's invite splash. """ @@ -601,7 +601,7 @@ class Guild(Hashable): channel upon creation. This parameter expects a :class:`dict` of overwrites with the target (either a :class:`Member` or a :class:`Role`) as the key and a :class:`PermissionOverwrite` as the value. - + Note -------- Creating a channel of a specified position will not update the position of @@ -641,7 +641,7 @@ class Guild(Hashable): The permissions will be automatically synced to category if no overwrites are provided. position: :class:`int` - The position in the channel list. This is a number that starts + The position in the channel list. This is a number that starts at 0. e.g. the top channel is position 0. topic: Optional[:class:`str`] The new channel's topic. @@ -679,7 +679,7 @@ class Guild(Hashable): This is similar to :meth:`create_text_channel` except makes a :class:`VoiceChannel` instead, in addition to having the following new parameters. - + Parameters ----------- bitrate: :class:`int` diff --git a/discord/http.py b/discord/http.py index 75c02457c..cde1a373a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -647,8 +647,11 @@ class HTTPClient: return self.request(r, reason=reason, json=payload) - def get_invite(self, invite_id): - return self.request(Route('GET', '/invite/{invite_id}', invite_id=invite_id)) + def get_invite(self, invite_id, *, with_counts=True): + params = { + 'with_counts': int(with_counts) + } + return self.request(Route('GET', '/invite/{invite_id}', invite_id=invite_id), params=params) def invites_from(self, guild_id): return self.request(Route('GET', '/guilds/{guild_id}/invites', guild_id=guild_id)) diff --git a/discord/invite.py b/discord/invite.py index 80483ab17..6b1a575a4 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -27,6 +27,141 @@ DEALINGS IN THE SOFTWARE. from .utils import parse_time from .mixins import Hashable from .object import Object +from .enums import ChannelType, VerificationLevel, try_enum +from collections import namedtuple + +class PartialInviteChannel(namedtuple('PartialInviteChannel', 'id name type')): + """Represents a "partial" invite channel. + + This model will be given when the user is not part of the + guild the :class:`Invite` resolves to. + + .. container:: operations + + .. describe:: x == y + + Checks if two partial channels are the same. + + .. describe:: x != y + + Checks if two partial channels are not the same. + + .. describe:: hash(x) + + Return the partial channel's hash. + + .. describe:: str(x) + + Returns the partial channel's name. + + Attributes + ----------- + name: :class:`str` + The partial channel's name. + id: :class:`int` + The partial channel's ID. + type: :class:`ChannelType` + The partial channel's type. + """ + + __slots__ = () + + def __str__(self): + return self.name + + @property + def mention(self): + """:class:`str` : The string that allows you to mention the channel.""" + return '<#%s>' % self.id + + @property + def created_at(self): + """Returns the channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + +class PartialInviteGuild(namedtuple('PartialInviteGuild', 'features icon id name splash verification_level')): + """Represents a "partial" invite guild. + + This model will be given when the user is not part of the + guild the :class:`Invite` resolves to. + + .. container:: operations + + .. describe:: x == y + + Checks if two partial guilds are the same. + + .. describe:: x != y + + Checks if two partial guilds are not the same. + + .. describe:: hash(x) + + Return the partial guild's hash. + + .. describe:: str(x) + + Returns the partial guild's name. + + Attributes + ----------- + name: :class:`str` + The partial guild's name. + id: :class:`int` + The partial guild's ID. + verification_level: :class:`VerificationLevel` + The partial guild's verification level. + features: List[:class:`str`] + A list of features the guild has. See :attr:`Guild.features` for more information. + icon: Optional[:class:`str`] + The partial guild's icon. + splash: Optional[:class:`str`] + The partial guild's invite splash. + """ + + __slots__ = () + + def __str__(self): + return self.name + + @property + def created_at(self): + """Returns the guild's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def icon_url(self): + """Returns the URL version of the guild's icon. Returns an empty string if it has no icon.""" + return self.icon_url_as() + + def icon_url_as(self, *, format='webp', size=1024): + """:class:`str`: The same operation as :meth:`Guild.icon_url_as`.""" + if not valid_icon_size(size): + raise InvalidArgument("size must be a power of 2 between 16 and 2048") + if format not in VALID_ICON_FORMATS: + raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS)) + + if self.icon is None: + return '' + + return 'https://cdn.discordapp.com/icons/{0.id}/{0.icon}.{1}?size={2}'.format(self, format, size) + + @property + def splash_url(self): + """Returns the URL version of the guild's invite splash. Returns an empty string if it has no splash.""" + return self.splash_url_as() + + def splash_url_as(self, *, format='webp', size=2048): + """:class:`str`: The same operation as :meth:`Guild.splash_url_as`.""" + if not valid_icon_size(size): + raise InvalidArgument("size must be a power of 2 between 16 and 2048") + if format not in VALID_ICON_FORMATS: + raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS)) + + if self.splash is None: + return '' + + return 'https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.{1}?size={2}'.format(self, format, size) class Invite(Hashable): """Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite. @@ -58,7 +193,7 @@ class Invite(Hashable): How long the before the invite expires in seconds. A value of 0 indicates that it doesn't expire. code: :class:`str` The URL fragment used for the invite. - guild: :class:`Guild` + guild: Union[:class:`Guild`, :class:`PartialInviteGuild`] The guild the invite is for. revoked: :class:`bool` Indicates if the invite has been revoked. @@ -73,13 +208,19 @@ class Invite(Hashable): How many times the invite can be used. inviter: :class:`User` The user who created the invite. - channel: :class:`abc.GuildChannel` + approximate_member_count: Optional[:class:`int`] + The approximate number of members in the guild. + approximate_presence_count: Optional[:class:`int`] + The approximate number of members currently active in the guild. + This includes idle, dnd, online, and invisible members. Offline members are excluded. + channel: Union[:class:`abc.GuildChannel`, :class:`PartialInviteChannel`] The channel the invite is for. """ __slots__ = ('max_age', 'code', 'guild', 'revoked', 'created_at', 'uses', - 'temporary', 'max_uses', 'inviter', 'channel', '_state') + 'temporary', 'max_uses', 'inviter', 'channel', '_state', + 'approximate_member_count', 'approximate_presence_count' ) def __init__(self, *, state, data): self._state = state @@ -91,6 +232,8 @@ class Invite(Hashable): self.temporary = data.get('temporary') self.uses = data.get('uses') self.max_uses = data.get('max_uses') + self.approximate_presence_count = data.get('approximate_presence_count') + self.approximate_member_count = data.get('approximate_member_count') inviter_data = data.get('inviter') self.inviter = None if inviter_data is None else self._state.store_user(inviter_data) @@ -104,17 +247,16 @@ class Invite(Hashable): if guild is not None: channel = guild.get_channel(channel_id) else: - guild = Object(id=guild_id) - channel = Object(id=channel_id) - guild.name = data['guild']['name'] - - guild.splash = data['guild']['splash'] - guild.splash_url = '' - if guild.splash: - guild.splash_url = 'https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.jpg?size=2048'.format(guild) - - channel.name = data['channel']['name'] - + channel_data = data['channel'] + guild_data = data['guild'] + channel_type = try_enum(ChannelType, channel_data['type']) + channel = PartialInviteChannel(id=channel_id, name=channel_data['name'], type=channel_type) + guild = PartialInviteGuild(id=guild_id, + name=guild_data['name'], + features=guild_data.get('features', []), + icon=guild_data.get('icon'), + splash=guild_data.get('splash'), + verification_level=try_enum(VerificationLevel, guild_data.get('verification_level'))) data['guild'] = guild data['channel'] = channel return cls(state=state, data=data) diff --git a/docs/api.rst b/docs/api.rst index bdb577804..5816f60f1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2035,6 +2035,17 @@ GroupChannel .. autocomethod:: typing :async-with: +PartialInviteGuild +~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: PartialInviteGuild() + :members: + +PartialInviteChannel +~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: PartialInviteChannel() + :members: Invite ~~~~~~~