diff --git a/discord/guild.py b/discord/guild.py index 82692ff73..2058f5e22 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -83,7 +83,7 @@ from .widget import Widget from .asset import Asset from .flags import SystemChannelFlags from .integrations import Integration, PartialIntegration, _integration_factory -from .scheduled_event import ScheduledEvent +from .scheduled_event import ScheduledEvent, ScheduledEventRecurrence from .stage_instance import StageInstance from .threads import Thread, ThreadMember from .sticker import GuildSticker @@ -3003,6 +3003,7 @@ class Guild(Hashable): description: str = ..., image: bytes = ..., reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -3019,6 +3020,7 @@ class Guild(Hashable): description: str = ..., image: bytes = ..., reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -3034,6 +3036,7 @@ class Guild(Hashable): description: str = ..., image: bytes = ..., reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -3049,6 +3052,7 @@ class Guild(Hashable): description: str = ..., image: bytes = ..., reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -3065,6 +3069,7 @@ class Guild(Hashable): description: str = MISSING, image: bytes = MISSING, reason: Optional[str] = None, + recurrence: Optional[ScheduledEventRecurrence] = MISSING, ) -> ScheduledEvent: r"""|coro| @@ -3111,6 +3116,9 @@ class Guild(Hashable): Required if the ``entity_type`` is :attr:`EntityType.external`. reason: Optional[:class:`str`] The reason for creating this scheduled event. Shows up on the audit log. + recurrence: Optional[:class:`ScheduledEventRecurrence`] + The recurrence rule this event will follow. If this is `None` then this is + a one-time event. Raises ------- @@ -3205,6 +3213,9 @@ class Guild(Hashable): ) payload['scheduled_end_time'] = end_time.isoformat() + if recurrence not in (MISSING, None): + payload['recurrence_rule'] = recurrence.to_dict() + if metadata: payload['entity_metadata'] = metadata diff --git a/discord/http.py b/discord/http.py index 409a36cc9..e032e0eaa 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1985,6 +1985,7 @@ class HTTPClient: 'description', 'entity_type', 'image', + 'recurrence_rule' ) payload = {k: v for k, v in payload.items() if k in valid_keys} @@ -2038,6 +2039,7 @@ class HTTPClient: 'description', 'entity_type', 'image', + 'recurrence_rule' ) payload = {k: v for k, v in payload.items() if k in valid_keys} @@ -2133,6 +2135,27 @@ class HTTPClient: ), params=params, ) + + def get_scheduled_event_counts( + self, + guild_id: Snowflake, + guild_scheduled_event_id: Snowflake, + scheduled_event_exception_ids: Tuple[Snowflake, ...] + ) -> Response[scheduled_event.GuildScheduledEventExceptionCounts]: + route: str = '/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/users/counts?' + + if len(scheduled_event_exception_ids) > 0: + for exception_id in scheduled_event_exception_ids: + route += f"guild_scheduled_event_exception_ids={exception_id}&" + + return self.request( + Route( + 'GET', + route, + guild_id=guild_id, + guild_scheduled_event_id=guild_scheduled_event_id + ) + ) # Application commands (global) diff --git a/discord/scheduled_event.py b/discord/scheduled_event.py index f74ae6706..a469b557e 100644 --- a/discord/scheduled_event.py +++ b/discord/scheduled_event.py @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, AsyncIterator, Dict, Optional, Union, overload, Literal +from typing import TYPE_CHECKING, AsyncIterator, Dict, Optional, Union, overload, Literal, List, Tuple from .asset import Asset from .enums import EventStatus, EntityType, PrivacyLevel, try_enum @@ -37,6 +37,8 @@ if TYPE_CHECKING: from .types.scheduled_event import ( GuildScheduledEvent as BaseGuildScheduledEventPayload, GuildScheduledEventWithUserCount as GuildScheduledEventWithUserCountPayload, + GuildScheduledEventRecurrence as GuildScheduledEventRecurrencePayload, + GuildScheduledEventExceptionCounts as GuildScheduledEventExceptionCountsPayload, EntityMetadata, ) @@ -51,10 +53,220 @@ if TYPE_CHECKING: # fmt: off __all__ = ( "ScheduledEvent", + "ScheduledEventRecurrence", + "ScheduledEventExceptionCount" ) # fmt: on +class ScheduledEventExceptionCount: + """Represents the exception counts in a Scheduled Event. + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two Exception Counts are equal. + """ + + def __init__(self, data: GuildScheduledEventExceptionCountsPayload) -> None: + self.count: int = int(data.get('guild_scheduled_event_count')) + + self._exception_snowflakes: Dict[Union[str, int], int] = data.get('guild_scheduled_event_exception_counts') + + @property + def exception_ids(self) -> List[int]: + """List[:class:`int`]: A list containing all the exception event IDs""" + return [int(id) for id in self._exception_snowflakes.keys()] + + @property + def exceptions(self) -> Dict[int, int]: + """Dict[:class:`int`, :class:`int`]: A dictionary containing all the + event IDs as keys and their respective exception counts as value. + """ + + return {int(snowflake): count for snowflake, count in self._exception_snowflakes.items()} + + +class ScheduledEventRecurrence: + """Represents a Scheduled Event Recurrence + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two Scheduled Event Recurrences are equal + + Parameters + ---------- + start: :class:`datetime.datetime` + When the first event of this series is started. + end: Optional[:class:`datetime.datetime`] + When the events of this series will stop. If it is `None`, it will repeat forever. + weekday: :class:`int` + An integer representing the weekday this event will repeat in. Monday is 0 + and Sunday is 6. + n_weekday: Tuple[:class:`int`, :class:`int`] + A tuple that contain the N weekday this event will repeat in. + + For example, if you want for this event to repeat the 1st Monday of the month, + then this param should have a value of `(1, 0)`. Where ``1`` represents the + 'first' and ``0`` the weekday, in this case, Monday. + month: :class:`int` + An integer representing the month this event will repeat in. + month_days: List[:class:`int`] + A list of integers representing the month days this event will repeat in. + + This marks the days of the month this event will repeat in, for example, if it + is set to `1`, this event will repeat the first day of every month. + year_days: List[:class:`int`] + A list of integers representing the year days this event will repeat in. + + This marks the days of the year this event will repeat in, for example, if it + is set to `1`, this event will repeat the first day of every year. + """ + + @overload + def __init__( + self, + start: datetime, + *, + weekdays: List[Literal[0, 1, 2, 3, 4, 5, 6]], + end: Optional[datetime] = ..., + ) -> None: + ... + + @overload + def __init__( + self, + start: datetime, + *, + n_weekday: Tuple[Literal[1, 2, 3, 4], int], + end: Optional[datetime] = ..., + ) -> None: + ... + + @overload + def __init__( + self, + start: datetime, + *, + month: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + month_days: List[int], + end: Optional[datetime] = ..., + ) -> None: + ... + + @overload + def __init__( + self, + start: datetime, + *, + year_days: List[int], + end: Optional[datetime] = ..., + ) -> None: + ... + + def __init__( + self, + start: datetime, + *, + weekdays: List[Literal[0, 1, 2, 3, 4, 5, 6]] = MISSING, + n_weekday: Tuple[Literal[1, 2, 3, 4], int] = MISSING, + month: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] = MISSING, + month_days: List[int] = MISSING, + year_days: List[int] = MISSING, + end: Optional[datetime] = MISSING, + ) -> None: + + if not start.tzinfo: + raise ValueError( + '\'start\' must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' + ) + + if end not in (MISSING, None): + if not end.tzinfo: + raise ValueError( + '\'end\' must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' + ) + + self.start: datetime = start + self.end: Optional[datetime] = end if end is not MISSING else None + + self.weekdays: Optional[List[int]] = weekdays if weekdays is not MISSING else None + self.n_weekday: Optional[Tuple[int, int]] = n_weekday if n_weekday is not MISSING else None + self.month: Optional[int] = month if month is not MISSING else None + self.month_days: Optional[List[int]] = month_days if month_days is not MISSING else None + self.year_days: Optional[List[int]] = year_days if year_days is not MISSING else None + self._interval: int = 1 + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return ( + self.start == other.start + ) + return NotImplemented + + def __set_interval(self, value: int) -> None: + # Inner function to set the interval to the one that we + # recieved from the API + self._interval: int = value + + @property + def frequency(self) -> int: + """:class:`int`: Returns the frequency of this recurrent scheduled event""" + + # This is now an internal parameter because if it is user-provided this could cause + # HTTPExceptions when creating or editing events. + + if self.weekdays is not None: + return 2 if len(self.weekdays) == 1 else 3 + elif self.n_weekday is not None: + return 1 + elif self.month is not None and self.month_days is not None: + return 0 + return 0 # In case None of the cases matches (i.e.: year_days) then we return 0 + + @property + def interval(self) -> int: + """:class:`int`: Returns the interval of this recurrent scheduled event""" + return self._interval + + def to_dict(self) -> GuildScheduledEventRecurrencePayload: + return { + "start": self.start.isoformat(), + "end": self.end.isoformat() if self.end else None, + "by_weekday": self.weekdays or [], + "by_month": [self.month,] if self.month else [], + "by_month_day": self.month_days or [], + "by_n_weekday": [self.n_weekday,] if self.n_weekday else [], + "by_year_day": self.year_days or [], + "count": None, # There isn't counts, yet + "frequency": self.frequency, + "interval": self.interval, + } # type: ignore + + @classmethod + def from_dict(cls, data: GuildScheduledEventRecurrencePayload) -> ScheduledEventRecurrence: + self: cls = cls( + start=datetime.fromisoformat(data.get('start')), + weekdays=data.get('by_weekday', MISSING), + n_weekdays=((d['n'], d['day']) for d in data.get('by_n_weekday')) if data.get('by_n_weekday', MISSING) is not MISSING else MISSING, + month=data.get('by_month')[0] if len(data.get('by_month', [])) > 0 and data.get('by_month', MISSING) is not MISSING else MISSING, + month_days=data.get('by_month_day', MISSING), + year_days=data.get('by_year_day', MISSING), + end=data.get('end', MISSING) + ) # type: ignore + + self.__set_interval(int(data.get('interval', 1))) + + return self + + class ScheduledEvent(Hashable): """Represents a scheduled event in a guild. @@ -104,6 +316,10 @@ class ScheduledEvent(Hashable): .. versionadded:: 2.2 location: Optional[:class:`str`] The location of the scheduled event. + recurrence: Optional[:class:`ScheduledEventRecurrence`] + The recurrence rule this event follows, if any. + + .. versionadded:: 2.4 """ __slots__ = ( @@ -125,6 +341,7 @@ class ScheduledEvent(Hashable): 'channel_id', 'creator_id', 'location', + 'recurrence', ) def __init__(self, *, state: ConnectionState, data: GuildScheduledEventPayload) -> None: @@ -145,6 +362,7 @@ class ScheduledEvent(Hashable): self._cover_image: Optional[str] = data.get('image', None) self.user_count: int = data.get('user_count', 0) self.creator_id: Optional[int] = _get_as_snowflake(data, 'creator_id') + self.recurrence: Optional[ScheduledEventRecurrence] = ScheduledEventRecurrence.from_dict(data.get('recurrence_rule')) if data.get('recurrence_rule', None) is not None else None creator = data.get('creator') self.creator: Optional[User] = self._state.store_user(creator) if creator else None @@ -310,6 +528,7 @@ class ScheduledEvent(Hashable): status: EventStatus = ..., image: bytes = ..., reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -327,6 +546,7 @@ class ScheduledEvent(Hashable): status: EventStatus = ..., image: bytes = ..., reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -344,6 +564,7 @@ class ScheduledEvent(Hashable): image: bytes = ..., location: str, reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -360,6 +581,7 @@ class ScheduledEvent(Hashable): status: EventStatus = ..., image: bytes = ..., reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -376,6 +598,7 @@ class ScheduledEvent(Hashable): image: bytes = ..., location: str, reason: Optional[str] = ..., + recurrence: Optional[ScheduledEventRecurrence] = ..., ) -> ScheduledEvent: ... @@ -393,6 +616,7 @@ class ScheduledEvent(Hashable): image: bytes = MISSING, location: str = MISSING, reason: Optional[str] = None, + recurrence: Optional[ScheduledEventRecurrence] = MISSING, ) -> ScheduledEvent: r"""|coro| @@ -441,6 +665,9 @@ class ScheduledEvent(Hashable): Required if the entity type is :attr:`EntityType.external`. reason: Optional[:class:`str`] The reason for editing the scheduled event. Shows up on the audit log. + recurrence: Optional[:class:`ScheduledEventRecurrence`] + The recurrence rule this event will follow, or `None` to set it to a + one-time event. Raises ------- @@ -551,6 +778,12 @@ class ScheduledEvent(Hashable): else: payload['scheduled_end_time'] = end_time + if recurrence is not MISSING: + if recurrence is not None: + payload['recurrence_rule'] = recurrence.to_dict() + else: + payload['recurrence_rule'] = None + if metadata: payload['entity_metadata'] = metadata @@ -675,6 +908,40 @@ class ScheduledEvent(Hashable): # There's no data left after this break + async def fetch_counts(self, *children: Snowflake) -> ScheduledEventExceptionCount: + """|coro| + + Retrieves all the counts for this Event children, if this event isn't + recurrent, then this will return `None`. + + This also contains the exceptions of this Scheduled event. + + .. versionadded:: 2.4 + + Parameters + ---------- + *children: :class:`Snowflake` + The snowflakes of the children to fetcht the counts of. + + Raises + ------ + HTTPException + Fetching the counts failed. + + Returns + ------- + Optional[:class:`ScheduledEventExceptionCount`] + The counts of this event, or `None` if this event isn't recurrent or + there isn't any exception. + """ + + if not self.recurrence: + return None + + data = await self._state.http.get_scheduled_event_counts(self.guild_id, self.id, tuple([child.id for child in children])) + + return ScheduledEventExceptionCount(data) + def _add_user(self, user: User) -> None: self._users[user.id] = user diff --git a/discord/types/scheduled_event.py b/discord/types/scheduled_event.py index 52200367f..4d76c2ef6 100644 --- a/discord/types/scheduled_event.py +++ b/discord/types/scheduled_event.py @@ -22,17 +22,38 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import List, Literal, Optional, TypedDict, Union +from typing import List, Literal, Optional, TypedDict, Union, Dict from typing_extensions import NotRequired from .snowflake import Snowflake from .user import User from .member import Member from .channel import PrivacyLevel as PrivacyLevel - EventStatus = Literal[1, 2, 3, 4] EntityType = Literal[1, 2, 3] +class _NWeekday(TypedDict): + n: int + day: Literal[0, 1, 2, 3, 4, 5, 6] + +class GuildScheduledEventRecurrence(TypedDict): + start: str + end: Optional[str] + frequency: int + interval: int + by_weekday: Optional[List[Literal[0, 1, 2, 3, 4, 5, 6]]] # NOTE: 0 = monday; 6 = sunday + by_n_weekday: Optional[List[_NWeekday]] + by_month: Optional[List[Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]]] + by_month_day: Optional[List[int]] # NOTE: day range between 1 and 31 + by_year_day: Optional[List[int]] + count: Optional[int] # maybe? +# NOTE: for this ^ enum, it is recommended to use "calendar" module constants: MONDAY; TUESDAY; WEDNESDAY; etc +# as they follow these patterns and is a built-in module. + +class GuildScheduledEventExceptionCounts(TypedDict): + guild_scheduled_event_count: int + guild_scheduled_event_exception_counts: Dict[Snowflake, int] +# NOTE: This class doesn't represent any of the user counts or the 'count' param in recurrence class _BaseGuildScheduledEvent(TypedDict): id: Snowflake @@ -42,6 +63,10 @@ class _BaseGuildScheduledEvent(TypedDict): scheduled_start_time: str privacy_level: PrivacyLevel status: EventStatus + auto_start: bool + guild_scheduled_events_exceptions: List # Didn't found items in the list yet + recurrence_rule: Optional[GuildScheduledEventRecurrence] + sku_ids: List[Snowflake] creator_id: NotRequired[Optional[Snowflake]] description: NotRequired[Optional[str]] creator: NotRequired[User]