Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/1520.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Support voice channel statuses.
- New field: :attr:`VoiceChannel.status`, changeable using :meth:`VoiceChannel.set_status`.
- New events: :func:`on_voice_channel_status_update` + :func:`on_voice_channel_start_time_update`.
- New audit log types: :attr:`AuditLogAction.voice_channel_status_update`, :attr:`~AuditLogAction.voice_channel_status_delete`.
10 changes: 10 additions & 0 deletions disnake/audit_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,10 @@ class _AuditLogProxyKickOrMemberRoleAction:
integration_type: str | None


class _AuditLogProxyVoiceStatusUpdate:
status: str


class AuditLogEntry(Hashable):
r"""Represents an Audit Log entry.

Expand Down Expand Up @@ -679,6 +683,11 @@ def _from_data(self, data: AuditLogEntryPayload) -> None:
"integration_type": extra.get("integration_type"),
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action is enums.AuditLogAction.voice_channel_status_update:
elems = {
"status": extra.get("status"),
}
self.extra = type("_AuditLogProxy", (), elems)()

self.extra: Any
# actually this but there's no reason to annoy users with this:
Expand All @@ -690,6 +699,7 @@ def _from_data(self, data: AuditLogEntryPayload) -> None:
# _AuditLogProxyStageInstanceAction,
# _AuditLogProxyAutoModAction,
# _AuditLogProxyKickOrMemberRoleAction,
# _AuditLogProxyVoiceStatusUpdate,
# Member, User, None,
# Role,
# ]
Expand Down
38 changes: 38 additions & 0 deletions disnake/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1356,12 +1356,20 @@ class VoiceChannel(disnake.abc.Messageable, VocalGuildChannel):
*not* point to an existing or valid message.

.. versionadded:: 2.3

status: :class:`str` | :data:`None`
The channel's status, if any.

This can be edited using :meth:`set_status`.

.. versionadded:: |vnext|
"""

__slots__ = (
"nsfw",
"slowmode_delay",
"last_message_id",
"status",
)

def __repr__(self) -> str:
Expand All @@ -1385,6 +1393,7 @@ def _update(self, guild: Guild, data: VoiceChannelPayload) -> None:
self.nsfw: bool = data.get("nsfw", False)
self.slowmode_delay: int = data.get("rate_limit_per_user", 0)
self.last_message_id: int | None = utils._get_as_snowflake(data, "last_message_id")
self.status: str | None = data.get("status")

async def _get_channel(self: Self) -> Self:
return self
Expand Down Expand Up @@ -1945,6 +1954,8 @@ async def send_soundboard_sound(self, sound: SoundboardSound, /) -> None:
:attr:`~Permissions.use_external_sounds` permission.
Additionally, you may not be muted or deafened.

.. versionadded:: 2.10

Parameters
----------
sound: :class:`SoundboardSound` | :class:`GuildSoundboardSound`
Expand All @@ -1966,6 +1977,33 @@ async def send_soundboard_sound(self, sound: SoundboardSound, /) -> None:
self.id, sound.id, source_guild_id=source_guild_id
)

async def set_status(self, status: str | None, /, *, reason: str | None = None) -> None:
"""|coro|

Edits the channel's status.

You must either have :attr:`~Permissions.manage_channels` permission,
or be connected to this voice channel and have :attr:`~Permissions.set_voice_channel_status`
permission to do this.

.. versionadded:: |vnext|

Parameters
----------
status: :class:`str` | :data:`None`
The new status to set for this channel. Up to 500 characters.
reason: :class:`str` | :data:`None`
The reason for changing this status. Shows up in the audit logs.

Raises
------
Forbidden
You do not have permissions to edit the status.
HTTPException
Editing the channel status failed.
"""
await self._state.http.set_voice_channel_status(self.id, status=status, reason=reason)


class StageChannel(disnake.abc.Messageable, VocalGuildChannel):
"""Represents a Discord guild stage channel.
Expand Down
20 changes: 19 additions & 1 deletion disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,8 @@ class AuditLogAction(Enum):
automod_quarantine_user = 146
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 @@ -841,6 +843,8 @@ def category(self) -> AuditLogActionCategory | None:
AuditLogAction.automod_quarantine_user: 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 @@ -886,8 +890,10 @@ def target_type(self) -> str | None:
return "automod_rule"
elif v < 147:
return "user"
elif v < 152:
elif v < 192:
return None
elif v < 194:
return "channel"
else:
return None

Expand Down Expand Up @@ -2120,6 +2126,18 @@ class Event(Enum):

.. versionadded:: 2.10
"""
voice_channel_status_update = "voice_channel_status_update"
"""Called when a voice channel status is updated.
Represents the :func:`on_voice_channel_status_update` event.

.. versionadded:: |vnext|
"""
voice_channel_start_time_update = "voice_channel_start_time_update"
"""Called when a voice channel start time is updated.
Represents the :func:`on_voice_channel_start_time_update` event.

.. versionadded:: |vnext|
"""
stage_instance_create = "stage_instance_create"
"""Called when a `StageInstance` is created for a `StageChannel`.
Represents the :func:`on_stage_instance_create` event.
Expand Down
14 changes: 14 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,20 @@ def delete_channel(
Route("DELETE", "/channels/{channel_id}", channel_id=channel_id), reason=reason
)

def set_voice_channel_status(
self,
channel_id: Snowflake,
*,
status: str | None,
reason: str | None = None,
) -> Response[None]:
payload = {"status": status}
return self.request(
Route("PUT", "/channels/{channel_id}/voice-status", channel_id=channel_id),
json=payload,
reason=reason,
)

# Thread management

def start_thread_with_message(
Expand Down
58 changes: 58 additions & 0 deletions disnake/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1881,6 +1881,64 @@ def parse_voice_channel_effect_send(self, data: gateway.VoiceChannelEffectSendEv
if channel and member:
self.dispatch("voice_channel_effect", channel, member, effect)

def parse_voice_channel_status_update(self, data: gateway.VoiceChannelStatusUpdate) -> None:
guild_id = int(data["guild_id"])
guild = self._get_guild(guild_id)

if guild is None:
_log.debug(
"VOICE_CHANNEL_STATUS_UPDATE referencing an unknown guild ID: %s. Discarding",
guild_id,
)
return

channel_id = int(data["id"])
channel = guild.get_channel(channel_id)
if channel is None:
_log.debug(
"VOICE_CHANNEL_STATUS_UPDATE referencing an unknown channel ID: %s. Discarding",
channel_id,
)
return

if isinstance(channel, VoiceChannel):
# in case stage channels ever get statuses too
old_status = channel.status
channel.status = data.get("status")
else:
old_status = None

self.dispatch("voice_channel_status_update", channel, old_status, data.get("status"))

def parse_voice_channel_start_time_update(
self, data: gateway.VoiceChannelStartTimeUpdate
) -> None:
guild_id = int(data["guild_id"])
guild = self._get_guild(guild_id)

if guild is None:
_log.debug(
"VOICE_CHANNEL_START_TIME_UPDATE referencing an unknown guild ID: %s. Discarding",
guild_id,
)
return

channel_id = int(data["id"])
channel = guild.get_channel(channel_id)
if channel is None:
_log.debug(
"VOICE_CHANNEL_START_TIME_UPDATE referencing an unknown channel ID: %s. Discarding",
channel_id,
)
return

timestamp = (
datetime.datetime.fromtimestamp(start_ts, tz=datetime.timezone.utc)
if (start_ts := data.get("voice_start_time"))
else None
)
self.dispatch("voice_channel_start_time_update", channel, timestamp)

# FIXME: this should be refactored. The `GroupChannel` path will never be hit,
# `raw.timestamp` exists so no need to parse it twice, and `.get_user` should be used before falling back
def parse_typing_start(self, data: gateway.TypingStartEvent) -> None:
Expand Down
3 changes: 3 additions & 0 deletions disnake/types/audit_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
146,
150,
151,
192,
193,
]


Expand Down Expand Up @@ -319,6 +321,7 @@ class AuditEntryInfo(TypedDict):
auto_moderation_rule_name: str
auto_moderation_rule_trigger_type: str
integration_type: str
status: str


class AuditLogEntry(TypedDict):
Expand Down
1 change: 1 addition & 0 deletions disnake/types/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class VoiceChannel(_BaseVocalGuildChannel):
last_message_id: NotRequired[Snowflake | None]
rate_limit_per_user: NotRequired[int]
video_quality_mode: NotRequired[VideoQualityMode]
status: NotRequired[str | None] # somewhat undocumented, only received via gw


class CategoryChannel(_BaseGuildChannel):
Expand Down
14 changes: 14 additions & 0 deletions disnake/types/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,3 +804,17 @@ class GuildSoundboardSoundDelete(TypedDict):
class GuildSoundboardSoundsUpdate(TypedDict):
guild_id: Snowflake
soundboard_sounds: list[GuildSoundboardSound]


# https://docs.discord.com/developers/events/gateway-events#voice-channel-status-update
class VoiceChannelStatusUpdate(TypedDict):
id: Snowflake
guild_id: Snowflake
status: str | None


# https://docs.discord.com/developers/events/gateway-events#voice-channel-start-time-update
class VoiceChannelStartTimeUpdate(TypedDict):
id: Snowflake
guild_id: Snowflake
voice_start_time: NotRequired[int | None]
17 changes: 17 additions & 0 deletions docs/api/audit_logs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1764,6 +1764,23 @@ AuditLogAction

.. versionadded:: 2.10

.. attribute:: voice_channel_status_update

A voice channel status was updated.

When this is the action, the type of :attr:`~AuditLogEntry.extra` is
set to an unspecified proxy object with one attribute:

- ``status``: The voice channel's new status.

.. versionadded:: |vnext|

.. attribute:: voice_channel_status_delete

A voice channel status was deleted.

.. versionadded:: |vnext|

AuditLogActionCategory
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
28 changes: 28 additions & 0 deletions docs/api/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,34 @@ This section documents events related to Discord channels and threads.
:param channel: The channel that had its webhooks updated.
:type channel: :class:`abc.GuildChannel`

.. function:: on_voice_channel_status_update(channel, old, new)

Called whenever a voice channel status is modified.

This requires :attr:`Intents.guilds` to be enabled.

.. versionadded:: |vnext|

:param channel: The channel that had its status updated.
:type channel: :class:`abc.GuildChannel`
:param old: The old status.
:type old: :class:`str` | :data:`None`
:param new: The new status.
:type new: :class:`str` | :data:`None`

.. function:: on_voice_channel_start_time_update(channel, start_time)

Called whenever a voice channel's start time changes.

This requires :attr:`Intents.guilds` to be enabled.

.. versionadded:: |vnext|

:param channel: The channel that had its start time updated.
:type channel: :class:`abc.GuildChannel`
:param start_time: The new start time.
:type start_time: :class:`datetime.datetime` | :data:`None`

Guilds
~~~~~~

Expand Down
Loading