Browse Source

Expose missing invite data, implement friend invites, improve invite methods

pull/10109/head
dolfies 3 years ago
parent
commit
3ecca7293d
  1. 140
      discord/client.py
  2. 16
      discord/http.py
  3. 101
      discord/invite.py
  4. 10
      discord/tracking.py

140
discord/client.py

@ -54,7 +54,7 @@ from .widget import Widget
from .guild import Guild from .guild import Guild
from .emoji import Emoji from .emoji import Emoji
from .channel import _private_channel_factory, _threaded_channel_factory, GroupChannel, PartialMessageable from .channel import _private_channel_factory, _threaded_channel_factory, GroupChannel, PartialMessageable
from .enums import ActivityType, ChannelType, ConnectionLinkType, ConnectionType, Status, InviteType, try_enum from .enums import ActivityType, ChannelType, ConnectionLinkType, ConnectionType, Status, try_enum
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .errors import * from .errors import *
from .enums import Status from .enums import Status
@ -1749,6 +1749,27 @@ class Client:
# Invite management # Invite management
async def invites(self) -> List[Invite]:
r"""|coro|
Gets a list of the user's friend :class:`.Invite`\s.
.. versionadded:: 2.0
Raises
------
HTTPException
Getting the invites failed.
Returns
--------
List[:class:`.Invite`]
The list of invites.
"""
state = self._connection
data = await state.http.get_friend_invites()
return [Invite.from_incomplete(state=state, data=d) for d in data]
async def fetch_invite( async def fetch_invite(
self, self,
url: Union[Invite, str], url: Union[Invite, str],
@ -1825,46 +1846,34 @@ class Client:
) )
return Invite.from_incomplete(state=self._connection, data=data) return Invite.from_incomplete(state=self._connection, data=data)
async def delete_invite(self, invite: Union[Invite, str], /) -> None: async def create_invite(self) -> Invite:
"""|coro| """|coro|
Revokes an :class:`.Invite`, URL, or ID to an invite. Creates a new friend :class:`.Invite`.
You must have the :attr:`~.Permissions.manage_channels` permission in
the associated guild to do this.
.. versionchanged:: 2.0
``invite`` parameter is now positional-only.
Parameters .. versionadded:: 2.0
----------
invite: Union[:class:`.Invite`, :class:`str`]
The invite to revoke.
Raises Raises
------- ------
Forbidden
You do not have permissions to revoke invites.
NotFound
The invite is invalid or expired.
HTTPException HTTPException
Revoking the invite failed. Creating the invite failed.
Returns
--------
:class:`.Invite`
The created friend invite.
""" """
resolved = utils.resolve_invite(invite) state = self._connection
await self.http.delete_invite(resolved.code) data = await state.http.create_friend_invite()
return Invite.from_incomplete(state=state, data=data)
async def accept_invite(self, invite: Union[Invite, str], /) -> Union[Guild, User, GroupChannel]: async def accept_invite(self, invite: Union[Invite, str], /) -> Invite:
"""|coro| """|coro|
Uses an invite. Uses an invite.
Either joins a guild, joins a group DM, or adds a friend. Either joins a guild, joins a group DM, or adds a friend.
.. versionadded:: 1.9 .. versionadded:: 2.0
.. versionchanged:: 2.0
``invite`` parameter is now positional-only.
Parameters Parameters
---------- ----------
@ -1878,9 +1887,8 @@ class Client:
Returns Returns
------- -------
:class:`.Guild` :class:`.Invite`
The guild joined. This is not the same guild that is The accepted invite.
added to cache.
""" """
if not isinstance(invite, Invite): if not isinstance(invite, Invite):
invite = await self.fetch_invite(invite, with_counts=False, with_expiration=False) invite = await self.fetch_invite(invite, with_counts=False, with_expiration=False)
@ -1896,14 +1904,68 @@ class Client:
'channel_type': getattr(invite.channel, 'type', MISSING), 'channel_type': getattr(invite.channel, 'type', MISSING),
} }
data = await state.http.accept_invite(invite.code, type, **kwargs) data = await state.http.accept_invite(invite.code, type, **kwargs)
if type is InviteType.guild: return Invite.from_incomplete(state=state, data=data, message=invite._message)
guild = Guild(data=data['guild'], state=state)
guild._cs_joined = True async def delete_invite(self, invite: Union[Invite, str], /) -> Invite:
return guild """|coro|
elif type is InviteType.group_dm:
return GroupChannel(data=data['channel'], state=state, me=state.user) # type: ignore Revokes an :class:`.Invite`, URL, or ID to an invite.
else:
return User(data=data['inviter'], state=state) You must have the :attr:`~.Permissions.manage_channels` permission in
the associated guild to do this.
.. versionchanged:: 2.0
``invite`` parameter is now positional-only.
.. versionchanged:: 2.0
The function now returns the deleted invite.
Parameters
----------
invite: Union[:class:`.Invite`, :class:`str`]
The invite to revoke.
Raises
-------
Forbidden
You do not have permissions to revoke invites.
NotFound
The invite is invalid or expired.
HTTPException
Revoking the invite failed.
Returns
--------
:class:`.Invite`
The deleted invite.
"""
resolved = utils.resolve_invite(invite)
state = self._connection
data = await state.http.delete_invite(resolved.code)
return Invite.from_incomplete(state=state, data=data)
async def revoke_invites(self) -> List[Invite]:
r"""|coro|
Revokes all of the user's friend :class:`.Invite`\s.
.. versionadded:: 2.0
Raises
------
HTTPException
Revoking the invites failed.
Returns
--------
List[:class:`.Invite`]
The revoked invites.
"""
state = self._connection
data = await state.http.delete_friend_invites()
return [Invite(state=state, data=d) for d in data]
# Miscellaneous stuff # Miscellaneous stuff

16
discord/http.py

@ -1721,8 +1721,14 @@ class HTTPClient:
payload = { payload = {
'max_age': max_age, 'max_age': max_age,
} }
props = ContextProperties._from_group_dm_invite()
return self.request(Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id), json=payload) return self.request(
Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id), json=payload, context_properties=props
)
def create_friend_invite(self) -> Response[invite.Invite]:
return self.request(Route('POST', '/users/@me/invites'), json={}, context_properties=ContextProperties._empty())
def get_invite( def get_invite(
self, self,
@ -1748,9 +1754,15 @@ class HTTPClient:
def invites_from_channel(self, channel_id: Snowflake) -> Response[List[invite.Invite]]: def invites_from_channel(self, channel_id: Snowflake) -> Response[List[invite.Invite]]:
return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id)) return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id))
def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Response[None]: def get_friend_invites(self) -> Response[List[invite.Invite]]:
return self.request(Route('GET', '/users/@me/invites'), context_properties=ContextProperties._empty())
def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Response[invite.Invite]:
return self.request(Route('DELETE', '/invites/{invite_id}', invite_id=invite_id), reason=reason) return self.request(Route('DELETE', '/invites/{invite_id}', invite_id=invite_id), reason=reason)
def delete_friend_invites(self) -> Response[List[invite.Invite]]:
return self.request(Route('DELETE', '/users/@me/invites'), context_properties=ContextProperties._empty())
# Role management # Role management
def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]: def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]:

101
discord/invite.py

@ -56,7 +56,6 @@ if TYPE_CHECKING:
from .user import User from .user import User
from .appinfo import PartialApplication from .appinfo import PartialApplication
from .message import Message from .message import Message
from .channel import GroupChannel
InviteGuildType = Union[Guild, 'PartialInviteGuild', Object] InviteGuildType = Union[Guild, 'PartialInviteGuild', Object]
InviteChannelType = Union[GuildChannel, 'PartialInviteChannel', Object, PrivateChannel] InviteChannelType = Union[GuildChannel, 'PartialInviteChannel', Object, PrivateChannel]
@ -68,7 +67,7 @@ class PartialInviteChannel:
"""Represents a "partial" invite channel. """Represents a "partial" invite channel.
This model will be given when the user is not part of the This model will be given when the user is not part of the
guild the :class:`Invite` resolves to. guild or group channel the :class:`Invite` resolves to.
.. container:: operations .. container:: operations
@ -98,19 +97,21 @@ class PartialInviteChannel:
The partial channel's type. The partial channel's type.
""" """
__slots__ = ('id', 'name', 'type') __slots__ = ('_state', 'id', 'name', 'type', '_icon')
def __new__(cls, data: Optional[InviteChannelPayload]): def __new__(cls, data: Optional[InviteChannelPayload], *args, **kwargs):
if data is None: if data is None:
return return
return super().__new__(cls) return super().__new__(cls)
def __init__(self, data: Optional[InviteChannelPayload]): def __init__(self, data: Optional[InviteChannelPayload], state: ConnectionState):
if data is None: if data is None:
return return
self._state = state
self.id: int = int(data['id']) self.id: int = int(data['id'])
self.name: str = data['name'] self.name: str = data['name']
self.type: ChannelType = try_enum(ChannelType, data['type']) self.type: ChannelType = try_enum(ChannelType, data['type'])
self._icon: Optional[str] = data.get('icon')
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@ -128,6 +129,18 @@ class PartialInviteChannel:
""":class:`datetime.datetime`: Returns the channel's creation time in UTC.""" """:class:`datetime.datetime`: Returns the channel's creation time in UTC."""
return snowflake_time(self.id) return snowflake_time(self.id)
@property
def icon(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the channel's icon asset if available.
Only applicable to channels of type :attr:`ChannelType.group`.
.. versionadded:: 2.0
"""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path='channel')
class PartialInviteGuild: class PartialInviteGuild:
"""Represents a "partial" invite guild. """Represents a "partial" invite guild.
@ -369,6 +382,22 @@ class Invite(Hashable):
The guild's welcome screen, if available. The guild's welcome screen, if available.
.. versionadded:: 2.0 .. versionadded:: 2.0
new_member: :class:`bool`
Whether the user was not previously a member of the guild.
.. versionadded:: 2.0
.. note::
This is only possibly ``True`` in accepted invite objects
(i.e. the objects received from :meth:`accept` and :meth:`use`).
show_verification_form: :class:`bool`
Whether the user should be shown the guild's member verification form.
.. versionadded:: 2.0
.. note::
This is only possibly ``True`` in accepted invite objects
(i.e. the objects received from :meth:`accept` and :meth:`use`).
""" """
__slots__ = ( __slots__ = (
@ -394,6 +423,8 @@ class Invite(Hashable):
'_message', '_message',
'welcome_screen', 'welcome_screen',
'type', 'type',
'new_member',
'show_verification_form',
) )
BASE = 'https://discord.gg' BASE = 'https://discord.gg'
@ -421,6 +452,12 @@ class Invite(Hashable):
self.approximate_member_count: Optional[int] = data.get('approximate_member_count') self.approximate_member_count: Optional[int] = data.get('approximate_member_count')
self._message: Optional[Message] = data.get('message') self._message: Optional[Message] = data.get('message')
# We inject some missing data here since we can assume it
if self.type in (InviteType.group_dm, InviteType.friend):
self.temporary = False
if self.max_uses is None:
self.max_uses = 5 if self.type is InviteType.friend else 0
expires_at = data.get('expires_at', None) expires_at = data.get('expires_at', None)
self.expires_at: Optional[datetime.datetime] = parse_time(expires_at) if expires_at else None self.expires_at: Optional[datetime.datetime] = parse_time(expires_at) if expires_at else None
@ -454,6 +491,10 @@ class Invite(Hashable):
) )
self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None
# Only present on accepted invites
self.new_member: bool = data.get('new_member', False)
self.show_verification_form: bool = data.get('show_verification_form', False)
@classmethod @classmethod
def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload, message: Optional[Message] = None) -> Self: def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload, message: Optional[Message] = None) -> Self:
guild: Optional[Union[Guild, PartialInviteGuild]] guild: Optional[Union[Guild, PartialInviteGuild]]
@ -473,7 +514,7 @@ class Invite(Hashable):
if welcome_screen is not None: if welcome_screen is not None:
welcome_screen = WelcomeScreen(data=welcome_screen, guild=guild) welcome_screen = WelcomeScreen(data=welcome_screen, guild=guild)
channel = PartialInviteChannel(data.get('channel')) channel = PartialInviteChannel(data.get('channel'), state)
channel = state.get_channel(getattr(channel, 'id', None)) or channel channel = state.get_channel(getattr(channel, 'id', None)) or channel
if message is not None: if message is not None:
@ -519,7 +560,7 @@ class Invite(Hashable):
if data is None: if data is None:
return None return None
return PartialInviteChannel(data) return PartialInviteChannel(data, self._state)
def __str__(self) -> str: def __str__(self) -> str:
return self.url return self.url
@ -570,7 +611,7 @@ class Invite(Hashable):
return self return self
async def use(self) -> Union[Guild, User, GroupChannel]: async def use(self) -> Invite:
"""|coro| """|coro|
Uses the invite. Uses the invite.
@ -587,8 +628,8 @@ class Invite(Hashable):
Returns Returns
------- -------
Union[:class:`Guild`, :class:`User`, :class:`GroupChannel`] :class:`Invite`
The guild/group DM joined, or user added as a friend. The accepted invite.
""" """
state = self._state state = self._state
type = self.type type = self.type
@ -601,22 +642,9 @@ class Invite(Hashable):
'channel_type': getattr(self.channel, 'type', MISSING), 'channel_type': getattr(self.channel, 'type', MISSING),
} }
data = await state.http.accept_invite(self.code, type, **kwargs) data = await state.http.accept_invite(self.code, type, **kwargs)
if type is InviteType.guild: return Invite.from_incomplete(state=state, data=data, message=message)
from .guild import Guild
guild = Guild(data=data['guild'], state=state)
guild._cs_joined = True
return guild
elif type is InviteType.group_dm:
from .channel import GroupChannel
return GroupChannel(data=data['channel'], state=state, me=state.user) # type: ignore async def accept(self) -> Invite:
else:
from .user import User
return User(data=data['inviter'], state=state)
async def accept(self) -> Union[Guild, User, GroupChannel]:
"""|coro| """|coro|
Uses the invite. Uses the invite.
@ -633,23 +661,29 @@ class Invite(Hashable):
Returns Returns
------- -------
Union[:class:`Guild`, :class:`User`, :class:`GroupChannel`] :class:`Invite`
The guild/group DM joined, or user added as a friend. The accepted invite.
""" """
return await self.use() return await self.use()
async def delete(self, *, reason: Optional[str] = None) -> None: async def delete(self, *, reason: Optional[str] = None) -> Invite:
"""|coro| """|coro|
Revokes the instant invite. Revokes the instant invite.
You must have the :attr:`~Permissions.manage_channels` permission to do this. In a guild context, you must have the :attr:`~Permissions.manage_channels` permission to do this.
.. versionchanged:: 2.0
The function now returns the deleted invite.
Parameters Parameters
----------- -----------
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for deleting this invite. Shows up on the audit log. The reason for deleting this invite. Shows up on the audit log.
Only applicable to guild invites.
Raises Raises
------- -------
Forbidden Forbidden
@ -658,5 +692,12 @@ class Invite(Hashable):
The invite is invalid or expired. The invite is invalid or expired.
HTTPException HTTPException
Revoking the invite failed. Revoking the invite failed.
Returns
--------
:class:`Invite`
The deleted invite.
""" """
await self._state.http.delete_invite(self.code, reason=reason) state = self._state
data = await state.http.delete_invite(self.code, reason=reason)
return Invite.from_incomplete(state=state, data=data)

10
discord/tracking.py

@ -84,7 +84,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M
def _encode_data(self, data) -> str: def _encode_data(self, data) -> str:
library = { library = {
'None': 'e30=', None: 'e30=',
# Locations # Locations
'Friends': 'eyJsb2NhdGlvbiI6IkZyaWVuZHMifQ==', 'Friends': 'eyJsb2NhdGlvbiI6IkZyaWVuZHMifQ==',
'ContextMenu': 'eyJsb2NhdGlvbiI6IkNvbnRleHRNZW51In0=', 'ContextMenu': 'eyJsb2NhdGlvbiI6IkNvbnRleHRNZW51In0=',
@ -99,13 +99,14 @@ class ContextProperties: # Thank you Discord-S.C.U.M
'Verify Email': 'eyJsb2NhdGlvbiI6IlZlcmlmeSBFbWFpbCJ9', 'Verify Email': 'eyJsb2NhdGlvbiI6IlZlcmlmeSBFbWFpbCJ9',
'New Group DM': 'eyJsb2NhdGlvbiI6Ik5ldyBHcm91cCBETSJ9', 'New Group DM': 'eyJsb2NhdGlvbiI6Ik5ldyBHcm91cCBETSJ9',
'Add Friends to DM': 'eyJsb2NhdGlvbiI6IkFkZCBGcmllbmRzIHRvIERNIn0=', 'Add Friends to DM': 'eyJsb2NhdGlvbiI6IkFkZCBGcmllbmRzIHRvIERNIn0=',
'Group DM Invite Create': 'eyJsb2NhdGlvbiI6Ikdyb3VwIERNIEludml0ZSBDcmVhdGUifQ==',
# Sources # Sources
'Chat Input Blocker - Lurker Mode': 'eyJzb3VyY2UiOiJDaGF0IElucHV0IEJsb2NrZXIgLSBMdXJrZXIgTW9kZSJ9', 'Chat Input Blocker - Lurker Mode': 'eyJzb3VyY2UiOiJDaGF0IElucHV0IEJsb2NrZXIgLSBMdXJrZXIgTW9kZSJ9',
'Notice - Lurker Mode': 'eyJzb3VyY2UiOiJOb3RpY2UgLSBMdXJrZXIgTW9kZSJ9', 'Notice - Lurker Mode': 'eyJzb3VyY2UiOiJOb3RpY2UgLSBMdXJrZXIgTW9kZSJ9',
} }
try: try:
return library[self.target or 'None'] return library[self.target]
except KeyError: except KeyError:
return b64encode(json.dumps(data, separators=(',', ':')).encode()).decode('utf-8') return b64encode(json.dumps(data, separators=(',', ':')).encode()).decode('utf-8')
@ -158,6 +159,11 @@ class ContextProperties: # Thank you Discord-S.C.U.M
data = {'location': 'Add Friends to DM'} data = {'location': 'Add Friends to DM'}
return cls(data) return cls(data)
@classmethod
def _from_group_dm_invite(cls) -> Self:
data = {'location': 'Group DM Invite Create'}
return cls(data)
@classmethod @classmethod
def _from_app(cls) -> Self: def _from_app(cls) -> Self:
data = {'location': '/app'} data = {'location': '/app'}

Loading…
Cancel
Save