From 44ec9f40c784114e108839621e45c2846a926fa1 Mon Sep 17 00:00:00 2001 From: dolfies Date: Sun, 3 Sep 2023 22:09:48 -0400 Subject: [PATCH] Update invite parameters and add stage interoperability --- discord/abc.py | 28 ++++++++----- discord/channel.py | 6 +-- discord/client.py | 11 ++--- discord/http.py | 8 ++-- discord/invite.py | 22 +++++++--- discord/stage_instance.py | 88 ++++++++++++++++++++++++++++++++++----- discord/types/channel.py | 9 ++++ discord/types/guild.py | 16 ++++++- discord/types/invite.py | 73 ++++++++++++++++++-------------- 9 files changed, 185 insertions(+), 76 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 99985880a..e284f0a1a 100644 --- a/discord/abc.py +++ b/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) diff --git a/discord/channel.py b/discord/channel.py index e249248e4..e0f5652be 100644 --- a/discord/channel.py +++ b/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) diff --git a/discord/client.py b/discord/client.py index c2a547582..54a7c707a 100644 --- a/discord/client.py +++ b/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) diff --git a/discord/http.py b/discord/http.py index d9789b0e7..4bb77ab06 100644 --- a/discord/http.py +++ b/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 diff --git a/discord/invite.py b/discord/invite.py index aaf5fcb33..50e653396 100644 --- a/discord/invite.py +++ b/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 diff --git a/discord/stage_instance.py b/discord/stage_instance.py index 6a66a23b2..f8aee8975 100644 --- a/discord/stage_instance.py +++ b/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'' + @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') diff --git a/discord/types/channel.py b/discord/types/channel.py index 638e24753..dcf18ede1 100644 --- a/discord/types/channel.py +++ b/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 diff --git a/discord/types/guild.py b/discord/types/guild.py index 528ebcb5e..30b699c63 100644 --- a/discord/types/guild.py +++ b/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): diff --git a/discord/types/invite.py b/discord/types/invite.py index d3f49b7c1..159d57b3c 100644 --- a/discord/types/invite.py +++ b/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]