diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d407f3758..b8b1ff5432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,16 @@ These changes are available on the `master` branch, but have not yet been releas ([#3328](https://github.com/Pycord-Development/pycord/pull/3328)) - Added `SlashCommandGroup.add_command`. ([#3346](https://github.com/Pycord-Development/pycord/pull/3346)) +- Added `Guild.fetch_voice_regions()` method to retrieve the currently available voice + regions for the guild. + ([#3347](https://github.com/Pycord-Development/pycord/pull/3347)) ### Changed +- The `rtc_region` parameters of channel creation and edit methods now also accept a + region ID `str` in addition to a `VoiceRegion` member. + ([#3347](https://github.com/Pycord-Development/pycord/pull/3347)) + ### Fixed - Fix `TypeError` when accessing `ApplicationCommand.guild_only` or @@ -29,6 +36,10 @@ These changes are available on the `master` branch, but have not yet been releas ### Deprecated +- Deprecated the `VoiceRegion` enum in favor of the region ID `str` or + `Guild.fetch_voice_regions()`. + ([#3347](https://github.com/Pycord-Development/pycord/pull/3347)) + ### Removed ## [2.8.1] - 2026-07-25 diff --git a/discord/channel.py b/discord/channel.py old mode 100644 new mode 100755 index dab70c4a92..14520be005 --- a/discord/channel.py +++ b/discord/channel.py @@ -2091,7 +2091,7 @@ async def edit( sync_permissions: int = ..., category: CategoryChannel | None = ..., overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., - rtc_region: VoiceRegion | None = ..., + rtc_region: VoiceRegion | str | None = ..., video_quality_mode: VideoQualityMode = ..., slowmode_delay: int = ..., nsfw: bool = ..., @@ -2135,10 +2135,16 @@ async def edit(self, *, reason=None, **options): The reason for editing this channel. Shows up on the audit log. overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] The overwrites to apply to channel permissions. Useful for creating secret channels. - rtc_region: Optional[:class:`VoiceRegion`] - The new region for the voice channel's voice communication. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The new region ID for the voice channel's voice communication. A value of ``None`` indicates automatic voice region detection. + .. versionchanged:: 2.9 + + A :class:`VoiceRegion` member is still accepted, but it is + deprecated in favor of the region ID :class:`str`, which + can be retrieved via :meth:`Guild.fetch_voice_regions`. + .. versionadded:: 1.7 video_quality_mode: :class:`VideoQualityMode` The camera video quality for the voice channel's participants. @@ -2778,7 +2784,7 @@ async def edit( sync_permissions: int = ..., category: CategoryChannel | None = ..., overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., - rtc_region: VoiceRegion | None = ..., + rtc_region: VoiceRegion | str | None = ..., video_quality_mode: VideoQualityMode = ..., reason: str | None = ..., ) -> StageChannel | None: ... @@ -2816,9 +2822,15 @@ async def edit(self, *, reason=None, **options): The reason for editing this channel. Shows up on the audit log. overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] The overwrites to apply to channel permissions. Useful for creating secret channels. - rtc_region: Optional[:class:`VoiceRegion`] - The new region for the stage channel's voice communication. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The new region ID for the stage channel's voice communication. A value of ``None`` indicates automatic voice region detection. + + .. versionchanged:: 2.9 + + A :class:`VoiceRegion` member is still accepted, but it is + deprecated in favor of the region ID :class:`str`, which + can be retrieved via :meth:`Guild.fetch_voice_regions`. video_quality_mode: :class:`VideoQualityMode` The camera video quality for the stage channel's participants. diff --git a/discord/enums.py b/discord/enums.py index 5011a338c2..5cb720e414 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -27,9 +27,23 @@ import types from collections import namedtuple +from collections.abc import Callable from enum import IntEnum from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Union +from .utils import warn_deprecated + +if TYPE_CHECKING: + from typing_extensions import deprecated +else: + + def deprecated(message: str) -> Callable[[T], T]: + def decorator(value: T) -> T: + return value + + return decorator + + __all__ = ( "Enum", "ChannelType", @@ -286,32 +300,64 @@ class MessageType(Enum): poll_result = 46 -class VoiceRegion(Enum): - """Voice region""" +class _VoiceRegionMeta(Enum.__class__): + def _warn(self, label: str) -> None: + warn_deprecated( + label, + instead="the region ID string or Guild.fetch_voice_regions()", + since="2.9", + removed="3.0", + stacklevel=4, + ) + + def __getattribute__(cls, name: str) -> Any: + members = super().__getattribute__("_enum_member_map_") + if name in members: + cls._warn(f"VoiceRegion.{name}") + return super().__getattribute__(name) + + def __getitem__(cls, name: str) -> Any: + member = super().__getitem__(name) + cls._warn(f"VoiceRegion[{name!r}]") + return member + + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + cls._warn(f"VoiceRegion({', '.join(map(repr, args))})") + return super().__call__(*args, **kwargs) + + def __getattr__(cls, name: str) -> Any: + if name.startswith("_"): + raise AttributeError(name) + cls._warn(f"VoiceRegion.{name}") + return create_unknown_value(cls, name) + + +@deprecated( + "VoiceRegion is deprecated in favour of the region ID str or " + "Guild.fetch_voice_regions() since version 2.9, and will be removed in version 3.0." +) +class VoiceRegion(Enum, metaclass=_VoiceRegionMeta): + """Specifies the region a voice server belongs to. + + .. deprecated:: 2.9 + The list of voice regions is dynamic, so this enum is deprecated in favor + of the region ID :class:`str`, which can be retrieved via + :meth:`Guild.fetch_voice_regions`, and will be removed in version 3.0. + """ - us_west = "us-west" - us_east = "us-east" - us_south = "us-south" - us_central = "us-central" - eu_west = "eu-west" - eu_central = "eu-central" - singapore = "singapore" - london = "london" - sydney = "sydney" - amsterdam = "amsterdam" - frankfurt = "frankfurt" brazil = "brazil" hongkong = "hongkong" - russia = "russia" + india = "india" japan = "japan" - southafrica = "southafrica" + rotterdam = "rotterdam" + singapore = "singapore" south_korea = "south-korea" - india = "india" - europe = "europe" - dubai = "dubai" - vip_us_east = "vip-us-east" - vip_us_west = "vip-us-west" - vip_amsterdam = "vip-amsterdam" + southafrica = "southafrica" + sydney = "sydney" + us_central = "us-central" + us_east = "us-east" + us_south = "us-south" + us_west = "us-west" def __str__(self): return self.value diff --git a/discord/guild.py b/discord/guild.py index b485123b52..f84cf8c32e 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -29,6 +29,7 @@ import datetime import unicodedata from collections.abc import Sequence +from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, @@ -147,6 +148,22 @@ class _GuildLimit(NamedTuple): filesize: int +@dataclass(frozen=True, slots=True) +class VoiceServerRegion: + """Represents a voice region a guild can use for voice channels. + + This is returned by :meth:`Guild.fetch_voice_regions`. + + .. versionadded:: 2.9 + """ + + id: str + name: str + optimal: bool + deprecated: bool + custom: bool + + class GuildRoleCounts(dict[int, int]): """A dictionary subclass that maps role IDs to their member counts. @@ -1602,7 +1619,7 @@ async def create_voice_channel( position: int = MISSING, bitrate: int = MISSING, user_limit: int = MISSING, - rtc_region: VoiceRegion | None = MISSING, + rtc_region: VoiceRegion | str | None = MISSING, video_quality_mode: VideoQualityMode = MISSING, overwrites: dict[Role | Member, PermissionOverwrite] = MISSING, slowmode_delay: int = MISSING, @@ -1629,10 +1646,16 @@ async def create_voice_channel( The channel's preferred audio bitrate in bits per second. user_limit: :class:`int` The channel's limit for number of members that can be in a voice channel. - rtc_region: Optional[:class:`VoiceRegion`] - The region for the voice channel's voice communication. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The region ID for the voice channel's voice communication. A value of ``None`` indicates automatic voice region detection. + .. versionchanged:: 2.9 + + A :class:`VoiceRegion` member is still accepted, but it is + deprecated in favor of the region ID :class:`str`, which + can be retrieved via :meth:`Guild.fetch_voice_regions`. + .. versionadded:: 1.7 video_quality_mode: :class:`VideoQualityMode` The camera video quality for the voice channel's participants. @@ -1713,7 +1736,7 @@ async def create_stage_channel( reason: str | None = None, bitrate: int = MISSING, user_limit: int = MISSING, - rtc_region: VoiceRegion | None = MISSING, + rtc_region: VoiceRegion | str | None = MISSING, video_quality_mode: VideoQualityMode = MISSING, slowmode_delay: int = MISSING, nsfw: bool = MISSING, @@ -1752,10 +1775,16 @@ async def create_stage_channel( .. versionadded:: 2.7 - rtc_region: Optional[:class:`VoiceRegion`] - The region for the voice channel's voice communication. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The region ID for the voice channel's voice communication. A value of ``None`` indicates automatic voice region detection. + .. versionchanged:: 2.9 + + A :class:`VoiceRegion` member is still accepted, but it is + deprecated in favor of the region ID :class:`str`, which + can be retrieved via :meth:`Guild.fetch_voice_regions`. + .. versionadded:: 2.7 video_quality_mode: :class:`VideoQualityMode` @@ -3786,6 +3815,43 @@ async def vanity_invite(self) -> Invite | None: payload["uses"] = payload.get("uses", 0) return Invite(state=self._state, data=payload, guild=self, channel=channel) + async def fetch_voice_regions(self) -> list[VoiceServerRegion]: + """|coro| + + Retrieves the voice regions that the guild has access to. + + The list of voice regions is dynamic, so this method is the + recommended way to get the currently available regions. + + .. versionadded:: 2.9 + + Each :class:`~discord.guild.VoiceServerRegion` has the following attributes: + + :attr:`~discord.guild.VoiceServerRegion.id` + The region ID, e.g. ``"us-west"``. Use this as the + :attr:`~discord.VoiceChannel.rtc_region` of a voice channel. + :attr:`~discord.guild.VoiceServerRegion.name` + The region's display name, e.g. ``"US West"``. + :attr:`~discord.guild.VoiceServerRegion.optimal` + Whether the region is optimal for the guild's members. + :attr:`~discord.guild.VoiceServerRegion.deprecated` + Whether the region is deprecated. + :attr:`~discord.guild.VoiceServerRegion.custom` + Whether the region is a custom region. + + Returns + ------- + List[:class:`~discord.guild.VoiceServerRegion`] + The list of voice regions the guild has access to. + + Raises + ------ + HTTPException + Retrieving the voice regions failed. + """ + regions = await self._state.http.get_guild_voice_regions(self.id) + return [VoiceServerRegion(**region) for region in regions] + # TODO: use MISSING when async iterators get refactored def audit_logs( self, diff --git a/discord/http.py b/discord/http.py index 40ebcd7c1f..223db40713 100644 --- a/discord/http.py +++ b/discord/http.py @@ -94,6 +94,7 @@ ) from .types.snowflake import Snowflake, SnowflakeList from .types.soundboard import SoundboardSound as SoundboardSoundPayload + from .types.voice import VoiceRegion as VoiceRegionPayload T = TypeVar("T") BE = TypeVar("BE", bound=BaseException) @@ -1043,6 +1044,17 @@ def guild_voice_state( return self.request(r, json=payload, reason=reason) + def get_guild_voice_regions( + self, + guild_id: Snowflake, + ) -> Response[list[VoiceRegionPayload]]: + return self.request( + Route("GET", "/guilds/{guild_id}/regions", guild_id=guild_id) + ) + + def get_voice_regions(self) -> Response[list[VoiceRegionPayload]]: + return self.request(Route("GET", "/voice/regions")) + def edit_profile(self, payload: dict[str, Any]) -> Response[user.User]: return self.request(Route("PATCH", "/users/@me"), json=payload) diff --git a/docs/api/enums.rst b/docs/api/enums.rst index efa16a4a5e..f9ca7fb9a7 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -615,60 +615,37 @@ of :class:`enum.Enum`. Specifies the region a voice server belongs to. - .. attribute:: amsterdam + .. deprecated:: 2.9 + + The list of voice regions is dynamic, so this enum is deprecated in favor + of the region ID :class:`str`, which can be retrieved via + :meth:`Guild.fetch_voice_regions`, and will be removed in version 3.0. - The Amsterdam region. .. attribute:: brazil The Brazil region. - .. attribute:: dubai - - The Dubai region. - - .. versionadded:: 1.3 - - .. attribute:: eu_central - - The EU Central region. - .. attribute:: eu_west - - The EU West region. - .. attribute:: europe - - The Europe region. - - .. versionadded:: 1.3 - - .. attribute:: frankfurt - - The Frankfurt region. .. attribute:: hongkong The Hong Kong region. .. attribute:: india The India region. - - .. versionadded:: 1.2 - .. attribute:: japan The Japan region. - .. attribute:: london + .. attribute:: rotterdam - The London region. - .. attribute:: russia + The Rotterdam region. - The Russia region. .. attribute:: singapore The Singapore region. - .. attribute:: southafrica - - The South Africa region. .. attribute:: south_korea The South Korea region. + .. attribute:: southafrica + + The South Africa region. .. attribute:: sydney The Sydney region. @@ -684,15 +661,6 @@ of :class:`enum.Enum`. .. attribute:: us_west The US West region. - .. attribute:: vip_amsterdam - - The Amsterdam region for VIP guilds. - .. attribute:: vip_us_east - - The US East region for VIP guilds. - .. attribute:: vip_us_west - - The US West region for VIP guilds. .. class:: VerificationLevel