diff --git a/discord/audit_logs.py b/discord/audit_logs.py index b2f6a72393..9a00f15715 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -278,8 +278,8 @@ class AuditLogChanges: "type": (None, _transform_type), "status": (None, _enum_transformer(enums.ScheduledEventStatus)), "entity_type": ( - "location_type", - _enum_transformer(enums.ScheduledEventLocationType), + "entity_type", + _enum_transformer(enums.ScheduledEventEntityType), ), "command_id": ("command_id", _transform_snowflake), "image_hash": ("image", _transform_scheduled_event_image), @@ -318,7 +318,11 @@ def __init__( "$add_allow_list", ]: self._handle_trigger_metadata( - self.before, self.after, entry, elem["new_value"], attr # type: ignore + self.before, + self.after, + entry, + elem["new_value"], + attr, # type: ignore ) continue elif attr in [ @@ -327,7 +331,11 @@ def __init__( "$remove_allow_list", ]: self._handle_trigger_metadata( - self.after, self.before, entry, elem["new_value"], attr # type: ignore + self.after, + self.before, + entry, + elem["new_value"], + attr, # type: ignore ) continue @@ -349,21 +357,6 @@ def __init__( if transformer: before = transformer(entry, before) - if attr == "location" and hasattr(self.before, "location_type"): - from .scheduled_events import ScheduledEventLocation - - if ( - self.before.location_type - is enums.ScheduledEventLocationType.external - ): - before = ScheduledEventLocation(state=state, value=before) - elif hasattr(self.before, "channel"): - before = ScheduledEventLocation( - state=state, value=self.before.channel - ) - - setattr(self.before, attr, before) - try: after = elem["new_value"] except KeyError: @@ -372,21 +365,6 @@ def __init__( if transformer: after = transformer(entry, after) - if attr == "location" and hasattr(self.after, "location_type"): - from .scheduled_events import ScheduledEventLocation - - if ( - self.after.location_type - is enums.ScheduledEventLocationType.external - ): - after = ScheduledEventLocation(state=state, value=after) - elif hasattr(self.after, "channel"): - after = ScheduledEventLocation( - state=state, value=self.after.channel - ) - - setattr(self.after, attr, after) - # add an alias if hasattr(self.after, "colour"): self.after.color = self.after.colour @@ -691,7 +669,12 @@ def _convert_target_invite(self, target_id: int) -> Invite: "uses": changeset.uses, } - obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel) # type: ignore + obj = Invite( + state=self._state, + data=fake_payload, + guild=self.guild, + channel=changeset.channel, + ) # type: ignore try: obj.inviter = changeset.inviter except AttributeError: diff --git a/discord/enums.py b/discord/enums.py index 63557c853b..f933cdabb7 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -61,6 +61,7 @@ "EmbeddedActivity", "ScheduledEventStatus", "ScheduledEventPrivacyLevel", + "ScheduledEventEntityType", "ScheduledEventLocationType", "InputTextStyle", "SlashCommandOptionType", @@ -955,13 +956,21 @@ def __int__(self): return self.value -class ScheduledEventLocationType(Enum): - """Scheduled event location type""" +class ScheduledEventEntityType(Enum): + """Scheduled event entity type""" stage_instance = 1 voice = 2 external = 3 + def __int__(self): + return self.value + + +# TODO(Paillat-dev): Add @deprecated notice using warnings.deprecated in relevant PR +class ScheduledEventLocationType(ScheduledEventEntityType): + """Scheduled event location type (deprecated alias for :class:`ScheduledEventEntityType`)""" + class AutoModTriggerType(Enum): """Automod trigger type""" diff --git a/discord/guild.py b/discord/guild.py index dd6c91fdc1..a0b44a2481 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -61,7 +61,7 @@ NotificationLevel, NSFWLevel, OnboardingMode, - ScheduledEventLocationType, + ScheduledEventEntityType, ScheduledEventPrivacyLevel, SortOrder, VerificationLevel, @@ -69,7 +69,13 @@ VoiceRegion, try_enum, ) -from .errors import ClientException, HTTPException, InvalidArgument, InvalidData +from .errors import ( + ClientException, + HTTPException, + InvalidArgument, + InvalidData, + ValidationError, +) from .file import File from .flags import SystemChannelFlags from .incidents import IncidentsData @@ -87,13 +93,17 @@ from .onboarding import Onboarding from .permissions import PermissionOverwrite from .role import Role, RoleColours -from .scheduled_events import ScheduledEvent, ScheduledEventLocation +from .scheduled_events import ( + ScheduledEvent, + ScheduledEventEntityMetadata, + ScheduledEventLocation, +) from .soundboard import SoundboardSound from .stage_instance import StageInstance from .sticker import GuildSticker from .threads import Thread, ThreadMember from .user import User -from .utils import _D, _FETCHABLE +from .utils import _D, _FETCHABLE, warn_deprecated from .welcome_screen import WelcomeScreen, WelcomeScreenChannel from .widget import Widget @@ -4276,6 +4286,7 @@ def get_scheduled_event(self, event_id: int, /) -> ScheduledEvent | None: """ return self._scheduled_events.get(event_id) + @overload async def create_scheduled_event( self, *, @@ -4283,30 +4294,74 @@ async def create_scheduled_event( description: str = MISSING, start_time: datetime.datetime, end_time: datetime.datetime = MISSING, - location: str | int | VoiceChannel | StageChannel | ScheduledEventLocation, + location: ( + str | int | VoiceChannel | StageChannel | ScheduledEventLocation + ) = MISSING, + reason: str | None = None, + image: bytes = MISSING, + ) -> ScheduledEvent | None: ... + + @overload + async def create_scheduled_event( + self, + *, + name: str, + description: str = MISSING, + scheduled_start_time: datetime.datetime, + scheduled_end_time: datetime.datetime = MISSING, + entity_type: ScheduledEventEntityType = MISSING, + entity_metadata: ScheduledEventEntityMetadata | None = MISSING, + channel_id: int = MISSING, + privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, + reason: str | None = None, + image: bytes = MISSING, + ) -> ScheduledEvent | None: ... + async def create_scheduled_event( + self, + *, + name: str, + description: str = MISSING, + scheduled_start_time: datetime.datetime, + scheduled_end_time: datetime.datetime = MISSING, + location: ( + str | int | VoiceChannel | StageChannel | ScheduledEventLocation + ) = MISSING, + entity_type: ScheduledEventEntityType = MISSING, + entity_metadata: ScheduledEventEntityMetadata | None = MISSING, + channel_id: int = MISSING, privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, image: bytes = MISSING, + start_time: datetime.datetime = MISSING, + end_time: datetime.datetime = MISSING, ) -> ScheduledEvent | None: """|coro| Creates a scheduled event. + For EXTERNAL events, ``entity_metadata`` with a location and ``end_time`` are required. + For STAGE_INSTANCE or VOICE events, ``channel_id`` is required. + Parameters ---------- name: :class:`str` The name of the scheduled event. description: Optional[:class:`str`] The description of the scheduled event. - start_time: :class:`datetime.datetime` + scheduled_start_time: :class:`datetime.datetime` A datetime object of when the scheduled event is supposed to start. - end_time: Optional[:class:`datetime.datetime`] + scheduled_end_time: Optional[:class:`datetime.datetime`] A datetime object of when the scheduled event is supposed to end. - location: :class:`ScheduledEventLocation` - The location of where the event is happening. + Required for EXTERNAL events. + entity_type: :class:`ScheduledEventEntityType` + The type of scheduled event (STAGE_INSTANCE, VOICE, or EXTERNAL). + entity_metadata: Optional[:class:`ScheduledEventEntityMetadata`] + The entity metadata (required for EXTERNAL events with a location). + channel_id: Optional[Union[:class:`int`, :class:`VoiceChannel`, :class:`StageChannel`]] + The channel ID for STAGE_INSTANCE or VOICE events. + Can be a channel object or a snowflake ID. privacy_level: :class:`ScheduledEventPrivacyLevel` The privacy level of the event. Currently, the only possible value - is :attr:`ScheduledEventPrivacyLevel.guild_only`, which is default, - so there is no need to change this parameter. + is :attr:`ScheduledEventPrivacyLevel.guild_only`, which is default. reason: Optional[:class:`str`] The reason to show in the audit log. image: Optional[:class:`bytes`] @@ -4323,34 +4378,74 @@ async def create_scheduled_event( You do not have the Manage Events permission. HTTPException The operation failed. + ValidationError + Invalid parameters for the event type. """ payload: dict[str, str | int] = { "name": name, - "scheduled_start_time": start_time.isoformat(), + "scheduled_start_time": scheduled_start_time.isoformat(), "privacy_level": int(privacy_level), + "entity_type": int(entity_type), } + if location is MISSING and entity_type is MISSING: + raise TypeError("Either location or entity_type must be provided.") + if start_time is MISSING and scheduled_start_time is MISSING: + raise TypeError( + "Either start_time or scheduled_start_time must be provided." + ) + if start_time is not MISSING: + warn_deprecated("start_time", "scheduled_start_time", "2.7") + if scheduled_start_time is MISSING: + scheduled_start_time = start_time - if not isinstance(location, ScheduledEventLocation): - location = ScheduledEventLocation(state=self._state, value=location) - - payload["entity_type"] = location.type.value - - if location.type == ScheduledEventLocationType.external: - payload["channel_id"] = None - payload["entity_metadata"] = {"location": location.value} - else: - payload["channel_id"] = location.value.id - payload["entity_metadata"] = None + if end_time is not MISSING: + warn_deprecated("end_time", "scheduled_end_time", "2.7") + if scheduled_end_time is MISSING: + scheduled_end_time = end_time + + if location is not MISSING: + warn_deprecated("location", "entity_metadata", "2.7", "3.0") + if entity_metadata is MISSING: + if not isinstance(location, (ScheduledEventLocation)): + location = ScheduledEventLocation(state=self._state, value=location) + if entity_type is MISSING: + entity_type = location.type + if location.type == ScheduledEventEntityType.external: + entity_metadata = ScheduledEventEntityMetadata(str(location)) + + if scheduled_end_time is not MISSING: + payload["scheduled_end_time"] = scheduled_end_time.isoformat() if description is not MISSING: payload["description"] = description - if end_time is not MISSING: - payload["scheduled_end_time"] = end_time.isoformat() - if image is not MISSING: payload["image"] = utils._bytes_to_base64_data(image) + if entity_type == ScheduledEventEntityType.external: + if entity_metadata is MISSING or entity_metadata is None: + raise ValidationError( + "entity_metadata with a location is required for EXTERNAL events." + ) + if not entity_metadata.location: + raise ValidationError( + "entity_metadata.location cannot be empty for EXTERNAL events." + ) + if scheduled_end_time is MISSING: + raise ValidationError( + "scheduled_end_time is required for EXTERNAL events." + ) + + payload["channel_id"] = None + payload["entity_metadata"] = entity_metadata.to_payload() + else: + if channel_id is MISSING: + raise ValidationError( + "channel_id is required for STAGE_INSTANCE and VOICE events." + ) + + payload["channel_id"] = channel_id + data = await self._state.http.create_scheduled_event( guild_id=self.id, reason=reason, **payload ) diff --git a/discord/iterators.py b/discord/iterators.py index b074aefdc4..0f8ac5244b 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -27,6 +27,7 @@ import asyncio import datetime +import itertools from typing import ( TYPE_CHECKING, Any, @@ -898,6 +899,7 @@ def __init__( with_member: bool = False, before: datetime.datetime | int | None = None, after: datetime.datetime | int | None = None, + use_cache: bool = False, ): if isinstance(before, datetime.datetime): before = Object(id=time_snowflake(before, high=False)) @@ -909,6 +911,7 @@ def __init__( self.with_member = with_member self.before = before self.after = after + self.use_cache = use_cache self.subscribers = asyncio.Queue() self.get_subscribers = self.event._state.http.get_scheduled_event_users @@ -948,12 +951,28 @@ def user_from_payload(self, data): return User(state=self.event._state, data=user) + async def _fill_from_cache(self): + """Fill subscribers queue from cached user IDs.""" + cached_user_ids = list(self.event._cached_subscribers.keys()) + + for user_id in itertools.islice(iter(cached_user_ids), self.retrieve): + member = self.event.guild.get_member(user_id) + if member: + await self.subscribers.put(member) + + self.limit = 0 + async def fill_subs(self): if not self._get_retrieve(): return + if self.use_cache: + await self._fill_from_cache() + return + before = self.before.id if self.before else None after = self.after.id if self.after else None + data = await self.get_subscribers( guild_id=self.event.guild.id, event_id=self.event.id, @@ -966,9 +985,8 @@ async def fill_subs(self): data_length = len(data) if data_length < self.retrieve: self.limit = 0 - elif data_length > 0: - if self.limit: - self.limit -= self.retrieve + elif data_length > 0 and self.limit is not None: + self.limit -= self.retrieve self.after = Object(id=int(data[-1]["user_id"])) for element in reversed(data): @@ -1277,7 +1295,7 @@ async def retrieve_inner(self) -> list[Message]: def __await__(self) -> Generator[Any, Any, MessagePin]: warn_deprecated( - f"Messageable.pins() returning a list of Message", + "Messageable.pins() returning a list of Message", since="2.7", removed="3.0", reference="The documentation of pins()", diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 7e339dcee7..03c2794f30 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -30,30 +30,34 @@ from . import utils from .asset import Asset from .enums import ( - ScheduledEventLocationType, + ScheduledEventEntityType, ScheduledEventPrivacyLevel, ScheduledEventStatus, try_enum, ) -from .errors import InvalidArgument, ValidationError +from .errors import ValidationError from .iterators import ScheduledEventSubscribersIterator from .mixins import Hashable from .object import Object -from .utils import warn_deprecated +from .utils import deprecated, warn_deprecated __all__ = ( "ScheduledEvent", "ScheduledEventLocation", + "ScheduledEventEntityMetadata", ) if TYPE_CHECKING: from .abc import Snowflake + from .channel import StageChannel, VoiceChannel from .guild import Guild - from .iterators import AsyncIterator from .member import Member from .state import ConnectionState - from .types.channel import StageChannel, VoiceChannel from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload +else: + ConnectionState = None + StageChannel = None + VoiceChannel = None MISSING = utils.MISSING @@ -71,13 +75,16 @@ class ScheduledEventLocation: | :class:`str` | :attr:`ScheduledEventLocationType.external` | +------------------------+---------------------------------------------------+ + .. deprecated:: 2.7 + Use :class:`ScheduledEventEntityMetadata` instead. + .. versionadded:: 2.0 Attributes ---------- value: Union[:class:`str`, :class:`StageChannel`, :class:`VoiceChannel`, :class:`Object`] The actual location of the scheduled event. - type: :class:`ScheduledEventLocationType` + type: :class:`ScheduledEventEntityType` The type of location. """ @@ -89,13 +96,20 @@ class ScheduledEventLocation: def __init__( self, *, - state: ConnectionState, - value: str | int | StageChannel | VoiceChannel, - ): - self._state = state - self.value: str | StageChannel | VoiceChannel | Object - if isinstance(value, int): - self.value = self._state.get_channel(id=int(value)) or Object(id=int(value)) + state: ConnectionState | None = None, + value: str | int | StageChannel | VoiceChannel | None = None, + ) -> None: + warn_deprecated("ScheduledEventLocation", "ScheduledEventEntityMetadata", "2.7") + self._state: ConnectionState | None = state + self.value: str | StageChannel | VoiceChannel | Object | None + if value is None: + self.value = None + elif isinstance(value, int): + self.value = ( + self._state.get_channel(id=int(value)) or Object(id=int(value)) + if self._state + else Object(id=int(value)) + ) else: self.value = value @@ -103,16 +117,57 @@ def __repr__(self) -> str: return f"" def __str__(self) -> str: - return str(self.value) + return str(self.value) if self.value else "" @property - def type(self) -> ScheduledEventLocationType: + def type(self) -> ScheduledEventEntityType: + """The type of location.""" if isinstance(self.value, str): - return ScheduledEventLocationType.external + return ScheduledEventEntityType.external elif self.value.__class__.__name__ == "StageChannel": - return ScheduledEventLocationType.stage_instance + return ScheduledEventEntityType.stage_instance elif self.value.__class__.__name__ == "VoiceChannel": - return ScheduledEventLocationType.voice + return ScheduledEventEntityType.voice + return ScheduledEventEntityType.voice + + +class ScheduledEventEntityMetadata: + """Represents a scheduled event's entity metadata. + + This contains additional metadata for the scheduled event, particularly + for external events which require a location string. + + .. versionadded:: 2.7 + + Attributes + ---------- + location: Optional[:class:`str`] + The location of the event (1-100 characters). Only present for EXTERNAL events. + """ + + __slots__ = ("location",) + + def __init__( + self, + location: str | None = None, + ) -> None: + self.location: str | None = location + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.location or "" + + def to_payload(self) -> dict[str, str]: + """Converts the entity metadata to a Discord API payload. + + Returns + ------- + dict[str, str] + A dictionary with the entity metadata fields for the API. + """ + return {"location": self.location} class ScheduledEvent(Hashable): @@ -146,16 +201,13 @@ class ScheduledEvent(Hashable): The name of the scheduled event. description: Optional[:class:`str`] The description of the scheduled event. - start_time: :class:`datetime.datetime` + scheduled_start_time: :class:`datetime.datetime` The time when the event will start - end_time: Optional[:class:`datetime.datetime`] + scheduled_end_time: Optional[:class:`datetime.datetime`] The time when the event is supposed to end. status: :class:`ScheduledEventStatus` The status of the scheduled event. - location: :class:`ScheduledEventLocation` - The location of the event. - See :class:`ScheduledEventLocation` for more information. - subscriber_count: Optional[:class:`int`] + user_count: :class:`int` The number of users that have marked themselves as interested in the event. creator_id: Optional[:class:`int`] The ID of the user who created the event. @@ -167,22 +219,33 @@ class ScheduledEvent(Hashable): The privacy level of the event. Currently, the only possible value is :attr:`ScheduledEventPrivacyLevel.guild_only`, which is default, so there is no need to use this attribute. + entity_type: :class:`ScheduledEventEntityType` + The type of scheduled event (STAGE_INSTANCE, VOICE, or EXTERNAL). + entity_id: Optional[:class:`int`] + The ID of an entity associated with the scheduled event. + entity_metadata: Optional[:class:`ScheduledEventEntityMetadata`] + Additional metadata for the scheduled event (e.g., location for EXTERNAL events). """ __slots__ = ( "id", "name", "description", - "start_time", - "end_time", + "scheduled_start_time", + "scheduled_end_time", "status", "creator_id", "creator", - "location", "guild", "_state", "_image", - "subscriber_count", + "user_count", + "_cached_subscribers", + "entity_type", + "privacy_level", + "channel_id", + "entity_id", + "entity_metadata", ) def __init__( @@ -200,27 +263,36 @@ def __init__( self.name: str = data.get("name") self.description: str | None = data.get("description", None) self._image: str | None = data.get("image", None) - self.start_time: datetime.datetime = datetime.datetime.fromisoformat( + self.scheduled_start_time: datetime.datetime = datetime.datetime.fromisoformat( data.get("scheduled_start_time") ) - if end_time := data.get("scheduled_end_time", None): - end_time = datetime.datetime.fromisoformat(end_time) - self.end_time: datetime.datetime | None = end_time + if scheduled_end_time := data.get("scheduled_end_time", None): + scheduled_end_time = datetime.datetime.fromisoformat(scheduled_end_time) + self.scheduled_end_time: datetime.datetime | None = scheduled_end_time self.status: ScheduledEventStatus = try_enum( ScheduledEventStatus, data.get("status") ) - self.subscriber_count: int | None = data.get("user_count", None) + self.entity_type: ScheduledEventEntityType = try_enum( + ScheduledEventEntityType, data.get("entity_type") + ) + self.privacy_level: ScheduledEventPrivacyLevel = try_enum( + ScheduledEventPrivacyLevel, data.get("privacy_level") + ) + self.channel_id: int | None = utils._get_as_snowflake(data, "channel_id") + self.entity_id: int | None = utils._get_as_snowflake(data, "entity_id") + + entity_metadata_data = data.get("entity_metadata") + self.entity_metadata: ScheduledEventEntityMetadata | None = ( + ScheduledEventEntityMetadata(location=entity_metadata_data.get("location")) + if entity_metadata_data + else None + ) + + self._cached_subscribers: set[int] = set() + self.user_count: int | None = data.get("user_count") self.creator_id: int | None = utils._get_as_snowflake(data, "creator_id") self.creator: Member | None = creator - - entity_metadata = data.get("entity_metadata") - channel_id = data.get("channel_id", None) - if channel_id is None: - self.location = ScheduledEventLocation( - state=state, value=entity_metadata["location"] - ) - else: - self.location = ScheduledEventLocation(state=state, value=int(channel_id)) + self.channel_id = data.get("channel_id", None) def __str__(self) -> str: return self.name @@ -230,23 +302,71 @@ def __repr__(self) -> str: f"" + f"channel_id={self.channel_id}>" ) + @property + @deprecated(instead="entity_metadata.location", since="2.7", removed="3.0") + def location(self) -> ScheduledEventLocation | None: + """Returns the location of the event.""" + if self.channel_id is None: + self.location = ScheduledEventLocation( + state=self._state, value=self.entity_metadata.location + ) + else: + self.location = ScheduledEventLocation( + state=self._state, value=self.channel_id + ) + @property def created_at(self) -> datetime.datetime: """Returns the scheduled event's creation time in UTC.""" return utils.snowflake_time(self.id) @property + @deprecated(instead="scheduled_start_time", since="2.7", removed="3.0") + def start_time(self) -> datetime.datetime: + """ + Returns the scheduled start time of the event. + + .. deprecated:: 2.7 + Use :attr:`scheduled_start_time` instead. + """ + return self.scheduled_start_time + + @property + @deprecated(instead="scheduled_end_time", since="2.7", removed="3.0") + def end_time(self) -> datetime.datetime | None: + """ + Returns the scheduled end time of the event. + + .. deprecated:: 2.7 + Use :attr:`scheduled_end_time` instead. + """ + return self.scheduled_end_time + + @property + @deprecated(instead="user_count", since="2.7", removed="3.0") + def subscriber_count(self) -> int | None: + """ + Returns the number of users subscribed to the event. + + .. deprecated:: 2.7 + Use :attr:`user_count` instead. + """ + return self.user_count + + @property + @deprecated(instead="user_count", since="2.7", removed="3.0") def interested(self) -> int | None: - """An alias to :attr:`.subscriber_count`""" - return self.subscriber_count + """An alias to :attr:`.user_count`""" + return self.user_count @property def url(self) -> str: @@ -281,55 +401,67 @@ async def edit( reason: str | None = None, name: str = MISSING, description: str = MISSING, - status: int | ScheduledEventStatus = MISSING, + status: ScheduledEventStatus = MISSING, location: ( str | int | VoiceChannel | StageChannel | ScheduledEventLocation ) = MISSING, - start_time: datetime.datetime = MISSING, - end_time: datetime.datetime = MISSING, - cover: bytes | None = MISSING, + scheduled_start_time: datetime.datetime = MISSING, + scheduled_end_time: datetime.datetime = MISSING, image: bytes | None = MISSING, - privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, + cover: bytes | None = MISSING, + privacy_level: ScheduledEventPrivacyLevel = MISSING, + entity_type: ScheduledEventEntityType = MISSING, + entity_metadata: ScheduledEventEntityMetadata | None = MISSING, ) -> ScheduledEvent | None: """|coro| - Edits the Scheduled Event's data + Edits the Scheduled Event's data. + + All parameters are optional. + + .. note:: - All parameters are optional unless ``location.type`` is - :attr:`ScheduledEventLocationType.external`, then ``end_time`` - is required. + When changing entity_type to EXTERNAL via entity_metadata, Discord will + automatically set ``channel_id`` to null. + + .. note:: + + The Discord API silently discards ``entity_metadata`` for non-EXTERNAL events. Will return a new :class:`.ScheduledEvent` object if applicable. Parameters ---------- name: :class:`str` - The new name of the event. + The new name of the event (1-100 characters). description: :class:`str` - The new description of the event. - location: :class:`.ScheduledEventLocation` - The location of the event. + The new description of the event (1-1000 characters). status: :class:`ScheduledEventStatus` The status of the event. It is recommended, however, to use :meth:`.start`, :meth:`.complete`, and - :meth:`cancel` to edit statuses instead. - start_time: :class:`datetime.datetime` - The new starting time for the event. - end_time: :class:`datetime.datetime` - The new ending time of the event. + :meth:`.cancel` to edit statuses instead. + Valid transitions: SCHEDULED → ACTIVE, ACTIVE → COMPLETED, SCHEDULED → CANCELED. + scheduled_start_time: :class:`datetime.datetime` + The new starting time for the event (ISO8601 format). + scheduled_end_time: :class:`datetime.datetime` + The new ending time of the event (ISO8601 format). privacy_level: :class:`ScheduledEventPrivacyLevel` - The privacy level of the event. Currently, the only possible value - is :attr:`ScheduledEventPrivacyLevel.guild_only`, which is default, - so there is no need to change this parameter. + The privacy level of the event. Currently only GUILD_ONLY is supported. + entity_type: :class:`ScheduledEventEntityType` + The type of scheduled event. When changing to EXTERNAL, you must also provide + ``entity_metadata`` with a location and ``scheduled_end_time``. + entity_metadata: Optional[:class:`ScheduledEventEntityMetadata`] + Additional metadata for the scheduled event. + When set for EXTERNAL events, must contain a location. + Will be silently discarded by Discord for non-EXTERNAL events. reason: Optional[:class:`str`] The reason to show in the audit log. image: Optional[:class:`bytes`] The cover image of the scheduled event. cover: Optional[:class:`bytes`] The cover image of the scheduled event. - .. deprecated:: 2.7 - Use the `image` argument instead. + Use the ``image`` parameter instead. Returns ------- @@ -343,6 +475,8 @@ async def edit( You do not have the Manage Events permission. HTTPException The operation failed. + ValidationError + Invalid parameters for the event type. """ payload: dict[str, Any] = {} @@ -358,57 +492,71 @@ async def edit( if privacy_level is not MISSING: payload["privacy_level"] = int(privacy_level) + if location is not MISSING: + warn_deprecated("location", "entity_metadata", "2.7", "3.0") + if entity_metadata is MISSING: + if not isinstance(location, (ScheduledEventLocation)): + location = ScheduledEventLocation(state=self._state, value=location) + if entity_type is MISSING: + entity_type = location.type + if location.type == ScheduledEventEntityType.external: + entity_metadata = ScheduledEventEntityMetadata(str(location)) + if cover is not MISSING: - warn_deprecated("cover", "image", "2.7") - if image is not MISSING: - raise InvalidArgument( - "cannot pass both `image` and `cover` to `ScheduledEvent.edit`" - ) - else: + warn_deprecated("cover", "image", "2.7", "3.0") + if image is MISSING: image = cover + if entity_type is not MISSING: + payload["entity_type"] = int(entity_type) + + if entity_metadata is not MISSING: + if entity_metadata is None: + payload["entity_metadata"] = None + else: + payload["entity_metadata"] = entity_metadata.to_payload() + if image is not MISSING: if image is None: payload["image"] = None else: payload["image"] = utils._bytes_to_base64_data(image) - if location is not MISSING: - if not isinstance( - location, (ScheduledEventLocation, utils._MissingSentinel) - ): - location = ScheduledEventLocation(state=self._state, value=location) - - if location.type is ScheduledEventLocationType.external: - payload["channel_id"] = None - payload["entity_metadata"] = {"location": str(location.value)} - else: - payload["channel_id"] = location.value.id - payload["entity_metadata"] = None + if scheduled_start_time is not MISSING: + payload["scheduled_start_time"] = scheduled_start_time.isoformat() - payload["entity_type"] = location.type.value + if scheduled_end_time is not MISSING: + payload["scheduled_end_time"] = scheduled_end_time.isoformat() - location = location if location is not MISSING else self.location - if end_time is MISSING and location.type is ScheduledEventLocationType.external: - end_time = self.end_time - if end_time is None: + if ( + entity_type is not MISSING + and entity_type == ScheduledEventEntityType.external + ): + if entity_metadata is MISSING or entity_metadata is None: + raise ValidationError( + "entity_metadata with a location is required when entity_type is EXTERNAL." + ) + if not entity_metadata.location: raise ValidationError( - "end_time needs to be passed if location type is external." + "entity_metadata.location cannot be empty for EXTERNAL events." ) - if start_time is not MISSING: - payload["scheduled_start_time"] = start_time.isoformat() + has_end_time = ( + scheduled_end_time is not MISSING or self.scheduled_end_time is not None + ) + if not has_end_time: + raise ValidationError( + "scheduled_end_time is required for EXTERNAL events." + ) - if end_time is not MISSING: - payload["scheduled_end_time"] = end_time.isoformat() + payload["channel_id"] = None - if payload != {}: - data = await self._state.http.edit_scheduled_event( - self.guild.id, self.id, **payload, reason=reason - ) - return ScheduledEvent( - data=data, guild=self.guild, creator=self.creator, state=self._state - ) + data = await self._state.http.edit_scheduled_event( + self.guild.id, self.id, **payload, reason=reason + ) + return ScheduledEvent( + data=data, guild=self.guild, creator=self.creator, state=self._state + ) async def delete(self) -> None: """|coro| @@ -515,6 +663,7 @@ def subscribers( as_member: bool = False, before: Snowflake | datetime.datetime | None = None, after: Snowflake | datetime.datetime | None = None, + use_cache: bool = False, ) -> ScheduledEventSubscribersIterator: """Returns an :class:`AsyncIterator` representing the users or members subscribed to the event. @@ -542,6 +691,10 @@ def subscribers( Retrieves users after this date or object. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time. + use_cache: Optional[:class:`bool`] + If ``True``, only use cached subscribers and skip API calls. + This is useful when calling from an event handler where the + event may have been deleted. Defaults to ``False``. Yields ------ @@ -572,7 +725,17 @@ def subscribers( async for member in event.subscribers(limit=100, as_member=True): print(member.display_name) + + Using only cached subscribers (e.g., in a delete event handler): :: + + async for member in event.subscribers(limit=100, as_member=True, use_cache=True): + print(member.display_name) """ return ScheduledEventSubscribersIterator( - event=self, limit=limit, with_member=as_member, before=before, after=after + event=self, + limit=limit, + with_member=as_member, + before=before, + after=after, + use_cache=use_cache, ) diff --git a/discord/state.py b/discord/state.py index c7a29b437e..dd5676c0a2 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1705,12 +1705,12 @@ def parse_guild_scheduled_event_user_add(self, data) -> None: payload.guild = guild self.dispatch("raw_scheduled_event_user_add", payload) - member = guild.get_member(data["user_id"]) - if member is not None: - event = guild.get_scheduled_event(data["guild_scheduled_event_id"]) - if event: - event.subscriber_count += 1 - guild._add_scheduled_event(event) + user_id = int(data["user_id"]) + event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) + if event: + event._cached_subscribers.add(user_id) + member = guild.get_member(user_id) + if member is not None: self.dispatch("scheduled_event_user_add", event, member) def parse_guild_scheduled_event_user_remove(self, data) -> None: @@ -1729,12 +1729,12 @@ def parse_guild_scheduled_event_user_remove(self, data) -> None: payload.guild = guild self.dispatch("raw_scheduled_event_user_remove", payload) - member = guild.get_member(data["user_id"]) - if member is not None: - event = guild.get_scheduled_event(data["guild_scheduled_event_id"]) - if event: - event.subscriber_count += 1 - guild._add_scheduled_event(event) + user_id = int(data["user_id"]) + event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) + if event: + event._cached_subscribers.discard(user_id) + member = guild.get_member(user_id) + if member is not None: self.dispatch("scheduled_event_user_remove", event, member) def parse_guild_integrations_update(self, data) -> None: diff --git a/discord/types/scheduled_events.py b/discord/types/scheduled_events.py index 9bb4ad0328..130dc99cc6 100644 --- a/discord/types/scheduled_events.py +++ b/discord/types/scheduled_events.py @@ -31,7 +31,7 @@ from .user import User ScheduledEventStatus = Literal[1, 2, 3, 4] -ScheduledEventLocationType = Literal[1, 2, 3] +ScheduledEventEntityType = Literal[1, 2, 3] ScheduledEventPrivacyLevel = Literal[2] @@ -47,7 +47,7 @@ class ScheduledEvent(TypedDict): scheduled_end_time: str | None privacy_level: ScheduledEventPrivacyLevel status: ScheduledEventStatus - entity_type: ScheduledEventLocationType + entity_type: ScheduledEventEntityType entity_id: Snowflake entity_metadata: ScheduledEventEntityMetadata creator: User