Browse Source

Update invite parameters and add stage interoperability

pull/10109/head
dolfies 2 years ago
parent
commit
44ec9f40c7
  1. 28
      discord/abc.py
  2. 6
      discord/channel.py
  3. 11
      discord/client.py
  4. 8
      discord/http.py
  5. 22
      discord/invite.py
  6. 88
      discord/stage_instance.py
  7. 9
      discord/types/channel.py
  8. 16
      discord/types/guild.py
  9. 73
      discord/types/invite.py

28
discord/abc.py

@ -60,6 +60,7 @@ from .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem
from .settings import ChannelSettings
from .commands import ApplicationCommand, BaseCommand, SlashCommand, UserCommand, MessageCommand, _command_factory
from .flags import InviteFlags
from . import utils
__all__ = (
@ -1475,7 +1476,7 @@ class GuildChannel:
max_uses: int = 0,
temporary: bool = False,
unique: bool = True,
validate: Optional[Union[Invite, str]] = None,
guest: bool = False,
target_type: Optional[InviteTarget] = None,
target_user: Optional[User] = None,
target_application: Optional[Snowflake] = None,
@ -1486,6 +1487,10 @@ class GuildChannel:
You must have :attr:`~discord.Permissions.create_instant_invite` to do this.
.. versionchanged:: 2.1
The ``validate`` parameter has been removed.
Parameters
------------
max_age: :class:`int`
@ -1497,30 +1502,28 @@ class GuildChannel:
temporary: :class:`bool`
Denotes that the invite grants temporary membership
(i.e. they get kicked after they disconnect). Defaults to ``False``.
guest: :class:`bool`
Denotes that the invite is a guest invite.
Guest invites grant temporary membership for the purposes of joining a voice channel.
Defaults to ``False``.
.. versionadded:: 2.1
unique: :class:`bool`
Indicates if a unique invite URL should be created. Defaults to True.
Indicates if a unique invite URL should be created. Defaults to ``True``.
If this is set to ``False`` then it will return a previously created
invite.
validate: Union[:class:`.Invite`, :class:`str`]
The existing channel invite to validate and return for reuse.
If this invite is invalid, a new invite will be created according to the parameters and returned.
.. versionadded:: 2.0
target_type: Optional[:class:`~discord.InviteTarget`]
The type of target for the voice channel invite, if any.
.. versionadded:: 2.0
target_user: Optional[:class:`~discord.User`]
The user whose stream to display for this invite, required if ``target_type`` is :attr:`.InviteTarget.stream`. The user must be streaming in the channel.
.. versionadded:: 2.0
target_application:: Optional[:class:`~discord.Application`]
The embedded application for the invite, required if ``target_type`` is :attr:`.InviteTarget.embedded_application`.
.. versionadded:: 2.0
reason: Optional[:class:`str`]
The reason for creating this invite. Shows up on the audit log.
@ -1542,6 +1545,9 @@ class GuildChannel:
raise ValueError('target_type parameter must be InviteTarget.stream, or InviteTarget.embedded_application')
if target_type == InviteTarget.unknown:
target_type = None
flags = InviteFlags()
if guest:
flags.guest = True
data = await self._state.http.create_invite(
self.id,
@ -1550,10 +1556,10 @@ class GuildChannel:
max_uses=max_uses,
temporary=temporary,
unique=unique,
validate=utils.resolve_invite(validate).code if validate else None,
target_type=target_type.value if target_type else None,
target_user_id=target_user.id if target_user else None,
target_application_id=target_application.id if target_application else None,
flags=flags.value,
)
return Invite.from_incomplete(data=data, state=self._state)

6
discord/channel.py

@ -1762,17 +1762,13 @@ class StageChannel(VocalGuildChannel):
:class:`StageInstance`
The newly created stage instance.
"""
payload: Dict[str, Any] = {'channel_id': self.id, 'topic': topic}
payload = {'channel_id': self.id, 'topic': topic, 'send_start_notification': send_start_notification}
if privacy_level is not MISSING:
if not isinstance(privacy_level, PrivacyLevel):
raise TypeError('privacy_level field must be of type PrivacyLevel')
payload['privacy_level'] = privacy_level.value
payload['send_start_notification'] = send_start_notification
data = await self._state.http.create_stage_instance(**payload, reason=reason)
return StageInstance(guild=self.guild, state=self._state, data=data)

11
discord/client.py

@ -2029,7 +2029,6 @@ class Client:
/,
*,
with_counts: bool = True,
with_expiration: bool = True,
scheduled_event_id: Optional[int] = None,
) -> Invite:
"""|coro|
@ -2046,6 +2045,10 @@ class Client:
``url`` parameter is now positional-only.
.. versionchanged:: 2.1
The ``with_expiration`` parameter has been removed.
Parameters
-----------
url: Union[:class:`.Invite`, :class:`str`]
@ -2054,11 +2057,6 @@ class Client:
Whether to include count information in the invite. This fills the
:attr:`.Invite.approximate_member_count` and :attr:`.Invite.approximate_presence_count`
fields.
with_expiration: :class:`bool`
Whether to include the expiration date of the invite. This fills the
:attr:`.Invite.expires_at` field.
.. versionadded:: 2.0
scheduled_event_id: Optional[:class:`int`]
The ID of the scheduled event this invite is for.
@ -2094,7 +2092,6 @@ class Client:
data = await self.http.get_invite(
resolved.code,
with_counts=with_counts,
with_expiration=with_expiration,
guild_scheduled_event_id=scheduled_event_id,
)
return Invite.from_incomplete(state=self._connection, data=data)

8
discord/http.py

@ -2390,18 +2390,19 @@ class HTTPClient:
max_uses: int = 0,
temporary: bool = False,
unique: bool = True,
validate: Optional[str] = None,
target_type: Optional[invite.InviteTargetType] = None,
target_user_id: Optional[Snowflake] = None,
target_application_id: Optional[Snowflake] = None,
flags: int = 0,
) -> Response[invite.Invite]:
payload = {
'max_age': max_age,
'max_uses': max_uses,
'target_type': target_type,
'temporary': temporary,
'validate': validate,
'flags': flags,
}
if unique:
payload['unique'] = unique
if target_user_id:
@ -2440,13 +2441,12 @@ class HTTPClient:
invite_id: str,
*,
with_counts: bool = True,
with_expiration: bool = True,
guild_scheduled_event_id: Optional[Snowflake] = None,
input_value: Optional[str] = None,
) -> Response[invite.Invite]:
params: Dict[str, Any] = {
'with_counts': str(with_counts).lower(),
'with_expiration': str(with_expiration).lower(),
'with_expiration': 'true', # No longer exists
}
if input_value:
params['inputValue'] = input_value

22
discord/invite.py

@ -32,6 +32,7 @@ from .flags import InviteFlags
from .mixins import Hashable
from .object import Object
from .scheduled_event import ScheduledEvent
from .stage_instance import StageInstance
from .utils import MISSING, _generate_session_id, _get_as_snowflake, parse_time, snowflake_time
from .welcome_screen import WelcomeScreen
@ -288,6 +289,9 @@ class PartialInviteGuild:
return None
return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes')
def _resolve_channel(self, channel_id: Optional[int], /):
return
class Invite(Hashable):
r"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
@ -335,6 +339,10 @@ class Invite(Hashable):
If it's not in the table above then it is available by all methods.
.. versionchanged:: 2.1
The ``revoked`` attribute has been removed.
Attributes
-----------
max_age: Optional[:class:`int`]
@ -348,8 +356,6 @@ class Invite(Hashable):
.. versionadded:: 2.0
guild: Optional[Union[:class:`Guild`, :class:`Object`, :class:`PartialInviteGuild`]]
The guild the invite is for. Can be ``None`` if not a guild invite.
revoked: Optional[:class:`bool`]
Indicates if the invite has been revoked.
created_at: Optional[:class:`datetime.datetime`]
An aware UTC datetime object denoting the time the invite was created.
temporary: Optional[:class:`bool`]
@ -404,6 +410,7 @@ class Invite(Hashable):
.. 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`
@ -412,6 +419,7 @@ class Invite(Hashable):
.. versionadded:: 2.0
.. note::
This is only possibly ``True`` in accepted invite objects
(i.e. the objects received from :meth:`accept` and :meth:`use`).
"""
@ -420,7 +428,6 @@ class Invite(Hashable):
'max_age',
'code',
'guild',
'revoked',
'created_at',
'uses',
'temporary',
@ -436,6 +443,7 @@ class Invite(Hashable):
'expires_at',
'scheduled_event',
'scheduled_event_id',
'stage_instance',
'_message',
'welcome_screen',
'type',
@ -461,7 +469,6 @@ class Invite(Hashable):
self.max_age: Optional[int] = data.get('max_age')
self.code: str = data['code']
self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get('guild'), guild)
self.revoked: Optional[bool] = data.get('revoked')
self.created_at: Optional[datetime.datetime] = parse_time(data.get('created_at'))
self.temporary: Optional[bool] = data.get('temporary')
self.uses: Optional[int] = data.get('uses')
@ -510,6 +517,11 @@ class Invite(Hashable):
)
self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None
stage_instance = data.get('stage_instance')
self.stage_instance: Optional[StageInstance] = (
StageInstance.from_invite(self, stage_instance) if stage_instance 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)
@ -537,7 +549,7 @@ class Invite(Hashable):
if channel_data and channel_data.get('type') == ChannelType.private.value:
channel_data['recipients'] = [data['inviter']] if 'inviter' in data else []
channel = PartialInviteChannel(channel_data, state)
channel = state.get_channel(getattr(channel, 'id', None)) or channel
channel = (state.get_channel(channel.id) or channel) if channel else None
return cls(state=state, data=data, guild=guild, channel=channel, welcome_screen=welcome_screen, message=message) # type: ignore

88
discord/stage_instance.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
from typing import List, Optional, TYPE_CHECKING
from .utils import MISSING, cached_slot_property, _get_as_snowflake
from .mixins import Hashable
@ -37,11 +37,15 @@ __all__ = (
# fmt: on
if TYPE_CHECKING:
from .types.channel import StageInstance as StageInstancePayload
from typing_extensions import Self
from .types.channel import StageInstance as StageInstancePayload, InviteStageInstance as InviteStageInstancePayload
from .state import ConnectionState
from .channel import StageChannel
from .guild import Guild
from .scheduled_event import ScheduledEvent
from .invite import Invite
from .member import Member
class StageInstance(Hashable):
@ -77,8 +81,12 @@ class StageInstance(Hashable):
The privacy level of the stage instance.
discoverable_disabled: :class:`bool`
Whether discoverability for the stage instance is disabled.
invite_code: Optional[:class:`str`]
The invite code of the stage instance, if public.
.. versionadded:: 2.1
scheduled_event_id: Optional[:class:`int`]
The ID of scheduled event that belongs to the stage instance if any.
The ID of the scheduled event that belongs to the stage instance, if any.
.. versionadded:: 2.0
"""
@ -91,44 +99,104 @@ class StageInstance(Hashable):
'topic',
'privacy_level',
'discoverable_disabled',
'invite_code',
'scheduled_event_id',
'_cs_channel',
'_cs_scheduled_event',
'_members',
'_participant_count',
)
def __init__(self, *, state: ConnectionState, guild: Guild, data: StageInstancePayload) -> None:
self._state: ConnectionState = state
self.guild: Guild = guild
self._members: Optional[List[Member]] = None
self._participant_count: Optional[int] = None
self._update(data)
def _update(self, data: StageInstancePayload) -> None:
def _update(self, data: StageInstancePayload, /) -> None:
self.id: int = int(data['id'])
self.channel_id: int = int(data['channel_id'])
self.topic: str = data['topic']
self.privacy_level: PrivacyLevel = try_enum(PrivacyLevel, data['privacy_level'])
self.discoverable_disabled: bool = data.get('discoverable_disabled', False)
self.invite_code: Optional[str] = data.get('invite_code')
self.scheduled_event_id: Optional[int] = _get_as_snowflake(data, 'guild_scheduled_event_id')
@staticmethod
def _resolve_stage_instance_id(invite: Invite) -> int:
try:
return invite.channel.instance.id # type: ignore
except AttributeError:
# This is a lie, but it doesn't matter
return invite.channel.id # type: ignore
@classmethod
def from_invite(cls, invite: Invite, data: InviteStageInstancePayload, /) -> Self:
state = invite._state
payload: StageInstancePayload = {
'id': cls._resolve_stage_instance_id(invite),
'guild_id': invite.guild.id, # type: ignore # Will always be defined
'channel_id': invite.channel.id, # type: ignore # Will always be defined
'topic': data['topic'],
'privacy_level': PrivacyLevel.public.value,
'discoverable_disabled': False,
'invite_code': invite.code,
'guild_scheduled_event_id': invite.scheduled_event.id if invite.scheduled_event else None,
}
self = cls(state=state, guild=invite.guild, data=payload) # type: ignore # Guild may be wrong type
self._members = [Member(data=mdata, state=state, guild=invite.guild) for mdata in data['members']] # type: ignore # Guild may be wrong type
self._participant_count = data.get('participant_count', len(self._members))
return self
def __repr__(self) -> str:
return f'<StageInstance id={self.id} guild={self.guild!r} channel_id={self.channel_id} topic={self.topic!r}>'
@property
def invite_url(self) -> Optional[str]:
"""Optional[:class:`str`]: The stage instance's invite URL, if public.
.. versionadded:: 2.1
"""
if self.invite_code is None:
return None
return f'https://discord.gg/{self.invite_code}'
@property
def discoverable(self) -> bool:
""":class:`bool`: Whether the stage instance is discoverable."""
return not self.discoverable_disabled
@cached_slot_property('_cs_channel')
@property
def channel(self) -> Optional[StageChannel]:
"""Optional[:class:`StageChannel`]: The channel that stage instance is running in."""
# The returned channel will always be a StageChannel or None
return self._state.get_channel(self.channel_id) # type: ignore
return self.guild._resolve_channel(self.channel_id) # type: ignore
@cached_slot_property('_cs_scheduled_event')
@property
def scheduled_event(self) -> Optional[ScheduledEvent]:
"""Optional[:class:`ScheduledEvent`]: The scheduled event that belongs to the stage instance."""
# Guild.get_scheduled_event() expects an int, we are passing Optional[int]
return self.guild.get_scheduled_event(self.scheduled_event_id) # type: ignore
@property
def speakers(self) -> List[Member]:
"""List[:class:`Member`]: The members that are speaking in the stage instance.
.. versionadded:: 2.1
"""
if self._members is not None or self.channel is None:
return self._members or []
return self.channel.speakers
@property
def participant_count(self) -> int:
""":class:`int`: The number of participants in the stage instance.
.. versionadded:: 2.1
"""
if self._participant_count is not None or self.channel is None:
return self._participant_count or 0
return len(self.channel.voice_states)
async def edit(
self,
*,
@ -161,10 +229,8 @@ class StageInstance(Hashable):
Editing a stage instance failed.
"""
payload = {}
if topic is not MISSING:
payload['topic'] = topic
if privacy_level is not MISSING:
if not isinstance(privacy_level, PrivacyLevel):
raise TypeError('privacy_level field must be of type PrivacyLevel')

9
discord/types/channel.py

@ -27,6 +27,7 @@ from typing_extensions import NotRequired
from .user import PartialUser
from .snowflake import Snowflake
from .guild import MemberWithUser
from .threads import ThreadMetadata, ThreadMember, ThreadArchiveDuration, ThreadType
@ -195,4 +196,12 @@ class StageInstance(TypedDict):
topic: str
privacy_level: PrivacyLevel
discoverable_disabled: bool
invite_code: Optional[str]
guild_scheduled_event_id: Optional[int]
class InviteStageInstance(TypedDict):
members: List[MemberWithUser]
participant_count: int
speaker_count: int
topic: str

16
discord/types/guild.py

@ -136,8 +136,20 @@ class UserGuild(BaseGuild):
approximate_presence_count: NotRequired[int]
class InviteGuild(Guild, total=False):
welcome_screen: WelcomeScreen
class InviteGuild(TypedDict):
id: Snowflake
name: str
icon: Optional[str]
description: Optional[str]
banner: Optional[str]
splash: Optional[str]
verification_level: VerificationLevel
features: List[str]
vanity_url_code: Optional[str]
premium_subscription_count: NotRequired[int]
nsfw: bool
nsfw_level: NSFWLevel
welcome_screen: NotRequired[WelcomeScreen]
class GuildWithCounts(Guild, _GuildCounts):

73
discord/types/invite.py

@ -27,67 +27,78 @@ from __future__ import annotations
from typing import Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
from .application import PartialApplication
from .channel import InviteStageInstance, PartialChannel
from .guild import InviteGuild, _GuildCounts
from .scheduled_event import GuildScheduledEvent
from .snowflake import Snowflake
from .guild import InviteGuild, _GuildCounts
from .channel import PartialChannel
from .user import PartialUser
from .application import PartialApplication
InviteTargetType = Literal[1, 2]
class _InviteMetadata(TypedDict, total=False):
uses: int
max_uses: int
max_age: int
temporary: bool
class _InviteMetadata(TypedDict):
uses: NotRequired[int]
max_uses: NotRequired[int]
max_age: NotRequired[int]
temporary: NotRequired[bool]
created_at: str
expires_at: Optional[str]
class VanityInvite(_InviteMetadata):
class _InviteTargetType(TypedDict, total=False):
target_type: InviteTargetType
target_user: PartialUser
target_application: PartialApplication
class VanityInvite:
code: Optional[str]
revoked: NotRequired[bool]
uses: int
class IncompleteInvite(_InviteMetadata):
class PartialInvite(_InviteTargetType):
code: str
channel: PartialChannel
type: Literal[0, 1, 2]
channel: Optional[PartialChannel]
guild_id: NotRequired[Snowflake]
guild: NotRequired[InviteGuild]
inviter: NotRequired[PartialUser]
flags: NotRequired[int]
expires_at: Optional[str]
guild_scheduled_event: NotRequired[GuildScheduledEvent]
stage_instance: NotRequired[InviteStageInstance]
class Invite(IncompleteInvite, total=False):
guild: InviteGuild
inviter: PartialUser
target_user: PartialUser
target_type: InviteTargetType
target_application: PartialApplication
guild_scheduled_event: GuildScheduledEvent
class InviteWithCounts(PartialInvite, _GuildCounts):
...
class InviteWithCounts(Invite, _GuildCounts):
class InviteWithMetadata(PartialInvite, _InviteMetadata):
...
class GatewayInviteCreate(TypedDict):
channel_id: Snowflake
Invite = Union[PartialInvite, InviteWithCounts, InviteWithMetadata]
class GatewayInviteCreate(_InviteTargetType):
code: str
type: Literal[0]
channel_id: Snowflake
guild_id: Snowflake
inviter: NotRequired[PartialUser]
expires_at: Optional[str]
created_at: str
uses: int
max_age: int
max_uses: int
temporary: bool
uses: bool
guild_id: Snowflake
inviter: NotRequired[PartialUser]
target_type: NotRequired[InviteTargetType]
target_user: NotRequired[PartialUser]
target_application: NotRequired[PartialApplication]
flags: NotRequired[int]
class GatewayInviteDelete(TypedDict):
channel_id: Snowflake
code: str
guild_id: NotRequired[Snowflake]
channel_id: Snowflake
guild_id: Snowflake
GatewayInvite = Union[GatewayInviteCreate, GatewayInviteDelete]

Loading…
Cancel
Save