# SPDX-License-Identifier: MIT from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, overload from .asset import Asset from .enums import ( ChannelType, GuildScheduledEventEntityType, GuildScheduledEventPrivacyLevel, GuildScheduledEventStatus, try_enum, ) from .mixins import Hashable from .utils import ( MISSING, _assetbytes_to_base64_data, _get_as_snowflake, cached_slot_property, isoformat_utc, parse_time, snowflake_time, ) if TYPE_CHECKING: from .abc import GuildChannel, Snowflake from .asset import AssetBytes from .guild import Guild from .iterators import GuildScheduledEventUserIterator from .state import ConnectionState from .types.guild_scheduled_event import ( GuildScheduledEvent as GuildScheduledEventPayload, GuildScheduledEventEntityMetadata as GuildScheduledEventEntityMetadataPayload, ) from .user import User __all__ = ("GuildScheduledEventMetadata", "GuildScheduledEvent") class GuildScheduledEventMetadata: """Represents a guild scheduled event entity metadata. .. versionadded:: 2.3 Attributes ---------- location: Optional[:class:`str`] The location of the guild scheduled event. If :attr:`GuildScheduledEvent.entity_type` is :class:`GuildScheduledEventEntityType.external`, this value is not ``None``. """ __slots__ = ("location",) def __init__(self, *, location: Optional[str] = None) -> None: self.location: Optional[str] = location def __repr__(self) -> str: return f"" def to_dict(self) -> GuildScheduledEventEntityMetadataPayload: if self.location is not None: return {"location": self.location} return {} @classmethod def from_dict( cls, data: GuildScheduledEventEntityMetadataPayload ) -> GuildScheduledEventMetadata: return GuildScheduledEventMetadata(location=data.get("location")) class GuildScheduledEvent(Hashable): """Represents a guild scheduled event. .. versionadded:: 2.3 .. container:: operations .. describe:: x == y Checks if two guild scheduled events are equal. .. describe:: x != y Checks if two guild scheduled events are not equal. .. describe:: hash(x) Returns the guild scheduled event's hash. Attributes ---------- id: :class:`int` The ID of the guild scheduled event. guild_id: :class:`int` The guild ID which the guild scheduled event belongs to. channel_id: Optional[:class:`int`] The channel ID in which the guild scheduled event will be hosted. This field is ``None`` if :attr:`entity_type` is :class:`GuildScheduledEventEntityType.external`. creator_id: Optional[:class:`int`] The ID of the user that created the guild scheduled event. This field is ``None`` for events created before October 25th, 2021. creator: Optional[:class:`User`] The user that created the guild scheduled event. This field is ``None`` for events created before October 25th, 2021. name: :class:`str` The name of the guild scheduled event (1-100 characters). description: :class:`str` The description of the guild scheduled event (1-1000 characters). scheduled_start_time: :class:`datetime.datetime` The time when the guild scheduled event will start. scheduled_end_time: Optional[:class:`datetime.datetime`] The time when the guild scheduled event will end, or ``None`` if the event does not have a scheduled time to end. privacy_level: :class:`GuildScheduledEventPrivacyLevel` The privacy level of the guild scheduled event. status: :class:`GuildScheduledEventStatus` The status of the guild scheduled event. entity_type: :class:`GuildScheduledEventEntityType` The type of the guild scheduled event. entity_id: Optional[:class:`int`] The ID of an entity associated with the guild scheduled event. entity_metadata: :class:`GuildScheduledEventMetadata` Additional metadata for the guild scheduled event. user_count: Optional[:class:`int`] The number of users subscribed to the guild scheduled event. If the guild scheduled event was fetched with ``with_user_count`` set to ``False``, this field is ``None``. """ __slots__ = ( "_state", "id", "guild_id", "channel_id", "creator_id", "name", "description", "scheduled_start_time", "scheduled_end_time", "privacy_level", "status", "entity_type", "entity_id", "entity_metadata", "creator", "user_count", "_image", "_cs_guild", "_cs_channel", ) def __init__(self, *, state: ConnectionState, data: GuildScheduledEventPayload) -> None: self._state: ConnectionState = state self._update(data) def _update(self, data: GuildScheduledEventPayload) -> None: self.id: int = int(data["id"]) self.guild_id: int = int(data["guild_id"]) self.channel_id: Optional[int] = _get_as_snowflake(data, "channel_id") self.creator_id: Optional[int] = _get_as_snowflake(data, "creator_id") self.name: str = data["name"] self.description: Optional[str] = data.get("description") self.scheduled_start_time: datetime = parse_time(data["scheduled_start_time"]) self.scheduled_end_time: Optional[datetime] = parse_time(data["scheduled_end_time"]) self.privacy_level: GuildScheduledEventPrivacyLevel = try_enum( GuildScheduledEventPrivacyLevel, data["privacy_level"] ) self.status: GuildScheduledEventStatus = try_enum(GuildScheduledEventStatus, data["status"]) self.entity_type: GuildScheduledEventEntityType = try_enum( GuildScheduledEventEntityType, data["entity_type"] ) self.entity_id: Optional[int] = _get_as_snowflake(data, "entity_id") metadata = data.get("entity_metadata") self.entity_metadata: Optional[GuildScheduledEventMetadata] = ( None if metadata is None else GuildScheduledEventMetadata.from_dict(metadata) ) creator_data = data.get("creator") self.creator: Optional[User] if creator_data is not None: self.creator = self._state.create_user(creator_data) else: self.creator = self._state.get_user(self.creator_id) self.user_count: Optional[int] = data.get("user_count") self._image: Optional[str] = data.get("image") def __repr__(self) -> str: attrs = ( ("id", self.id), ("guild_id", self.guild_id), ("channel_id", self.channel_id), ("name", self.name), ("description", self.description), ("scheduled_start_time", self.scheduled_start_time), ("scheduled_end_time", self.scheduled_end_time), ("privacy_level", self.privacy_level), ("status", self.status), ("entity_type", self.entity_type), ("entity_metadata", self.entity_metadata), ("creator", self.creator), ) inner = " ".join(f"{k!s}={v!r}" for k, v in attrs) return f"<{self.__class__.__name__} {inner}>" def __str__(self) -> str: return self.name @property def created_at(self) -> datetime: """:class:`datetime.datetime`: Returns the guild scheduled event's creation time in UTC. .. versionadded:: 2.6 """ return snowflake_time(self.id) @property def url(self) -> str: """:class:`str`: The guild scheduled event's URL. .. versionadded:: 2.6 """ return f"https://discord.com/events/{self.guild_id}/{self.id}" @cached_slot_property("_cs_guild") def guild(self) -> Optional[Guild]: """Optional[:class:`Guild`]: The guild which the guild scheduled event belongs to.""" return self._state._get_guild(self.guild_id) @cached_slot_property("_cs_channel") def channel(self) -> Optional[GuildChannel]: """Optional[:class:`abc.GuildChannel`]: The channel in which the guild scheduled event will be hosted. This will be ``None`` if :attr:`entity_type` is :class:`GuildScheduledEventEntityType.external`. """ if self.channel_id is None: return None guild = self.guild return None if guild is None else guild.get_channel(self.channel_id) @property def image(self) -> Optional[Asset]: """Optional[:class:`Asset`]: The cover image asset of the guild scheduled event, if available.""" if self._image is None: return None return Asset._from_guild_scheduled_event_image(self._state, self.id, self._image) async def delete(self) -> None: """|coro| Deletes the guild scheduled event. You must have :attr:`.Permissions.manage_events` permission to do this. Raises ------ Forbidden You do not have proper permissions to delete the event. NotFound The event does not exist. HTTPException Deleting the event failed. """ await self._state.http.delete_guild_scheduled_event(self.guild_id, self.id) # note: unlike `Guild.create_scheduled_event`, we don't remove `entity_type`-specific defaults # from parameters in the overloads here, as the entity_type might be the same as before, # in which case those parameters don't have to be passed # no entity_type specified @overload async def edit( self, *, name: str = ..., description: Optional[str] = ..., image: Optional[AssetBytes] = ..., channel: Optional[Snowflake] = ..., privacy_level: GuildScheduledEventPrivacyLevel = ..., scheduled_start_time: datetime = ..., scheduled_end_time: Optional[datetime] = ..., entity_metadata: Optional[GuildScheduledEventMetadata] = ..., status: GuildScheduledEventStatus = ..., reason: Optional[str] = ..., ) -> GuildScheduledEvent: ... # new entity_type is `external`, no channel @overload async def edit( self, *, entity_type: Literal[GuildScheduledEventEntityType.external], channel: None = ..., name: str = ..., description: Optional[str] = ..., image: Optional[AssetBytes] = ..., privacy_level: GuildScheduledEventPrivacyLevel = ..., scheduled_start_time: datetime = ..., scheduled_end_time: datetime = ..., entity_metadata: GuildScheduledEventMetadata = ..., status: GuildScheduledEventStatus = ..., reason: Optional[str] = ..., ) -> GuildScheduledEvent: ... # new entity_type is `voice` or `stage_instance`, no entity_metadata @overload async def edit( self, *, entity_type: Literal[ GuildScheduledEventEntityType.voice, GuildScheduledEventEntityType.stage_instance, ], name: str = ..., description: Optional[str] = ..., image: Optional[AssetBytes] = ..., channel: Snowflake = ..., privacy_level: GuildScheduledEventPrivacyLevel = ..., scheduled_start_time: datetime = ..., scheduled_end_time: Optional[datetime] = ..., status: GuildScheduledEventStatus = ..., reason: Optional[str] = ..., ) -> GuildScheduledEvent: ... # channel=None, no entity_type @overload async def edit( self, *, channel: None, name: str = ..., description: Optional[str] = ..., image: Optional[AssetBytes] = ..., privacy_level: GuildScheduledEventPrivacyLevel = ..., scheduled_start_time: datetime = ..., scheduled_end_time: datetime = ..., entity_metadata: GuildScheduledEventMetadata = ..., status: GuildScheduledEventStatus = ..., reason: Optional[str] = ..., ) -> GuildScheduledEvent: ... # valid channel, no entity_type @overload async def edit( self, *, channel: Snowflake, name: str = ..., description: Optional[str] = ..., image: Optional[AssetBytes] = ..., privacy_level: GuildScheduledEventPrivacyLevel = ..., scheduled_start_time: datetime = ..., scheduled_end_time: Optional[datetime] = ..., status: GuildScheduledEventStatus = ..., reason: Optional[str] = ..., ) -> GuildScheduledEvent: ... async def edit( self, *, name: str = MISSING, description: Optional[str] = MISSING, image: Optional[AssetBytes] = MISSING, channel: Optional[Snowflake] = MISSING, privacy_level: GuildScheduledEventPrivacyLevel = MISSING, scheduled_start_time: datetime = MISSING, scheduled_end_time: Optional[datetime] = MISSING, entity_type: GuildScheduledEventEntityType = MISSING, entity_metadata: Optional[GuildScheduledEventMetadata] = MISSING, status: GuildScheduledEventStatus = MISSING, reason: Optional[str] = None, ) -> GuildScheduledEvent: """|coro| Edits the guild scheduled event. You must have :attr:`.Permissions.manage_events` permission to do this. .. versionchanged:: 2.6 Updates must follow requirements of :func:`Guild.create_scheduled_event` .. versionchanged:: 2.6 Now raises :exc:`TypeError` instead of :exc:`ValueError` for invalid parameter types. .. versionchanged:: 2.6 Removed ``channel_id`` parameter in favor of ``channel``. .. versionchanged:: 2.6 Naive datetime parameters are now assumed to be in the local timezone instead of UTC. Parameters ---------- name: :class:`str` The name of the guild scheduled event. description: Optional[:class:`str`] The description of the guild scheduled event. image: Optional[|resource_type|] The cover image of the guild scheduled event. Set to ``None`` to remove the image. .. versionadded:: 2.4 .. versionchanged:: 2.5 Now accepts various resource types in addition to :class:`bytes`. channel: Optional[:class:`.abc.Snowflake`] The channel in which the guild scheduled event will be hosted. Set to ``None`` if changing ``entity_type`` to :class:`GuildScheduledEventEntityType.external`. .. versionadded:: 2.6 privacy_level: :class:`GuildScheduledEventPrivacyLevel` The privacy level of the guild scheduled event. scheduled_start_time: :class:`datetime.datetime` The time to schedule the guild scheduled event. If the datetime is naive, it is assumed to be local time. scheduled_end_time: Optional[:class:`datetime.datetime`] The time when the guild scheduled event is scheduled to end. If the datetime is naive, it is assumed to be local time. entity_type: :class:`GuildScheduledEventEntityType` The entity type of the guild scheduled event. entity_metadata: Optional[:class:`GuildScheduledEventMetadata`] The entity metadata of the guild scheduled event. status: :class:`GuildScheduledEventStatus` The status of the guild scheduled event. See also :func:`start`, :func:`end`, and :func:`cancel`. reason: Optional[:class:`str`] The reason for editing the guild scheduled event. Shows up on the audit log. Raises ------ Forbidden You do not have proper permissions to edit the event. NotFound The event does not exist or the ``image`` asset couldn't be found. HTTPException Editing the event failed. TypeError The ``image`` asset is a lottie sticker (see :func:`Sticker.read`), one of ``entity_type``, ``privacy_level``, ``entity_metadata`` or ``status`` is not of the correct type, or the provided channel's type is neither :class:`ChannelType.voice` nor :class:`ChannelType.stage_voice`. Returns ------- :class:`GuildScheduledEvent` The newly updated guild scheduled event instance. """ fields: Dict[str, Any] = {} if entity_type is MISSING: if channel is None: entity_type = GuildScheduledEventEntityType.external elif channel is not MISSING and isinstance( channel_type := getattr(channel, "type", None), ChannelType ): if channel_type is ChannelType.voice: entity_type = GuildScheduledEventEntityType.voice elif channel_type is ChannelType.stage_voice: entity_type = GuildScheduledEventEntityType.stage_instance else: raise TypeError("channel type must be either 'voice' or 'stage_voice'") if privacy_level is not MISSING: if not isinstance(privacy_level, GuildScheduledEventPrivacyLevel): raise TypeError( "privacy_level must be an instance of GuildScheduledEventPrivacyLevel" ) fields["privacy_level"] = privacy_level.value if entity_type is not MISSING: if not isinstance(entity_type, GuildScheduledEventEntityType): raise TypeError("entity_type must be an instance of GuildScheduledEventEntityType") fields["entity_type"] = entity_type.value if entity_metadata is not MISSING: if entity_metadata is None: fields["entity_metadata"] = None elif isinstance(entity_metadata, GuildScheduledEventMetadata): fields["entity_metadata"] = entity_metadata.to_dict() else: raise TypeError( "entity_metadata must be an instance of GuildScheduledEventMetadata" ) if status is not MISSING: if not isinstance(status, GuildScheduledEventStatus): raise TypeError("status must be an instance of GuildScheduledEventStatus") fields["status"] = status.value if name is not MISSING: fields["name"] = name if description is not MISSING: fields["description"] = description if image is not MISSING: fields["image"] = await _assetbytes_to_base64_data(image) if channel is not MISSING: fields["channel_id"] = channel.id if channel is not None else None elif entity_type is GuildScheduledEventEntityType.external: # special case, as the API wants `channel_id=null` if type is being changed to `external` fields["channel_id"] = None if scheduled_start_time is not MISSING: fields["scheduled_start_time"] = isoformat_utc(scheduled_start_time) if scheduled_end_time is not MISSING: fields["scheduled_end_time"] = isoformat_utc(scheduled_end_time) data = await self._state.http.edit_guild_scheduled_event( guild_id=self.guild_id, event_id=self.id, reason=reason, **fields ) return GuildScheduledEvent(state=self._state, data=data) async def start(self, *, reason: Optional[str] = None) -> GuildScheduledEvent: """|coro| Starts the guild scheduled event. Changes the event status to :attr:`~GuildScheduledEventStatus.active`. You must have :attr:`.Permissions.manage_events` permission to do this. .. versionadded:: 2.7 Parameters ---------- reason: Optional[:class:`str`] The reason for starting the guild scheduled event. Shows up on the audit log. Raises ------ ValueError The event has already started or ended, or was cancelled. Forbidden You do not have permissions to start the event. HTTPException Starting the event failed. Returns ------- :class:`GuildScheduledEvent` The started guild scheduled event instance. """ if self.status is not GuildScheduledEventStatus.scheduled: raise ValueError("This event is not scheduled") return await self._edit_status(GuildScheduledEventStatus.active, reason=reason) async def end(self, *, reason: Optional[str] = None) -> GuildScheduledEvent: """|coro| Ends the guild scheduled event. Changes the event status to :attr:`~GuildScheduledEventStatus.completed`. You must have :attr:`.Permissions.manage_events` permission to do this. .. versionadded:: 2.7 Parameters ---------- reason: Optional[:class:`str`] The reason for ending the guild scheduled event. Shows up on the audit log. Raises ------ ValueError The event has not started yet, has already ended, or was cancelled. Forbidden You do not have permissions to end the event. HTTPException Ending the event failed. Returns ------- :class:`GuildScheduledEvent` The ended guild scheduled event instance. """ if self.status is not GuildScheduledEventStatus.active: raise ValueError("This event is not active") return await self._edit_status(GuildScheduledEventStatus.completed, reason=reason) async def cancel(self, *, reason: Optional[str] = None) -> GuildScheduledEvent: """|coro| Cancels the guild scheduled event. Changes the event status to :attr:`~GuildScheduledEventStatus.cancelled`. You must have :attr:`.Permissions.manage_events` permission to do this. .. versionadded:: 2.7 Parameters ---------- reason: Optional[:class:`str`] The reason for cancelling the guild scheduled event. Shows up on the audit log. Raises ------ ValueError The event has already started or ended, or was already cancelled. Forbidden You do not have permissions to cancel the event. HTTPException Cancelling the event failed. Returns ------- :class:`GuildScheduledEvent` The cancelled guild scheduled event instance. """ if self.status is not GuildScheduledEventStatus.scheduled: raise ValueError("This event is not scheduled") return await self._edit_status(GuildScheduledEventStatus.cancelled, reason=reason) # shortcut for editing just the event status, bypasses other edit logic async def _edit_status( self, status: GuildScheduledEventStatus, *, reason: Optional[str] ) -> GuildScheduledEvent: data = await self._state.http.edit_guild_scheduled_event( guild_id=self.guild_id, event_id=self.id, reason=reason, status=status.value, ) return GuildScheduledEvent(state=self._state, data=data) def fetch_users( self, *, limit: Optional[int] = None, with_members: bool = True, before: Optional[Snowflake] = None, after: Optional[Snowflake] = None, ) -> GuildScheduledEventUserIterator: """|coro| Returns an :class:`AsyncIterator` of users subscribed to the guild scheduled event. If ``before`` is specified, users are returned in reverse order, i.e. starting with the highest ID. .. versionchanged:: 2.5 Now returns an :class:`AsyncIterator` instead of a list of the first 100 users. Parameters ---------- limit: Optional[:class:`int`] The number of users to retrieve. with_members: :class:`bool` Whether to include some users as members. Defaults to ``True``. before: Optional[:class:`abc.Snowflake`] Retrieve users before this object. after: Optional[:class:`abc.Snowflake`] Retrieve users after this object. Raises ------ Forbidden You do not have proper permissions to fetch the users. NotFound The event does not exist. HTTPException Retrieving the users failed. Yields ------ Union[:class:`User`, :class:`Member`] The member (if retrievable) or user subscribed to the guild scheduled event. Examples -------- Usage :: async for user in event.fetch_users(limit=500): print(user.name) Flattening into a list :: users = await event.fetch_users(limit=250).flatten() """ from .iterators import GuildScheduledEventUserIterator # cyclic import return GuildScheduledEventUserIterator( self, limit=limit, with_members=with_members, before=before, after=after, )