From 879bde76bb5288f714d129aec754d638ca7946ad Mon Sep 17 00:00:00 2001 From: Puncher <65789180+Puncher1@users.noreply.github.com> Date: Sat, 7 Oct 2023 22:58:39 +0200 Subject: [PATCH 1/8] Add audit log --- discord/audit_logs.py | 27 +++++++++++++++++++++++++-- discord/enums.py | 6 ++++++ discord/types/audit_log.py | 2 ++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index f0924039e577..e4b21528ee68 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', @@ -267,6 +267,13 @@ def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAc return [AutoModRuleAction.from_data(action) for action in 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) @@ -360,7 +367,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), @@ -532,6 +539,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. @@ -619,6 +631,7 @@ def _from_data(self, data: AuditLogEntryPayload) -> None: _AuditLogProxyMessageBulkDelete, _AuditLogProxyAutoModAction, _AuditLogProxyMemberKickOrMemberRoleUpdate, + _AuditLogProxyVoiceChannelStatusAction, Member, User, None, PartialIntegration, Role, Object ] = None @@ -695,6 +708,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 @@ -872,3 +892,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/enums.py b/discord/enums.py index 254f86bc789d..147ac0b93973 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -373,6 +373,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 @@ -435,6 +437,8 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.automod_timeout_member: None, AuditLogAction.creator_monetization_request_created: None, AuditLogAction.creator_monetization_terms_accepted: None, + AuditLogAction.voice_channel_status_update: AuditLogActionCategory.update, + AuditLogAction.voice_channel_status_delete: AuditLogActionCategory.delete, } # fmt: on return lookup[self] @@ -480,6 +484,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/types/audit_log.py b/discord/types/audit_log.py index 3b7022e79e46..ca15b4c36e1c 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -95,6 +95,8 @@ 145, 150, 151, + 192, + 193, ] From e3290b5c490ee6fe1a6833b8768b3b988c5a21ec Mon Sep 17 00:00:00 2001 From: Puncher <65789180+Puncher1@users.noreply.github.com> Date: Sat, 7 Oct 2023 23:23:48 +0200 Subject: [PATCH 2/8] Add audit log docs --- docs/api.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 89b05a8c3678..17d4c1d08b15 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2839,6 +2839,40 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. 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.4 + + .. 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.4 + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. From 1b40e06d76ab4e02f14a37657f5ead56c9577365 Mon Sep 17 00:00:00 2001 From: Puncher <65789180+Puncher1@users.noreply.github.com> Date: Sun, 8 Oct 2023 15:56:06 +0200 Subject: [PATCH 3/8] Add status attribute, permission, events --- discord/channel.py | 12 +++++++++++- discord/permissions.py | 19 ++++++++++++++++--- discord/state.py | 14 ++++++++++++++ discord/types/channel.py | 1 + discord/types/gateway.py | 6 ++++++ docs/api.rst | 13 +++++++++++++ 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 02f2de075cc9..04347c39387e 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1316,9 +1316,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.4 """ - __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/permissions.py b/discord/permissions.py index 5ba5ea4af7c2..20390e8d5b68 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_0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + return cls(0b0000_0000_0000_0001_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) @classmethod def _timeout_mask(cls) -> int: @@ -284,8 +284,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.4 + 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: @@ -722,6 +726,14 @@ 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.4 + """ + return 1 << 48 + def _augment_from_permissions(cls): cls.VALID_NAMES = set(Permissions.VALID_FLAGS) @@ -842,6 +854,7 @@ class PermissionOverwrite: send_voice_messages: Optional[bool] create_expressions: Optional[bool] create_events: Optional[bool] + set_voice_channel_status: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/state.py b/discord/state.py index 8dcc30c245c5..ca00a91db031 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1584,6 +1584,20 @@ 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: + guild = self._get_guild(int(data['guild_id'])) + if guild is not None: + channel = guild.get_channel(int(data['id'])) + if channel is not None: + old_status = channel.status # type: ignore # will be a voice channel + status = data['status'] or None # empty string -> None + channel.status = status # type: ignore # will be a voice channel + self.dispatch('voice_channel_status_update', channel, old_status, status) + else: + _log.debug('VOICE_CHANNEL_STATUS_UPDATE referencing unknown channel ID: %s. Discarding.', data['id']) + else: + _log.debug('VOICE_CHANNEL_STATUS_UPDATE referencing unknown guild ID: %s. Discarding.', data['guild_id']) + def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: if isinstance(channel, (TextChannel, Thread, VoiceChannel)): return channel.guild.get_member(user_id) diff --git a/discord/types/channel.py b/discord/types/channel.py index d5d82b5c6461..a3bb5735e3d8 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -87,6 +87,7 @@ class VoiceChannel(_BaseTextChannel): user_limit: int rtc_region: NotRequired[Optional[str]] video_quality_mode: NotRequired[VideoQualityMode] + status: NotRequired[Optional[str]] class CategoryChannel(_BaseGuildChannel): diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 0c50671e1094..2a4f205955d9 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -347,3 +347,9 @@ class AutoModerationActionExecution(TypedDict): class GuildAuditLogEntryCreate(AuditLogEntry): guild_id: Snowflake + + +class VoiceChannelStatusUpdate(TypedDict): + id: Snowflake + guild_id: Snowflake + status: Optional[str] diff --git a/docs/api.rst b/docs/api.rst index 17d4c1d08b15..580f818eebbe 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -370,6 +370,19 @@ Channels :param payload: The raw event payload data. :type payload: :class:`RawTypingEvent` +.. function:: on_voice_channel_status_update(channel, before, after) + + Called whenever the status of a voice channel has changed. + + .. versionadded:: 2.4 + + :param channel: The channel whose status has changed. + :type channel: :class:`VoiceChannel` + :param before: The status before the update. + :type before: Optional[:class:`str`] + :param after: The status after the update. + :type after: Optional[:class:`str`] + Connection ~~~~~~~~~~~ From 23d2231cf427d18c7f29b588a554cd2feeb4be0e Mon Sep 17 00:00:00 2001 From: Puncher <65789180+Puncher1@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:44:16 +0100 Subject: [PATCH 4/8] Change event to raw --- discord/raw_models.py | 31 ++++++++++++++++++++++++++++++- discord/state.py | 17 +++++++---------- docs/api.rst | 19 ++++++++++++------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index 556df52451ab..42c661defea3 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 -from .utils import _get_as_snowflake +from .utils import _get_as_snowflake, MISSING from .app_commands import AppCommandPermissions from .colour import Colour @@ -47,6 +47,7 @@ ThreadMembersUpdate, TypingStartEvent, GuildMemberRemoveEvent, + VoiceChannelStatusUpdate, ) from .types.command import GuildApplicationCommandPermissions from .message import Message @@ -75,6 +76,7 @@ 'RawTypingEvent', 'RawMemberRemoveEvent', 'RawAppCommandPermissionsUpdateEvent', + 'RawVoiceChannelStatusUpdateEvent', ) @@ -503,3 +505,30 @@ def __init__(self, *, data: GuildApplicationCommandPermissions, state: Connectio self.permissions: List[AppCommandPermissions] = [ AppCommandPermissions(data=perm, guild=self.guild, state=state) for perm in data['permissions'] ] + + +class RawVoiceChannelStatusUpdateEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_voice_channel_status_update` event. + + .. versionadded:: 2.4 + + 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 1afeb6fde3d3..a0d59ed3e0c8 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1586,18 +1586,15 @@ 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: - guild = self._get_guild(int(data['guild_id'])) + raw = RawVoiceChannelStatusUpdateEvent(data) + guild = self._get_guild(raw.guild_id) if guild is not None: - channel = guild.get_channel(int(data['id'])) + channel = guild.get_channel(raw.channel_id) if channel is not None: - old_status = channel.status # type: ignore # will be a voice channel - status = data['status'] or None # empty string -> None - channel.status = status # type: ignore # will be a voice channel - self.dispatch('voice_channel_status_update', channel, old_status, status) - else: - _log.debug('VOICE_CHANNEL_STATUS_UPDATE referencing unknown channel ID: %s. Discarding.', data['id']) - else: - _log.debug('VOICE_CHANNEL_STATUS_UPDATE referencing unknown guild ID: %s. Discarding.', data['guild_id']) + 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) diff --git a/docs/api.rst b/docs/api.rst index c3393976c4a1..9ef41d7b2f2d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -370,18 +370,15 @@ Channels :param payload: The raw event payload data. :type payload: :class:`RawTypingEvent` -.. function:: on_voice_channel_status_update(channel, before, after) +.. 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.4 - :param channel: The channel whose status has changed. - :type channel: :class:`VoiceChannel` - :param before: The status before the update. - :type before: Optional[:class:`str`] - :param after: The status after the update. - :type after: Optional[:class:`str`] + :param payload: The raw event payload data. + :type payload: :class:`RawVoiceChannelStatusUpdateEvent` Connection ~~~~~~~~~~~ @@ -4971,6 +4968,14 @@ RawAppCommandPermissionsUpdateEvent .. autoclass:: RawAppCommandPermissionsUpdateEvent() :members: +RawVoiceChannelStatusUpdateEvent +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawVoiceChannelStatusUpdateEvent + +.. autoclass:: RawVoiceChannelStatusUpdateEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~ From a2efba41c45cc85928f45c61b985b8ea4d818d77 Mon Sep 17 00:00:00 2001 From: Puncher <65789180+Puncher1@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:44:37 +0100 Subject: [PATCH 5/8] lint --- discord/types/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/gateway.py b/discord/types/gateway.py index f9a78aaa8553..b43f502f38d2 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -355,5 +355,5 @@ class VoiceChannelStatusUpdate(TypedDict): guild_id: Snowflake status: Optional[str] - + EntitlementCreateEvent = EntitlementUpdateEvent = EntitlementDeleteEvent = Entitlement From 3d827096d1b770d367c41fb8a0028b3c1a26f642 Mon Sep 17 00:00:00 2001 From: Puncher <65789180+Puncher1@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:46:19 +0100 Subject: [PATCH 6/8] lint --- discord/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/state.py b/discord/state.py index a0d59ed3e0c8..fee8c8113b9d 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1592,7 +1592,7 @@ def parse_voice_channel_status_update(self, data: gw.VoiceChannelStatusUpdate) - 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 + channel.status = raw.status # type: ignore # must be a voice channel self.dispatch('raw_voice_channel_status_update', raw) From 33280cd42cda0de295d6855ed38529d92e78bf95 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:47:56 +0200 Subject: [PATCH 7/8] Fix wrong bit in permissions.all --- discord/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/permissions.py b/discord/permissions.py index e0e472064a89..57fef398b137 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_0111_1111_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: From 67c1a9bc284b7f8880b8e7052b59396cf3cd3340 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:03:31 +0200 Subject: [PATCH 8/8] Update version directives --- discord/channel.py | 2 +- discord/permissions.py | 4 ++-- discord/raw_models.py | 2 +- docs/api.rst | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 354d73707417..eb4e422754fa 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1484,7 +1484,7 @@ class VoiceChannel(VocalGuildChannel): This is not available for the fetch methods such as :func:`Guild.fetch_channel` or :func:`Client.fetch_channel` - .. versionadded:: 2.4 + .. versionadded:: 2.5 """ __slots__ = ('status',) diff --git a/discord/permissions.py b/discord/permissions.py index 57fef398b137..9ec0044a865a 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -316,7 +316,7 @@ def voice(cls) -> Self: """A factory method that creates a :class:`Permissions` with all "Voice" permissions from the official Discord UI set to ``True``. - .. versionchanged:: 2.4 + .. 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) @@ -768,7 +768,7 @@ def send_voice_messages(self) -> int: def set_voice_channel_status(self) -> int: """:class:`bool`: Returns ``True`` if a user can set the status of voice channels. - .. versionadded:: 2.4 + .. versionadded:: 2.5 """ return 1 << 48 diff --git a/discord/raw_models.py b/discord/raw_models.py index 324f6c38078b..7cdd255ca3ea 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -564,7 +564,7 @@ def __init__(self, data: PollVoteActionEvent) -> None: class RawVoiceChannelStatusUpdateEvent(_RawReprMixin): """Represents the payload for a :func:`on_raw_voice_channel_status_update` event. - .. versionadded:: 2.4 + .. versionadded:: 2.5 Attributes ---------- diff --git a/docs/api.rst b/docs/api.rst index 08140206781b..3c9b5aad81e3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -375,7 +375,7 @@ Channels 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.4 + .. versionadded:: 2.5 :param payload: The raw event payload data. :type payload: :class:`RawVoiceChannelStatusUpdateEvent` @@ -3077,7 +3077,7 @@ of :class:`enum.Enum`. When this is the action, :attr:`AuditLogEntry.changes` is empty. - .. versionadded:: 2.4 + .. versionadded:: 2.5 .. attribute:: voice_channel_status_delete @@ -3094,7 +3094,7 @@ of :class:`enum.Enum`. When this is the action, :attr:`AuditLogEntry.changes` is empty. - .. versionadded:: 2.4 + .. versionadded:: 2.5 .. class:: AuditLogActionCategory