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 .emoji import Emoji
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 .errors import *
from .enums import Status
@ -1749,6 +1749,27 @@ class Client:
# 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(
self,
url: Union[Invite, str],
@ -1825,46 +1846,34 @@ class Client:
)
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|
Revokes an :class:`.Invite`, URL, or ID to an 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.
Creates a new friend :class:`.Invite`.
Parameters
----------
invite: Union[:class:`.Invite`, :class:`str`]
The invite to revoke.
.. versionadded:: 2.0
Raises
-------
Forbidden
You do not have permissions to revoke invites.
NotFound
The invite is invalid or expired.
------
HTTPException
Revoking the invite failed.
Creating the invite failed.
Returns
--------
:class:`.Invite`
The created friend invite.
"""
resolved = utils.resolve_invite(invite)
await self.http.delete_invite(resolved.code)
state = self._connection
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|
Uses an invite.
Either joins a guild, joins a group DM, or adds a friend.
.. versionadded:: 1.9
.. versionchanged:: 2.0
``invite`` parameter is now positional-only.
.. versionadded:: 2.0
Parameters
----------
@ -1878,9 +1887,8 @@ class Client:
Returns
-------
:class:`.Guild`
The guild joined. This is not the same guild that is
added to cache.
:class:`.Invite`
The accepted invite.
"""
if not isinstance(invite, Invite):
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),
}
data = await state.http.accept_invite(invite.code, type, **kwargs)
if type is InviteType.guild:
guild = Guild(data=data['guild'], state=state)
guild._cs_joined = True
return guild
elif type is InviteType.group_dm:
return GroupChannel(data=data['channel'], state=state, me=state.user) # type: ignore
else:
return User(data=data['inviter'], state=state)
return Invite.from_incomplete(state=state, data=data, message=invite._message)
async def delete_invite(self, invite: Union[Invite, str], /) -> Invite:
"""|coro|
Revokes an :class:`.Invite`, URL, or ID to an 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.
.. 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

16
discord/http.py

@ -1721,8 +1721,14 @@ class HTTPClient:
payload = {
'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(
self,
@ -1748,9 +1754,15 @@ class HTTPClient:
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))
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)
def delete_friend_invites(self) -> Response[List[invite.Invite]]:
return self.request(Route('DELETE', '/users/@me/invites'), context_properties=ContextProperties._empty())
# Role management
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 .appinfo import PartialApplication
from .message import Message
from .channel import GroupChannel
InviteGuildType = Union[Guild, 'PartialInviteGuild', Object]
InviteChannelType = Union[GuildChannel, 'PartialInviteChannel', Object, PrivateChannel]
@ -68,7 +67,7 @@ class PartialInviteChannel:
"""Represents a "partial" invite channel.
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
@ -98,19 +97,21 @@ class PartialInviteChannel:
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:
return
return super().__new__(cls)
def __init__(self, data: Optional[InviteChannelPayload]):
def __init__(self, data: Optional[InviteChannelPayload], state: ConnectionState):
if data is None:
return
self._state = state
self.id: int = int(data['id'])
self.name: str = data['name']
self.type: ChannelType = try_enum(ChannelType, data['type'])
self._icon: Optional[str] = data.get('icon')
def __str__(self) -> str:
return self.name
@ -128,6 +129,18 @@ class PartialInviteChannel:
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
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:
"""Represents a "partial" invite guild.
@ -369,6 +382,22 @@ class Invite(Hashable):
The guild's welcome screen, if available.
.. 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__ = (
@ -394,6 +423,8 @@ class Invite(Hashable):
'_message',
'welcome_screen',
'type',
'new_member',
'show_verification_form',
)
BASE = 'https://discord.gg'
@ -421,6 +452,12 @@ class Invite(Hashable):
self.approximate_member_count: Optional[int] = data.get('approximate_member_count')
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)
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
# 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
def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload, message: Optional[Message] = None) -> Self:
guild: Optional[Union[Guild, PartialInviteGuild]]
@ -473,7 +514,7 @@ class Invite(Hashable):
if welcome_screen is not None:
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
if message is not None:
@ -519,7 +560,7 @@ class Invite(Hashable):
if data is None:
return None
return PartialInviteChannel(data)
return PartialInviteChannel(data, self._state)
def __str__(self) -> str:
return self.url
@ -570,7 +611,7 @@ class Invite(Hashable):
return self
async def use(self) -> Union[Guild, User, GroupChannel]:
async def use(self) -> Invite:
"""|coro|
Uses the invite.
@ -587,8 +628,8 @@ class Invite(Hashable):
Returns
-------
Union[:class:`Guild`, :class:`User`, :class:`GroupChannel`]
The guild/group DM joined, or user added as a friend.
:class:`Invite`
The accepted invite.
"""
state = self._state
type = self.type
@ -601,22 +642,9 @@ class Invite(Hashable):
'channel_type': getattr(self.channel, 'type', MISSING),
}
data = await state.http.accept_invite(self.code, type, **kwargs)
if type is InviteType.guild:
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 Invite.from_incomplete(state=state, data=data, message=message)
return GroupChannel(data=data['channel'], state=state, me=state.user) # type: ignore
else:
from .user import User
return User(data=data['inviter'], state=state)
async def accept(self) -> Union[Guild, User, GroupChannel]:
async def accept(self) -> Invite:
"""|coro|
Uses the invite.
@ -633,23 +661,29 @@ class Invite(Hashable):
Returns
-------
Union[:class:`Guild`, :class:`User`, :class:`GroupChannel`]
The guild/group DM joined, or user added as a friend.
:class:`Invite`
The accepted invite.
"""
return await self.use()
async def delete(self, *, reason: Optional[str] = None) -> None:
async def delete(self, *, reason: Optional[str] = None) -> Invite:
"""|coro|
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
-----------
reason: Optional[:class:`str`]
The reason for deleting this invite. Shows up on the audit log.
Only applicable to guild invites.
Raises
-------
Forbidden
@ -658,5 +692,12 @@ class Invite(Hashable):
The invite is invalid or expired.
HTTPException
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:
library = {
'None': 'e30=',
None: 'e30=',
# Locations
'Friends': 'eyJsb2NhdGlvbiI6IkZyaWVuZHMifQ==',
'ContextMenu': 'eyJsb2NhdGlvbiI6IkNvbnRleHRNZW51In0=',
@ -99,13 +99,14 @@ class ContextProperties: # Thank you Discord-S.C.U.M
'Verify Email': 'eyJsb2NhdGlvbiI6IlZlcmlmeSBFbWFpbCJ9',
'New Group DM': 'eyJsb2NhdGlvbiI6Ik5ldyBHcm91cCBETSJ9',
'Add Friends to DM': 'eyJsb2NhdGlvbiI6IkFkZCBGcmllbmRzIHRvIERNIn0=',
'Group DM Invite Create': 'eyJsb2NhdGlvbiI6Ikdyb3VwIERNIEludml0ZSBDcmVhdGUifQ==',
# Sources
'Chat Input Blocker - Lurker Mode': 'eyJzb3VyY2UiOiJDaGF0IElucHV0IEJsb2NrZXIgLSBMdXJrZXIgTW9kZSJ9',
'Notice - Lurker Mode': 'eyJzb3VyY2UiOiJOb3RpY2UgLSBMdXJrZXIgTW9kZSJ9',
}
try:
return library[self.target or 'None']
return library[self.target]
except KeyError:
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'}
return cls(data)
@classmethod
def _from_group_dm_invite(cls) -> Self:
data = {'location': 'Group DM Invite Create'}
return cls(data)
@classmethod
def _from_app(cls) -> Self:
data = {'location': '/app'}

Loading…
Cancel
Save