From 5d78f43e558cdd55e22eb12a090d3d75fd258a8e Mon Sep 17 00:00:00 2001
From: Rapptz <rapptz@gmail.com>
Date: Tue, 12 Feb 2019 20:21:29 -0500
Subject: [PATCH] Expose more information from partial invites, along with
 counts.

This adds the following information.

* `PartialInviteGuild` to replace `Object` patching
* `PartialInviteChannel` to replace `Object` patching
* Invite.approximate_member_count and Invite.approximate_presence_count

The new partial objects provide better documentation on what is
expected when you fetch random invites.

Fixes #1830
---
 discord/__init__.py |   2 +-
 discord/client.py   |  14 ++--
 discord/guild.py    |  10 +--
 discord/http.py     |   7 +-
 discord/invite.py   | 170 ++++++++++++++++++++++++++++++++++++++++----
 docs/api.rst        |  11 +++
 6 files changed, 187 insertions(+), 27 deletions(-)

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
 ~~~~~~~