Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add voice channel status events and permission #9604

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
27 changes: 25 additions & 2 deletions discord/audit_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -258,6 +258,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)


Expand Down Expand Up @@ -351,7 +358,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),
Expand Down Expand Up @@ -523,6 +530,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.

Expand Down Expand Up @@ -610,6 +622,7 @@ def _from_data(self, data: AuditLogEntryPayload) -> None:
_AuditLogProxyMessageBulkDelete,
_AuditLogProxyAutoModAction,
_AuditLogProxyMemberKickOrMemberRoleUpdate,
_AuditLogProxyVoiceChannelStatusAction,
Member, User, None, PartialIntegration,
Role, Object
] = None
Expand Down Expand Up @@ -686,6 +699,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
Expand Down Expand Up @@ -863,3 +883,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)
12 changes: 11 additions & 1 deletion discord/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
6 changes: 6 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,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
Expand Down Expand Up @@ -438,6 +440,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]
Expand Down Expand Up @@ -483,6 +487,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):
Expand Down
19 changes: 16 additions & 3 deletions discord/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]] = {}
Expand Down
14 changes: 14 additions & 0 deletions discord/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,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 parse_entitlement_create(self, data: gw.EntitlementCreateEvent) -> None:
entitlement = Entitlement(data=data, state=self)
self.dispatch('entitlement_create', entitlement)
Expand Down
2 changes: 2 additions & 0 deletions discord/types/audit_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
145,
150,
151,
192,
193,
]


Expand Down
1 change: 1 addition & 0 deletions discord/types/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 6 additions & 0 deletions discord/types/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,10 @@ class GuildAuditLogEntryCreate(AuditLogEntry):
guild_id: Snowflake


class VoiceChannelStatusUpdate(TypedDict):
id: Snowflake
guild_id: Snowflake
status: Optional[str]


EntitlementCreateEvent = EntitlementUpdateEvent = EntitlementDeleteEvent = Entitlement
47 changes: 47 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`]
codeofandrin marked this conversation as resolved.
Show resolved Hide resolved

Connection
~~~~~~~~~~~

Expand Down Expand Up @@ -2880,6 +2893,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.
Expand Down
Loading