Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ These changes are available on the `master` branch, but have not yet been releas

- Support for **Python 3.14**.
([#2948](https://github.com/Pycord-Development/pycord/pull/2948))
- Added the ability to fetch and cache voice channel status and start time on demand,
and at startup. ([#3210](https://github.com/Pycord-Development/pycord/pull/3210))
- Added `voice_start_time` to `VoiceChannel` and `StageChannel`, as well as the
corresponding `on_voice_channel_start_time_update`.
([#3210](https://github.com/Pycord-Development/pycord/pull/3210))

### Changed

Expand Down
38 changes: 33 additions & 5 deletions discord/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,8 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
"last_message_id",
"flags",
"nsfw",
"status",
"_voice_start_time",
)

def __init__(
Expand All @@ -1615,6 +1617,7 @@ def __init__(
self._state: ConnectionState = state
self.id: int = int(data["id"])
self._update(guild, data)
self._update_status(status=data.get("status", MISSING))

def _get_voice_client_key(self) -> tuple[int, str]:
return self.guild.id, "guild_id"
Expand Down Expand Up @@ -1650,6 +1653,24 @@ def _update(
self.nsfw: bool = data.get("nsfw", False)
self._fill_overwrites(data)

def _update_status(
self, *, status: str | None = MISSING, voice_start_time: int | None = MISSING
):
if status is not MISSING:
self.status = status
if voice_start_time is not MISSING:
self._voice_start_time = voice_start_time

@property
def voice_start_time(self) -> datetime.datetime | None:
""":class:`datetime.datetime` | :class:`None`: The time that the voice session started.

.. versionadded:: 2.9
"""
if self._voice_start_time is None:
return None
return datetime.datetime.fromtimestamp(self._voice_start_time, tz=datetime.UTC)

@property
def _sorting_bucket(self) -> int:
return ChannelType.voice.value
Expand Down Expand Up @@ -1781,13 +1802,9 @@ def __init__(
data: VoiceChannelPayload,
):
self.status: str | None = None
self._voice_start_time: int | None = None
super().__init__(state=state, guild=guild, data=data)

def _update(self, guild: Guild, data: VoiceChannelPayload):
super()._update(guild, data)
if data.get("status"):
self.status = data.get("status")

def __repr__(self) -> str:
attrs = [
("id", self.id),
Expand Down Expand Up @@ -2233,6 +2250,7 @@ async def set_status(
Sets the status of the voice channel.

You must have the :attr:`~Permissions.set_voice_channel_status` permission to use this.
If the bot is not connected to the voice channel, this also requires :attr:`~Permissions.manage_channels`.

Parameters
----------
Expand Down Expand Up @@ -2339,6 +2357,16 @@ class StageChannel(discord.abc.Messageable, VocalGuildChannel):

__slots__ = ("topic",)

def __init__(
self,
*,
state: ConnectionState,
guild: Guild,
data: StageChannelPayload,
):
self._voice_start_time: int | None = None
super().__init__(state=state, guild=guild, data=data)

def _update(self, guild: Guild, data: StageChannelPayload) -> None:
super()._update(guild, data)
self.topic = data.get("topic")
Expand Down
8 changes: 8 additions & 0 deletions discord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ class Client:
Whether to automatically fetch and cache the default soundboard sounds on startup. Defaults to ``True``.

.. versionadded:: 2.8
cache_channel_info: :class:`bool`
Whether to automatically request and cache voice channel statuses on startup. Defaults to ``False``.

.. versionadded:: 2.9

Attributes
-----------
Expand Down Expand Up @@ -719,6 +723,10 @@ async def connect(self, *, reconnect: bool = True) -> None:
except ReconnectWebSocket as e:
_log.info("Got a request to %s the websocket.", e.op)
self.dispatch("disconnect")
if not e.resume:
# Since we aren't resuming, channel info can fall out of date
# So we re-request it
self._connection._request_channel_info = True
ws_params.update(
sequence=self.ws.sequence,
resume=e.resume,
Expand Down
18 changes: 18 additions & 0 deletions discord/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
import aiohttp

from . import utils
from .abc import Snowflake
from .activity import BaseActivity
from .errors import ConnectionClosed, InvalidArgument
from .types.channel import RequestChannelInfoField

if TYPE_CHECKING:
from typing_extensions import Self
Expand Down Expand Up @@ -283,6 +285,7 @@ class DiscordWebSocket:
HEARTBEAT_ACK = 11
GUILD_SYNC = 12
REQUEST_SOUNDBOARD_SOUNDS = 31
REQUEST_CHANNEL_INFO = 43

if TYPE_CHECKING:
token: str | None
Expand Down Expand Up @@ -772,6 +775,21 @@ async def request_soundboard_sounds(self, guild_ids):
_log.debug("Requesting soundboard sounds for guilds %s.", guild_ids)
await self.send_as_json(payload)

async def request_channel_info(
self, guild_id: int, fields: list[RequestChannelInfoField]
):
payload = {
"op": self.REQUEST_CHANNEL_INFO,
"d": {"guild_id": guild_id, "fields": fields},
}

_log.debug(
"Requesting channel info for guild %s with fields %s.",
guild_id,
", ".join(fields),
)
await self.send_as_json(payload)

async def close(self, code: int = 4000) -> None:
if self._keep_alive:
self._keep_alive.stop()
Expand Down
20 changes: 20 additions & 0 deletions discord/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
from .monetization import Entitlement
from .onboarding import Onboarding
from .permissions import PermissionOverwrite
from .raw_models import ChannelInfo
from .role import Role, RoleColours
from .scheduled_events import ScheduledEvent, ScheduledEventLocation
from .soundboard import SoundboardSound
Expand Down Expand Up @@ -3954,6 +3955,25 @@ async def chunk(self, *, cache: bool = True) -> None:
if not self._state.is_guild_evicted(self):
return await self._state.chunk_guild(self, cache=cache)

async def request_channel_info(
self, *, cache: bool = True
) -> None | list[ChannelInfo]:
"""|coro|

Requests all channel statuses for this guild over the websocket.

.. versionadded:: 2.9

Parameters
----------
cache: :class:`bool`
Whether to cache the channel statuses as well.
"""

if not self._state.is_guild_evicted(self):
return await self._state.request_guild_channel_info(self, cache=cache)
return None

async def query_members(
self,
query: str | None = None,
Expand Down
54 changes: 54 additions & 0 deletions discord/raw_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
ThreadMembersUpdateEvent,
ThreadUpdateEvent,
TypingEvent,
VoiceChannelStartTimeUpdateEvent,
VoiceChannelStatusUpdateEvent,
VoiceServerUpdateEvent,
VoiceStateEvent,
Expand Down Expand Up @@ -98,7 +99,9 @@
"RawSoundboardSoundDeleteEvent",
"RawVoiceServerUpdateEvent",
"RawVoiceStateUpdateEvent",
"RawVoiceChannelStartTimeUpdateEvent",
"RawMemberUpdateEvent",
"ChannelInfo",
)


Expand Down Expand Up @@ -1030,3 +1033,54 @@ def __init__(self, data: MemberUpdateEvent, member: Member) -> None:
self.data: MemberUpdateEvent = data
self.cached_member: Member | None = None
self.member: Member = member


class RawVoiceChannelStartTimeUpdateEvent(_RawReprMixin):
"""Represents the payload for an :func:`on_raw_voice_channel_start_time_update` event.

.. versionadded:: 2.9

Attributes
----------
id: :class:`int`
The channel ID where the voice channel start time update originated from.
guild_id: :class:`int`
The guild ID where the voice channel start time update originated from.
voice_start_time: Optional[:class:`datetime.datetime`]
The new new voice channel start time.
data: :class:`dict`
Comment thread
Paillat-dev marked this conversation as resolved.
The raw data sent by the `gateway <https://docs.discord.com/developers/events/gateway-events-events#voice-channel-start-time-update>`__.
"""

__slots__ = ("id", "guild_id", "voice_start_time", "data")

def __init__(self, data: VoiceChannelStartTimeUpdateEvent) -> None:
self.id: int = int(data["id"])
self.guild_id: int = int(data["guild_id"])
self.voice_start_time: datetime.datetime | None = (
datetime.datetime.fromtimestamp(data["voice_start_time"], tz=datetime.UTC)
if data.get("voice_start_time")
else None
)
self.data: VoiceChannelStartTimeUpdateEvent = data


class ChannelInfo(_RawReprMixin):
"""Represents the gateway response to a request for channel information.

.. versionadded:: 2.9

Attributes
----------
id: :class:`int`
The ID of the channel this info is associated with.
status: :class:`str` | None
The voice channel status.
voice_start_time: :class:`int` | None
The Unix timestamp (in seconds) of when the voice session started.
"""

def __init__(self, data):
self.id: int = int(data["id"])
self.status: str | None = data.get("status", utils.MISSING)
self.voice_start_time: int | None = data.get("voice_start_time", utils.MISSING)
Loading
Loading