Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b651abc
feat: voice channel info from gateway
plun1331 Apr 20, 2026
a302973
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
22f00e6
fix: remove debug print
plun1331 Apr 20, 2026
3b9b441
Merge remote-tracking branch 'origin/feat/channel-status-gateway' int…
plun1331 Apr 20, 2026
262faf0
chore: update changelog
plun1331 Apr 20, 2026
6a0a3da
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
be20f9c
docs: add permission note
plun1331 Apr 20, 2026
ce6e55b
Merge remote-tracking branch 'origin/feat/channel-status-gateway' int…
plun1331 Apr 20, 2026
439f19e
docs: ChannelInfo
plun1331 Apr 20, 2026
636bc8f
fix: add ChannelInfo to all
plun1331 Apr 20, 2026
85b5934
fix: target for 2.9
plun1331 Apr 20, 2026
de0f644
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
972996e
docs: events
plun1331 Apr 20, 2026
891c511
Merge remote-tracking branch 'origin/feat/channel-status-gateway' int…
plun1331 Apr 20, 2026
9e98160
Apply suggestions from code review
plun1331 Apr 22, 2026
cf0ebf3
Apply suggestions from code review
plun1331 May 2, 2026
9b0a76f
Merge branch 'master' into feat/channel-status-gateway
plun1331 May 2, 2026
b862ca3
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 2, 2026
b960f0d
Apply suggestions from code review
plun1331 May 20, 2026
f9e457e
Merge branch 'master' into feat/channel-status-gateway
plun1331 May 20, 2026
308467a
Merge branch 'master' into feat/channel-status-gateway
plun1331 Jun 14, 2026
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ These changes are available on the `master` branch, but have not yet been releas

### Added

- 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

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

def __init__(
Expand All @@ -1623,6 +1625,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 @@ -1658,6 +1661,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 @@ -1789,13 +1810,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 @@ -2241,6 +2258,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 @@ -2347,6 +2365,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")
self.voice_start_time: int | None = data.get("voice_start_time")
Loading
Loading