Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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 @@ -13,6 +13,12 @@ released.

### 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 @@ -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")
self.voice_start_time: int | None = data.get("voice_start_time")
Loading
Loading