From 43227c4086adbd5fd13238eaab04e4edbab12f3d Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:49:29 +0200 Subject: [PATCH 01/43] at first i only wanted to add cached members... --- discord/enums.py | 9 ++ discord/http.py | 1 + discord/iterators.py | 26 +++- discord/scheduled_events.py | 236 +++++++++++++++++++++++++----------- discord/state.py | 26 ++-- 5 files changed, 211 insertions(+), 87 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 63557c853b..dbecaf73a6 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -61,6 +61,7 @@ "EmbeddedActivity", "ScheduledEventStatus", "ScheduledEventPrivacyLevel", + "ScheduledEventEntityType", "ScheduledEventLocationType", "InputTextStyle", "SlashCommandOptionType", @@ -955,6 +956,14 @@ def __int__(self): return self.value +class ScheduledEventEntityType(Enum): + """Scheduled event entity type""" + + stage_instance = 1 + voice = 2 + external = 3 + + class ScheduledEventLocationType(Enum): """Scheduled event location type""" diff --git a/discord/http.py b/discord/http.py index ae64703ba6..14a4c22562 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2467,6 +2467,7 @@ def edit_scheduled_event( "status", "entity_metadata", "image", + "recurrence_rule", ) payload = {k: v for k, v in payload.items() if k in valid_keys} 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..5fab94d673 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -30,12 +30,13 @@ from . import utils from .asset import Asset from .enums import ( + ScheduledEventEntityType, ScheduledEventLocationType, 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 @@ -44,12 +45,12 @@ __all__ = ( "ScheduledEvent", "ScheduledEventLocation", + "ScheduledEventEntityMetadata", ) if TYPE_CHECKING: from .abc import Snowflake from .guild import Guild - from .iterators import AsyncIterator from .member import Member from .state import ConnectionState from .types.channel import StageChannel, VoiceChannel @@ -115,6 +116,42 @@ def type(self) -> ScheduledEventLocationType: return ScheduledEventLocationType.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, *, data: dict[str, str]) -> str | None: + self.location: str | None = data.get("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): """Represents a Discord Guild Scheduled Event. @@ -155,7 +192,7 @@ class ScheduledEvent(Hashable): 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,6 +204,14 @@ 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). + recurrence_rule: Optional[:class:`dict`] + The definition for how often this event should recur. """ __slots__ = ( @@ -182,7 +227,14 @@ class ScheduledEvent(Hashable): "guild", "_state", "_image", - "subscriber_count", + "user_count", + "_cached_subscribers", + "entity_type", + "privacy_level", + "recurrence_rule", + "channel_id", + "entity_id", + "entity_metadata", ) def __init__( @@ -209,15 +261,32 @@ def __init__( 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.recurrence_rule: dict | None = data.get("recurrence_rule", None) + 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(data=entity_metadata_data) + if entity_metadata_data + else None + ) + + self._cached_subscribers: dict[int, int] = {} + 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: + if channel_id is None and entity_metadata_data: self.location = ScheduledEventLocation( - state=state, value=entity_metadata["location"] + state=state, value=entity_metadata_data["location"] ) else: self.location = ScheduledEventLocation(state=state, value=int(channel_id)) @@ -234,7 +303,7 @@ def __repr__(self) -> str: f"end_time={self.end_time} " f"location={self.location!r} " f"status={self.status.name} " - f"subscriber_count={self.subscriber_count} " + f"user_count={self.user_count} " f"creator_id={self.creator_id}>" ) @@ -245,8 +314,8 @@ def created_at(self) -> datetime.datetime: @property 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: @@ -282,54 +351,62 @@ async def edit( name: str = MISSING, description: str = MISSING, status: int | ScheduledEventStatus = MISSING, - location: ( - str | int | VoiceChannel | StageChannel | ScheduledEventLocation - ) = MISSING, + entity_type: ScheduledEventEntityType = MISSING, start_time: datetime.datetime = MISSING, end_time: datetime.datetime = MISSING, cover: bytes | None = MISSING, image: bytes | None = MISSING, - privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, + privacy_level: ScheduledEventPrivacyLevel = MISSING, + entity_metadata: ScheduledEventEntityMetadata | None = MISSING, + recurrence_rule: dict = MISSING, ) -> ScheduledEvent | None: """|coro| - Edits the Scheduled Event's data + Edits the Scheduled Event's data. + + All parameters are optional. + + .. note:: + + When changing entity_type to EXTERNAL via entity_metadata, Discord will + automatically set ``channel_id`` to null. + + .. note:: - All parameters are optional unless ``location.type`` is - :attr:`ScheduledEventLocationType.external`, then ``end_time`` - is required. + 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. + :meth:`.cancel` to edit statuses instead. + Valid transitions: SCHEDULED → ACTIVE, ACTIVE → COMPLETED, SCHEDULED → CANCELED. + 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``. start_time: :class:`datetime.datetime` - The new starting time for the event. + The new starting time for the event (ISO8601 format). end_time: :class:`datetime.datetime` - The new ending time of the event. + 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_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. + recurrence_rule: :class:`dict` + The definition for how often this event should recur. 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. Returns ------- @@ -343,6 +420,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] = {} @@ -355,17 +434,20 @@ async def edit( if status is not MISSING: payload["status"] = int(status) + if entity_type is not MISSING: + payload["entity_type"] = int(entity_type) + if privacy_level is not MISSING: payload["privacy_level"] = int(privacy_level) - 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`" - ) + if entity_metadata is not MISSING: + if entity_metadata is None: + payload["entity_metadata"] = None else: - image = cover + payload["entity_metadata"] = entity_metadata.to_payload() + + if recurrence_rule is not MISSING: + payload["recurrence_rule"] = recurrence_rule if image is not MISSING: if image is None: @@ -373,42 +455,39 @@ async def edit( 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 start_time is not MISSING: + payload["scheduled_start_time"] = start_time.isoformat() - payload["entity_type"] = location.type.value + if end_time is not MISSING: + payload["scheduled_end_time"] = 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 = end_time is not MISSING or self.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 +594,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 +622,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 +656,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 8222f5fbe5..665c5cbd31 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1703,12 +1703,13 @@ 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[user_id] = user_id + guild._add_scheduled_event(event) + 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: @@ -1727,12 +1728,13 @@ 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.pop(user_id, None) + guild._add_scheduled_event(event) + 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: From fe47678e5ac559d1eb6cdf93e74f972f8707bb9f Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:07:30 +0200 Subject: [PATCH 02/43] correcting some bs, like some unknow type added for i dont know what reason --- discord/audit_logs.py | 34 ++++++------- discord/enums.py | 8 --- discord/guild.py | 84 +++++++++++++++++++++++-------- discord/http.py | 1 + discord/scheduled_events.py | 9 ++-- discord/types/scheduled_events.py | 4 +- 6 files changed, 85 insertions(+), 55 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index b2f6a72393..d9a1a8920c 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -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: @@ -691,7 +684,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 dbecaf73a6..a5ee990f14 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -964,14 +964,6 @@ class ScheduledEventEntityType(Enum): external = 3 -class ScheduledEventLocationType(Enum): - """Scheduled event location type""" - - stage_instance = 1 - voice = 2 - external = 3 - - class AutoModTriggerType(Enum): """Automod trigger type""" diff --git a/discord/guild.py b/discord/guild.py index f910affe5c..423c7fc43b 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -58,7 +58,7 @@ NotificationLevel, NSFWLevel, OnboardingMode, - ScheduledEventLocationType, + ScheduledEventEntityType, ScheduledEventPrivacyLevel, SortOrder, VerificationLevel, @@ -66,7 +66,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 @@ -84,7 +90,10 @@ 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, +) from .soundboard import SoundboardSound from .stage_instance import StageInstance from .sticker import GuildSticker @@ -4215,14 +4224,20 @@ async def create_scheduled_event( description: str = MISSING, start_time: datetime.datetime, end_time: datetime.datetime = MISSING, - location: str | int | VoiceChannel | StageChannel | ScheduledEventLocation, + entity_type: ScheduledEventEntityType, + entity_metadata: ScheduledEventEntityMetadata | None = MISSING, + channel_id: int = MISSING, privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, image: bytes = MISSING, + recurrence_rule: dict = 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` @@ -4233,16 +4248,23 @@ async def create_scheduled_event( A datetime object of when the scheduled event is supposed to start. 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`] The cover image of the scheduled event + recurrence_rule: Optional[:class:`dict`] + The definition for how often this event should recur. Returns ------- @@ -4255,34 +4277,52 @@ 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(), "privacy_level": int(privacy_level), + "entity_type": int(entity_type.value), } - 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: + payload["scheduled_end_time"] = 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 recurrence_rule is not MISSING: + payload["recurrence_rule"] = recurrence_rule + + 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 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/http.py b/discord/http.py index 14a4c22562..d1c831e9a4 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2428,6 +2428,7 @@ def create_scheduled_event( "entity_type", "entity_metadata", "image", + "recurrence_rule", ) payload = {k: v for k, v in payload.items() if k in valid_keys} diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 5fab94d673..fadb0d68ea 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -31,7 +31,6 @@ from .asset import Asset from .enums import ( ScheduledEventEntityType, - ScheduledEventLocationType, ScheduledEventPrivacyLevel, ScheduledEventStatus, try_enum, @@ -107,13 +106,13 @@ def __str__(self) -> str: return str(self.value) @property - def type(self) -> ScheduledEventLocationType: + def type(self) -> ScheduledEventEntityType: 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 class ScheduledEventEntityMetadata: 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 From fff9b3bd9d0b55d54fc0bf3ef2368f35935074bc Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:40:44 +0200 Subject: [PATCH 03/43] use discord variable name, try an implementation for audit logs --- discord/audit_logs.py | 24 ++++++++++++------ discord/enums.py | 2 +- discord/guild.py | 16 ++++++------ discord/scheduled_events.py | 50 ++++++++++++++++++++----------------- 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index d9a1a8920c..573427fb80 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), @@ -357,6 +357,19 @@ def __init__( if transformer: before = transformer(entry, before) + if attr == "location" and hasattr(self.before, "entity_type"): + from .scheduled_events import ScheduledEventLocation + + if ( + self.before.entity_type + is enums.ScheduledEventEntityType.external + ): + before = ScheduledEventLocation(state=state, value=before) + elif hasattr(self.before, "channel"): + before = ScheduledEventLocation( + state=state, value=self.before.channel + ) + try: after = elem["new_value"] except KeyError: @@ -365,13 +378,10 @@ def __init__( if transformer: after = transformer(entry, after) - if attr == "location" and hasattr(self.after, "location_type"): + if attr == "location" and hasattr(self.after, "entity_type"): from .scheduled_events import ScheduledEventLocation - if ( - self.after.location_type - is enums.ScheduledEventLocationType.external - ): + if self.after.entity_type is enums.ScheduledEventEntityType.external: after = ScheduledEventLocation(state=state, value=after) elif hasattr(self.after, "channel"): after = ScheduledEventLocation( diff --git a/discord/enums.py b/discord/enums.py index a5ee990f14..e4ae91737f 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -62,7 +62,7 @@ "ScheduledEventStatus", "ScheduledEventPrivacyLevel", "ScheduledEventEntityType", - "ScheduledEventLocationType", + "ScheduledEventEntityType", "InputTextStyle", "SlashCommandOptionType", "AutoModTriggerType", diff --git a/discord/guild.py b/discord/guild.py index 423c7fc43b..c4a861e86c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4222,8 +4222,8 @@ async def create_scheduled_event( *, name: str, description: str = MISSING, - start_time: datetime.datetime, - end_time: datetime.datetime = MISSING, + scheduled_start_time: datetime.datetime, + scheduled_end_time: datetime.datetime = MISSING, entity_type: ScheduledEventEntityType, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, channel_id: int = MISSING, @@ -4244,9 +4244,9 @@ async def create_scheduled_event( 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. Required for EXTERNAL events. entity_type: :class:`ScheduledEventEntityType` @@ -4282,13 +4282,13 @@ async def create_scheduled_event( """ 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.value), } - if end_time is not MISSING: - payload["scheduled_end_time"] = end_time.isoformat() + if scheduled_end_time is not MISSING: + payload["scheduled_end_time"] = scheduled_end_time.isoformat() if description is not MISSING: payload["description"] = description @@ -4308,7 +4308,7 @@ async def create_scheduled_event( raise ValidationError( "entity_metadata.location cannot be empty for EXTERNAL events." ) - if end_time is MISSING: + if scheduled_end_time is MISSING: raise ValidationError( "scheduled_end_time is required for EXTERNAL events." ) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index fadb0d68ea..60f49cbae8 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -131,8 +131,11 @@ class ScheduledEventEntityMetadata: __slots__ = ("location",) - def __init__(self, *, data: dict[str, str]) -> str | None: - self.location: str | None = data.get("location") + def __init__( + self, + location: str | None = None, + ) -> None: + self.location: str | None = location def __repr__(self) -> str: return f"" @@ -182,9 +185,9 @@ 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. @@ -217,8 +220,8 @@ class ScheduledEvent(Hashable): "id", "name", "description", - "start_time", - "end_time", + "scheduled_start_time", + "scheduled_end_time", "status", "creator_id", "creator", @@ -251,12 +254,12 @@ 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") ) @@ -272,7 +275,7 @@ def __init__( entity_metadata_data = data.get("entity_metadata") self.entity_metadata: ScheduledEventEntityMetadata | None = ( - ScheduledEventEntityMetadata(data=entity_metadata_data) + ScheduledEventEntityMetadata(location=entity_metadata_data.get("location")) if entity_metadata_data else None ) @@ -299,7 +302,7 @@ def __repr__(self) -> str: f"name={self.name} " f"description={self.description} " f"start_time={self.start_time} " - f"end_time={self.end_time} " + f"end_time={self.scheduled_end_time} " f"location={self.location!r} " f"status={self.status.name} " f"user_count={self.user_count} " @@ -351,9 +354,8 @@ async def edit( description: str = MISSING, status: int | ScheduledEventStatus = MISSING, entity_type: ScheduledEventEntityType = 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 = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, @@ -390,9 +392,9 @@ async def edit( 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``. - start_time: :class:`datetime.datetime` + scheduled_start_time: :class:`datetime.datetime` The new starting time for the event (ISO8601 format). - end_time: :class:`datetime.datetime` + 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 only GUILD_ONLY is supported. @@ -434,7 +436,7 @@ async def edit( payload["status"] = int(status) if entity_type is not MISSING: - payload["entity_type"] = int(entity_type) + payload["entity_type"] = int(entity_type.value) if privacy_level is not MISSING: payload["privacy_level"] = int(privacy_level) @@ -454,11 +456,11 @@ async def edit( else: payload["image"] = utils._bytes_to_base64_data(image) - if start_time is not MISSING: - payload["scheduled_start_time"] = start_time.isoformat() + if scheduled_start_time is not MISSING: + payload["scheduled_start_time"] = scheduled_start_time.isoformat() - if end_time is not MISSING: - payload["scheduled_end_time"] = end_time.isoformat() + if scheduled_end_time is not MISSING: + payload["scheduled_end_time"] = scheduled_end_time.isoformat() if ( entity_type is not MISSING @@ -473,7 +475,9 @@ async def edit( "entity_metadata.location cannot be empty for EXTERNAL events." ) - has_end_time = end_time is not MISSING or self.end_time is not None + 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." From 9e6a986a89e9070f8a24a5d2103595472cb5b1c7 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Tue, 9 Dec 2025 19:09:12 +0200 Subject: [PATCH 04/43] feat: add recurrence support for scheduled events with new enums and types --- discord/enums.py | 41 ++++++ discord/guild.py | 10 +- discord/scheduled_events.py | 209 +++++++++++++++++++++++++++++- discord/types/scheduled_events.py | 35 +++++ 4 files changed, 285 insertions(+), 10 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index e4ae91737f..b9fe919ede 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -63,6 +63,9 @@ "ScheduledEventPrivacyLevel", "ScheduledEventEntityType", "ScheduledEventEntityType", + "ScheduledEventRecurrenceFrequency", + "ScheduledEventRecurrenceWeekday", + "ScheduledEventRecurrenceMonth", "InputTextStyle", "SlashCommandOptionType", "AutoModTriggerType", @@ -964,6 +967,44 @@ class ScheduledEventEntityType(Enum): external = 3 +class ScheduledEventRecurrenceFrequency(Enum): + """Scheduled event recurrence frequency""" + + yearly = 0 + monthly = 1 + weekly = 2 + daily = 3 + + +class ScheduledEventRecurrenceWeekday(Enum): + """Scheduled event recurrence weekday""" + + monday = 0 + tuesday = 1 + wednesday = 2 + thursday = 3 + friday = 4 + saturday = 5 + sunday = 6 + + +class ScheduledEventRecurrenceMonth(Enum): + """Scheduled event recurrence month""" + + january = 1 + february = 2 + march = 3 + april = 4 + may = 5 + june = 6 + july = 7 + august = 8 + september = 9 + october = 10 + november = 11 + december = 12 + + class AutoModTriggerType(Enum): """Automod trigger type""" diff --git a/discord/guild.py b/discord/guild.py index c4a861e86c..20a96c059a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -93,6 +93,7 @@ from .scheduled_events import ( ScheduledEvent, ScheduledEventEntityMetadata, + ScheduledEventRecurrenceRule, ) from .soundboard import SoundboardSound from .stage_instance import StageInstance @@ -4230,7 +4231,7 @@ async def create_scheduled_event( privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, image: bytes = MISSING, - recurrence_rule: dict = MISSING, + recurrence_rule: ScheduledEventRecurrenceRule | None = MISSING, ) -> ScheduledEvent | None: """|coro| Creates a scheduled event. @@ -4263,7 +4264,7 @@ async def create_scheduled_event( The reason to show in the audit log. image: Optional[:class:`bytes`] The cover image of the scheduled event - recurrence_rule: Optional[:class:`dict`] + recurrence_rule: Optional[Union[:class:`ScheduledEventRecurrenceRule`, :class:`dict`]] The definition for how often this event should recur. Returns @@ -4297,7 +4298,10 @@ async def create_scheduled_event( payload["image"] = utils._bytes_to_base64_data(image) if recurrence_rule is not MISSING: - payload["recurrence_rule"] = recurrence_rule + if recurrence_rule is None: + payload["recurrence_rule"] = None + else: + payload["recurrence_rule"] = recurrence_rule.to_payload() if entity_type == ScheduledEventEntityType.external: if entity_metadata is MISSING or entity_metadata is None: diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 60f49cbae8..76679fbcb7 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -33,6 +33,9 @@ ScheduledEventEntityType, ScheduledEventPrivacyLevel, ScheduledEventStatus, + ScheduledEventRecurrenceFrequency, + ScheduledEventRecurrenceMonth, + ScheduledEventRecurrenceWeekday, try_enum, ) from .errors import ValidationError @@ -45,6 +48,8 @@ "ScheduledEvent", "ScheduledEventLocation", "ScheduledEventEntityMetadata", + "ScheduledEventRecurrenceRule", + "ScheduledEventRecurrenceNWeekday", ) if TYPE_CHECKING: @@ -53,7 +58,10 @@ from .member import Member from .state import ConnectionState from .types.channel import StageChannel, VoiceChannel - from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload + from .types.scheduled_events import ( + ScheduledEvent as ScheduledEventPayload, + ScheduledEventRecurrenceRule as ScheduledEventRecurrenceRulePayload, + ) MISSING = utils.MISSING @@ -154,6 +162,185 @@ def to_payload(self) -> dict[str, str]: return {"location": self.location} +class ScheduledEventRecurrenceNWeekday: + """Represents a recurrence rule n-weekday entry. + + Attributes + ---------- + n: :class:`int` + The week to reoccur on. 1 - 5. + day: :class:`ScheduledEventRecurrenceWeekday` + The day within the week to reoccur on. + """ + + __slots__ = ("n", "day") + + def __init__(self, *, n: int, day: ScheduledEventRecurrenceWeekday | int) -> None: + self.n: int = n + self.day: ScheduledEventRecurrenceWeekday = try_enum( + ScheduledEventRecurrenceWeekday, day + ) + + def __repr__(self) -> str: + return f"" + + def to_payload(self) -> dict[str, int]: + return {"n": int(self.n), "day": int(self.day)} + + +class ScheduledEventRecurrenceRule: + """Represents a recurrence rule for a scheduled event. + + Discord's recurrence rule is a subset of :mod:`dateutil.rrule` / iCalendar. + + Attributes + ---------- + start: :class:`datetime.datetime` + Starting time of the recurrence interval. + end: Optional[:class:`datetime.datetime`] + Ending time of the recurrence interval. + frequency: :class:`ScheduledEventRecurrenceFrequency` + How often the event occurs. + interval: :class:`int` + The spacing between events for the given frequency. + by_weekday: Optional[list[:class:`ScheduledEventRecurrenceWeekday`]] + Specific days within a week for the event to recur on. + by_n_weekday: Optional[list[:class:`ScheduledEventRecurrenceNWeekday`]] + Specific days within a specific week to recur on. + by_month: Optional[list[:class:`ScheduledEventRecurrenceMonth`]] + Specific months for the event to recur on. + by_month_day: Optional[list[:class:`int`]] + Specific dates within a month for the event to recur on. + by_year_day: Optional[list[:class:`int`]] + Specific day numbers within a year for the event to recur on (1-364). + count: Optional[:class:`int`] + Number of times the event can recur before stopping. + """ + + __slots__ = ( + "start", + "end", + "frequency", + "interval", + "by_weekday", + "by_n_weekday", + "by_month", + "by_month_day", + "by_year_day", + "count", + ) + + def __init__( + self, + *, + start: datetime.datetime, + frequency: ScheduledEventRecurrenceFrequency | int, + interval: int, + end: datetime.datetime | None = None, + by_weekday: list[ScheduledEventRecurrenceWeekday | int] | None = None, + by_n_weekday: list[ScheduledEventRecurrenceNWeekday | dict[str, int]] + | None = None, + by_month: list[ScheduledEventRecurrenceMonth | int] | None = None, + by_month_day: list[int] | None = None, + by_year_day: list[int] | None = None, + count: int | None = None, + ) -> None: + self.start: datetime.datetime = start + self.end: datetime.datetime | None = end + self.frequency: ScheduledEventRecurrenceFrequency = try_enum( + ScheduledEventRecurrenceFrequency, frequency + ) + self.interval: int = interval + self.by_weekday: list[ScheduledEventRecurrenceWeekday] | None = ( + [try_enum(ScheduledEventRecurrenceWeekday, day) for day in by_weekday] + if by_weekday is not None + else None + ) + if by_n_weekday is not None: + self.by_n_weekday = [ + entry + if isinstance(entry, ScheduledEventRecurrenceNWeekday) + else ScheduledEventRecurrenceNWeekday(**entry) + for entry in by_n_weekday + ] + else: + self.by_n_weekday = None + self.by_month: list[ScheduledEventRecurrenceMonth] | None = ( + [try_enum(ScheduledEventRecurrenceMonth, month) for month in by_month] + if by_month is not None + else None + ) + self.by_month_day: list[int] | None = by_month_day + self.by_year_day: list[int] | None = by_year_day + self.count: int | None = count + + def __repr__(self) -> str: + return ( + f"" + ) + + @classmethod + def from_data( + cls, data: ScheduledEventRecurrenceRulePayload + ) -> ScheduledEventRecurrenceRule: + start = utils.parse_time(data["start"]) + end = utils.parse_time(data.get("end")) + by_weekday = data.get("by_weekday") + + raw_by_n_weekday = data.get("by_n_weekday") + by_n_weekday = ( + [ScheduledEventRecurrenceNWeekday(**entry) for entry in raw_by_n_weekday] + if raw_by_n_weekday + else None + ) + + return cls( + start=start, + end=end, + frequency=data["frequency"], + interval=data["interval"], + by_weekday=by_weekday, + by_n_weekday=by_n_weekday, + by_month=data.get("by_month"), + by_month_day=data.get("by_month_day"), + by_year_day=data.get("by_year_day"), + count=data.get("count"), + ) + + def to_payload(self) -> dict[str, Any]: + payload: dict[str, Any] = { + "start": self.start.isoformat(), + "frequency": int(self.frequency), + "interval": int(self.interval), + } + + if self.end is not None: + payload["end"] = self.end.isoformat() + + if self.by_weekday is not None: + payload["by_weekday"] = [int(day) for day in self.by_weekday] + + if self.by_n_weekday is not None: + payload["by_n_weekday"] = [ + entry.to_payload() for entry in self.by_n_weekday + ] + + if self.by_month is not None: + payload["by_month"] = [int(month) for month in self.by_month] + + if self.by_month_day is not None: + payload["by_month_day"] = self.by_month_day + + if self.by_year_day is not None: + payload["by_year_day"] = self.by_year_day + + if self.count is not None: + payload["count"] = self.count + + return payload + + class ScheduledEvent(Hashable): """Represents a Discord Guild Scheduled Event. @@ -212,7 +399,7 @@ class ScheduledEvent(Hashable): 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). - recurrence_rule: Optional[:class:`dict`] + recurrence_rule: Optional[:class:`ScheduledEventRecurrenceRule`] The definition for how often this event should recur. """ @@ -269,7 +456,12 @@ def __init__( self.privacy_level: ScheduledEventPrivacyLevel = try_enum( ScheduledEventPrivacyLevel, data.get("privacy_level") ) - self.recurrence_rule: dict | None = data.get("recurrence_rule", None) + recurrence_rule_data = data.get("recurrence_rule") + self.recurrence_rule: ScheduledEventRecurrenceRule | None = ( + ScheduledEventRecurrenceRule.from_data(recurrence_rule_data) + if recurrence_rule_data + else None + ) self.channel_id: int | None = utils._get_as_snowflake(data, "channel_id") self.entity_id: int | None = utils._get_as_snowflake(data, "entity_id") @@ -301,7 +493,7 @@ def __repr__(self) -> str: f" ScheduledEvent | None: """|coro| @@ -402,7 +594,7 @@ async def edit( 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. - recurrence_rule: :class:`dict` + recurrence_rule: Union[:class:`ScheduledEventRecurrenceRule`, :class:`dict`] The definition for how often this event should recur. reason: Optional[:class:`str`] The reason to show in the audit log. @@ -448,7 +640,10 @@ async def edit( payload["entity_metadata"] = entity_metadata.to_payload() if recurrence_rule is not MISSING: - payload["recurrence_rule"] = recurrence_rule + if isinstance(recurrence_rule, ScheduledEventRecurrenceRule): + payload["recurrence_rule"] = recurrence_rule.to_payload() + else: + payload["recurrence_rule"] = recurrence_rule if image is not MISSING: if image is None: diff --git a/discord/types/scheduled_events.py b/discord/types/scheduled_events.py index 130dc99cc6..c86b6b02f5 100644 --- a/discord/types/scheduled_events.py +++ b/discord/types/scheduled_events.py @@ -33,6 +33,40 @@ ScheduledEventStatus = Literal[1, 2, 3, 4] ScheduledEventEntityType = Literal[1, 2, 3] ScheduledEventPrivacyLevel = Literal[2] +ScheduledEventRecurrenceFrequency = Literal[0, 1, 2, 3] +ScheduledEventRecurrenceWeekday = Literal[0, 1, 2, 3, 4, 5, 6] +ScheduledEventRecurrenceMonth = Literal[ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, +] + + +class ScheduledEventRecurrenceNWeekday(TypedDict): + n: int + day: ScheduledEventRecurrenceWeekday + + +class ScheduledEventRecurrenceRule(TypedDict, total=False): + start: str + end: str | None + frequency: ScheduledEventRecurrenceFrequency + interval: int + by_weekday: list[ScheduledEventRecurrenceWeekday] + by_n_weekday: list[ScheduledEventRecurrenceNWeekday] + by_month: list[ScheduledEventRecurrenceMonth] + by_month_day: list[int] + by_year_day: list[int] + count: int class ScheduledEvent(TypedDict): @@ -52,6 +86,7 @@ class ScheduledEvent(TypedDict): entity_metadata: ScheduledEventEntityMetadata creator: User user_count: int | None + recurrence_rule: ScheduledEventRecurrenceRule | None class ScheduledEventEntityMetadata(TypedDict): From 7b2c31f733129567d14f156ac72b7336348895a8 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Tue, 9 Dec 2025 20:08:17 +0200 Subject: [PATCH 05/43] refactor: remove ScheduledEventLocation class and related attributes from audit logs --- discord/audit_logs.py | 25 ---------- discord/scheduled_events.py | 95 +++++-------------------------------- 2 files changed, 12 insertions(+), 108 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 573427fb80..9a00f15715 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -357,19 +357,6 @@ def __init__( if transformer: before = transformer(entry, before) - if attr == "location" and hasattr(self.before, "entity_type"): - from .scheduled_events import ScheduledEventLocation - - if ( - self.before.entity_type - is enums.ScheduledEventEntityType.external - ): - before = ScheduledEventLocation(state=state, value=before) - elif hasattr(self.before, "channel"): - before = ScheduledEventLocation( - state=state, value=self.before.channel - ) - try: after = elem["new_value"] except KeyError: @@ -378,18 +365,6 @@ def __init__( if transformer: after = transformer(entry, after) - if attr == "location" and hasattr(self.after, "entity_type"): - from .scheduled_events import ScheduledEventLocation - - if self.after.entity_type is enums.ScheduledEventEntityType.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 diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 76679fbcb7..83918341c1 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -41,12 +41,10 @@ from .errors import ValidationError from .iterators import ScheduledEventSubscribersIterator from .mixins import Hashable -from .object import Object from .utils import warn_deprecated __all__ = ( "ScheduledEvent", - "ScheduledEventLocation", "ScheduledEventEntityMetadata", "ScheduledEventRecurrenceRule", "ScheduledEventRecurrenceNWeekday", @@ -57,7 +55,6 @@ from .guild import Guild from .member import Member from .state import ConnectionState - from .types.channel import StageChannel, VoiceChannel from .types.scheduled_events import ( ScheduledEvent as ScheduledEventPayload, ScheduledEventRecurrenceRule as ScheduledEventRecurrenceRulePayload, @@ -66,63 +63,6 @@ MISSING = utils.MISSING -class ScheduledEventLocation: - """Represents a scheduled event's location. - - Setting the ``value`` to its corresponding type will set the location type automatically: - - +------------------------+---------------------------------------------------+ - | Type of Input | Location Type | - +========================+===================================================+ - | :class:`StageChannel` | :attr:`ScheduledEventLocationType.stage_instance` | - | :class:`VoiceChannel` | :attr:`ScheduledEventLocationType.voice` | - | :class:`str` | :attr:`ScheduledEventLocationType.external` | - +------------------------+---------------------------------------------------+ - - .. versionadded:: 2.0 - - Attributes - ---------- - value: Union[:class:`str`, :class:`StageChannel`, :class:`VoiceChannel`, :class:`Object`] - The actual location of the scheduled event. - type: :class:`ScheduledEventLocationType` - The type of location. - """ - - __slots__ = ( - "_state", - "value", - ) - - 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)) - else: - self.value = value - - def __repr__(self) -> str: - return f"" - - def __str__(self) -> str: - return str(self.value) - - @property - def type(self) -> ScheduledEventEntityType: - if isinstance(self.value, str): - return ScheduledEventEntityType.external - elif self.value.__class__.__name__ == "StageChannel": - return ScheduledEventEntityType.stage_instance - elif self.value.__class__.__name__ == "VoiceChannel": - return ScheduledEventEntityType.voice - - class ScheduledEventEntityMetadata: """Represents a scheduled event's entity metadata. @@ -185,7 +125,7 @@ def __repr__(self) -> str: return f"" def to_payload(self) -> dict[str, int]: - return {"n": int(self.n), "day": int(self.day)} + return {"n": int(self.n), "day": int(self.day.value)} class ScheduledEventRecurrenceRule: @@ -234,7 +174,7 @@ def __init__( self, *, start: datetime.datetime, - frequency: ScheduledEventRecurrenceFrequency | int, + frequency: ScheduledEventRecurrenceFrequency, interval: int, end: datetime.datetime | None = None, by_weekday: list[ScheduledEventRecurrenceWeekday | int] | None = None, @@ -247,9 +187,7 @@ def __init__( ) -> None: self.start: datetime.datetime = start self.end: datetime.datetime | None = end - self.frequency: ScheduledEventRecurrenceFrequency = try_enum( - ScheduledEventRecurrenceFrequency, frequency - ) + self.frequency: ScheduledEventRecurrenceFrequency = frequency self.interval: int = interval self.by_weekday: list[ScheduledEventRecurrenceWeekday] | None = ( [try_enum(ScheduledEventRecurrenceWeekday, day) for day in by_weekday] @@ -311,7 +249,7 @@ def from_data( def to_payload(self) -> dict[str, Any]: payload: dict[str, Any] = { "start": self.start.isoformat(), - "frequency": int(self.frequency), + "frequency": int(self.frequency.value), "interval": int(self.interval), } @@ -319,7 +257,7 @@ def to_payload(self) -> dict[str, Any]: payload["end"] = self.end.isoformat() if self.by_weekday is not None: - payload["by_weekday"] = [int(day) for day in self.by_weekday] + payload["by_weekday"] = [int(day.value) for day in self.by_weekday] if self.by_n_weekday is not None: payload["by_n_weekday"] = [ @@ -327,7 +265,7 @@ def to_payload(self) -> dict[str, Any]: ] if self.by_month is not None: - payload["by_month"] = [int(month) for month in self.by_month] + payload["by_month"] = [int(month.value) for month in self.by_month] if self.by_month_day is not None: payload["by_month_day"] = self.by_month_day @@ -378,9 +316,6 @@ class ScheduledEvent(Hashable): 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. user_count: :class:`int` The number of users that have marked themselves as interested in the event. creator_id: Optional[:class:`int`] @@ -476,14 +411,7 @@ def __init__( 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 - - channel_id = data.get("channel_id", None) - if channel_id is None and entity_metadata_data: - self.location = ScheduledEventLocation( - state=state, value=entity_metadata_data["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 @@ -499,6 +427,7 @@ def __repr__(self) -> str: f"status={self.status.name} " f"user_count={self.user_count} " f"creator_id={self.creator_id}>" + f"channel_id={self.channel_id}>" ) @property @@ -544,14 +473,14 @@ async def edit( reason: str | None = None, name: str = MISSING, description: str = MISSING, - status: int | ScheduledEventStatus = MISSING, + status: ScheduledEventStatus = MISSING, entity_type: ScheduledEventEntityType = MISSING, scheduled_start_time: datetime.datetime = MISSING, scheduled_end_time: datetime.datetime = MISSING, image: bytes | None = MISSING, privacy_level: ScheduledEventPrivacyLevel = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, - recurrence_rule: ScheduledEventRecurrenceRule | dict = MISSING, + recurrence_rule: ScheduledEventRecurrenceRule | None = MISSING, ) -> ScheduledEvent | None: """|coro| @@ -625,13 +554,13 @@ async def edit( payload["description"] = description if status is not MISSING: - payload["status"] = int(status) + payload["status"] = int(status.value) if entity_type is not MISSING: payload["entity_type"] = int(entity_type.value) if privacy_level is not MISSING: - payload["privacy_level"] = int(privacy_level) + payload["privacy_level"] = int(privacy_level.value) if entity_metadata is not MISSING: if entity_metadata is None: From 5a2581e07e1fb2f430aaa7c85d6d6bdac38729a2 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:29:34 +0200 Subject: [PATCH 06/43] feat: enhance scheduled event recurrence with validation and serialization tests --- discord/enums.py | 12 +++ discord/guild.py | 2 +- discord/scheduled_events.py | 186 ++++++++++++++++++++++++++++++------ 3 files changed, 171 insertions(+), 29 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index b9fe919ede..6dfb771f2a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -966,6 +966,9 @@ class ScheduledEventEntityType(Enum): voice = 2 external = 3 + def __int__(self): + return self.value + class ScheduledEventRecurrenceFrequency(Enum): """Scheduled event recurrence frequency""" @@ -975,6 +978,9 @@ class ScheduledEventRecurrenceFrequency(Enum): weekly = 2 daily = 3 + def __int__(self): + return self.value + class ScheduledEventRecurrenceWeekday(Enum): """Scheduled event recurrence weekday""" @@ -987,6 +993,9 @@ class ScheduledEventRecurrenceWeekday(Enum): saturday = 5 sunday = 6 + def __int__(self): + return self.value + class ScheduledEventRecurrenceMonth(Enum): """Scheduled event recurrence month""" @@ -1004,6 +1013,9 @@ class ScheduledEventRecurrenceMonth(Enum): november = 11 december = 12 + def __int__(self): + return self.value + class AutoModTriggerType(Enum): """Automod trigger type""" diff --git a/discord/guild.py b/discord/guild.py index 20a96c059a..af7405bea2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4285,7 +4285,7 @@ async def create_scheduled_event( "name": name, "scheduled_start_time": scheduled_start_time.isoformat(), "privacy_level": int(privacy_level), - "entity_type": int(entity_type.value), + "entity_type": int(entity_type), } if scheduled_end_time is not MISSING: diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 83918341c1..64100707dd 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -118,14 +118,14 @@ class ScheduledEventRecurrenceNWeekday: def __init__(self, *, n: int, day: ScheduledEventRecurrenceWeekday | int) -> None: self.n: int = n self.day: ScheduledEventRecurrenceWeekday = try_enum( - ScheduledEventRecurrenceWeekday, day + ScheduledEventRecurrenceWeekday, int(day) ) def __repr__(self) -> str: return f"" def to_payload(self) -> dict[str, int]: - return {"n": int(self.n), "day": int(self.day.value)} + return {"n": int(self.n), "day": int(self.day)} class ScheduledEventRecurrenceRule: @@ -174,12 +174,11 @@ def __init__( self, *, start: datetime.datetime, - frequency: ScheduledEventRecurrenceFrequency, + frequency: ScheduledEventRecurrenceFrequency | int, interval: int, end: datetime.datetime | None = None, by_weekday: list[ScheduledEventRecurrenceWeekday | int] | None = None, - by_n_weekday: list[ScheduledEventRecurrenceNWeekday | dict[str, int]] - | None = None, + by_n_weekday: list[ScheduledEventRecurrenceNWeekday] | None = None, by_month: list[ScheduledEventRecurrenceMonth | int] | None = None, by_month_day: list[int] | None = None, by_year_day: list[int] | None = None, @@ -187,25 +186,19 @@ def __init__( ) -> None: self.start: datetime.datetime = start self.end: datetime.datetime | None = end - self.frequency: ScheduledEventRecurrenceFrequency = frequency + self.frequency: ScheduledEventRecurrenceFrequency = try_enum( + ScheduledEventRecurrenceFrequency, int(frequency) + ) self.interval: int = interval self.by_weekday: list[ScheduledEventRecurrenceWeekday] | None = ( - [try_enum(ScheduledEventRecurrenceWeekday, day) for day in by_weekday] - if by_weekday is not None + [try_enum(ScheduledEventRecurrenceWeekday, int(day)) for day in by_weekday] + if by_weekday else None ) - if by_n_weekday is not None: - self.by_n_weekday = [ - entry - if isinstance(entry, ScheduledEventRecurrenceNWeekday) - else ScheduledEventRecurrenceNWeekday(**entry) - for entry in by_n_weekday - ] - else: - self.by_n_weekday = None + self.by_n_weekday: list[ScheduledEventRecurrenceNWeekday] | None = by_n_weekday self.by_month: list[ScheduledEventRecurrenceMonth] | None = ( - [try_enum(ScheduledEventRecurrenceMonth, month) for month in by_month] - if by_month is not None + [try_enum(ScheduledEventRecurrenceMonth, int(month)) for month in by_month] + if by_month else None ) self.by_month_day: list[int] | None = by_month_day @@ -224,7 +217,13 @@ def from_data( ) -> ScheduledEventRecurrenceRule: start = utils.parse_time(data["start"]) end = utils.parse_time(data.get("end")) - by_weekday = data.get("by_weekday") + + raw_by_weekday = data.get("by_weekday") + by_weekday = ( + [try_enum(ScheduledEventRecurrenceWeekday, day) for day in raw_by_weekday] + if raw_by_weekday + else None + ) raw_by_n_weekday = data.get("by_n_weekday") by_n_weekday = ( @@ -233,23 +232,44 @@ def from_data( else None ) + raw_by_month = data.get("by_month") + by_month = ( + [try_enum(ScheduledEventRecurrenceMonth, month) for month in raw_by_month] + if raw_by_month + else None + ) + return cls( start=start, end=end, - frequency=data["frequency"], + frequency=try_enum(ScheduledEventRecurrenceFrequency, data["frequency"]), interval=data["interval"], by_weekday=by_weekday, by_n_weekday=by_n_weekday, - by_month=data.get("by_month"), + by_month=by_month, by_month_day=data.get("by_month_day"), by_year_day=data.get("by_year_day"), count=data.get("count"), ) def to_payload(self) -> dict[str, Any]: + """Convert the recurrence rule to an API payload. + + Raises + ------ + ValidationError + If the recurrence rule violates Discord's system limitations. + + Returns + ------- + dict[str, Any] + The recurrence rule as a dictionary suitable for the Discord API. + """ + self.validate() + payload: dict[str, Any] = { "start": self.start.isoformat(), - "frequency": int(self.frequency.value), + "frequency": self.frequency.value, "interval": int(self.interval), } @@ -257,7 +277,7 @@ def to_payload(self) -> dict[str, Any]: payload["end"] = self.end.isoformat() if self.by_weekday is not None: - payload["by_weekday"] = [int(day.value) for day in self.by_weekday] + payload["by_weekday"] = [int(day) for day in self.by_weekday] if self.by_n_weekday is not None: payload["by_n_weekday"] = [ @@ -265,7 +285,7 @@ def to_payload(self) -> dict[str, Any]: ] if self.by_month is not None: - payload["by_month"] = [int(month.value) for month in self.by_month] + payload["by_month"] = [int(month) for month in self.by_month] if self.by_month_day is not None: payload["by_month_day"] = self.by_month_day @@ -278,6 +298,116 @@ def to_payload(self) -> dict[str, Any]: return payload + def validate(self) -> None: + """Validate the recurrence rule against Discord's system limitations. + + Raises + ------ + ValidationError + If the recurrence rule violates any system limitations. + """ + # Mutually exclusive combinations + has_by_weekday = self.by_weekday is not None + has_by_n_weekday = self.by_n_weekday is not None + has_by_month = self.by_month is not None + has_by_month_day = self.by_month_day is not None + + if has_by_weekday and has_by_n_weekday: + raise ValidationError("by_weekday and by_n_weekday are mutually exclusive") + + if has_by_month and has_by_n_weekday: + raise ValidationError("by_month and by_n_weekday are mutually exclusive") + + if has_by_month != has_by_month_day: + raise ValidationError( + "by_month and by_month_day must both be provided together" + ) + + # Daily frequency (0) constraints + if self.frequency == ScheduledEventRecurrenceFrequency.yearly: + if has_by_weekday: + raise ValidationError("by_weekday is not valid for yearly events") + + # Weekly frequency (2) constraints + if self.frequency == ScheduledEventRecurrenceFrequency.weekly: + if has_by_weekday: + if len(self.by_weekday) != 1: + raise ValidationError( + "by_weekday must have exactly 1 day for weekly events" + ) + + if has_by_n_weekday: + raise ValidationError("by_n_weekday is not valid for weekly events") + + if has_by_month or has_by_month_day: + raise ValidationError( + "by_month and by_month_day are not valid for weekly events" + ) + + # interval can only be 2 (every-other week) or 1 (weekly) + if self.interval not in (1, 2): + raise ValidationError("interval for weekly events can only be 1 or 2") + + # Daily frequency (3) constraints + if self.frequency == ScheduledEventRecurrenceFrequency.daily: + if has_by_n_weekday: + raise ValidationError("by_n_weekday is not valid for daily events") + + if has_by_month or has_by_month_day: + raise ValidationError( + "by_month and by_month_day are not valid for daily events" + ) + + if has_by_weekday: + # Validate known sets of weekdays for daily events + allowed_sets = [ + [0, 1, 2, 3, 4], # Monday - Friday + [1, 2, 3, 4, 5], # Tuesday - Saturday + [6, 0, 1, 2, 3], # Sunday - Thursday + [4, 5], # Friday & Saturday + [5, 6], # Saturday & Sunday + [6, 0], # Sunday & Monday + ] + weekday_values = [day.value for day in self.by_weekday] + weekday_values.sort() + + if weekday_values not in allowed_sets: + raise ValidationError( + "by_weekday for daily events must be one of the allowed sets: " + "[0,1,2,3,4], [1,2,3,4,5], [6,0,1,2,3], [4,5], [5,6], [6,0]" + ) + + # Monthly frequency (1) constraints + if self.frequency == ScheduledEventRecurrenceFrequency.monthly: + if has_by_n_weekday: + if len(self.by_n_weekday) != 1: + raise ValidationError( + "by_n_weekday must have exactly 1 entry for monthly events" + ) + + if has_by_weekday: + raise ValidationError("by_weekday is not valid for monthly events") + + if has_by_month or has_by_month_day: + raise ValidationError( + "by_month and by_month_day are not valid for monthly events" + ) + + # Yearly frequency (0) constraints + if self.frequency == ScheduledEventRecurrenceFrequency.yearly: + if has_by_n_weekday: + raise ValidationError("by_n_weekday is not valid for yearly events") + + if not (has_by_month and has_by_month_day): + raise ValidationError( + "by_month and by_month_day must both be provided for yearly events" + ) + + if len(self.by_month) != 1 or len(self.by_month_day) != 1: + raise ValidationError( + "by_month and by_month_day must each have exactly 1 entry for yearly events" + ) + class ScheduledEvent(Hashable): """Represents a Discord Guild Scheduled Event. @@ -554,13 +684,13 @@ async def edit( payload["description"] = description if status is not MISSING: - payload["status"] = int(status.value) + payload["status"] = int(status) if entity_type is not MISSING: - payload["entity_type"] = int(entity_type.value) + payload["entity_type"] = int(entity_type) if privacy_level is not MISSING: - payload["privacy_level"] = int(privacy_level.value) + payload["privacy_level"] = int(privacy_level) if entity_metadata is not MISSING: if entity_metadata is None: From 9d1d5d7fb52863726715921c6c6b5e8078f25aaa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 19:51:55 +0000 Subject: [PATCH 07/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/scheduled_events.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 64100707dd..ac1138db08 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -32,10 +32,10 @@ from .enums import ( ScheduledEventEntityType, ScheduledEventPrivacyLevel, - ScheduledEventStatus, ScheduledEventRecurrenceFrequency, ScheduledEventRecurrenceMonth, ScheduledEventRecurrenceWeekday, + ScheduledEventStatus, try_enum, ) from .errors import ValidationError @@ -55,8 +55,8 @@ from .guild import Guild from .member import Member from .state import ConnectionState + from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload from .types.scheduled_events import ( - ScheduledEvent as ScheduledEventPayload, ScheduledEventRecurrenceRule as ScheduledEventRecurrenceRulePayload, ) @@ -255,15 +255,15 @@ def from_data( def to_payload(self) -> dict[str, Any]: """Convert the recurrence rule to an API payload. - Raises - ------ - ValidationError - If the recurrence rule violates Discord's system limitations. - Returns ------- dict[str, Any] The recurrence rule as a dictionary suitable for the Discord API. + + Raises + ------ + ValidationError + If the recurrence rule violates Discord's system limitations. """ self.validate() From 8c85a05f17ef1aee8396e0b57b90db138fc33799 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:39:25 +0200 Subject: [PATCH 08/43] revert breaking change --- discord/enums.py | 7 +- discord/scheduled_events.py | 149 +++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 6dfb771f2a..f48d863a2e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -30,6 +30,7 @@ from enum import IntEnum from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Union + __all__ = ( "Enum", "ChannelType", @@ -62,7 +63,7 @@ "ScheduledEventStatus", "ScheduledEventPrivacyLevel", "ScheduledEventEntityType", - "ScheduledEventEntityType", + "ScheduledEventLocationType", "ScheduledEventRecurrenceFrequency", "ScheduledEventRecurrenceWeekday", "ScheduledEventRecurrenceMonth", @@ -970,6 +971,10 @@ def __int__(self): return self.value +class ScheduledEventLocationType(ScheduledEventEntityType): + """Scheduled event location type (deprecated alias for ScheduledEventEntityType)""" + + class ScheduledEventRecurrenceFrequency(Enum): """Scheduled event recurrence frequency""" diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index ac1138db08..e8fb5156a1 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -41,10 +41,12 @@ from .errors import ValidationError from .iterators import ScheduledEventSubscribersIterator from .mixins import Hashable -from .utils import warn_deprecated +from .object import Object +from .utils import warn_deprecated, deprecated __all__ = ( "ScheduledEvent", + "ScheduledEventLocation", "ScheduledEventEntityMetadata", "ScheduledEventRecurrenceRule", "ScheduledEventRecurrenceNWeekday", @@ -52,6 +54,7 @@ if TYPE_CHECKING: from .abc import Snowflake + from .channel import StageChannel, VoiceChannel from .guild import Guild from .member import Member from .state import ConnectionState @@ -59,10 +62,83 @@ from .types.scheduled_events import ( ScheduledEventRecurrenceRule as ScheduledEventRecurrenceRulePayload, ) +else: + ConnectionState = None + StageChannel = None + VoiceChannel = None MISSING = utils.MISSING +class ScheduledEventLocation: + """Represents a scheduled event's location. + + Setting the ``value`` to its corresponding type will set the location type automatically: + + +------------------------+-----------------------------------------------+ + | Type of Input | Location Type | + +========================+===============================================+ + | :class:`StageChannel` | :attr:`ScheduledEventEntityType.stage_instance` | + | :class:`VoiceChannel` | :attr:`ScheduledEventEntityType.voice` | + | :class:`str` | :attr:`ScheduledEventEntityType.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:`ScheduledEventEntityType` + The type of location. + """ + + __slots__ = ( + "_state", + "value", + ) + + def __init__( + self, + *, + 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 + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(self.value) if self.value else "" + + @property + def type(self) -> ScheduledEventEntityType: + """The type of location.""" + if isinstance(self.value, str): + return ScheduledEventEntityType.external + elif self.value.__class__.__name__ == "StageChannel": + return ScheduledEventEntityType.stage_instance + elif self.value.__class__.__name__ == "VoiceChannel": + return ScheduledEventEntityType.voice + return ScheduledEventEntityType.voice + + class ScheduledEventEntityMetadata: """Represents a scheduled event's entity metadata. @@ -477,7 +553,6 @@ class ScheduledEvent(Hashable): "status", "creator_id", "creator", - "location", "guild", "_state", "_image", @@ -560,11 +635,59 @@ def __repr__(self) -> str: 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 def interested(self) -> int | None: """An alias to :attr:`.user_count`""" @@ -604,10 +727,14 @@ async def edit( name: str = MISSING, description: str = MISSING, status: ScheduledEventStatus = MISSING, + location: ( + str | int | VoiceChannel | StageChannel | ScheduledEventLocation + ) = MISSING, entity_type: ScheduledEventEntityType = MISSING, scheduled_start_time: datetime.datetime = MISSING, scheduled_end_time: datetime.datetime = MISSING, image: bytes | None = MISSING, + cover: bytes | None = MISSING, privacy_level: ScheduledEventPrivacyLevel = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, recurrence_rule: ScheduledEventRecurrenceRule | None = MISSING, @@ -659,6 +786,11 @@ async def edit( 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 ``image`` instead. Returns ------- @@ -704,6 +836,19 @@ async def edit( else: payload["recurrence_rule"] = recurrence_rule + if cover is not MISSING: + warn_deprecated("cover", "image", "2.7", "3.0") + if image is MISSING: + image = cover + + 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 location.type == ScheduledEventEntityType.external: + entity_metadata = ScheduledEventEntityMetadata(str(location)) + if image is not MISSING: if image is None: payload["image"] = None From 40d79b6564eb373b55b3a531b02f83f33fe58cc9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:39:57 +0000 Subject: [PATCH 09/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/enums.py | 1 - discord/scheduled_events.py | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index f48d863a2e..85315040d8 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -30,7 +30,6 @@ from enum import IntEnum from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Union - __all__ = ( "Enum", "ChannelType", diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index e8fb5156a1..8a09f01c45 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -42,7 +42,7 @@ from .iterators import ScheduledEventSubscribersIterator from .mixins import Hashable from .object import Object -from .utils import warn_deprecated, deprecated +from .utils import deprecated, warn_deprecated __all__ = ( "ScheduledEvent", @@ -638,9 +638,7 @@ def __repr__(self) -> str: @property @deprecated(instead="entity_metadata.location", since="2.7", removed="3.0") def location(self) -> ScheduledEventLocation | None: - """ - Returns the location of the event. - """ + """Returns the location of the event.""" if self.channel_id is None: self.location = ScheduledEventLocation( state=self._state, value=self.entity_metadata.location From b5dc3e9171473eeb86695eb6fa9aaa579e166b19 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 12 Dec 2025 07:39:55 +0100 Subject: [PATCH 10/43] Update discord/enums.py Co-authored-by: Paillat Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/enums.py b/discord/enums.py index 85315040d8..7bceb5619e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -971,7 +971,7 @@ def __int__(self): class ScheduledEventLocationType(ScheduledEventEntityType): - """Scheduled event location type (deprecated alias for ScheduledEventEntityType)""" + """Scheduled event location type (deprecated alias for :class:`ScheduledEventEntityType`)""" class ScheduledEventRecurrenceFrequency(Enum): From 13420ce98d0875fb264f0862a612b45483c43639 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:20:44 +0200 Subject: [PATCH 11/43] paillat comment --- discord/enums.py | 10 ++++++++++ discord/scheduled_events.py | 17 +++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 7bceb5619e..e17a8823c5 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1021,6 +1021,16 @@ def __int__(self): return self.value +class ScheduledEventRecurrenceInterval(Enum): + """Scheduled event recurrence interval spacing""" + + single = 1 + every_other = 2 + + def __int__(self): + return self.value + + class AutoModTriggerType(Enum): """Automod trigger type""" diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 8a09f01c45..b25c27c448 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -33,6 +33,7 @@ ScheduledEventEntityType, ScheduledEventPrivacyLevel, ScheduledEventRecurrenceFrequency, + ScheduledEventRecurrenceInterval, ScheduledEventRecurrenceMonth, ScheduledEventRecurrenceWeekday, ScheduledEventStatus, @@ -217,7 +218,7 @@ class ScheduledEventRecurrenceRule: Ending time of the recurrence interval. frequency: :class:`ScheduledEventRecurrenceFrequency` How often the event occurs. - interval: :class:`int` + interval: :class:`ScheduledEventRecurrenceInterval` The spacing between events for the given frequency. by_weekday: Optional[list[:class:`ScheduledEventRecurrenceWeekday`]] Specific days within a week for the event to recur on. @@ -251,7 +252,7 @@ def __init__( *, start: datetime.datetime, frequency: ScheduledEventRecurrenceFrequency | int, - interval: int, + interval: ScheduledEventRecurrenceInterval | int, end: datetime.datetime | None = None, by_weekday: list[ScheduledEventRecurrenceWeekday | int] | None = None, by_n_weekday: list[ScheduledEventRecurrenceNWeekday] | None = None, @@ -265,7 +266,9 @@ def __init__( self.frequency: ScheduledEventRecurrenceFrequency = try_enum( ScheduledEventRecurrenceFrequency, int(frequency) ) - self.interval: int = interval + self.interval: ScheduledEventRecurrenceInterval = try_enum( + ScheduledEventRecurrenceInterval, int(interval) + ) self.by_weekday: list[ScheduledEventRecurrenceWeekday] | None = ( [try_enum(ScheduledEventRecurrenceWeekday, int(day)) for day in by_weekday] if by_weekday @@ -319,7 +322,7 @@ def from_data( start=start, end=end, frequency=try_enum(ScheduledEventRecurrenceFrequency, data["frequency"]), - interval=data["interval"], + interval=try_enum(ScheduledEventRecurrenceInterval, data["interval"]), by_weekday=by_weekday, by_n_weekday=by_n_weekday, by_month=by_month, @@ -784,12 +787,6 @@ async def edit( 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 ``image`` instead. - Returns ------- Optional[:class:`.ScheduledEvent`] From 469a7b53ae5afb0cca9009e0695d18c85dbaf535 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:21:35 +0000 Subject: [PATCH 12/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/scheduled_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index b25c27c448..814a2c37c9 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -787,6 +787,7 @@ async def edit( The reason to show in the audit log. image: Optional[:class:`bytes`] The cover image of the scheduled event. + Returns ------- Optional[:class:`.ScheduledEvent`] From a2f09fe2c253e389469fb184809178a33c47e89a Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:19:22 +0200 Subject: [PATCH 13/43] feat: add overloads for ScheduledEventRecurrenceRule constructor --- discord/scheduled_events.py | 50 ++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 814a2c37c9..955177f26f 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -25,7 +25,7 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, overload from . import utils from .asset import Asset @@ -247,6 +247,54 @@ class ScheduledEventRecurrenceRule: "count", ) + @overload + def __init__( + self, + *, + start: datetime.datetime, + frequency: ScheduledEventRecurrenceFrequency | int, + interval: ScheduledEventRecurrenceInterval | int, + end: datetime.datetime | None = None, + by_weekday: list[ScheduledEventRecurrenceWeekday | int], + by_n_weekday: None = None, + by_month: None = None, + by_month_day: None = None, + by_year_day: list[int] | None = None, + count: int | None = None, + ) -> None: ... + + @overload + def __init__( + self, + *, + start: datetime.datetime, + frequency: ScheduledEventRecurrenceFrequency | int, + interval: ScheduledEventRecurrenceInterval | int, + end: datetime.datetime | None = None, + by_weekday: None = None, + by_n_weekday: list[ScheduledEventRecurrenceNWeekday], + by_month: None = None, + by_month_day: None = None, + by_year_day: list[int] | None = None, + count: int | None = None, + ) -> None: ... + + @overload + def __init__( + self, + *, + start: datetime.datetime, + frequency: ScheduledEventRecurrenceFrequency | int, + interval: ScheduledEventRecurrenceInterval | int, + end: datetime.datetime | None = None, + by_weekday: None = None, + by_n_weekday: None = None, + by_month: list[ScheduledEventRecurrenceMonth | int], + by_month_day: list[int], + by_year_day: list[int] | None = None, + count: int | None = None, + ) -> None: ... + def __init__( self, *, From 6dfd511d0168f473cf223121433e358802b0137a Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:26:36 +0200 Subject: [PATCH 14/43] =?UTF-8?q?refactor:=20=F0=9F=97=91=EF=B8=8F=20Remov?= =?UTF-8?q?e=20scheduled=20event=20recurrence=20classes=20and=20related=20?= =?UTF-8?q?attributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discord/enums.py | 60 ----- discord/guild.py | 10 - discord/http.py | 2 - discord/scheduled_events.py | 386 +----------------------------- discord/types/scheduled_events.py | 54 ----- 5 files changed, 1 insertion(+), 511 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index e17a8823c5..55b420733a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -63,9 +63,6 @@ "ScheduledEventPrivacyLevel", "ScheduledEventEntityType", "ScheduledEventLocationType", - "ScheduledEventRecurrenceFrequency", - "ScheduledEventRecurrenceWeekday", - "ScheduledEventRecurrenceMonth", "InputTextStyle", "SlashCommandOptionType", "AutoModTriggerType", @@ -974,63 +971,6 @@ class ScheduledEventLocationType(ScheduledEventEntityType): """Scheduled event location type (deprecated alias for :class:`ScheduledEventEntityType`)""" -class ScheduledEventRecurrenceFrequency(Enum): - """Scheduled event recurrence frequency""" - - yearly = 0 - monthly = 1 - weekly = 2 - daily = 3 - - def __int__(self): - return self.value - - -class ScheduledEventRecurrenceWeekday(Enum): - """Scheduled event recurrence weekday""" - - monday = 0 - tuesday = 1 - wednesday = 2 - thursday = 3 - friday = 4 - saturday = 5 - sunday = 6 - - def __int__(self): - return self.value - - -class ScheduledEventRecurrenceMonth(Enum): - """Scheduled event recurrence month""" - - january = 1 - february = 2 - march = 3 - april = 4 - may = 5 - june = 6 - july = 7 - august = 8 - september = 9 - october = 10 - november = 11 - december = 12 - - def __int__(self): - return self.value - - -class ScheduledEventRecurrenceInterval(Enum): - """Scheduled event recurrence interval spacing""" - - single = 1 - every_other = 2 - - def __int__(self): - return self.value - - class AutoModTriggerType(Enum): """Automod trigger type""" diff --git a/discord/guild.py b/discord/guild.py index 27e5d56f77..7c532b7bcb 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -96,7 +96,6 @@ from .scheduled_events import ( ScheduledEvent, ScheduledEventEntityMetadata, - ScheduledEventRecurrenceRule, ) from .soundboard import SoundboardSound from .stage_instance import StageInstance @@ -4299,7 +4298,6 @@ async def create_scheduled_event( privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, image: bytes = MISSING, - recurrence_rule: ScheduledEventRecurrenceRule | None = MISSING, ) -> ScheduledEvent | None: """|coro| Creates a scheduled event. @@ -4332,8 +4330,6 @@ async def create_scheduled_event( The reason to show in the audit log. image: Optional[:class:`bytes`] The cover image of the scheduled event - recurrence_rule: Optional[Union[:class:`ScheduledEventRecurrenceRule`, :class:`dict`]] - The definition for how often this event should recur. Returns ------- @@ -4365,12 +4361,6 @@ async def create_scheduled_event( if image is not MISSING: payload["image"] = utils._bytes_to_base64_data(image) - if recurrence_rule is not MISSING: - if recurrence_rule is None: - payload["recurrence_rule"] = None - else: - payload["recurrence_rule"] = recurrence_rule.to_payload() - if entity_type == ScheduledEventEntityType.external: if entity_metadata is MISSING or entity_metadata is None: raise ValidationError( diff --git a/discord/http.py b/discord/http.py index 746aeb8bfb..2ea80b292b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2398,7 +2398,6 @@ def create_scheduled_event( "entity_type", "entity_metadata", "image", - "recurrence_rule", ) payload = {k: v for k, v in payload.items() if k in valid_keys} @@ -2438,7 +2437,6 @@ def edit_scheduled_event( "status", "entity_metadata", "image", - "recurrence_rule", ) payload = {k: v for k, v in payload.items() if k in valid_keys} diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 955177f26f..4ce83a5234 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -25,17 +25,13 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Any, overload +from typing import TYPE_CHECKING, Any from . import utils from .asset import Asset from .enums import ( ScheduledEventEntityType, ScheduledEventPrivacyLevel, - ScheduledEventRecurrenceFrequency, - ScheduledEventRecurrenceInterval, - ScheduledEventRecurrenceMonth, - ScheduledEventRecurrenceWeekday, ScheduledEventStatus, try_enum, ) @@ -49,8 +45,6 @@ "ScheduledEvent", "ScheduledEventLocation", "ScheduledEventEntityMetadata", - "ScheduledEventRecurrenceRule", - "ScheduledEventRecurrenceNWeekday", ) if TYPE_CHECKING: @@ -60,9 +54,6 @@ from .member import Member from .state import ConnectionState from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload - from .types.scheduled_events import ( - ScheduledEventRecurrenceRule as ScheduledEventRecurrenceRulePayload, - ) else: ConnectionState = None StageChannel = None @@ -179,363 +170,6 @@ def to_payload(self) -> dict[str, str]: return {"location": self.location} -class ScheduledEventRecurrenceNWeekday: - """Represents a recurrence rule n-weekday entry. - - Attributes - ---------- - n: :class:`int` - The week to reoccur on. 1 - 5. - day: :class:`ScheduledEventRecurrenceWeekday` - The day within the week to reoccur on. - """ - - __slots__ = ("n", "day") - - def __init__(self, *, n: int, day: ScheduledEventRecurrenceWeekday | int) -> None: - self.n: int = n - self.day: ScheduledEventRecurrenceWeekday = try_enum( - ScheduledEventRecurrenceWeekday, int(day) - ) - - def __repr__(self) -> str: - return f"" - - def to_payload(self) -> dict[str, int]: - return {"n": int(self.n), "day": int(self.day)} - - -class ScheduledEventRecurrenceRule: - """Represents a recurrence rule for a scheduled event. - - Discord's recurrence rule is a subset of :mod:`dateutil.rrule` / iCalendar. - - Attributes - ---------- - start: :class:`datetime.datetime` - Starting time of the recurrence interval. - end: Optional[:class:`datetime.datetime`] - Ending time of the recurrence interval. - frequency: :class:`ScheduledEventRecurrenceFrequency` - How often the event occurs. - interval: :class:`ScheduledEventRecurrenceInterval` - The spacing between events for the given frequency. - by_weekday: Optional[list[:class:`ScheduledEventRecurrenceWeekday`]] - Specific days within a week for the event to recur on. - by_n_weekday: Optional[list[:class:`ScheduledEventRecurrenceNWeekday`]] - Specific days within a specific week to recur on. - by_month: Optional[list[:class:`ScheduledEventRecurrenceMonth`]] - Specific months for the event to recur on. - by_month_day: Optional[list[:class:`int`]] - Specific dates within a month for the event to recur on. - by_year_day: Optional[list[:class:`int`]] - Specific day numbers within a year for the event to recur on (1-364). - count: Optional[:class:`int`] - Number of times the event can recur before stopping. - """ - - __slots__ = ( - "start", - "end", - "frequency", - "interval", - "by_weekday", - "by_n_weekday", - "by_month", - "by_month_day", - "by_year_day", - "count", - ) - - @overload - def __init__( - self, - *, - start: datetime.datetime, - frequency: ScheduledEventRecurrenceFrequency | int, - interval: ScheduledEventRecurrenceInterval | int, - end: datetime.datetime | None = None, - by_weekday: list[ScheduledEventRecurrenceWeekday | int], - by_n_weekday: None = None, - by_month: None = None, - by_month_day: None = None, - by_year_day: list[int] | None = None, - count: int | None = None, - ) -> None: ... - - @overload - def __init__( - self, - *, - start: datetime.datetime, - frequency: ScheduledEventRecurrenceFrequency | int, - interval: ScheduledEventRecurrenceInterval | int, - end: datetime.datetime | None = None, - by_weekday: None = None, - by_n_weekday: list[ScheduledEventRecurrenceNWeekday], - by_month: None = None, - by_month_day: None = None, - by_year_day: list[int] | None = None, - count: int | None = None, - ) -> None: ... - - @overload - def __init__( - self, - *, - start: datetime.datetime, - frequency: ScheduledEventRecurrenceFrequency | int, - interval: ScheduledEventRecurrenceInterval | int, - end: datetime.datetime | None = None, - by_weekday: None = None, - by_n_weekday: None = None, - by_month: list[ScheduledEventRecurrenceMonth | int], - by_month_day: list[int], - by_year_day: list[int] | None = None, - count: int | None = None, - ) -> None: ... - - def __init__( - self, - *, - start: datetime.datetime, - frequency: ScheduledEventRecurrenceFrequency | int, - interval: ScheduledEventRecurrenceInterval | int, - end: datetime.datetime | None = None, - by_weekday: list[ScheduledEventRecurrenceWeekday | int] | None = None, - by_n_weekday: list[ScheduledEventRecurrenceNWeekday] | None = None, - by_month: list[ScheduledEventRecurrenceMonth | int] | None = None, - by_month_day: list[int] | None = None, - by_year_day: list[int] | None = None, - count: int | None = None, - ) -> None: - self.start: datetime.datetime = start - self.end: datetime.datetime | None = end - self.frequency: ScheduledEventRecurrenceFrequency = try_enum( - ScheduledEventRecurrenceFrequency, int(frequency) - ) - self.interval: ScheduledEventRecurrenceInterval = try_enum( - ScheduledEventRecurrenceInterval, int(interval) - ) - self.by_weekday: list[ScheduledEventRecurrenceWeekday] | None = ( - [try_enum(ScheduledEventRecurrenceWeekday, int(day)) for day in by_weekday] - if by_weekday - else None - ) - self.by_n_weekday: list[ScheduledEventRecurrenceNWeekday] | None = by_n_weekday - self.by_month: list[ScheduledEventRecurrenceMonth] | None = ( - [try_enum(ScheduledEventRecurrenceMonth, int(month)) for month in by_month] - if by_month - else None - ) - self.by_month_day: list[int] | None = by_month_day - self.by_year_day: list[int] | None = by_year_day - self.count: int | None = count - - def __repr__(self) -> str: - return ( - f"" - ) - - @classmethod - def from_data( - cls, data: ScheduledEventRecurrenceRulePayload - ) -> ScheduledEventRecurrenceRule: - start = utils.parse_time(data["start"]) - end = utils.parse_time(data.get("end")) - - raw_by_weekday = data.get("by_weekday") - by_weekday = ( - [try_enum(ScheduledEventRecurrenceWeekday, day) for day in raw_by_weekday] - if raw_by_weekday - else None - ) - - raw_by_n_weekday = data.get("by_n_weekday") - by_n_weekday = ( - [ScheduledEventRecurrenceNWeekday(**entry) for entry in raw_by_n_weekday] - if raw_by_n_weekday - else None - ) - - raw_by_month = data.get("by_month") - by_month = ( - [try_enum(ScheduledEventRecurrenceMonth, month) for month in raw_by_month] - if raw_by_month - else None - ) - - return cls( - start=start, - end=end, - frequency=try_enum(ScheduledEventRecurrenceFrequency, data["frequency"]), - interval=try_enum(ScheduledEventRecurrenceInterval, data["interval"]), - by_weekday=by_weekday, - by_n_weekday=by_n_weekday, - by_month=by_month, - by_month_day=data.get("by_month_day"), - by_year_day=data.get("by_year_day"), - count=data.get("count"), - ) - - def to_payload(self) -> dict[str, Any]: - """Convert the recurrence rule to an API payload. - - Returns - ------- - dict[str, Any] - The recurrence rule as a dictionary suitable for the Discord API. - - Raises - ------ - ValidationError - If the recurrence rule violates Discord's system limitations. - """ - self.validate() - - payload: dict[str, Any] = { - "start": self.start.isoformat(), - "frequency": self.frequency.value, - "interval": int(self.interval), - } - - if self.end is not None: - payload["end"] = self.end.isoformat() - - if self.by_weekday is not None: - payload["by_weekday"] = [int(day) for day in self.by_weekday] - - if self.by_n_weekday is not None: - payload["by_n_weekday"] = [ - entry.to_payload() for entry in self.by_n_weekday - ] - - if self.by_month is not None: - payload["by_month"] = [int(month) for month in self.by_month] - - if self.by_month_day is not None: - payload["by_month_day"] = self.by_month_day - - if self.by_year_day is not None: - payload["by_year_day"] = self.by_year_day - - if self.count is not None: - payload["count"] = self.count - - return payload - - def validate(self) -> None: - """Validate the recurrence rule against Discord's system limitations. - - Raises - ------ - ValidationError - If the recurrence rule violates any system limitations. - """ - # Mutually exclusive combinations - has_by_weekday = self.by_weekday is not None - has_by_n_weekday = self.by_n_weekday is not None - has_by_month = self.by_month is not None - has_by_month_day = self.by_month_day is not None - - if has_by_weekday and has_by_n_weekday: - raise ValidationError("by_weekday and by_n_weekday are mutually exclusive") - - if has_by_month and has_by_n_weekday: - raise ValidationError("by_month and by_n_weekday are mutually exclusive") - - if has_by_month != has_by_month_day: - raise ValidationError( - "by_month and by_month_day must both be provided together" - ) - - # Daily frequency (0) constraints - if self.frequency == ScheduledEventRecurrenceFrequency.yearly: - if has_by_weekday: - raise ValidationError("by_weekday is not valid for yearly events") - - # Weekly frequency (2) constraints - if self.frequency == ScheduledEventRecurrenceFrequency.weekly: - if has_by_weekday: - if len(self.by_weekday) != 1: - raise ValidationError( - "by_weekday must have exactly 1 day for weekly events" - ) - - if has_by_n_weekday: - raise ValidationError("by_n_weekday is not valid for weekly events") - - if has_by_month or has_by_month_day: - raise ValidationError( - "by_month and by_month_day are not valid for weekly events" - ) - - # interval can only be 2 (every-other week) or 1 (weekly) - if self.interval not in (1, 2): - raise ValidationError("interval for weekly events can only be 1 or 2") - - # Daily frequency (3) constraints - if self.frequency == ScheduledEventRecurrenceFrequency.daily: - if has_by_n_weekday: - raise ValidationError("by_n_weekday is not valid for daily events") - - if has_by_month or has_by_month_day: - raise ValidationError( - "by_month and by_month_day are not valid for daily events" - ) - - if has_by_weekday: - # Validate known sets of weekdays for daily events - allowed_sets = [ - [0, 1, 2, 3, 4], # Monday - Friday - [1, 2, 3, 4, 5], # Tuesday - Saturday - [6, 0, 1, 2, 3], # Sunday - Thursday - [4, 5], # Friday & Saturday - [5, 6], # Saturday & Sunday - [6, 0], # Sunday & Monday - ] - weekday_values = [day.value for day in self.by_weekday] - weekday_values.sort() - - if weekday_values not in allowed_sets: - raise ValidationError( - "by_weekday for daily events must be one of the allowed sets: " - "[0,1,2,3,4], [1,2,3,4,5], [6,0,1,2,3], [4,5], [5,6], [6,0]" - ) - - # Monthly frequency (1) constraints - if self.frequency == ScheduledEventRecurrenceFrequency.monthly: - if has_by_n_weekday: - if len(self.by_n_weekday) != 1: - raise ValidationError( - "by_n_weekday must have exactly 1 entry for monthly events" - ) - - if has_by_weekday: - raise ValidationError("by_weekday is not valid for monthly events") - - if has_by_month or has_by_month_day: - raise ValidationError( - "by_month and by_month_day are not valid for monthly events" - ) - - # Yearly frequency (0) constraints - if self.frequency == ScheduledEventRecurrenceFrequency.yearly: - if has_by_n_weekday: - raise ValidationError("by_n_weekday is not valid for yearly events") - - if not (has_by_month and has_by_month_day): - raise ValidationError( - "by_month and by_month_day must both be provided for yearly events" - ) - - if len(self.by_month) != 1 or len(self.by_month_day) != 1: - raise ValidationError( - "by_month and by_month_day must each have exactly 1 entry for yearly events" - ) - - class ScheduledEvent(Hashable): """Represents a Discord Guild Scheduled Event. @@ -591,8 +225,6 @@ class ScheduledEvent(Hashable): 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). - recurrence_rule: Optional[:class:`ScheduledEventRecurrenceRule`] - The definition for how often this event should recur. """ __slots__ = ( @@ -611,7 +243,6 @@ class ScheduledEvent(Hashable): "_cached_subscribers", "entity_type", "privacy_level", - "recurrence_rule", "channel_id", "entity_id", "entity_metadata", @@ -647,12 +278,6 @@ def __init__( self.privacy_level: ScheduledEventPrivacyLevel = try_enum( ScheduledEventPrivacyLevel, data.get("privacy_level") ) - recurrence_rule_data = data.get("recurrence_rule") - self.recurrence_rule: ScheduledEventRecurrenceRule | None = ( - ScheduledEventRecurrenceRule.from_data(recurrence_rule_data) - if recurrence_rule_data - else None - ) self.channel_id: int | None = utils._get_as_snowflake(data, "channel_id") self.entity_id: int | None = utils._get_as_snowflake(data, "entity_id") @@ -786,7 +411,6 @@ async def edit( cover: bytes | None = MISSING, privacy_level: ScheduledEventPrivacyLevel = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, - recurrence_rule: ScheduledEventRecurrenceRule | None = MISSING, ) -> ScheduledEvent | None: """|coro| @@ -829,8 +453,6 @@ async def edit( 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. - recurrence_rule: Union[:class:`ScheduledEventRecurrenceRule`, :class:`dict`] - The definition for how often this event should recur. reason: Optional[:class:`str`] The reason to show in the audit log. image: Optional[:class:`bytes`] @@ -874,12 +496,6 @@ async def edit( else: payload["entity_metadata"] = entity_metadata.to_payload() - if recurrence_rule is not MISSING: - if isinstance(recurrence_rule, ScheduledEventRecurrenceRule): - payload["recurrence_rule"] = recurrence_rule.to_payload() - else: - payload["recurrence_rule"] = recurrence_rule - if cover is not MISSING: warn_deprecated("cover", "image", "2.7", "3.0") if image is MISSING: diff --git a/discord/types/scheduled_events.py b/discord/types/scheduled_events.py index c86b6b02f5..5c3a9d0584 100644 --- a/discord/types/scheduled_events.py +++ b/discord/types/scheduled_events.py @@ -33,60 +33,6 @@ ScheduledEventStatus = Literal[1, 2, 3, 4] ScheduledEventEntityType = Literal[1, 2, 3] ScheduledEventPrivacyLevel = Literal[2] -ScheduledEventRecurrenceFrequency = Literal[0, 1, 2, 3] -ScheduledEventRecurrenceWeekday = Literal[0, 1, 2, 3, 4, 5, 6] -ScheduledEventRecurrenceMonth = Literal[ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, -] - - -class ScheduledEventRecurrenceNWeekday(TypedDict): - n: int - day: ScheduledEventRecurrenceWeekday - - -class ScheduledEventRecurrenceRule(TypedDict, total=False): - start: str - end: str | None - frequency: ScheduledEventRecurrenceFrequency - interval: int - by_weekday: list[ScheduledEventRecurrenceWeekday] - by_n_weekday: list[ScheduledEventRecurrenceNWeekday] - by_month: list[ScheduledEventRecurrenceMonth] - by_month_day: list[int] - by_year_day: list[int] - count: int - - -class ScheduledEvent(TypedDict): - id: Snowflake - guild_id: Snowflake - channel_id: Snowflake - creator_id: Snowflake - name: str - description: str - image: str | None - scheduled_start_time: str - scheduled_end_time: str | None - privacy_level: ScheduledEventPrivacyLevel - status: ScheduledEventStatus - entity_type: ScheduledEventEntityType - entity_id: Snowflake - entity_metadata: ScheduledEventEntityMetadata - creator: User - user_count: int | None - recurrence_rule: ScheduledEventRecurrenceRule | None class ScheduledEventEntityMetadata(TypedDict): From 7fdf52ed1920e2c0ea4952ebc431a8ba3c034cd0 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:03:12 +0200 Subject: [PATCH 15/43] Update scheduled_events.py --- discord/types/scheduled_events.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/discord/types/scheduled_events.py b/discord/types/scheduled_events.py index 5c3a9d0584..130dc99cc6 100644 --- a/discord/types/scheduled_events.py +++ b/discord/types/scheduled_events.py @@ -35,6 +35,25 @@ ScheduledEventPrivacyLevel = Literal[2] +class ScheduledEvent(TypedDict): + id: Snowflake + guild_id: Snowflake + channel_id: Snowflake + creator_id: Snowflake + name: str + description: str + image: str | None + scheduled_start_time: str + scheduled_end_time: str | None + privacy_level: ScheduledEventPrivacyLevel + status: ScheduledEventStatus + entity_type: ScheduledEventEntityType + entity_id: Snowflake + entity_metadata: ScheduledEventEntityMetadata + creator: User + user_count: int | None + + class ScheduledEventEntityMetadata(TypedDict): location: str From bafd5498fa1400301ae8cea20762d6c86204cd7d Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:37:12 +0200 Subject: [PATCH 16/43] =?UTF-8?q?refactor:=20=F0=9F=97=91=EF=B8=8F=20Chang?= =?UTF-8?q?e=20=5Fcached=5Fsubscribers=20from=20dict=20to=20set=20for=20im?= =?UTF-8?q?proved=20performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discord/scheduled_events.py | 2 +- discord/state.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 4ce83a5234..26380fc7c0 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -288,7 +288,7 @@ def __init__( else None ) - self._cached_subscribers: dict[int, int] = {} + 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 diff --git a/discord/state.py b/discord/state.py index e4262c7efc..faf95e7d47 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1708,7 +1708,7 @@ def parse_guild_scheduled_event_user_add(self, data) -> None: user_id = int(data["user_id"]) event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) if event: - event._cached_subscribers[user_id] = user_id + event._cached_subscribers.add(user_id) guild._add_scheduled_event(event) member = guild.get_member(user_id) if member is not None: @@ -1733,7 +1733,7 @@ def parse_guild_scheduled_event_user_remove(self, data) -> None: user_id = int(data["user_id"]) event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) if event: - event._cached_subscribers.pop(user_id, None) + event._cached_subscribers.discard(user_id) guild._add_scheduled_event(event) member = guild.get_member(user_id) if member is not None: From 8f887d3ef344fddd6e1e91d6b5d6ba1f5e7413d1 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:51:59 +0200 Subject: [PATCH 17/43] paillat comment --- discord/scheduled_events.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 26380fc7c0..c0d83d2498 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -404,12 +404,12 @@ async def edit( location: ( str | int | VoiceChannel | StageChannel | ScheduledEventLocation ) = MISSING, - entity_type: ScheduledEventEntityType = MISSING, scheduled_start_time: datetime.datetime = MISSING, scheduled_end_time: datetime.datetime = MISSING, image: bytes | None = MISSING, cover: bytes | None = MISSING, privacy_level: ScheduledEventPrivacyLevel = MISSING, + entity_type: ScheduledEventEntityType = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, ) -> ScheduledEvent | None: """|coro| @@ -440,15 +440,15 @@ async def edit( to use :meth:`.start`, :meth:`.complete`, and :meth:`.cancel` to edit statuses instead. Valid transitions: SCHEDULED → ACTIVE, ACTIVE → COMPLETED, SCHEDULED → CANCELED. - 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``. 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 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. From 6772e09046ff29d06f4a6b5842e99f4c3d04013d Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:47:24 +0100 Subject: [PATCH 18/43] Update discord/enums.py Co-authored-by: Paillat Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/enums.py b/discord/enums.py index 55b420733a..f933cdabb7 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -967,6 +967,7 @@ 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`)""" From 79194b56bb246f1f416db8c45974b20ee447134b Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:54:56 +0200 Subject: [PATCH 19/43] =?UTF-8?q?refactor:=20=F0=9F=97=91=EF=B8=8F=20Updat?= =?UTF-8?q?e=20location=20type=20references=20and=20deprecate=20'cover'=20?= =?UTF-8?q?parameter=20in=20ScheduledEvent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discord/scheduled_events.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index c0d83d2498..9c79f53f1e 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -67,13 +67,13 @@ class ScheduledEventLocation: Setting the ``value`` to its corresponding type will set the location type automatically: - +------------------------+-----------------------------------------------+ - | Type of Input | Location Type | - +========================+===============================================+ - | :class:`StageChannel` | :attr:`ScheduledEventEntityType.stage_instance` | - | :class:`VoiceChannel` | :attr:`ScheduledEventEntityType.voice` | - | :class:`str` | :attr:`ScheduledEventEntityType.external` | - +------------------------+-----------------------------------------------+ + +------------------------+---------------------------------------------------+ + | Type of Input | Location Type | + +========================+===================================================+ + | :class:`StageChannel` | :attr:`ScheduledEventLocationType.stage_instance` | + | :class:`VoiceChannel` | :attr:`ScheduledEventLocationType.voice` | + | :class:`str` | :attr:`ScheduledEventLocationType.external` | + +------------------------+---------------------------------------------------+ .. deprecated:: 2.7 Use :class:`ScheduledEventEntityMetadata` instead. @@ -363,6 +363,7 @@ def subscriber_count(self) -> int | None: return self.user_count @property + @deprecated(instead="user_count", since="2.7", removed="3.0") def interested(self) -> int | None: """An alias to :attr:`.user_count`""" return self.user_count @@ -457,7 +458,10 @@ async def edit( 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`` parameter instead. Returns ------- Optional[:class:`.ScheduledEvent`] From 70c37cba5f3b61d72a72c4609dd5bb74381a1a15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:55:27 +0000 Subject: [PATCH 20/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/scheduled_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 9c79f53f1e..0b79650b9a 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -462,6 +462,7 @@ async def edit( The cover image of the scheduled event. .. deprecated:: 2.7 Use the ``image`` parameter instead. + Returns ------- Optional[:class:`.ScheduledEvent`] From 2800a0817d5385a71b2c59f446c9e661dec18158 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:34:34 +0200 Subject: [PATCH 21/43] =?UTF-8?q?refactor:=20=F0=9F=97=91=EF=B8=8F=20Remov?= =?UTF-8?q?e=20unnecessary=20calls=20to=20=5Fadd=5Fscheduled=5Fevent=20in?= =?UTF-8?q?=20ConnectionState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discord/state.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/state.py b/discord/state.py index faf95e7d47..dd5676c0a2 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1709,7 +1709,6 @@ def parse_guild_scheduled_event_user_add(self, data) -> None: event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) if event: event._cached_subscribers.add(user_id) - guild._add_scheduled_event(event) member = guild.get_member(user_id) if member is not None: self.dispatch("scheduled_event_user_add", event, member) @@ -1734,7 +1733,6 @@ def parse_guild_scheduled_event_user_remove(self, data) -> None: event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) if event: event._cached_subscribers.discard(user_id) - guild._add_scheduled_event(event) member = guild.get_member(user_id) if member is not None: self.dispatch("scheduled_event_user_remove", event, member) From b9ddb026861e928485d6f367e8aa8874da09356c Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:52:41 +0200 Subject: [PATCH 22/43] =?UTF-8?q?refactor:=20=F0=9F=97=91=EF=B8=8F=20Enhan?= =?UTF-8?q?ce=20create=5Fscheduled=5Fevent=20method=20with=20overloads=20a?= =?UTF-8?q?nd=20deprecate=20location=20parameter=20in=20ScheduledEvent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discord/guild.py | 70 +++++++++++++++++++++++++++++++++++-- discord/scheduled_events.py | 30 ++++++++-------- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 7c532b7bcb..750e060d50 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -41,7 +41,7 @@ Union, overload, ) - +from .scheduled_events import ScheduledEventLocation from typing_extensions import override from . import abc, utils @@ -102,7 +102,7 @@ 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 @@ -4285,6 +4285,24 @@ def get_scheduled_event(self, event_id: int, /) -> ScheduledEvent | None: """ return self._scheduled_events.get(event_id) + @overload + async def create_scheduled_event( + self, + *, + name: str, + description: str = MISSING, + start_time: datetime.datetime, + end_time: datetime.datetime = MISSING, + location: str + | int + | VoiceChannel + | StageChannel + | ScheduledEventLocation = MISSING, + reason: str | None = None, + image: bytes = MISSING, + ) -> ScheduledEvent | None: ... + + @overload async def create_scheduled_event( self, *, @@ -4292,12 +4310,33 @@ async def create_scheduled_event( description: str = MISSING, scheduled_start_time: datetime.datetime, scheduled_end_time: datetime.datetime = MISSING, - entity_type: ScheduledEventEntityType, + 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. @@ -4351,6 +4390,31 @@ async def create_scheduled_event( "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 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() diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 0b79650b9a..03c2794f30 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -489,31 +489,33 @@ async def edit( if status is not MISSING: payload["status"] = int(status) - if entity_type is not MISSING: - payload["entity_type"] = int(entity_type) - if privacy_level is not MISSING: payload["privacy_level"] = int(privacy_level) - if entity_metadata is not MISSING: - if entity_metadata is None: - payload["entity_metadata"] = None - else: - payload["entity_metadata"] = entity_metadata.to_payload() - - if cover is not MISSING: - warn_deprecated("cover", "image", "2.7", "3.0") - if image is MISSING: - image = cover - 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", "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 From 7b4f7f5a4eaf95f4c695c5c02df32f5c980bee9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:53:15 +0000 Subject: [PATCH 23/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/guild.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 750e060d50..a0b44a2481 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -41,7 +41,7 @@ Union, overload, ) -from .scheduled_events import ScheduledEventLocation + from typing_extensions import override from . import abc, utils @@ -96,6 +96,7 @@ from .scheduled_events import ( ScheduledEvent, ScheduledEventEntityMetadata, + ScheduledEventLocation, ) from .soundboard import SoundboardSound from .stage_instance import StageInstance @@ -4293,11 +4294,9 @@ async def create_scheduled_event( description: str = MISSING, start_time: datetime.datetime, end_time: datetime.datetime = MISSING, - location: str - | int - | VoiceChannel - | StageChannel - | ScheduledEventLocation = MISSING, + location: ( + str | int | VoiceChannel | StageChannel | ScheduledEventLocation + ) = MISSING, reason: str | None = None, image: bytes = MISSING, ) -> ScheduledEvent | None: ... @@ -4324,11 +4323,9 @@ async def create_scheduled_event( description: str = MISSING, scheduled_start_time: datetime.datetime, scheduled_end_time: datetime.datetime = MISSING, - location: str - | int - | VoiceChannel - | StageChannel - | ScheduledEventLocation = MISSING, + location: ( + str | int | VoiceChannel | StageChannel | ScheduledEventLocation + ) = MISSING, entity_type: ScheduledEventEntityType = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, channel_id: int = MISSING, From a548783dcb66a1cad6fc5c7d29a2642fcd9493cf Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:21:04 +0100 Subject: [PATCH 24/43] reverse --- discord/audit_logs.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 9a00f15715..e78915da25 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -278,7 +278,7 @@ class AuditLogChanges: "type": (None, _transform_type), "status": (None, _enum_transformer(enums.ScheduledEventStatus)), "entity_type": ( - "entity_type", + "location_type", _enum_transformer(enums.ScheduledEventEntityType), ), "command_id": ("command_id", _transform_snowflake), @@ -365,6 +365,18 @@ 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.ScheduledEventEntityType.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 From a594faaa82d797bb9a5a5eed9a94928cb6297c45 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Sun, 15 Feb 2026 17:06:49 +0100 Subject: [PATCH 25/43] feat: Add entity metadata transformation for scheduled events in audit logs --- discord/audit_logs.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index e78915da25..1e70cc3982 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -51,7 +51,7 @@ from .guild import Guild from .member import Member from .role import Role - from .scheduled_events import ScheduledEvent + from .scheduled_events import ScheduledEvent, ScheduledEventEntityMetadata from .stage_instance import StageInstance from .state import ConnectionState from .sticker import GuildSticker @@ -217,6 +217,20 @@ def _transform_communication_disabled_until( return None +def _transform_entity_metadata( + entry: AuditLogEntry, data: dict | str | None +) -> ScheduledEventEntityMetadata | None: + from .scheduled_events import ScheduledEventEntityMetadata + + if data is None: + return None + if isinstance(data, dict): + location = data.get("location") + else: + location = data + return ScheduledEventEntityMetadata(location=location) + + class AuditLogDiff: def __len__(self) -> int: return len(self.__dict__) @@ -271,6 +285,8 @@ class AuditLogChanges: "default_notifications", _enum_transformer(enums.NotificationLevel), ), + "entity_metadata": (None, _transform_entity_metadata), + "location": (None, _transform_entity_metadata), "rtc_region": (None, _enum_transformer(enums.VoiceRegion)), "video_quality_mode": (None, _enum_transformer(enums.VideoQualityMode)), "privacy_level": (None, _enum_transformer(enums.StagePrivacyLevel)), @@ -365,17 +381,10 @@ 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.ScheduledEventEntityType.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) + if attr == "location": + setattr(self.after, "entity_metadata", after) + setattr(self.before, "entity_metadata", before) # add an alias if hasattr(self.after, "colour"): From ef960ff1daf6c7960f0c5ddf0bf3c947408967bf Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:44:53 +0100 Subject: [PATCH 26/43] refactor: Update deprecated property decorators to use typing_extensions --- discord/scheduled_events.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 03c2794f30..7f5f44a8b8 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -26,6 +26,7 @@ import datetime from typing import TYPE_CHECKING, Any +import typing_extensions from . import utils from .asset import Asset @@ -39,7 +40,7 @@ from .iterators import ScheduledEventSubscribersIterator from .mixins import Hashable from .object import Object -from .utils import deprecated, warn_deprecated +from .utils import warn_deprecated __all__ = ( "ScheduledEvent", @@ -312,7 +313,9 @@ def __repr__(self) -> str: ) @property - @deprecated(instead="entity_metadata.location", since="2.7", removed="3.0") + @typing_extensions.deprecated( + "location is deprecated since 2.7 and will be removed in 3.0, consider using entity_metadata instead", + ) def location(self) -> ScheduledEventLocation | None: """Returns the location of the event.""" if self.channel_id is None: @@ -330,7 +333,9 @@ def created_at(self) -> datetime.datetime: return utils.snowflake_time(self.id) @property - @deprecated(instead="scheduled_start_time", since="2.7", removed="3.0") + @typing_extensions.deprecated( + "start_time is deprecated since 2.7 and will be removed in 3.0, consider using scheduled_start_time instead", + ) def start_time(self) -> datetime.datetime: """ Returns the scheduled start time of the event. @@ -341,7 +346,9 @@ def start_time(self) -> datetime.datetime: return self.scheduled_start_time @property - @deprecated(instead="scheduled_end_time", since="2.7", removed="3.0") + @typing_extensions.deprecated( + "end_time is deprecated since 2.7 and will be removed in 3.0, consider using scheduled_end_time instead", + ) def end_time(self) -> datetime.datetime | None: """ Returns the scheduled end time of the event. @@ -352,7 +359,9 @@ def end_time(self) -> datetime.datetime | None: return self.scheduled_end_time @property - @deprecated(instead="user_count", since="2.7", removed="3.0") + @typing_extensions.deprecated( + "subscriber_count is deprecated since 2.7 and will be removed in 3.0, consider using user_count instead", + ) def subscriber_count(self) -> int | None: """ Returns the number of users subscribed to the event. @@ -363,7 +372,9 @@ def subscriber_count(self) -> int | None: return self.user_count @property - @deprecated(instead="user_count", since="2.7", removed="3.0") + @typing_extensions.deprecated( + "interested is deprecated since 2.7 and will be removed in 3.0, consider using user_count instead", + ) def interested(self) -> int | None: """An alias to :attr:`.user_count`""" return self.user_count From 8c4c395bef5b93aa1410bd63c126c16abdaccc86 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:45:30 +0100 Subject: [PATCH 27/43] fix: Update type hint for _transform_entity_metadata to specify dict[str, str] --- discord/audit_logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 1e70cc3982..6a9ab92d2f 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -218,7 +218,7 @@ def _transform_communication_disabled_until( def _transform_entity_metadata( - entry: AuditLogEntry, data: dict | str | None + entry: AuditLogEntry, data: dict[str, str] | str | None ) -> ScheduledEventEntityMetadata | None: from .scheduled_events import ScheduledEventEntityMetadata From ddd0c0ebaf3ee64969bda7ccf7ec925276cc6090 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:00:15 +0000 Subject: [PATCH 28/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/scheduled_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 7f5f44a8b8..7a88e69291 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -26,6 +26,7 @@ import datetime from typing import TYPE_CHECKING, Any + import typing_extensions from . import utils From 28b4682a7f053b9584ee6fa9904a693b4e51aabb Mon Sep 17 00:00:00 2001 From: Lee4test Date: Mon, 13 Apr 2026 01:21:12 +0900 Subject: [PATCH 29/43] fix: improve docstring wording of display methods (#3120) * fix: Improve docstring wording of 'for regular members' * Improve docstring wording of display methods * style(pre-commit): auto fixes from pre-commit.com hooks * Update discord/user.py Signed-off-by: Paillat --------- Signed-off-by: Paillat Co-authored-by: Paillat Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Paillat --- discord/member.py | 15 ++++++--------- discord/user.py | 3 ++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/discord/member.py b/discord/member.py index 0353472c8d..3773a18ee5 100644 --- a/discord/member.py +++ b/discord/member.py @@ -663,9 +663,8 @@ def display_name(self) -> str: def display_avatar(self) -> Asset: """Returns the member's display avatar. - For regular members this is just their avatar, but - if they have a guild specific avatar then that - is returned instead. + Returns the user's guild avatar. + If the user does not have a guild avatar, their global avatar is returned instead. .. versionadded:: 2.0 """ @@ -688,9 +687,8 @@ def guild_avatar(self) -> Asset | None: def display_avatar_decoration(self) -> Asset | None: """Returns the member's displayed avatar decoration. - For regular members this is just their avatar decoration, but - if they have a guild specific avatar decoration then that - is returned instead. + Returns the user's guild avatar decoration. + If the user does not have a guild avatar decoration, their global avatar decoration is returned instead. .. versionadded:: 2.8 """ @@ -713,9 +711,8 @@ def guild_avatar_decoration(self) -> Asset | None: def display_banner(self) -> Asset | None: """Returns the member's display banner. - For regular members this is just their banner, but - if they have a guild specific banner then that - is returned instead. + Returns the user's guild banner. + If the user does not have a guild banner, their global banner is returned instead. .. versionadded:: 2.7 """ diff --git a/discord/user.py b/discord/user.py index 3448092948..94ebf8e070 100644 --- a/discord/user.py +++ b/discord/user.py @@ -251,7 +251,8 @@ def default_avatar(self) -> Asset: def display_avatar(self) -> Asset: """Returns the user's display avatar. - For regular users this is just their default avatar or uploaded avatar. + Returns the user's uploaded avatar. + If the user has not uploaded any avatar, their default avatar is returned instead. .. versionadded:: 2.0 """ From c78136da259156bfe8918c86e552f41180d8ad0e Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:37:43 +0300 Subject: [PATCH 30/43] fix: Update privacy_level default to MISSING and add deprecation warning --- discord/guild.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index a0b44a2481..eeded89f41 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4329,7 +4329,7 @@ async def create_scheduled_event( entity_type: ScheduledEventEntityType = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, channel_id: int = MISSING, - privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, + privacy_level: ScheduledEventPrivacyLevel = MISSING, reason: str | None = None, image: bytes = MISSING, start_time: datetime.datetime = MISSING, @@ -4384,9 +4384,16 @@ async def create_scheduled_event( payload: dict[str, str | int] = { "name": name, "scheduled_start_time": scheduled_start_time.isoformat(), - "privacy_level": int(privacy_level), "entity_type": int(entity_type), } + + if privacy_level is not MISSING: + warn_deprecated( + "privacy_level", + None, + "3.0", + extra="It is ignored by the API and will be removed in a future version.", + ) 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: From b86cd0730089bc61f555aac500b12c0b5edf298c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:48:42 +0000 Subject: [PATCH 31/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 1be81b3db7..8effd81fca 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -61,8 +61,8 @@ NotificationLevel, NSFWLevel, OnboardingMode, - ScheduledEventEntityType, RoleType, + ScheduledEventEntityType, ScheduledEventPrivacyLevel, SortOrder, VerificationLevel, From 8cd349546861a93be578aeb8abae264656c70aae Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 17 Apr 2026 19:06:05 +0200 Subject: [PATCH 32/43] feat: Misc changes and fixes --- discord/audit_logs.py | 1 + discord/guild.py | 28 +++++++++++++++------------- discord/iterators.py | 2 +- discord/scheduled_events.py | 28 ++++++++++++++-------------- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 6a9ab92d2f..de7067af39 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -381,6 +381,7 @@ def __init__( if transformer: after = transformer(entry, after) + setattr(self.before, attr, before) setattr(self.after, attr, after) if attr == "location": setattr(self.after, "entity_metadata", after) diff --git a/discord/guild.py b/discord/guild.py index 8effd81fca..0fb131e4b3 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4382,12 +4382,6 @@ async def create_scheduled_event( ValidationError Invalid parameters for the event type. """ - payload: dict[str, str | int] = { - "name": name, - "scheduled_start_time": scheduled_start_time.isoformat(), - "entity_type": int(entity_type), - } - if privacy_level is not MISSING: warn_deprecated( "privacy_level", @@ -4414,12 +4408,20 @@ async def create_scheduled_event( if location is not MISSING: warn_deprecated("location", "entity_metadata", "2.7", "3.0") if entity_metadata is MISSING: - if not isinstance(location, (ScheduledEventLocation)): + if not isinstance(location, ScheduledEventLocation): location = ScheduledEventLocation(state=self._state, value=location) - if entity_type is MISSING: - entity_type = location.type + if entity_type is MISSING: + entity_type = location.type if location.type == ScheduledEventEntityType.external: entity_metadata = ScheduledEventEntityMetadata(str(location)) + else: + channel_id = location.value.id + + payload: dict[str, str | int] = { + "name": name, + "scheduled_start_time": scheduled_start_time.isoformat(), + "entity_type": int(entity_type), + } if scheduled_end_time is not MISSING: payload["scheduled_end_time"] = scheduled_end_time.isoformat() @@ -4433,15 +4435,15 @@ async def create_scheduled_event( 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." + "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." + "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." + "scheduled_end_time is required for external events." ) payload["channel_id"] = None @@ -4449,7 +4451,7 @@ async def create_scheduled_event( else: if channel_id is MISSING: raise ValidationError( - "channel_id is required for STAGE_INSTANCE and VOICE events." + "channel_id is required for stage_instance and voice events." ) payload["channel_id"] = channel_id diff --git a/discord/iterators.py b/discord/iterators.py index 1f8117a96d..59aa08f2f5 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -956,7 +956,7 @@ def user_from_payload(self, data): async def _fill_from_cache(self): """Fill subscribers queue from cached user IDs.""" - cached_user_ids = list(self.event._cached_subscribers.keys()) + cached_user_ids = list(self.event._cached_subscribers) for user_id in itertools.islice(iter(cached_user_ids), self.retrieve): member = self.event.guild.get_member(user_id) diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 7a88e69291..e0ef2e4658 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -294,7 +294,6 @@ def __init__( 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 - self.channel_id = data.get("channel_id", None) def __str__(self) -> str: return self.name @@ -306,10 +305,10 @@ def __repr__(self) -> str: f"description={self.description} " f"start_time={self.scheduled_start_time} " f"end_time={self.scheduled_end_time} " - f"location={self.location!r} " + f"entity_metadata={self.entity_metadata!r} " f"status={self.status.name} " f"user_count={self.user_count} " - f"creator_id={self.creator_id}>" + f"creator_id={self.creator_id} " f"channel_id={self.channel_id}>" ) @@ -320,13 +319,12 @@ def __repr__(self) -> str: def location(self) -> ScheduledEventLocation | None: """Returns the location of the event.""" if self.channel_id is None: - self.location = ScheduledEventLocation( + if self.entity_metadata is None: + return None + return ScheduledEventLocation( state=self._state, value=self.entity_metadata.location ) - else: - self.location = ScheduledEventLocation( - state=self._state, value=self.channel_id - ) + return ScheduledEventLocation(state=self._state, value=self.channel_id) @property def created_at(self) -> datetime.datetime: @@ -507,12 +505,14 @@ async def edit( if location is not MISSING: warn_deprecated("location", "entity_metadata", "2.7", "3.0") if entity_metadata is MISSING: - if not isinstance(location, (ScheduledEventLocation)): + if not isinstance(location, ScheduledEventLocation): location = ScheduledEventLocation(state=self._state, value=location) - if entity_type is MISSING: - entity_type = location.type + if entity_type is MISSING: + entity_type = location.type if location.type == ScheduledEventEntityType.external: entity_metadata = ScheduledEventEntityMetadata(str(location)) + else: + payload["channel_id"] = location.value.id if cover is not MISSING: warn_deprecated("cover", "image", "2.7", "3.0") @@ -546,11 +546,11 @@ async def edit( ): if entity_metadata is MISSING or entity_metadata is None: raise ValidationError( - "entity_metadata with a location is required when entity_type is EXTERNAL." + "entity_metadata with a location is required when entity_type is external." ) if not entity_metadata.location: raise ValidationError( - "entity_metadata.location cannot be empty for EXTERNAL events." + "entity_metadata.location cannot be empty for external events." ) has_end_time = ( @@ -558,7 +558,7 @@ async def edit( ) if not has_end_time: raise ValidationError( - "scheduled_end_time is required for EXTERNAL events." + "scheduled_end_time is required for external events." ) payload["channel_id"] = None From 61078d3008ff57c0d06cb1676164b8cab8d480d0 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:30:51 +0300 Subject: [PATCH 33/43] fix: Refactor comparison methods and update VoiceRegion enum values --- discord/enums.py | 47 ++++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 94c7e7007b..7ba357c1af 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -97,21 +97,17 @@ def _create_value_cls(name, comparable): cls.__repr__ = lambda self: f"<{name}.{self.name}: {self.value!r}>" cls.__str__ = lambda self: f"{name}.{self.name}" if comparable: - cls.__le__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value <= other.value + cls.__le__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value <= other.value ) - cls.__ge__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value >= other.value + cls.__ge__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value >= other.value ) - cls.__lt__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value < other.value + cls.__lt__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value < other.value ) - cls.__gt__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value > other.value + cls.__gt__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value > other.value ) return cls @@ -290,29 +286,18 @@ class MessageType(Enum): class VoiceRegion(Enum): """Voice region""" - us_west = "us-west" - us_east = "us-east" - us_south = "us-south" - us_central = "us-central" - eu_west = "eu-west" - eu_central = "eu-central" - singapore = "singapore" - london = "london" - sydney = "sydney" - amsterdam = "amsterdam" - frankfurt = "frankfurt" brazil = "brazil" hongkong = "hongkong" - russia = "russia" + india = "india" japan = "japan" + rotterdam = "rotterdam" + singapore = "singapore" southafrica = "southafrica" - south_korea = "south-korea" - india = "india" - europe = "europe" - dubai = "dubai" - vip_us_east = "vip-us-east" - vip_us_west = "vip-us-west" - vip_amsterdam = "vip-amsterdam" + sydney = "sydney" + us_central = "us-central" + us_east = "us-east" + us_south = "us-south" + us_west = "us-west" def __str__(self): return self.value From 45c2bebdba7f0c9680d4bce5a4d27ed1479329e9 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:12:19 +0300 Subject: [PATCH 34/43] fix: Add TypeError for unresolved entity_type in Guild class Co-authored-by: Copilot --- discord/guild.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 0fb131e4b3..69e401784e 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4417,6 +4417,11 @@ async def create_scheduled_event( else: channel_id = location.value.id + if entity_type is MISSING: + raise TypeError( + "entity_type could not be resolved. Pass entity_type explicitly " + "or provide a location with a resolvable type." + ) payload: dict[str, str | int] = { "name": name, "scheduled_start_time": scheduled_start_time.isoformat(), From fd65324110e719e0a1a130f7ee3ec8ea0827b9e8 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Sat, 23 May 2026 11:49:16 +0300 Subject: [PATCH 35/43] reverse voice region change by mistake --- discord/enums.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 7ba357c1af..ce622138d7 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -286,18 +286,29 @@ class MessageType(Enum): class VoiceRegion(Enum): """Voice region""" + us_west = "us-west" + us_east = "us-east" + us_south = "us-south" + us_central = "us-central" + eu_west = "eu-west" + eu_central = "eu-central" + singapore = "singapore" + london = "london" + sydney = "sydney" + amsterdam = "amsterdam" + frankfurt = "frankfurt" brazil = "brazil" hongkong = "hongkong" - india = "india" + russia = "russia" japan = "japan" - rotterdam = "rotterdam" - singapore = "singapore" southafrica = "southafrica" - sydney = "sydney" - us_central = "us-central" - us_east = "us-east" - us_south = "us-south" - us_west = "us-west" + south_korea = "south-korea" + india = "india" + europe = "europe" + dubai = "dubai" + vip_us_east = "vip-us-east" + vip_us_west = "vip-us-west" + vip_amsterdam = "vip-amsterdam" def __str__(self): return self.value From 4742b6512f2000609e36688e647c87a4ceb5dea4 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Sat, 23 May 2026 12:31:42 +0300 Subject: [PATCH 36/43] paillait comment --- discord/guild.py | 15 +++----- discord/scheduled_events.py | 44 ++++++++++++++--------- discord/ui/checkbox.py | 7 ++-- discord/ui/core.py | 71 +++++++++++++++++++++++++++++++++++++ discord/ui/select.py | 63 +++++++++++++++++++++++--------- 5 files changed, 155 insertions(+), 45 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 69e401784e..c79acf7cad 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4330,7 +4330,7 @@ async def create_scheduled_event( entity_type: ScheduledEventEntityType = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, channel_id: int = MISSING, - privacy_level: ScheduledEventPrivacyLevel = MISSING, + privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, image: bytes = MISSING, start_time: datetime.datetime = MISSING, @@ -4382,13 +4382,6 @@ async def create_scheduled_event( ValidationError Invalid parameters for the event type. """ - if privacy_level is not MISSING: - warn_deprecated( - "privacy_level", - None, - "3.0", - extra="It is ignored by the API and will be removed in a future version.", - ) 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: @@ -4396,12 +4389,12 @@ async def create_scheduled_event( "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") + warn_deprecated("start_time", "scheduled_start_time", "2.7", "3.0") if scheduled_start_time is MISSING: scheduled_start_time = start_time if end_time is not MISSING: - warn_deprecated("end_time", "scheduled_end_time", "2.7") + warn_deprecated("end_time", "scheduled_end_time", "2.7", "3.0") if scheduled_end_time is MISSING: scheduled_end_time = end_time @@ -4422,10 +4415,12 @@ async def create_scheduled_event( "entity_type could not be resolved. Pass entity_type explicitly " "or provide a location with a resolvable type." ) + payload: dict[str, str | int] = { "name": name, "scheduled_start_time": scheduled_start_time.isoformat(), "entity_type": int(entity_type), + "privacy_level": int(privacy_level), } if scheduled_end_time is not MISSING: diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index e0ef2e4658..1eba26b99b 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -56,10 +56,7 @@ from .member import Member from .state import ConnectionState from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload -else: - ConnectionState = None - StageChannel = None - VoiceChannel = None + MISSING = utils.MISSING @@ -100,10 +97,12 @@ def __init__( *, state: ConnectionState | None = None, value: str | int | StageChannel | VoiceChannel | None = None, + _suppress_deprecation: bool = False, ) -> None: - warn_deprecated("ScheduledEventLocation", "ScheduledEventEntityMetadata", "2.7") + if not _suppress_deprecation: + warn_deprecated("ScheduledEventLocation", "ScheduledEventEntityMetadata", "2.7") self._state: ConnectionState | None = state - self.value: str | StageChannel | VoiceChannel | Object | None + self.value: str | "StageChannel" | "VoiceChannel" | Object | None if value is None: self.value = None elif isinstance(value, int): @@ -161,12 +160,12 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.location or "" - def to_payload(self) -> dict[str, str]: + def to_payload(self) -> dict[str, str | None]: """Converts the entity metadata to a Discord API payload. Returns ------- - dict[str, str] + dict[str, str | None] A dictionary with the entity metadata fields for the API. """ return {"location": self.location} @@ -322,9 +321,9 @@ def location(self) -> ScheduledEventLocation | None: if self.entity_metadata is None: return None return ScheduledEventLocation( - state=self._state, value=self.entity_metadata.location + state=self._state, value=self.entity_metadata.location, _suppress_deprecation=True ) - return ScheduledEventLocation(state=self._state, value=self.channel_id) + return ScheduledEventLocation(state=self._state, value=self.channel_id, _suppress_deprecation=True) @property def created_at(self) -> datetime.datetime: @@ -422,6 +421,8 @@ async def edit( privacy_level: ScheduledEventPrivacyLevel = MISSING, entity_type: ScheduledEventEntityType = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, + start_time: datetime.datetime = MISSING, + end_time: datetime.datetime = MISSING, ) -> ScheduledEvent | None: """|coro| @@ -519,6 +520,16 @@ async def edit( if image is MISSING: image = cover + if start_time is not MISSING: + warn_deprecated("start_time", "scheduled_start_time", "2.7", "3.0") + if scheduled_start_time is MISSING: + scheduled_start_time = start_time + + if end_time is not MISSING: + warn_deprecated("end_time", "scheduled_end_time", "2.7", "3.0") + if scheduled_end_time is MISSING: + scheduled_end_time = end_time + if entity_type is not MISSING: payload["entity_type"] = int(entity_type) @@ -563,12 +574,13 @@ async def edit( payload["channel_id"] = None - 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 - ) + 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 + ) async def delete(self) -> None: """|coro| diff --git a/discord/ui/checkbox.py b/discord/ui/checkbox.py index 5915f9938d..936d1997be 100644 --- a/discord/ui/checkbox.py +++ b/discord/ui/checkbox.py @@ -29,6 +29,7 @@ from ..components import Checkbox as CheckboxComponent from ..enums import ComponentType +from .core import ComponentLimits from .item import ModalItem __all__ = ("Checkbox",) @@ -105,8 +106,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError(f"custom_id must be str not {value.__class__.__name__}") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.CUSTOM_ID_MAX: + raise ValueError( + f"custom_id must be {ComponentLimits.CUSTOM_ID_MAX} characters or fewer" + ) self.underlying.custom_id = value @property diff --git a/discord/ui/core.py b/discord/ui/core.py index 21bb16871a..a4e76be8ed 100644 --- a/discord/ui/core.py +++ b/discord/ui/core.py @@ -34,6 +34,77 @@ __all__ = ("ItemInterface",) +class ComponentLimits: + # View constraints + VIEW_CHILDREN_MAX = 40 + + # ActionRow constraints + ACTION_ROW_CHILDREN_MAX = 5 + + # Button constraints + BUTTON_LABEL_MAX = 80 + + # MediaGallery constraints + MEDIA_GALLERY_ITEMS_MIN = 1 + MEDIA_GALLERY_ITEMS_MAX = 10 + + # MediaGalleryItem constraints + MEDIA_GALLERY_ITEM_DESCRIPTION_MAX = 256 + + # Select constraints + SELECT_PLACEHOLDER_MAX = 150 + SELECT_OPTIONS_MAX = 25 + SELECT_DEFAULT_VALUES_MAX = 25 + + # Select option constraints + SELECT_OPTION_LABEL_MAX = 100 + SELECT_OPTION_VALUE_MAX = 100 + SELECT_OPTION_DESCRIPTION_MAX = 100 + + # Section constraints + SECTION_ACCESSORY_MAX = 1 + SECTION_CHILDREN_MIN = 1 + SECTION_CHILDREN_MAX = 3 + + # TextInput constraints + TEXT_INPUT_MAX_COUNT = 5 + TEXT_INPUT_LABEL_MAX = 45 + TEXT_INPUT_PLACEHOLDER_MAX = 100 + TEXT_INPUT_MIN_LENGTH_MIN = 0 + TEXT_INPUT_MIN_LENGTH_MAX = 4000 + TEXT_INPUT_MAX_LENGTH_MIN = 1 + TEXT_INPUT_MAX_LENGTH_MAX = 4000 + TEXT_INPUT_VALUE_MAX = 4000 + + # TextDisplay constraints + TEXT_DISPLAY_CONTENT_MAX = 4000 + + # Thumbnail constraints + THUMBNAIL_DESCRIPTION_MAX = 256 + + # Custom ID constraints + CUSTOM_ID_MIN = 1 + CUSTOM_ID_MAX = 100 + + # RadioGroup constraints + RADIO_OPTIONS_MAX = 10 + + # CheckboxGroup constraints + CHECKBOX_OPTIONS_MAX = 10 + CHECKBOX_MIN_VALUES_MIN = 0 + CHECKBOX_MIN_VALUES_MAX = 10 + CHECKBOX_MAX_VALUES_MIN = 1 + CHECKBOX_MAX_VALUES_MAX = 10 + + # FileUpload constraints + FILE_UPLOAD_MIN_FILES = 0 + FILE_UPLOAD_MAX_FILES_MIN = 1 + FILE_UPLOAD_MAX_FILES_MAX = 10 + + # Modal constraints + MODAL_TITLE_MAX = 45 + MODAL_ROWS_MAX = 5 + if TYPE_CHECKING: from typing_extensions import Self diff --git a/discord/ui/select.py b/discord/ui/select.py index d6f5a105ca..d0c517a259 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -45,6 +45,7 @@ from ..threads import Thread from ..user import User from ..utils import MISSING +from .core import ComponentLimits from .item import ItemCallbackType, ModalItem, ViewItem __all__ = ( @@ -265,12 +266,24 @@ def __init__( super().__init__() self._selected_values: list[str] = [] self._interaction: Interaction | None = None - if min_values < 0 or min_values > 25: - raise ValueError("min_values must be between 0 and 25") - if max_values < 1 or max_values > 25: - raise ValueError("max_values must be between 1 and 25") - if placeholder and len(placeholder) > 150: - raise ValueError("placeholder must be 150 characters or fewer") + if ( + min_values < ComponentLimits.SELECT_MIN_VALUE_MIN + or min_values > ComponentLimits.SELECT_MIN_VALUE_MAX + ): + raise ValueError( + f"min_values must be between {ComponentLimits.SELECT_MIN_VALUE_MIN} and {ComponentLimits.SELECT_MIN_VALUE_MAX}" + ) + if ( + max_values < ComponentLimits.SELECT_MAX_VALUE_MIN + or max_values > ComponentLimits.SELECT_MAX_VALUE_MAX + ): + raise ValueError( + f"max_values must be between {ComponentLimits.SELECT_MAX_VALUE_MIN} and {ComponentLimits.SELECT_MAX_VALUE_MAX}" + ) + if placeholder and len(placeholder) > ComponentLimits.SELECT_PLACEHOLDER_MAX: + raise ValueError( + f"placeholder must be {ComponentLimits.SELECT_PLACEHOLDER_MAX} characters or fewer" + ) if not isinstance(custom_id, str) and custom_id is not None: raise TypeError( f"expected custom_id to be str, not {custom_id.__class__.__name__}" @@ -377,8 +390,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError("custom_id must be None or str") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.CUSTOM_ID_MAX: + raise ValueError( + f"custom_id must be {ComponentLimits.CUSTOM_ID_MAX} characters or fewer" + ) self.underlying.custom_id = value self._provided_custom_id = value is not None @@ -391,8 +406,10 @@ def placeholder(self) -> str | None: def placeholder(self, value: str | None): if value is not None and not isinstance(value, str): raise TypeError("placeholder must be None or str") - if value and len(value) > 150: - raise ValueError("placeholder must be 150 characters or fewer") + if value and len(value) > ComponentLimits.SELECT_PLACEHOLDER_MAX: + raise ValueError( + f"placeholder must be {ComponentLimits.SELECT_PLACEHOLDER_MAX} characters or fewer" + ) self.underlying.placeholder = value @@ -403,8 +420,13 @@ def min_values(self) -> int: @min_values.setter def min_values(self, value: int): - if value < 0 or value > 25: - raise ValueError("min_values must be between 0 and 25") + if ( + value < ComponentLimits.SELECT_MIN_VALUE_MIN + or value > ComponentLimits.SELECT_MIN_VALUE_MAX + ): + raise ValueError( + f"min_values must be between {ComponentLimits.SELECT_MIN_VALUE_MIN} and {ComponentLimits.SELECT_MIN_VALUE_MAX}" + ) self.underlying.min_values = int(value) @property @@ -414,8 +436,13 @@ def max_values(self) -> int: @max_values.setter def max_values(self, value: int): - if value < 1 or value > 25: - raise ValueError("max_values must be between 1 and 25") + if ( + value < ComponentLimits.SELECT_MAX_VALUE_MIN + or value > ComponentLimits.SELECT_MAX_VALUE_MAX + ): + raise ValueError( + f"max_values must be between {ComponentLimits.SELECT_MAX_VALUE_MIN} and {ComponentLimits.SELECT_MAX_VALUE_MAX}" + ) self.underlying.max_values = int(value) @property @@ -575,8 +602,10 @@ def append_default_value( if self.type is ComponentType.string_select: raise TypeError("string_select selects do not allow default_values") - if len(self.default_values) >= 25: - raise ValueError("maximum number of default values exceeded (25)") + if len(self.default_values) >= ComponentLimits.SELECT_DEFAULT_VALUES_MAX: + raise ValueError( + f"maximum number of default values exceeded ({ComponentLimits.SELECT_DEFAULT_VALUES_MAX})" + ) if not isinstance(value, SelectDefaultValue): value = SelectDefaultValue._handle_model(value) @@ -654,7 +683,7 @@ def append_option(self, option: SelectOption) -> Self: if self.underlying.type is not ComponentType.string_select: raise Exception("options can only be set on string selects") - if len(self.underlying.options) > 25: + if len(self.underlying.options) > ComponentLimits.SELECT_OPTIONS_MAX: raise ValueError("maximum number of options already provided") self.underlying.options.append(option) From fe312f01c6951f4087e35c23935a5436d8fcfdf7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 09:32:11 +0000 Subject: [PATCH 37/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/guild.py | 2 +- discord/scheduled_events.py | 14 ++++++++++---- discord/ui/core.py | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index c79acf7cad..9c99b5ce15 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4415,7 +4415,7 @@ async def create_scheduled_event( "entity_type could not be resolved. Pass entity_type explicitly " "or provide a location with a resolvable type." ) - + payload: dict[str, str | int] = { "name": name, "scheduled_start_time": scheduled_start_time.isoformat(), diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index 1eba26b99b..e51a994859 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -100,9 +100,11 @@ def __init__( _suppress_deprecation: bool = False, ) -> None: if not _suppress_deprecation: - warn_deprecated("ScheduledEventLocation", "ScheduledEventEntityMetadata", "2.7") + warn_deprecated( + "ScheduledEventLocation", "ScheduledEventEntityMetadata", "2.7" + ) self._state: ConnectionState | None = state - self.value: str | "StageChannel" | "VoiceChannel" | Object | None + self.value: str | StageChannel | VoiceChannel | Object | None if value is None: self.value = None elif isinstance(value, int): @@ -321,9 +323,13 @@ def location(self) -> ScheduledEventLocation | None: if self.entity_metadata is None: return None return ScheduledEventLocation( - state=self._state, value=self.entity_metadata.location, _suppress_deprecation=True + state=self._state, + value=self.entity_metadata.location, + _suppress_deprecation=True, ) - return ScheduledEventLocation(state=self._state, value=self.channel_id, _suppress_deprecation=True) + return ScheduledEventLocation( + state=self._state, value=self.channel_id, _suppress_deprecation=True + ) @property def created_at(self) -> datetime.datetime: diff --git a/discord/ui/core.py b/discord/ui/core.py index a4e76be8ed..d449f2e810 100644 --- a/discord/ui/core.py +++ b/discord/ui/core.py @@ -34,6 +34,7 @@ __all__ = ("ItemInterface",) + class ComponentLimits: # View constraints VIEW_CHILDREN_MAX = 40 From e216d2efa82d3c34be22e4ba80f5f6ddd82439ea Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 10 Jun 2026 13:07:48 +0200 Subject: [PATCH 38/43] Fix a bunch of stuff --- discord/audit_logs.py | 48 +++++++++++++------------ discord/enums.py | 29 +++++++++++++-- discord/guild.py | 35 ++++++++++++------ discord/iterators.py | 16 +++++++-- discord/scheduled_events.py | 37 +++++++++++-------- discord/state.py | 13 ++++++- discord/ui/checkbox.py | 7 ++-- discord/ui/core.py | 72 ------------------------------------- discord/ui/select.py | 63 +++++++++----------------------- 9 files changed, 143 insertions(+), 177 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index de7067af39..6eae0135b0 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -51,7 +51,7 @@ from .guild import Guild from .member import Member from .role import Role - from .scheduled_events import ScheduledEvent, ScheduledEventEntityMetadata + from .scheduled_events import ScheduledEvent from .stage_instance import StageInstance from .state import ConnectionState from .sticker import GuildSticker @@ -217,20 +217,6 @@ def _transform_communication_disabled_until( return None -def _transform_entity_metadata( - entry: AuditLogEntry, data: dict[str, str] | str | None -) -> ScheduledEventEntityMetadata | None: - from .scheduled_events import ScheduledEventEntityMetadata - - if data is None: - return None - if isinstance(data, dict): - location = data.get("location") - else: - location = data - return ScheduledEventEntityMetadata(location=location) - - class AuditLogDiff: def __len__(self) -> int: return len(self.__dict__) @@ -285,18 +271,13 @@ class AuditLogChanges: "default_notifications", _enum_transformer(enums.NotificationLevel), ), - "entity_metadata": (None, _transform_entity_metadata), - "location": (None, _transform_entity_metadata), "rtc_region": (None, _enum_transformer(enums.VoiceRegion)), "video_quality_mode": (None, _enum_transformer(enums.VideoQualityMode)), "privacy_level": (None, _enum_transformer(enums.StagePrivacyLevel)), "format_type": (None, _enum_transformer(enums.StickerFormatType)), "type": (None, _transform_type), "status": (None, _enum_transformer(enums.ScheduledEventStatus)), - "entity_type": ( - "location_type", - _enum_transformer(enums.ScheduledEventEntityType), - ), + "entity_type": (None, _enum_transformer(enums.ScheduledEventEntityType)), "command_id": ("command_id", _transform_snowflake), "image_hash": ("image", _transform_scheduled_event_image), "trigger_type": (None, _enum_transformer(enums.AutoModTriggerType)), @@ -384,10 +365,31 @@ def __init__( setattr(self.before, attr, before) setattr(self.after, attr, after) if attr == "location": - setattr(self.after, "entity_metadata", after) - setattr(self.before, "entity_metadata", before) + from .scheduled_events import ScheduledEventEntityMetadata + + setattr( + self.after, + "entity_metadata", + ( + ScheduledEventEntityMetadata(location=after) + if after is not None + else None + ), + ) + setattr( + self.before, + "entity_metadata", + ( + ScheduledEventEntityMetadata(location=before) + if before is not None + else None + ), + ) # add an alias + if hasattr(self.after, "entity_type"): + self.after.location_type = self.after.entity_type + self.before.location_type = self.before.entity_type if hasattr(self.after, "colour"): self.after.color = self.after.colour self.before.color = self.before.colour diff --git a/discord/enums.py b/discord/enums.py index ce622138d7..f399282ad9 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -30,6 +30,8 @@ from enum import IntEnum from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Union +from typing_extensions import deprecated + __all__ = ( "Enum", "ChannelType", @@ -978,9 +980,30 @@ 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 _DeprecatedScheduledEventLocationTypeMeta(EnumMeta): + @deprecated( + "ScheduledEventLocationType is deprecated since 2.9 and will be removed in 3.0, " + "use ScheduledEventEntityType instead", + ) + def __call__(cls, value): + return ScheduledEventEntityType(value) + + +class ScheduledEventLocationType( + ScheduledEventEntityType, metaclass=_DeprecatedScheduledEventLocationTypeMeta +): + """Scheduled event location type (deprecated alias for :class:`ScheduledEventEntityType`) + + .. deprecated:: 2.9 + Use :class:`ScheduledEventEntityType` instead. + """ + + stage_instance = 1 + voice = 2 + external = 3 + + def __int__(self): + return self.value class AutoModTriggerType(Enum): diff --git a/discord/guild.py b/discord/guild.py index 9c99b5ce15..bb740b08dd 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4298,6 +4298,7 @@ async def create_scheduled_event( location: ( str | int | VoiceChannel | StageChannel | ScheduledEventLocation ) = MISSING, + privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, reason: str | None = None, image: bytes = MISSING, ) -> ScheduledEvent | None: ... @@ -4312,11 +4313,12 @@ async def create_scheduled_event( 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, + channel_id: int | VoiceChannel | StageChannel = MISSING, + privacy_level: ScheduledEventPrivacyLevel = MISSING, reason: str | None = None, image: bytes = MISSING, ) -> ScheduledEvent | None: ... + async def create_scheduled_event( self, *, @@ -4329,8 +4331,8 @@ async def create_scheduled_event( ) = MISSING, entity_type: ScheduledEventEntityType = MISSING, entity_metadata: ScheduledEventEntityMetadata | None = MISSING, - channel_id: int = MISSING, - privacy_level: ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel.guild_only, + channel_id: int | VoiceChannel | StageChannel = MISSING, + privacy_level: ScheduledEventPrivacyLevel = MISSING, reason: str | None = None, image: bytes = MISSING, start_time: datetime.datetime = MISSING, @@ -4389,20 +4391,24 @@ async def create_scheduled_event( "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", "3.0") + warn_deprecated("start_time", "scheduled_start_time", "2.9", "3.0") if scheduled_start_time is MISSING: scheduled_start_time = start_time if end_time is not MISSING: - warn_deprecated("end_time", "scheduled_end_time", "2.7", "3.0") + warn_deprecated("end_time", "scheduled_end_time", "2.9", "3.0") 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") + warn_deprecated("location", "entity_metadata", "2.9", "3.0") if entity_metadata is MISSING: if not isinstance(location, ScheduledEventLocation): - location = ScheduledEventLocation(state=self._state, value=location) + location = ScheduledEventLocation( + state=self._state, + value=location, + _suppress_deprecation=True, + ) if entity_type is MISSING: entity_type = location.type if location.type == ScheduledEventEntityType.external: @@ -4416,11 +4422,20 @@ async def create_scheduled_event( "or provide a location with a resolvable type." ) - payload: dict[str, str | int] = { + if privacy_level is not MISSING: + warn_deprecated("privacy_level", since="2.9") + resolved_privacy_level = privacy_level + else: + resolved_privacy_level = ScheduledEventPrivacyLevel.guild_only + + if channel_id is not MISSING and not isinstance(channel_id, int): + channel_id = channel_id.id + + payload: dict[str, str | int | None] = { "name": name, "scheduled_start_time": scheduled_start_time.isoformat(), "entity_type": int(entity_type), - "privacy_level": int(privacy_level), + "privacy_level": int(resolved_privacy_level), } if scheduled_end_time is not MISSING: diff --git a/discord/iterators.py b/discord/iterators.py index 59aa08f2f5..33216f1faf 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -27,7 +27,7 @@ import asyncio import datetime -import itertools +import warnings from typing import ( TYPE_CHECKING, Any, @@ -916,6 +916,13 @@ def __init__( self.after = after self.use_cache = use_cache + if use_cache and not with_member: + warnings.warn( + "use_cache=True only yields cached members; as_member=False is ignored.", + UserWarning, + stacklevel=2, + ) + self.subscribers = asyncio.Queue() self.get_subscribers = self.event._state.http.get_scheduled_event_users @@ -957,11 +964,16 @@ def user_from_payload(self, data): async def _fill_from_cache(self): """Fill subscribers queue from cached user IDs.""" cached_user_ids = list(self.event._cached_subscribers) + remaining = self.limit - for user_id in itertools.islice(iter(cached_user_ids), self.retrieve): + for user_id in cached_user_ids: + if remaining is not None and remaining <= 0: + break member = self.event.guild.get_member(user_id) if member: await self.subscribers.put(member) + if remaining is not None: + remaining -= 1 self.limit = 0 diff --git a/discord/scheduled_events.py b/discord/scheduled_events.py index e51a994859..356b7c50b5 100644 --- a/discord/scheduled_events.py +++ b/discord/scheduled_events.py @@ -74,7 +74,7 @@ class ScheduledEventLocation: | :class:`str` | :attr:`ScheduledEventLocationType.external` | +------------------------+---------------------------------------------------+ - .. deprecated:: 2.7 + .. deprecated:: 2.9 Use :class:`ScheduledEventEntityMetadata` instead. .. versionadded:: 2.0 @@ -101,7 +101,7 @@ def __init__( ) -> None: if not _suppress_deprecation: warn_deprecated( - "ScheduledEventLocation", "ScheduledEventEntityMetadata", "2.7" + "ScheduledEventLocation", "ScheduledEventEntityMetadata", "2.9" ) self._state: ConnectionState | None = state self.value: str | StageChannel | VoiceChannel | Object | None @@ -140,7 +140,7 @@ class ScheduledEventEntityMetadata: This contains additional metadata for the scheduled event, particularly for external events which require a location string. - .. versionadded:: 2.7 + .. versionadded:: 2.9 Attributes ---------- @@ -315,7 +315,7 @@ def __repr__(self) -> str: @property @typing_extensions.deprecated( - "location is deprecated since 2.7 and will be removed in 3.0, consider using entity_metadata instead", + "location is deprecated since 2.9 and will be removed in 3.0, consider using entity_metadata instead", ) def location(self) -> ScheduledEventLocation | None: """Returns the location of the event.""" @@ -338,46 +338,46 @@ def created_at(self) -> datetime.datetime: @property @typing_extensions.deprecated( - "start_time is deprecated since 2.7 and will be removed in 3.0, consider using scheduled_start_time instead", + "start_time is deprecated since 2.9 and will be removed in 3.0, consider using scheduled_start_time instead", ) def start_time(self) -> datetime.datetime: """ Returns the scheduled start time of the event. - .. deprecated:: 2.7 + .. deprecated:: 2.9 Use :attr:`scheduled_start_time` instead. """ return self.scheduled_start_time @property @typing_extensions.deprecated( - "end_time is deprecated since 2.7 and will be removed in 3.0, consider using scheduled_end_time instead", + "end_time is deprecated since 2.9 and will be removed in 3.0, consider using scheduled_end_time instead", ) def end_time(self) -> datetime.datetime | None: """ Returns the scheduled end time of the event. - .. deprecated:: 2.7 + .. deprecated:: 2.9 Use :attr:`scheduled_end_time` instead. """ return self.scheduled_end_time @property @typing_extensions.deprecated( - "subscriber_count is deprecated since 2.7 and will be removed in 3.0, consider using user_count instead", + "subscriber_count is deprecated since 2.9 and will be removed in 3.0, consider using user_count instead", ) def subscriber_count(self) -> int | None: """ Returns the number of users subscribed to the event. - .. deprecated:: 2.7 + .. deprecated:: 2.9 Use :attr:`user_count` instead. """ return self.user_count @property @typing_extensions.deprecated( - "interested is deprecated since 2.7 and will be removed in 3.0, consider using user_count instead", + "interested is deprecated since 2.9 and will be removed in 3.0, consider using user_count instead", ) def interested(self) -> int | None: """An alias to :attr:`.user_count`""" @@ -510,16 +510,21 @@ async def edit( payload["privacy_level"] = int(privacy_level) if location is not MISSING: - warn_deprecated("location", "entity_metadata", "2.7", "3.0") + warn_deprecated("location", "entity_metadata", "2.9", "3.0") if entity_metadata is MISSING: if not isinstance(location, ScheduledEventLocation): - location = ScheduledEventLocation(state=self._state, value=location) + location = ScheduledEventLocation( + state=self._state, + value=location, + _suppress_deprecation=True, + ) if entity_type is MISSING: entity_type = location.type if location.type == ScheduledEventEntityType.external: entity_metadata = ScheduledEventEntityMetadata(str(location)) else: payload["channel_id"] = location.value.id + payload["entity_metadata"] = None if cover is not MISSING: warn_deprecated("cover", "image", "2.7", "3.0") @@ -527,12 +532,12 @@ async def edit( image = cover if start_time is not MISSING: - warn_deprecated("start_time", "scheduled_start_time", "2.7", "3.0") + warn_deprecated("start_time", "scheduled_start_time", "2.9", "3.0") if scheduled_start_time is MISSING: scheduled_start_time = start_time if end_time is not MISSING: - warn_deprecated("end_time", "scheduled_end_time", "2.7", "3.0") + warn_deprecated("end_time", "scheduled_end_time", "2.9", "3.0") if scheduled_end_time is MISSING: scheduled_end_time = end_time @@ -725,6 +730,8 @@ def subscribers( 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``. + Only members currently cached in the guild are yielded, and + ``as_member=False`` is ignored with a warning. Yields ------ diff --git a/discord/state.py b/discord/state.py index 9b6b393d69..0420c61696 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1678,10 +1678,12 @@ def parse_guild_scheduled_event_update(self, data) -> None: if not data.get("creator", None) else guild.get_member(data.get("creator_id")) ) + old_event = guild.get_scheduled_event(int(data["id"])) scheduled_event = ScheduledEvent( state=self, guild=guild, creator=creator, data=data ) - old_event = guild.get_scheduled_event(int(data["id"])) + if old_event is not None: + scheduled_event._cached_subscribers = old_event._cached_subscribers.copy() guild._add_scheduled_event(scheduled_event) self.dispatch("scheduled_event_update", old_event, scheduled_event) @@ -1702,10 +1704,15 @@ def parse_guild_scheduled_event_delete(self, data) -> None: if not data.get("creator", None) else guild.get_member(data.get("creator_id")) ) + cached_event = guild.get_scheduled_event(int(data["id"])) scheduled_event = ScheduledEvent( state=self, guild=guild, creator=creator, data=data ) scheduled_event.status = ScheduledEventStatus.canceled + if cached_event is not None: + scheduled_event._cached_subscribers = ( + cached_event._cached_subscribers.copy() + ) guild._remove_scheduled_event(scheduled_event) self.dispatch("scheduled_event_delete", scheduled_event) @@ -1729,6 +1736,8 @@ def parse_guild_scheduled_event_user_add(self, data) -> None: event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) if event: event._cached_subscribers.add(user_id) + if event.user_count is not None: + event.user_count += 1 member = guild.get_member(user_id) if member is not None: self.dispatch("scheduled_event_user_add", event, member) @@ -1753,6 +1762,8 @@ def parse_guild_scheduled_event_user_remove(self, data) -> None: event = guild.get_scheduled_event(int(data["guild_scheduled_event_id"])) if event: event._cached_subscribers.discard(user_id) + if event.user_count is not None: + event.user_count -= 1 member = guild.get_member(user_id) if member is not None: self.dispatch("scheduled_event_user_remove", event, member) diff --git a/discord/ui/checkbox.py b/discord/ui/checkbox.py index 936d1997be..5915f9938d 100644 --- a/discord/ui/checkbox.py +++ b/discord/ui/checkbox.py @@ -29,7 +29,6 @@ from ..components import Checkbox as CheckboxComponent from ..enums import ComponentType -from .core import ComponentLimits from .item import ModalItem __all__ = ("Checkbox",) @@ -106,10 +105,8 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError(f"custom_id must be str not {value.__class__.__name__}") - if len(value) > ComponentLimits.CUSTOM_ID_MAX: - raise ValueError( - f"custom_id must be {ComponentLimits.CUSTOM_ID_MAX} characters or fewer" - ) + if len(value) > 100: + raise ValueError("custom_id must be 100 characters or fewer") self.underlying.custom_id = value @property diff --git a/discord/ui/core.py b/discord/ui/core.py index d449f2e810..21bb16871a 100644 --- a/discord/ui/core.py +++ b/discord/ui/core.py @@ -35,78 +35,6 @@ __all__ = ("ItemInterface",) -class ComponentLimits: - # View constraints - VIEW_CHILDREN_MAX = 40 - - # ActionRow constraints - ACTION_ROW_CHILDREN_MAX = 5 - - # Button constraints - BUTTON_LABEL_MAX = 80 - - # MediaGallery constraints - MEDIA_GALLERY_ITEMS_MIN = 1 - MEDIA_GALLERY_ITEMS_MAX = 10 - - # MediaGalleryItem constraints - MEDIA_GALLERY_ITEM_DESCRIPTION_MAX = 256 - - # Select constraints - SELECT_PLACEHOLDER_MAX = 150 - SELECT_OPTIONS_MAX = 25 - SELECT_DEFAULT_VALUES_MAX = 25 - - # Select option constraints - SELECT_OPTION_LABEL_MAX = 100 - SELECT_OPTION_VALUE_MAX = 100 - SELECT_OPTION_DESCRIPTION_MAX = 100 - - # Section constraints - SECTION_ACCESSORY_MAX = 1 - SECTION_CHILDREN_MIN = 1 - SECTION_CHILDREN_MAX = 3 - - # TextInput constraints - TEXT_INPUT_MAX_COUNT = 5 - TEXT_INPUT_LABEL_MAX = 45 - TEXT_INPUT_PLACEHOLDER_MAX = 100 - TEXT_INPUT_MIN_LENGTH_MIN = 0 - TEXT_INPUT_MIN_LENGTH_MAX = 4000 - TEXT_INPUT_MAX_LENGTH_MIN = 1 - TEXT_INPUT_MAX_LENGTH_MAX = 4000 - TEXT_INPUT_VALUE_MAX = 4000 - - # TextDisplay constraints - TEXT_DISPLAY_CONTENT_MAX = 4000 - - # Thumbnail constraints - THUMBNAIL_DESCRIPTION_MAX = 256 - - # Custom ID constraints - CUSTOM_ID_MIN = 1 - CUSTOM_ID_MAX = 100 - - # RadioGroup constraints - RADIO_OPTIONS_MAX = 10 - - # CheckboxGroup constraints - CHECKBOX_OPTIONS_MAX = 10 - CHECKBOX_MIN_VALUES_MIN = 0 - CHECKBOX_MIN_VALUES_MAX = 10 - CHECKBOX_MAX_VALUES_MIN = 1 - CHECKBOX_MAX_VALUES_MAX = 10 - - # FileUpload constraints - FILE_UPLOAD_MIN_FILES = 0 - FILE_UPLOAD_MAX_FILES_MIN = 1 - FILE_UPLOAD_MAX_FILES_MAX = 10 - - # Modal constraints - MODAL_TITLE_MAX = 45 - MODAL_ROWS_MAX = 5 - - if TYPE_CHECKING: from typing_extensions import Self diff --git a/discord/ui/select.py b/discord/ui/select.py index d0c517a259..d6f5a105ca 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -45,7 +45,6 @@ from ..threads import Thread from ..user import User from ..utils import MISSING -from .core import ComponentLimits from .item import ItemCallbackType, ModalItem, ViewItem __all__ = ( @@ -266,24 +265,12 @@ def __init__( super().__init__() self._selected_values: list[str] = [] self._interaction: Interaction | None = None - if ( - min_values < ComponentLimits.SELECT_MIN_VALUE_MIN - or min_values > ComponentLimits.SELECT_MIN_VALUE_MAX - ): - raise ValueError( - f"min_values must be between {ComponentLimits.SELECT_MIN_VALUE_MIN} and {ComponentLimits.SELECT_MIN_VALUE_MAX}" - ) - if ( - max_values < ComponentLimits.SELECT_MAX_VALUE_MIN - or max_values > ComponentLimits.SELECT_MAX_VALUE_MAX - ): - raise ValueError( - f"max_values must be between {ComponentLimits.SELECT_MAX_VALUE_MIN} and {ComponentLimits.SELECT_MAX_VALUE_MAX}" - ) - if placeholder and len(placeholder) > ComponentLimits.SELECT_PLACEHOLDER_MAX: - raise ValueError( - f"placeholder must be {ComponentLimits.SELECT_PLACEHOLDER_MAX} characters or fewer" - ) + if min_values < 0 or min_values > 25: + raise ValueError("min_values must be between 0 and 25") + if max_values < 1 or max_values > 25: + raise ValueError("max_values must be between 1 and 25") + if placeholder and len(placeholder) > 150: + raise ValueError("placeholder must be 150 characters or fewer") if not isinstance(custom_id, str) and custom_id is not None: raise TypeError( f"expected custom_id to be str, not {custom_id.__class__.__name__}" @@ -390,10 +377,8 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError("custom_id must be None or str") - if len(value) > ComponentLimits.CUSTOM_ID_MAX: - raise ValueError( - f"custom_id must be {ComponentLimits.CUSTOM_ID_MAX} characters or fewer" - ) + if len(value) > 100: + raise ValueError("custom_id must be 100 characters or fewer") self.underlying.custom_id = value self._provided_custom_id = value is not None @@ -406,10 +391,8 @@ def placeholder(self) -> str | None: def placeholder(self, value: str | None): if value is not None and not isinstance(value, str): raise TypeError("placeholder must be None or str") - if value and len(value) > ComponentLimits.SELECT_PLACEHOLDER_MAX: - raise ValueError( - f"placeholder must be {ComponentLimits.SELECT_PLACEHOLDER_MAX} characters or fewer" - ) + if value and len(value) > 150: + raise ValueError("placeholder must be 150 characters or fewer") self.underlying.placeholder = value @@ -420,13 +403,8 @@ def min_values(self) -> int: @min_values.setter def min_values(self, value: int): - if ( - value < ComponentLimits.SELECT_MIN_VALUE_MIN - or value > ComponentLimits.SELECT_MIN_VALUE_MAX - ): - raise ValueError( - f"min_values must be between {ComponentLimits.SELECT_MIN_VALUE_MIN} and {ComponentLimits.SELECT_MIN_VALUE_MAX}" - ) + if value < 0 or value > 25: + raise ValueError("min_values must be between 0 and 25") self.underlying.min_values = int(value) @property @@ -436,13 +414,8 @@ def max_values(self) -> int: @max_values.setter def max_values(self, value: int): - if ( - value < ComponentLimits.SELECT_MAX_VALUE_MIN - or value > ComponentLimits.SELECT_MAX_VALUE_MAX - ): - raise ValueError( - f"max_values must be between {ComponentLimits.SELECT_MAX_VALUE_MIN} and {ComponentLimits.SELECT_MAX_VALUE_MAX}" - ) + if value < 1 or value > 25: + raise ValueError("max_values must be between 1 and 25") self.underlying.max_values = int(value) @property @@ -602,10 +575,8 @@ def append_default_value( if self.type is ComponentType.string_select: raise TypeError("string_select selects do not allow default_values") - if len(self.default_values) >= ComponentLimits.SELECT_DEFAULT_VALUES_MAX: - raise ValueError( - f"maximum number of default values exceeded ({ComponentLimits.SELECT_DEFAULT_VALUES_MAX})" - ) + if len(self.default_values) >= 25: + raise ValueError("maximum number of default values exceeded (25)") if not isinstance(value, SelectDefaultValue): value = SelectDefaultValue._handle_model(value) @@ -683,7 +654,7 @@ def append_option(self, option: SelectOption) -> Self: if self.underlying.type is not ComponentType.string_select: raise Exception("options can only be set on string selects") - if len(self.underlying.options) > ComponentLimits.SELECT_OPTIONS_MAX: + if len(self.underlying.options) > 25: raise ValueError("maximum number of options already provided") self.underlying.options.append(option) From ea24bd3442e877d3e660a9b9d623a86891d733fc Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 10 Jun 2026 13:11:55 +0200 Subject: [PATCH 39/43] chore: Undo, will do this later --- discord/enums.py | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index f399282ad9..2d5cfc0606 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -100,23 +100,23 @@ def _create_value_cls(name, comparable): cls.__str__ = lambda self: f"{name}.{self.name}" if comparable: cls.__le__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value <= other.value + isinstance(other, self.__class__) and self.value <= other.value ) cls.__ge__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value >= other.value + isinstance(other, self.__class__) and self.value >= other.value ) cls.__lt__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value < other.value + isinstance(other, self.__class__) and self.value < other.value ) cls.__gt__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value > other.value + isinstance(other, self.__class__) and self.value > other.value ) return cls def _is_descriptor(obj): return ( - hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__") + hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__") ) @@ -887,7 +887,7 @@ def from_datatype(cls, datatype): from .ext.bridge import BridgeContext if not issubclass( - datatype, (ApplicationContext, BridgeContext) + datatype, (ApplicationContext, BridgeContext) ): # TODO: prevent ctx being passed here in cog commands raise TypeError( f"Invalid class {datatype} used as an input type for an Option" @@ -980,30 +980,8 @@ def __int__(self): return self.value -class _DeprecatedScheduledEventLocationTypeMeta(EnumMeta): - @deprecated( - "ScheduledEventLocationType is deprecated since 2.9 and will be removed in 3.0, " - "use ScheduledEventEntityType instead", - ) - def __call__(cls, value): - return ScheduledEventEntityType(value) - - -class ScheduledEventLocationType( - ScheduledEventEntityType, metaclass=_DeprecatedScheduledEventLocationTypeMeta -): - """Scheduled event location type (deprecated alias for :class:`ScheduledEventEntityType`) - - .. deprecated:: 2.9 - Use :class:`ScheduledEventEntityType` instead. - """ - - stage_instance = 1 - voice = 2 - external = 3 - - def __int__(self): - return self.value +# TODO(Paillat-dev): Add @deprecated notice using warnings.deprecated or in some other way +ScheduledEventLocationType = ScheduledEventEntityType class AutoModTriggerType(Enum): From 373c8152c65579367564af3e4a8573296e967997 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:12:51 +0000 Subject: [PATCH 40/43] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/enums.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 2d5cfc0606..ff8e86c135 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -100,23 +100,23 @@ def _create_value_cls(name, comparable): cls.__str__ = lambda self: f"{name}.{self.name}" if comparable: cls.__le__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value <= other.value + isinstance(other, self.__class__) and self.value <= other.value ) cls.__ge__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value >= other.value + isinstance(other, self.__class__) and self.value >= other.value ) cls.__lt__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value < other.value + isinstance(other, self.__class__) and self.value < other.value ) cls.__gt__ = lambda self, other: ( - isinstance(other, self.__class__) and self.value > other.value + isinstance(other, self.__class__) and self.value > other.value ) return cls def _is_descriptor(obj): return ( - hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__") + hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__") ) @@ -887,7 +887,7 @@ def from_datatype(cls, datatype): from .ext.bridge import BridgeContext if not issubclass( - datatype, (ApplicationContext, BridgeContext) + datatype, (ApplicationContext, BridgeContext) ): # TODO: prevent ctx being passed here in cog commands raise TypeError( f"Invalid class {datatype} used as an input type for an Option" From c8536dacc40e5d8ee61bb7e350299c5eef93ad08 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 10 Jun 2026 13:17:07 +0200 Subject: [PATCH 41/43] docs: CHANGELOG.md --- CHANGELOG.md | 183 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 115 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5173e5795c..6c540a9b7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ These changes are available on the `master` branch, but have not yet been releas ### Added +- Added `ScheduledEventEntityType` enum and `ScheduledEventEntityMetadata` class, added + missing attributes and API-aligned parameters to `ScheduledEvent`, + `Guild.create_scheduled_event`, and `ScheduledEvent.edit`, and added `use_cache` to + `ScheduledEvent.subscribers()`. + ([#3025](https://github.com/Pycord-Development/pycord/pull/3025)) + ### Changed ### Fixed @@ -22,9 +28,15 @@ These changes are available on the `master` branch, but have not yet been releas ([#3231](https://github.com/Pycord-Development/pycord/pull/3231)) - Allow `ForumTag` to be created without an emoji. ([#3245](https://github.com/Pycord-Development/pycord/pull/3245)) +- Fixed `ScheduledEvent` subscriber cache not being kept in sync correctly. + ([#3025](https://github.com/Pycord-Development/pycord/pull/3025)) ### Deprecated +- Deprecated `ScheduledEventLocationType`, `ScheduledEventLocation`, and several + `ScheduledEvent` attributes and related methods' parameters in favor of their API-aligned + names. ([#3025](https://github.com/Pycord-Development/pycord/pull/3025)) + ### Removed ## [2.8.0] - 2026-05-18 @@ -114,8 +126,8 @@ These changes are available on the `master` branch, but have not yet been releas - Removed the guild creation and ownership-related methods and arguments due to updated restrictions. ([#3056](https://github.com/Pycord-Development/pycord/pull/3056)) - - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, - `Template.create_guild`, and `Client.create_guild`. + - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, + `Template.create_guild`, and `Client.create_guild`. ## [2.8.0rc2] - 2026-04-14 @@ -196,8 +208,8 @@ These changes are available on the `master` branch, but have not yet been releas - Removed the guild creation and ownership-related methods and arguments due to updated restrictions. ([#3056](https://github.com/Pycord-Development/pycord/pull/3056)) - - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, - `Template.create_guild`, and `Client.create_guild`. + - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, + `Template.create_guild`, and `Client.create_guild`. ## [2.7.2] - 2026-04-14 @@ -290,12 +302,12 @@ These changes are available on the `master` branch, but have not yet been releas ([#2908](https://github.com/Pycord-Development/pycord/pull/2908)) - Added support for select default values. ([#2899](https://github.com/Pycord-Development/pycord/pull/2899)) - - Adds a new generic parameter to selects to type `ui.Select.values` return type. - - Adds `SelectDefaultValue` object to create select default values. - - Adds `SelectDefaultValueType` enum. - - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the - different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, - `ui.MentionableSelect`, and `ui.ChannelSelect`. + - Adds a new generic parameter to selects to type `ui.Select.values` return type. + - Adds `SelectDefaultValue` object to create select default values. + - Adds `SelectDefaultValueType` enum. + - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the + different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, + `ui.MentionableSelect`, and `ui.ChannelSelect`. - Added `store` parameter to `View` and `Modal` classes. ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - Added `Webhook.parent` and `Webhook.from_interaction` @@ -348,14 +360,14 @@ These changes are available on the `master` branch, but have not yet been releas - Added `VoiceMessage` subclass of `File` to allow voice messages to be sent. ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) - Added the following soundboard-related features: - - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, - `SoundboardSound.edit()`, and `SoundboardSound.delete()`. - - Access Discord default sounds with `Client.fetch_default_sounds()`. - - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. - - New `on_voice_channel_effect_send` event for sound and emoji effects. - - Soundboard limits based on guild premium tier (8-48 slots) in - `Guild.soundboard_limit`. - ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) + - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, + `SoundboardSound.edit()`, and `SoundboardSound.delete()`. + - Access Discord default sounds with `Client.fetch_default_sounds()`. + - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. + - New `on_voice_channel_effect_send` event for sound and emoji effects. + - Soundboard limits based on guild premium tier (8-48 slots) in + `Guild.soundboard_limit`. + ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) - Added new `Subscription` object and related methods/events. ([#2564](https://github.com/Pycord-Development/pycord/pull/2564)) - Added `Message.forward_to`, `Message.snapshots`, and other related attributes. @@ -398,20 +410,20 @@ These changes are available on the `master` branch, but have not yet been releas - Overhauled support for Components V2 and new Modal components ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible - with new features. - - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new - components. - - `DesignerView` and `Container` do not support `Button` and `Select` directly; use - `discord.ui.ActionRow` instead. - - `DesignerModal` does not support `InputText` and `Select` directly; use - `discord.ui.Label` instead. - - Removed `InputText.description`, `Select.label` and `Select.description`; these are - now attributes of `Label`. - - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items - inherit from these. - - All view and modal classes now inherit from a base `ItemInterface` class, split into - `BaseView` and `BaseModal` + - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible + with new features. + - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new + components. + - `DesignerView` and `Container` do not support `Button` and `Select` directly; use + `discord.ui.ActionRow` instead. + - `DesignerModal` does not support `InputText` and `Select` directly; use + `discord.ui.Label` instead. + - Removed `InputText.description`, `Select.label` and `Select.description`; these are + now attributes of `Label`. + - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items + inherit from these. + - All view and modal classes now inherit from a base `ItemInterface` class, split into + `BaseView` and `BaseModal` - Renamed `cover` property of `ScheduledEvent` and `cover` argument of `ScheduledEvent.edit` to `image`. ([#2496](https://github.com/Pycord-Development/pycord/pull/2496)) @@ -592,12 +604,12 @@ These changes are available on the `master` branch, but have not yet been releas ([#2908](https://github.com/Pycord-Development/pycord/pull/2908)) - Added support for select default values. ([#2899](https://github.com/Pycord-Development/pycord/pull/2899)) - - Adds a new generic parameter to selects to type `ui.Select.values` return type. - - Adds `SelectDefaultValue` object to create select default values. - - Adds `SelectDefaultValueType` enum. - - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the - different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, - `ui.MentionableSelect`, and `ui.ChannelSelect`. + - Adds a new generic parameter to selects to type `ui.Select.values` return type. + - Adds `SelectDefaultValue` object to create select default values. + - Adds `SelectDefaultValueType` enum. + - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the + different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, + `ui.MentionableSelect`, and `ui.ChannelSelect`. - Added `store` parameter to `View` and `Modal` classes. ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - Added `Webhook.parent` and `Webhook.from_interaction` @@ -619,20 +631,20 @@ These changes are available on the `master` branch, but have not yet been releas - Overhauled support for Components V2 and new Modal components ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible - with new features. - - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new - components. - - `DesignerView` and `Container` do not support `Button` and `Select` directly; use - `discord.ui.ActionRow` instead. - - `DesignerModal` does not support `InputText` and `Select` directly; use - `discord.ui.Label` instead. - - Removed `InputText.description`, `Select.label` and `Select.description`; these are - now attributes of `Label`. - - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items - inherit from these. - - All view and modal classes now inherit from a base `ItemInterface` class, split into - `BaseView` and `BaseModal` + - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible + with new features. + - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new + components. + - `DesignerView` and `Container` do not support `Button` and `Select` directly; use + `discord.ui.ActionRow` instead. + - `DesignerModal` does not support `InputText` and `Select` directly; use + `discord.ui.Label` instead. + - Removed `InputText.description`, `Select.label` and `Select.description`; these are + now attributes of `Label`. + - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items + inherit from these. + - All view and modal classes now inherit from a base `ItemInterface` class, split into + `BaseView` and `BaseModal` ### Fixed @@ -715,14 +727,14 @@ These changes are available on the `master` branch, but have not yet been releas - Added `VoiceMessage` subclass of `File` to allow voice messages to be sent. ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) - Added the following soundboard-related features: - - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, - `SoundboardSound.edit()`, and `SoundboardSound.delete()`. - - Access Discord default sounds with `Client.fetch_default_sounds()`. - - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. - - New `on_voice_channel_effect_send` event for sound and emoji effects. - - Soundboard limits based on guild premium tier (8-48 slots) in - `Guild.soundboard_limit`. - ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) + - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, + `SoundboardSound.edit()`, and `SoundboardSound.delete()`. + - Access Discord default sounds with `Client.fetch_default_sounds()`. + - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. + - New `on_voice_channel_effect_send` event for sound and emoji effects. + - Soundboard limits based on guild premium tier (8-48 slots) in + `Guild.soundboard_limit`. + ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) - Added new `Subscription` object and related methods/events. ([#2564](https://github.com/Pycord-Development/pycord/pull/2564)) - Added `Message.forward_to`, `Message.snapshots`, and other related attributes. @@ -1785,46 +1797,81 @@ These changes are available on the `master` branch, but have not yet been releas ([#1240](https://github.com/Pycord-Development/pycord/pull/1240)) [unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.8.0...HEAD + [2.8.0]: https://github.com/Pycord-Development/pycord/compare/v2.7.2...v2.8.0 + [2.8.0rc1]: https://github.com/Pycord-Development/pycord/compare/v2.8.0rc1...v2.8.0rc2 + [2.8.0rc1]: https://github.com/Pycord-Development/pycord/compare/v2.7.2...v2.8.0rc1 + [2.7.2]: https://github.com/Pycord-Development/pycord/compare/v2.7.1...v2.7.2 + [2.7.1]: https://github.com/Pycord-Development/pycord/compare/v2.7.0...v2.7.1 + [2.7.0]: https://github.com/Pycord-Development/pycord/compare/v2.7.0rc2...v2.7.0 + [2.7.0rc2]: https://github.com/Pycord-Development/pycord/compare/v2.7.0rc1...v2.7.0rc2 + [2.7.0rc1]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...v2.7.0rc1 + [2.6.1]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...v2.6.1 + [2.6.0]: https://github.com/Pycord-Development/pycord/compare/v2.5.0...v2.6.0 + [2.5.0]: https://github.com/Pycord-Development/pycord/compare/v2.4.1...v2.5.0 + [2.4.1]: https://github.com/Pycord-Development/pycord/compare/v2.4.0...v2.4.1 + [2.4.0]: https://github.com/Pycord-Development/pycord/compare/v2.3.3...v2.4.0 + [2.3.3]: https://github.com/Pycord-Development/pycord/compare/v2.3.2...v2.3.3 + [2.3.2]: https://github.com/Pycord-Development/pycord/compare/v2.3.1...v2.3.2 + [2.3.1]: https://github.com/Pycord-Development/pycord/compare/v2.3.0...v2.3.1 + [2.3.0]: https://github.com/Pycord-Development/pycord/compare/v2.2.2...v2.3.0 + [2.2.2]: https://github.com/Pycord-Development/pycord/compare/v2.2.1...v2.2.2 + [2.2.1]: https://github.com/Pycord-Development/pycord/compare/v2.2.0...v2.2.1 + [2.2.0]: https://github.com/Pycord-Development/pycord/compare/v2.1.3...v2.2.0 + [2.1.3]: https://github.com/Pycord-Development/pycord/compare/v2.1.2...v2.1.3 + [2.1.2]: https://github.com/Pycord-Development/pycord/compare/v2.1.1...v2.1.2 + [2.1.1]: https://github.com/Pycord-Development/pycord/compare/v2.1.0...v2.1.1 + [2.1.0]: https://github.com/Pycord-Development/pycord/compare/v2.0.1...v2.1.0 + [2.0.1]: https://github.com/Pycord-Development/pycord/compare/v2.0.0...v2.0.1 + [2.0.0]: https://github.com/Pycord-Development/pycord/compare/v2.0.0-rc.1...v2.0.0 + [2.0.0-rc.1]: - https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.7...v2.0.0-rc.1 +https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.7...v2.0.0-rc.1 + [2.0.0-beta.7]: - https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.6...v2.0.0-beta.7 +https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.6...v2.0.0-beta.7 + [2.0.0-beta.6]: - https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.5...v2.0.0-beta.6 +https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.5...v2.0.0-beta.6 + [2.0.0-beta.5]: - https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.4...v2.0.0-beta.5 +https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.4...v2.0.0-beta.5 + [2.0.0-beta.4]: - https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.3...v2.0.0-beta.4 +https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.3...v2.0.0-beta.4 + [2.0.0-beta.3]: - https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.2...v2.0.0-beta.3 +https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.2...v2.0.0-beta.3 + [2.0.0-beta.2]: - https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.1...v2.0.0-beta.2 +https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.1...v2.0.0-beta.2 + [2.0.0-beta.1]: - https://github.com/Pycord-Development/pycord/compare/v1.7.3...v2.0.0-beta.1 +https://github.com/Pycord-Development/pycord/compare/v1.7.3...v2.0.0-beta.1 + [version guarantees]: https://docs.pycord.dev/en/stable/version_guarantees.html From cc872939af40c625360e4a40e277e00fe836ef0e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:17:51 +0000 Subject: [PATCH 42/43] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 175 +++++++++++++++++++++------------------------------ 1 file changed, 70 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c540a9b7c..70ed100b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,8 +34,8 @@ These changes are available on the `master` branch, but have not yet been releas ### Deprecated - Deprecated `ScheduledEventLocationType`, `ScheduledEventLocation`, and several - `ScheduledEvent` attributes and related methods' parameters in favor of their API-aligned - names. ([#3025](https://github.com/Pycord-Development/pycord/pull/3025)) + `ScheduledEvent` attributes and related methods' parameters in favor of their + API-aligned names. ([#3025](https://github.com/Pycord-Development/pycord/pull/3025)) ### Removed @@ -126,8 +126,8 @@ These changes are available on the `master` branch, but have not yet been releas - Removed the guild creation and ownership-related methods and arguments due to updated restrictions. ([#3056](https://github.com/Pycord-Development/pycord/pull/3056)) - - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, - `Template.create_guild`, and `Client.create_guild`. + - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, + `Template.create_guild`, and `Client.create_guild`. ## [2.8.0rc2] - 2026-04-14 @@ -208,8 +208,8 @@ These changes are available on the `master` branch, but have not yet been releas - Removed the guild creation and ownership-related methods and arguments due to updated restrictions. ([#3056](https://github.com/Pycord-Development/pycord/pull/3056)) - - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, - `Template.create_guild`, and `Client.create_guild`. + - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, + `Template.create_guild`, and `Client.create_guild`. ## [2.7.2] - 2026-04-14 @@ -302,12 +302,12 @@ These changes are available on the `master` branch, but have not yet been releas ([#2908](https://github.com/Pycord-Development/pycord/pull/2908)) - Added support for select default values. ([#2899](https://github.com/Pycord-Development/pycord/pull/2899)) - - Adds a new generic parameter to selects to type `ui.Select.values` return type. - - Adds `SelectDefaultValue` object to create select default values. - - Adds `SelectDefaultValueType` enum. - - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the - different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, - `ui.MentionableSelect`, and `ui.ChannelSelect`. + - Adds a new generic parameter to selects to type `ui.Select.values` return type. + - Adds `SelectDefaultValue` object to create select default values. + - Adds `SelectDefaultValueType` enum. + - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the + different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, + `ui.MentionableSelect`, and `ui.ChannelSelect`. - Added `store` parameter to `View` and `Modal` classes. ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - Added `Webhook.parent` and `Webhook.from_interaction` @@ -360,14 +360,14 @@ These changes are available on the `master` branch, but have not yet been releas - Added `VoiceMessage` subclass of `File` to allow voice messages to be sent. ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) - Added the following soundboard-related features: - - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, - `SoundboardSound.edit()`, and `SoundboardSound.delete()`. - - Access Discord default sounds with `Client.fetch_default_sounds()`. - - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. - - New `on_voice_channel_effect_send` event for sound and emoji effects. - - Soundboard limits based on guild premium tier (8-48 slots) in - `Guild.soundboard_limit`. - ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) + - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, + `SoundboardSound.edit()`, and `SoundboardSound.delete()`. + - Access Discord default sounds with `Client.fetch_default_sounds()`. + - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. + - New `on_voice_channel_effect_send` event for sound and emoji effects. + - Soundboard limits based on guild premium tier (8-48 slots) in + `Guild.soundboard_limit`. + ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) - Added new `Subscription` object and related methods/events. ([#2564](https://github.com/Pycord-Development/pycord/pull/2564)) - Added `Message.forward_to`, `Message.snapshots`, and other related attributes. @@ -410,20 +410,20 @@ These changes are available on the `master` branch, but have not yet been releas - Overhauled support for Components V2 and new Modal components ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible - with new features. - - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new - components. - - `DesignerView` and `Container` do not support `Button` and `Select` directly; use - `discord.ui.ActionRow` instead. - - `DesignerModal` does not support `InputText` and `Select` directly; use - `discord.ui.Label` instead. - - Removed `InputText.description`, `Select.label` and `Select.description`; these are - now attributes of `Label`. - - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items - inherit from these. - - All view and modal classes now inherit from a base `ItemInterface` class, split into - `BaseView` and `BaseModal` + - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible + with new features. + - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new + components. + - `DesignerView` and `Container` do not support `Button` and `Select` directly; use + `discord.ui.ActionRow` instead. + - `DesignerModal` does not support `InputText` and `Select` directly; use + `discord.ui.Label` instead. + - Removed `InputText.description`, `Select.label` and `Select.description`; these are + now attributes of `Label`. + - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items + inherit from these. + - All view and modal classes now inherit from a base `ItemInterface` class, split into + `BaseView` and `BaseModal` - Renamed `cover` property of `ScheduledEvent` and `cover` argument of `ScheduledEvent.edit` to `image`. ([#2496](https://github.com/Pycord-Development/pycord/pull/2496)) @@ -604,12 +604,12 @@ These changes are available on the `master` branch, but have not yet been releas ([#2908](https://github.com/Pycord-Development/pycord/pull/2908)) - Added support for select default values. ([#2899](https://github.com/Pycord-Development/pycord/pull/2899)) - - Adds a new generic parameter to selects to type `ui.Select.values` return type. - - Adds `SelectDefaultValue` object to create select default values. - - Adds `SelectDefaultValueType` enum. - - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the - different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, - `ui.MentionableSelect`, and `ui.ChannelSelect`. + - Adds a new generic parameter to selects to type `ui.Select.values` return type. + - Adds `SelectDefaultValue` object to create select default values. + - Adds `SelectDefaultValueType` enum. + - Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the + different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`, + `ui.MentionableSelect`, and `ui.ChannelSelect`. - Added `store` parameter to `View` and `Modal` classes. ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - Added `Webhook.parent` and `Webhook.from_interaction` @@ -631,20 +631,20 @@ These changes are available on the `master` branch, but have not yet been releas - Overhauled support for Components V2 and new Modal components ([#2904](https://github.com/Pycord-Development/pycord/pull/2904/)) - - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible - with new features. - - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new - components. - - `DesignerView` and `Container` do not support `Button` and `Select` directly; use - `discord.ui.ActionRow` instead. - - `DesignerModal` does not support `InputText` and `Select` directly; use - `discord.ui.Label` instead. - - Removed `InputText.description`, `Select.label` and `Select.description`; these are - now attributes of `Label`. - - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items - inherit from these. - - All view and modal classes now inherit from a base `ItemInterface` class, split into - `BaseView` and `BaseModal` + - Revert `discord.ui.View` and `discord.ui.Modal` to 2.6.1 behavior; not compatible + with new features. + - Implemented `discord.ui.DesignerView` and `discord.ui.DesignerModal` to support new + components. + - `DesignerView` and `Container` do not support `Button` and `Select` directly; use + `discord.ui.ActionRow` instead. + - `DesignerModal` does not support `InputText` and `Select` directly; use + `discord.ui.Label` instead. + - Removed `InputText.description`, `Select.label` and `Select.description`; these are + now attributes of `Label`. + - `discord.ui.Item` is now a base class for `ViewItem` and `ModalItem`; all items + inherit from these. + - All view and modal classes now inherit from a base `ItemInterface` class, split into + `BaseView` and `BaseModal` ### Fixed @@ -727,14 +727,14 @@ These changes are available on the `master` branch, but have not yet been releas - Added `VoiceMessage` subclass of `File` to allow voice messages to be sent. ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) - Added the following soundboard-related features: - - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, - `SoundboardSound.edit()`, and `SoundboardSound.delete()`. - - Access Discord default sounds with `Client.fetch_default_sounds()`. - - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. - - New `on_voice_channel_effect_send` event for sound and emoji effects. - - Soundboard limits based on guild premium tier (8-48 slots) in - `Guild.soundboard_limit`. - ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) + - Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`, + `SoundboardSound.edit()`, and `SoundboardSound.delete()`. + - Access Discord default sounds with `Client.fetch_default_sounds()`. + - Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`. + - New `on_voice_channel_effect_send` event for sound and emoji effects. + - Soundboard limits based on guild premium tier (8-48 slots) in + `Guild.soundboard_limit`. + ([#2623](https://github.com/Pycord-Development/pycord/pull/2623)) - Added new `Subscription` object and related methods/events. ([#2564](https://github.com/Pycord-Development/pycord/pull/2564)) - Added `Message.forward_to`, `Message.snapshots`, and other related attributes. @@ -1797,81 +1797,46 @@ These changes are available on the `master` branch, but have not yet been releas ([#1240](https://github.com/Pycord-Development/pycord/pull/1240)) [unreleased]: https://github.com/Pycord-Development/pycord/compare/v2.8.0...HEAD - [2.8.0]: https://github.com/Pycord-Development/pycord/compare/v2.7.2...v2.8.0 - [2.8.0rc1]: https://github.com/Pycord-Development/pycord/compare/v2.8.0rc1...v2.8.0rc2 - [2.8.0rc1]: https://github.com/Pycord-Development/pycord/compare/v2.7.2...v2.8.0rc1 - [2.7.2]: https://github.com/Pycord-Development/pycord/compare/v2.7.1...v2.7.2 - [2.7.1]: https://github.com/Pycord-Development/pycord/compare/v2.7.0...v2.7.1 - [2.7.0]: https://github.com/Pycord-Development/pycord/compare/v2.7.0rc2...v2.7.0 - [2.7.0rc2]: https://github.com/Pycord-Development/pycord/compare/v2.7.0rc1...v2.7.0rc2 - [2.7.0rc1]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...v2.7.0rc1 - [2.6.1]: https://github.com/Pycord-Development/pycord/compare/v2.6.0...v2.6.1 - [2.6.0]: https://github.com/Pycord-Development/pycord/compare/v2.5.0...v2.6.0 - [2.5.0]: https://github.com/Pycord-Development/pycord/compare/v2.4.1...v2.5.0 - [2.4.1]: https://github.com/Pycord-Development/pycord/compare/v2.4.0...v2.4.1 - [2.4.0]: https://github.com/Pycord-Development/pycord/compare/v2.3.3...v2.4.0 - [2.3.3]: https://github.com/Pycord-Development/pycord/compare/v2.3.2...v2.3.3 - [2.3.2]: https://github.com/Pycord-Development/pycord/compare/v2.3.1...v2.3.2 - [2.3.1]: https://github.com/Pycord-Development/pycord/compare/v2.3.0...v2.3.1 - [2.3.0]: https://github.com/Pycord-Development/pycord/compare/v2.2.2...v2.3.0 - [2.2.2]: https://github.com/Pycord-Development/pycord/compare/v2.2.1...v2.2.2 - [2.2.1]: https://github.com/Pycord-Development/pycord/compare/v2.2.0...v2.2.1 - [2.2.0]: https://github.com/Pycord-Development/pycord/compare/v2.1.3...v2.2.0 - [2.1.3]: https://github.com/Pycord-Development/pycord/compare/v2.1.2...v2.1.3 - [2.1.2]: https://github.com/Pycord-Development/pycord/compare/v2.1.1...v2.1.2 - [2.1.1]: https://github.com/Pycord-Development/pycord/compare/v2.1.0...v2.1.1 - [2.1.0]: https://github.com/Pycord-Development/pycord/compare/v2.0.1...v2.1.0 - [2.0.1]: https://github.com/Pycord-Development/pycord/compare/v2.0.0...v2.0.1 - [2.0.0]: https://github.com/Pycord-Development/pycord/compare/v2.0.0-rc.1...v2.0.0 - [2.0.0-rc.1]: -https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.7...v2.0.0-rc.1 - + https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.7...v2.0.0-rc.1 [2.0.0-beta.7]: -https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.6...v2.0.0-beta.7 - + https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.6...v2.0.0-beta.7 [2.0.0-beta.6]: -https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.5...v2.0.0-beta.6 - + https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.5...v2.0.0-beta.6 [2.0.0-beta.5]: -https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.4...v2.0.0-beta.5 - + https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.4...v2.0.0-beta.5 [2.0.0-beta.4]: -https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.3...v2.0.0-beta.4 - + https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.3...v2.0.0-beta.4 [2.0.0-beta.3]: -https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.2...v2.0.0-beta.3 - + https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.2...v2.0.0-beta.3 [2.0.0-beta.2]: -https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.1...v2.0.0-beta.2 - + https://github.com/Pycord-Development/pycord/compare/v2.0.0-beta.1...v2.0.0-beta.2 [2.0.0-beta.1]: -https://github.com/Pycord-Development/pycord/compare/v1.7.3...v2.0.0-beta.1 - + https://github.com/Pycord-Development/pycord/compare/v1.7.3...v2.0.0-beta.1 [version guarantees]: https://docs.pycord.dev/en/stable/version_guarantees.html From d87c944d7ea08d0d198bfb8f329ccee38e1ce2c8 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 10 Jun 2026 13:26:45 +0200 Subject: [PATCH 43/43] fix: Allow because of deprecation --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index bb740b08dd..e1899b8fe7 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4324,7 +4324,7 @@ async def create_scheduled_event( *, name: str, description: str = MISSING, - scheduled_start_time: datetime.datetime, + scheduled_start_time: datetime.datetime = MISSING, scheduled_end_time: datetime.datetime = MISSING, location: ( str | int | VoiceChannel | StageChannel | ScheduledEventLocation