diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 59d563829257..69f6dc0d883b 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -43,7 +43,7 @@ from .sticker import GuildSticker from .threads import Thread from .integrations import PartialIntegration -from .channel import ForumChannel, StageChannel, ForumTag +from .channel import ForumChannel, StageChannel, ForumTag, VoiceChannel __all__ = ( 'AuditLogDiff', @@ -239,6 +239,13 @@ def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji: return PartialEmoji(name=data) +def _transform_status(entry: AuditLogEntry, data: Union[int, str]) -> Union[enums.EventStatus, str]: + if entry.action.name.startswith('scheduled_event_'): + return enums.try_enum(enums.EventStatus, data) + else: + return data # type: ignore # voice channel status is str + + E = TypeVar('E', bound=enums.Enum) @@ -332,7 +339,7 @@ class AuditLogChanges: 'communication_disabled_until': ('timed_out_until', _transform_timestamp), 'expire_behavior': (None, _enum_transformer(enums.ExpireBehaviour)), 'mfa_level': (None, _enum_transformer(enums.MFALevel)), - 'status': (None, _enum_transformer(enums.EventStatus)), + 'status': (None, _transform_status), 'entity_type': (None, _enum_transformer(enums.EntityType)), 'preferred_locale': (None, _enum_transformer(enums.Locale)), 'image_hash': ('cover_image', _transform_cover_image), @@ -590,6 +597,11 @@ class _AuditLogProxyMemberKickOrMemberRoleUpdate(_AuditLogProxy): integration_type: Optional[str] +class _AuditLogProxyVoiceChannelStatusAction(_AuditLogProxy): + status: Optional[str] + channel: abc.GuildChannel + + class AuditLogEntry(Hashable): r"""Represents an Audit Log entry. @@ -677,6 +689,7 @@ def _from_data(self, data: AuditLogEntryPayload) -> None: _AuditLogProxyMessageBulkDelete, _AuditLogProxyAutoModAction, _AuditLogProxyMemberKickOrMemberRoleUpdate, + _AuditLogProxyVoiceChannelStatusAction, Member, User, None, PartialIntegration, Role, Object ] = None @@ -753,6 +766,13 @@ def _from_data(self, data: AuditLogEntryPayload) -> None: app_id = int(extra['application_id']) self.extra = self._get_integration_by_app_id(app_id) or Object(app_id, type=PartialIntegration) + elif self.action.name.startswith('voice_channel_status'): + channel_id = int(extra['channel_id']) + status = extra.get('status') + self.extra = _AuditLogProxyVoiceChannelStatusAction( + status=status, channel=self.guild.get_channel(channel_id) or Object(id=channel_id, type=VoiceChannel) + ) + # this key is not present when the above is present, typically. # It's a list of { new_value: a, old_value: b, key: c } # where new_value and old_value are not guaranteed to be there depending @@ -930,3 +950,6 @@ def _convert_target_webhook(self, target_id: int) -> Union[Webhook, Object]: from .webhook import Webhook return self._webhooks.get(target_id) or Object(target_id, type=Webhook) + + def _convert_target_voice_channel_status(self, target_id: int) -> Union[abc.GuildChannel, Object]: + return self.guild.get_channel(target_id) or Object(id=target_id) diff --git a/discord/channel.py b/discord/channel.py index b8858f356693..eb4e422754fa 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1479,9 +1479,19 @@ class VoiceChannel(VocalGuildChannel): :attr:`~Permissions.manage_messages` bypass slowmode. .. versionadded:: 2.2 + status: Optional[:class:`str`] + The status of the voice channel. ``None`` if no status is set. + This is not available for the fetch methods such as :func:`Guild.fetch_channel` + or :func:`Client.fetch_channel` + + .. versionadded:: 2.5 """ - __slots__ = () + __slots__ = ('status',) + + def __init__(self, *, state: ConnectionState, guild: Guild, data: VoiceChannelPayload) -> None: + super().__init__(state=state, guild=guild, data=data) + self.status: Optional[str] = data.get('status') or None # empty string -> None def __repr__(self) -> str: attrs = [ diff --git a/discord/enums.py b/discord/enums.py index 3aecfc92b654..9686e5888ea5 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -390,6 +390,8 @@ class AuditLogAction(Enum): automod_timeout_member = 145 creator_monetization_request_created = 150 creator_monetization_terms_accepted = 151 + voice_channel_status_update = 192 + voice_channel_status_delete = 193 # fmt: on @property @@ -455,6 +457,8 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.soundboard_sound_create: AuditLogActionCategory.create, AuditLogAction.soundboard_sound_update: AuditLogActionCategory.update, AuditLogAction.soundboard_sound_delete: AuditLogActionCategory.delete, + AuditLogAction.voice_channel_status_update: AuditLogActionCategory.update, + AuditLogAction.voice_channel_status_delete: AuditLogActionCategory.delete, } # fmt: on return lookup[self] @@ -500,6 +504,8 @@ def target_type(self) -> Optional[str]: return 'user' elif v < 152: return 'creator_monetization' + elif v < 194: + return 'voice_channel_status' class UserFlags(Enum): diff --git a/discord/permissions.py b/discord/permissions.py index b553e2578161..9ec0044a865a 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -187,7 +187,7 @@ def all(cls) -> Self: permissions set to ``True``. """ # Some of these are 0 because we don't want to set unnecessary bits - return cls(0b0000_0000_0000_0110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + return cls(0b0000_0000_0000_0111_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) @classmethod def _timeout_mask(cls) -> int: @@ -314,8 +314,12 @@ def text(cls) -> Self: @classmethod def voice(cls) -> Self: """A factory method that creates a :class:`Permissions` with all - "Voice" permissions from the official Discord UI set to ``True``.""" - return cls(0b0000_0000_0000_0000_0010_0100_1000_0000_0000_0011_1111_0000_0000_0011_0000_0000) + "Voice" permissions from the official Discord UI set to ``True``. + + .. versionchanged:: 2.5 + Added :attr:`set_voice_channel_status` permission. + """ + return cls(0b0000_0000_0000_0001_0010_0100_1000_0000_0000_0011_1111_0000_0000_0011_0000_0000) @classmethod def stage(cls) -> Self: @@ -761,6 +765,13 @@ def send_voice_messages(self) -> int: return 1 << 46 @flag_value + def set_voice_channel_status(self) -> int: + """:class:`bool`: Returns ``True`` if a user can set the status of voice channels. + + .. versionadded:: 2.5 + """ + return 1 << 48 + def send_polls(self) -> int: """:class:`bool`: Returns ``True`` if a user can send poll messages. @@ -907,6 +918,7 @@ class PermissionOverwrite: send_polls: Optional[bool] create_polls: Optional[bool] use_external_apps: Optional[bool] + set_voice_channel_status: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/raw_models.py b/discord/raw_models.py index 8d3ad328fb4c..7cdd255ca3ea 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -28,7 +28,7 @@ from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union from .enums import ChannelType, try_enum, ReactionType -from .utils import _get_as_snowflake +from .utils import _get_as_snowflake, MISSING from .app_commands import AppCommandPermissions from .colour import Colour @@ -50,6 +50,7 @@ TypingStartEvent, GuildMemberRemoveEvent, PollVoteActionEvent, + VoiceChannelStatusUpdate, ) from .types.command import GuildApplicationCommandPermissions from .message import Message @@ -79,6 +80,7 @@ 'RawMemberRemoveEvent', 'RawAppCommandPermissionsUpdateEvent', 'RawPollVoteActionEvent', + 'RawVoiceChannelStatusUpdateEvent', ) @@ -557,3 +559,30 @@ def __init__(self, data: PollVoteActionEvent) -> None: self.message_id: int = int(data['message_id']) self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') self.answer_id: int = int(data['answer_id']) + + +class RawVoiceChannelStatusUpdateEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_voice_channel_status_update` event. + + .. versionadded:: 2.5 + + Attributes + ---------- + channel_id: :class:`int` + The id of the voice channel whose status was updated. + guild_id: :class:`int` + The id of the guild the voice channel is in. + status: Optional[:class:`str`] + The newly updated status of the voice channel. ``None`` if no status is set. + cached_status: Optional[:class:`str`] + The cached status, if the voice channel is found in the internal channel cache otherwise :attr:`utils.MISSING`. + Represents the status before it is modified. ``None`` if no status was set. + """ + + __slots__ = ('channel_id', 'guild_id', 'status', 'cached_status') + + def __init__(self, data: VoiceChannelStatusUpdate): + self.channel_id: int = int(data['id']) + self.guild_id: int = int(data['guild_id']) + self.status: Optional[str] = data['status'] or None + self.cached_status: Optional[str] = MISSING diff --git a/discord/state.py b/discord/state.py index 83628af3243f..c592d3f8ecc2 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1690,6 +1690,17 @@ def parse_typing_start(self, data: gw.TypingStartEvent) -> None: self.dispatch('raw_typing', raw) + def parse_voice_channel_status_update(self, data: gw.VoiceChannelStatusUpdate) -> None: + raw = RawVoiceChannelStatusUpdateEvent(data) + guild = self._get_guild(raw.guild_id) + if guild is not None: + channel = guild.get_channel(raw.channel_id) + if channel is not None: + raw.cached_status = channel.status # type: ignore # must be a voice channel + channel.status = raw.status # type: ignore # must be a voice channel + + self.dispatch('raw_voice_channel_status_update', raw) + def parse_entitlement_create(self, data: gw.EntitlementCreateEvent) -> None: entitlement = Entitlement(data=data, state=self) self.dispatch('entitlement_create', entitlement) diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index 2c37542fddc7..3a3f9aeb286b 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -99,6 +99,8 @@ 145, 150, 151, + 192, + 193, ] diff --git a/discord/types/channel.py b/discord/types/channel.py index 4b593e55426a..80db24152c2a 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -88,6 +88,7 @@ class VoiceChannel(_BaseTextChannel): user_limit: int rtc_region: NotRequired[Optional[str]] video_quality_mode: NotRequired[VideoQualityMode] + status: NotRequired[Optional[str]] VoiceChannelEffectAnimationType = Literal[0, 1] diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 6261c70dd864..47ee5aa721c2 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -364,6 +364,12 @@ class GuildAuditLogEntryCreate(AuditLogEntry): guild_id: Snowflake +class VoiceChannelStatusUpdate(TypedDict): + id: Snowflake + guild_id: Snowflake + status: Optional[str] + + EntitlementCreateEvent = EntitlementUpdateEvent = EntitlementDeleteEvent = Entitlement diff --git a/docs/api.rst b/docs/api.rst index 3531dde06c0c..3c9b5aad81e3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -370,6 +370,16 @@ Channels :param payload: The raw event payload data. :type payload: :class:`RawTypingEvent` +.. function:: on_raw_voice_channel_status_update(payload) + + Called whenever the status of a voice channel has changed. + This is called regardless of the voice channel being in the internal cache. + + .. versionadded:: 2.5 + + :param payload: The raw event payload data. + :type payload: :class:`RawVoiceChannelStatusUpdateEvent` + Connection ~~~~~~~~~~~ @@ -3052,6 +3062,40 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 + .. attribute:: voice_channel_status_update + + The status of a voice channel was updated. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + a :class:`VoiceChannel`. + + When this is the action, the type of :attr:`~AuditLogEntry.extra` is + set to an unspecified proxy object with 2 attributes: + + - ``status``: The status of the voice channel. + - ``channel``: The channel of which the status was updated. + + When this is the action, :attr:`AuditLogEntry.changes` is empty. + + .. versionadded:: 2.5 + + .. attribute:: voice_channel_status_delete + + The status of a voice channel was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + a :class:`VoiceChannel`. + + When this is the action, the type of :attr:`~AuditLogEntry.extra` is + set to an unspecified proxy object with 2 attributes: + + - ``status``: The status of the voice channel. For this action this is ``None``. + - ``channel``: The channel of which the status was updated. + + When this is the action, :attr:`AuditLogEntry.changes` is empty. + + .. versionadded:: 2.5 + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. @@ -5321,6 +5365,14 @@ RawPollVoteActionEvent .. autoclass:: RawPollVoteActionEvent() :members: +RawVoiceChannelStatusUpdateEvent +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawVoiceChannelStatusUpdateEvent + +.. autoclass:: RawVoiceChannelStatusUpdateEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~